diff --git a/man/org.freedesktop.home1.xml b/man/org.freedesktop.home1.xml index b1bb6587dc4..8ac3d96086c 100644 --- a/man/org.freedesktop.home1.xml +++ b/man/org.freedesktop.home1.xml @@ -72,6 +72,9 @@ node /org/freedesktop/home1 { RegisterHome(in s user_record); UnregisterHome(in s user_name); CreateHome(in s user_record); + CreateHomeEx(in s user_record, + in a{sh} blobs, + in t flags); RealizeHome(in s user_name, in s secret); RemoveHome(in s user_name); @@ -81,6 +84,9 @@ node /org/freedesktop/home1 { AuthenticateHome(in s user_name, in s secret); UpdateHome(in s user_record); + UpdateHomeEx(in s user_record, + in a{sh} blobs, + in t flags); ResizeHome(in s user_name, in t size, in s secret); @@ -151,6 +157,8 @@ node /org/freedesktop/home1 { + + @@ -161,6 +169,8 @@ node /org/freedesktop/home1 { + + @@ -265,6 +275,19 @@ node /org/freedesktop/home1 { the user record locally and creates a home directory matching it, depending on the settings specified in the record in combination with local configuration. + CreateHomeEx() is like CreateHome(), but it allows the + home directory to be created with a pre-populated blob directory (see + User Record Blob Directories for more info). + This can be done via the dictionary passed as the blobs argument to this method: the values + are open file descriptors to regular files, and the keys are the filenames that should contain their respective + file's data in the blob directory. Note that for security reasons, the file descriptors passed into this method + must have enough privileges to read their target file and thus cannot be O_PATH; this + is done to ensure the caller is actually permitted to read the file they are asking to publish in the + blob directories. If the user record passed as the first argument contains a blobManifest + field it will be enforced; otherwise, a blobManifest field will be generated and inserted + into the record. The flags argument may be used for future expansion, but for now + pass 0. + RealizeHome() creates a home directory whose user record is already registered locally. This takes a user name plus a user record consisting only of the secret section. Invoking RegisterHome() followed by @@ -308,6 +331,14 @@ node /org/freedesktop/home1 { as the storage resized using ResizeHome(). This method is equivalent to Update() on the org.freedesktop.home1.Home interface. + UpdateHomeEx() is like UpdateHome(), but it allows for + changes to the blob directory (see User Record Blob + Directories for more info). The blobs argument works in the same way as + CreateHomeEx(), so check there for details. The new blob directory contents passed into + this method will completely replace the user's existing blob directory. The flags argument + may be used for future expansion, but for now pass 0. This method is equivalent to UpdateEx() + on the org.freedesktop.home1.Home interface. + ResizeHome() resizes the storage associated with a user record. Takes a user name, a disk size in bytes and a user record consisting only of the secret section as argument. If the size is specified as UINT64_MAX the storage is resized to the @@ -438,6 +469,9 @@ node /org/freedesktop/home1/home { Fixate(in s secret); Authenticate(in s secret); Update(in s user_record); + UpdateEx(in s user_record, + in a{sh} blobs, + in t flags); Resize(in t size, in s secret); ChangePassword(in s new_secret, @@ -504,6 +538,8 @@ node /org/freedesktop/home1/home { + + @@ -540,11 +576,11 @@ node /org/freedesktop/home1/home { Activate(), ActivateIfReferenced(), Deactivate(), Unregister(), Realize(), Remove(), Fixate(), Authenticate(), - Update(), Resize(), ChangePassword(), - Lock(), Unlock(), Acquire(), - Ref(), RefUnrestricted(), Release(), - InhibitSuspend() operate like their matching counterparts on the - org.freedesktop.home1.Manager interface (see above). The main difference is that + Update(), UpdateEx(), Resize(), + ChangePassword(), Lock(), Unlock(), + Acquire(), Ref(), RefUnrestricted(), + Release(), InhibitSuspend() operate like their matching counterparts + on the org.freedesktop.home1.Manager interface (see above). The main difference is that they are methods of the home directory objects, and hence carry no additional user name parameter. Which of the two flavors of methods to call depends on the handles to the user known on the client side: if only the user name is known, it's preferable to use the methods on the manager object @@ -575,11 +611,13 @@ node /org/freedesktop/home1/home { History The Manager Object - InhibitSuspendHome(), ActivateHomeIfReferenced(), RefHomeUnrestricted() wer added in version 256. + InhibitSuspendHome(), ActivateHomeIfReferenced(), RefHomeUnrestricted(), + CreateHomeEx(), and UpdateHomeEx() were added in version 256. Home Objects - InhibitSuspend(), ActivateIfReferenced() and RefUnrestricted() were added in version 256. + InhibitSuspend(), ActivateIfReferenced(), RefUnrestricted(), and + UpdateEx() were added in version 256. diff --git a/src/home/home-util.c b/src/home/home-util.c index 9c9c0ff78aa..973523652dc 100644 --- a/src/home/home-util.c +++ b/src/home/home-util.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "dns-domain.h" +#include "fd-util.h" #include "home-util.h" #include "libcrypt-util.h" #include "memory-util.h" @@ -9,6 +10,8 @@ #include "strv.h" #include "user-util.h" +DEFINE_HASH_OPS_FULL(blob_fd_hash_ops, char, path_hash_func, path_compare, free, void, close_fd_ptr); + bool suitable_user_name(const char *name) { /* Checks whether the specified name is suitable for management via homed. Note that client-side diff --git a/src/home/home-util.h b/src/home/home-util.h index ead8f5637ee..f2e5787a546 100644 --- a/src/home/home-util.h +++ b/src/home/home-util.h @@ -5,6 +5,7 @@ #include "sd-bus.h" +#include "hash-funcs.h" #include "time-util.h" #include "user-record.h" @@ -20,6 +21,8 @@ /* This should be 83% right now, i.e. 100 of (100 + 20). Let's protect us against accidental changes. */ assert_cc(USER_DISK_SIZE_DEFAULT_PERCENT == 83U); +extern const struct hash_ops blob_fd_hash_ops; + bool suitable_user_name(const char *name); int suitable_realm(const char *realm); int suitable_image_path(const char *path); diff --git a/src/home/homed-bus.c b/src/home/homed-bus.c index 24b421a58c0..cbd84c231c7 100644 --- a/src/home/homed-bus.c +++ b/src/home/homed-bus.c @@ -1,6 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "fd-util.h" +#include "home-util.h" #include "homed-bus.h" +#include "missing_fcntl.h" +#include "stat-util.h" #include "strv.h" int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error) { @@ -64,3 +68,72 @@ int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, U *ret = TAKE_PTR(hr); return 0; } + +int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error) { + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + int r; + + assert(m); + assert(ret); + + /* We want to differentiate between blobs being NULL (not passed at all) + * and empty (passed from dbus, but it was empty) */ + r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(m, 'a', "{sh}"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *filename = NULL; + _cleanup_close_ int fd = -EBADF; + const char *_filename = NULL; + int _fd, flags; + + r = sd_bus_message_read(m, "{sh}", &_filename, &_fd); + if (r < 0) + return r; + if (r == 0) + break; + + filename = strdup(_filename); + if (!filename) + return -ENOMEM; + + fd = fcntl(_fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) + return -errno; + + r = suitable_blob_filename(filename); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid blob directory filename: %s", filename); + + r = fd_verify_regular(fd); + if (r < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for %s is not a regular file", filename); + + flags = fcntl(fd, F_GETFL); + if (flags < 0) + return -errno; + + /* Refuse fds w/ unexpected flags set. In particular, we don't want to permit O_PATH FDs, since + * those don't actually guarentee that the client has access to the file. */ + if ((flags & ~(O_ACCMODE|RAW_O_LARGEFILE)) != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "FD for %s has unexpected flags set", filename); + + r = hashmap_put(blobs, filename, FD_TO_PTR(fd)); + if (r < 0) + return r; + TAKE_PTR(filename); /* Ownership transferred to hashmap */ + TAKE_FD(fd); + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + *ret = TAKE_PTR(blobs); + return 0; +} diff --git a/src/home/homed-bus.h b/src/home/homed-bus.h index 977679b10ad..0660a5936c2 100644 --- a/src/home/homed-bus.h +++ b/src/home/homed-bus.h @@ -3,8 +3,10 @@ #include "sd-bus.h" -#include "user-record.h" +#include "hashmap.h" #include "json.h" +#include "user-record.h" int bus_message_read_secret(sd_bus_message *m, UserRecord **ret, sd_bus_error *error); int bus_message_read_home_record(sd_bus_message *m, UserRecordLoadFlags flags, UserRecord **ret, sd_bus_error *error); +int bus_message_read_blobs(sd_bus_message *m, Hashmap **ret, sd_bus_error *error); diff --git a/src/home/homed-home-bus.c b/src/home/homed-home-bus.c index 81f0df4c956..f54de1f581a 100644 --- a/src/home/homed-home-bus.c +++ b/src/home/homed-home-bus.c @@ -295,7 +295,7 @@ int bus_home_method_realize( if (r == 0) return 1; /* Will call us back */ - r = home_create(h, secret, error); + r = home_create(h, secret, NULL, 0, error); if (r < 0) return r; @@ -416,7 +416,13 @@ int bus_home_method_authenticate( return 1; } -int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord *hr, sd_bus_error *error) { +int bus_home_method_update_record( + Home *h, + sd_bus_message *message, + UserRecord *hr, + Hashmap *blobs, + uint64_t flags, + sd_bus_error *error) { int r; assert(h); @@ -427,6 +433,9 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord * if (r < 0) return r; + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Provided flags are unsupported."); + r = home_verify_polkit_async( h, message, @@ -438,7 +447,7 @@ int bus_home_method_update_record(Home *h, sd_bus_message *message, UserRecord * if (r == 0) return 1; /* Will call us back */ - r = home_update(h, hr, error); + r = home_update(h, hr, blobs, flags, error); if (r < 0) return r; @@ -458,6 +467,8 @@ int bus_home_method_update( sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Home *h = ASSERT_PTR(userdata); int r; @@ -467,7 +478,17 @@ int bus_home_method_update( if (r < 0) return r; - return bus_home_method_update_record(h, message, hr, error); + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + } + + return bus_home_method_update_record(h, message, hr, blobs, flags, error); } int bus_home_method_resize( @@ -892,6 +913,11 @@ const sd_bus_vtable home_vtable[] = { SD_BUS_NO_RESULT, bus_home_method_update, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("UpdateEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + bus_home_method_update, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD_WITH_ARGS("Resize", SD_BUS_ARGS("t", size, "s", secret), SD_BUS_NO_RESULT, diff --git a/src/home/homed-home-bus.h b/src/home/homed-home-bus.h index a791c763126..9ba2cf12525 100644 --- a/src/home/homed-home-bus.h +++ b/src/home/homed-home-bus.h @@ -17,7 +17,7 @@ int bus_home_method_remove(sd_bus_message *message, void *userdata, sd_bus_error int bus_home_method_fixate(sd_bus_message *message, void *userdata, sd_bus_error *error); int bus_home_method_authenticate(sd_bus_message *message, void *userdata, sd_bus_error *error); int bus_home_method_update(sd_bus_message *message, void *userdata, sd_bus_error *error); -int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, sd_bus_error *error); +int bus_home_method_update_record(Home *home, sd_bus_message *message, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error); int bus_home_method_resize(sd_bus_message *message, void *userdata, sd_bus_error *error); int bus_home_method_change_password(sd_bus_message *message, void *userdata, sd_bus_error *error); int bus_home_method_lock(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 7bb078ee2c8..a487eb1fd1a 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -55,7 +55,13 @@ assert_cc(HOME_UID_MIN <= HOME_UID_MAX); assert_cc(HOME_USERS_MAX <= (HOME_UID_MAX - HOME_UID_MIN + 1)); -static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret); +static int home_start_work( + Home *h, + const char *verb, + UserRecord *hr, + UserRecord *secret, + Hashmap *blobs, + uint64_t flags); DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(operation_hash_ops, void, trivial_hash_func, trivial_compare_func, Operation, operation_unref); @@ -738,7 +744,7 @@ static void home_fixate_finish(Home *h, int ret, UserRecord *hr) { if (IN_SET(h->state, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)) { - r = home_start_work(h, "activate", h->record, secret); + r = home_start_work(h, "activate", h->record, secret, NULL, 0); if (r < 0) { h->current_operation = operation_result_unref(h->current_operation, r, NULL); home_set_state(h, _HOME_STATE_INVALID); @@ -1178,10 +1184,17 @@ static int home_on_worker_process(sd_event_source *s, const siginfo_t *si, void return 0; } -static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord *secret) { - _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; +static int home_start_work( + Home *h, + const char *verb, + UserRecord *hr, + UserRecord *secret, + Hashmap *blobs, + uint64_t flags) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *fdmap = NULL; _cleanup_(erase_and_freep) char *formatted = NULL; _cleanup_close_ int stdin_fd = -EBADF, stdout_fd = -EBADF; + _cleanup_free_ int *blob_fds = NULL; pid_t pid = 0; int r; @@ -1209,6 +1222,37 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord return r; } + if (blobs) { + const char *blob_filename = NULL; + void *fd_ptr; + size_t i = 0; + + blob_fds = new(int, hashmap_size(blobs)); + if (!blob_fds) + return -ENOMEM; + + /* homework needs to be able to tell the difference between blobs being null + * (the fdmap field is completely missing) and it being empty (the field is an + * empty object) */ + r = json_variant_new_object(&fdmap, NULL, 0); + if (r < 0) + return r; + + HASHMAP_FOREACH_KEY(fd_ptr, blob_filename, blobs) { + blob_fds[i] = PTR_TO_FD(fd_ptr); + + r = json_variant_set_field_integer(&fdmap, blob_filename, i); + if (r < 0) + return r; + + i++; + } + + r = json_variant_set_field(&v, HOMEWORK_BLOB_FDMAP_FIELD, fdmap); + if (r < 0) + return r; + } + r = json_variant_format(v, 0, &formatted); if (r < 0) return r; @@ -1225,8 +1269,9 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord r = safe_fork_full("(sd-homework)", (int[]) { stdin_fd, stdout_fd, STDERR_FILENO }, - NULL, 0, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); + blob_fds, hashmap_size(blobs), + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_CLOEXEC_OFF|FORK_PACK_FDS|FORK_DEATHSIG_SIGTERM| + FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid); if (r < 0) return r; if (r == 0) { @@ -1271,6 +1316,10 @@ static int home_start_work(Home *h, const char *verb, UserRecord *hr, UserRecord if (r < 0) log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m"); + r = setenv_systemd_log_level(); + if (r < 0) + log_warning_errno(r, "Failed to update $SYSTEMD_LOG_LEVEL, ignoring: %m"); + /* Allow overriding the homework path via an environment variable, to make debugging * easier. */ homework = getenv("SYSTEMD_HOMEWORK_PATH") ?: SYSTEMD_HOMEWORK_PATH; @@ -1343,7 +1392,7 @@ static int home_fixate_internal( assert(secret); assert(IN_SET(for_state, HOME_FIXATING, HOME_FIXATING_FOR_ACTIVATION, HOME_FIXATING_FOR_ACQUIRE)); - r = home_start_work(h, "inspect", h->record, secret); + r = home_start_work(h, "inspect", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1392,7 +1441,7 @@ static int home_activate_internal(Home *h, UserRecord *secret, HomeState for_sta assert(secret); assert(IN_SET(for_state, HOME_ACTIVATING, HOME_ACTIVATING_FOR_ACQUIRE)); - r = home_start_work(h, "activate", h->record, secret); + r = home_start_work(h, "activate", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1444,7 +1493,7 @@ static int home_authenticate_internal(Home *h, UserRecord *secret, HomeState for assert(secret); assert(IN_SET(for_state, HOME_AUTHENTICATING, HOME_AUTHENTICATING_WHILE_ACTIVE, HOME_AUTHENTICATING_FOR_ACQUIRE)); - r = home_start_work(h, "inspect", h->record, secret); + r = home_start_work(h, "inspect", h->record, secret, NULL, 0); if (r < 0) return r; @@ -1489,7 +1538,7 @@ static int home_deactivate_internal(Home *h, bool force, sd_bus_error *error) { home_unpin(h); /* unpin so that we can deactivate */ - r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL); + r = home_start_work(h, force ? "deactivate-force" : "deactivate", h->record, NULL, NULL, 0); if (r < 0) /* Operation failed before it even started, reacquire pin fd, if state still dictates so */ home_update_pin_fd(h, _HOME_STATE_INVALID); @@ -1526,7 +1575,7 @@ int home_deactivate(Home *h, bool force, sd_bus_error *error) { return home_deactivate_internal(h, force, error); } -int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { +int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error) { int r; assert(h); @@ -1569,7 +1618,7 @@ int home_create(Home *h, UserRecord *secret, sd_bus_error *error) { return r; } - r = home_start_work(h, "create", h->record, secret); + r = home_start_work(h, "create", h->record, secret, blobs, flags); if (r < 0) return r; @@ -1599,7 +1648,7 @@ int home_remove(Home *h, sd_bus_error *error) { return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "Home %s is currently being used, or an operation on home %s is currently being executed.", h->user_name, h->user_name); } - r = home_start_work(h, "remove", h->record, NULL); + r = home_start_work(h, "remove", h->record, NULL, NULL, 0); if (r < 0) return r; @@ -1643,6 +1692,8 @@ static int home_update_internal( const char *verb, UserRecord *hr, UserRecord *secret, + Hashmap *blobs, + uint64_t flags, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *new_hr = NULL, *saved_secret = NULL, *signed_hr = NULL; @@ -1666,6 +1717,15 @@ static int home_update_internal( secret = saved_secret; } + if (blobs) { + const char *failed = NULL; + r = user_record_ensure_blob_manifest(hr, blobs, &failed); + if (r == -EINVAL) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest."); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed)); + } + r = manager_verify_user_record(h->manager, hr); switch (r) { @@ -1708,14 +1768,14 @@ static int home_update_internal( return sd_bus_error_set(error, BUS_ERROR_HOME_RECORD_MISMATCH, "Home record different but timestamp remained the same, refusing."); } - r = home_start_work(h, verb, new_hr, secret); + r = home_start_work(h, verb, new_hr, secret, blobs, flags); if (r < 0) return r; return 0; } -int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { +int home_update(Home *h, UserRecord *hr, Hashmap *blobs, uint64_t flags, sd_bus_error *error) { HomeState state; int r; @@ -1743,7 +1803,7 @@ int home_update(Home *h, UserRecord *hr, sd_bus_error *error) { if (r < 0) return r; - r = home_update_internal(h, "update", hr, NULL, error); + r = home_update_internal(h, "update", hr, NULL, blobs, flags, error); if (r < 0) return r; @@ -1830,7 +1890,7 @@ int home_resize(Home *h, c = TAKE_PTR(signed_c); } - r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, error); + r = home_update_internal(h, automatic ? "resize-auto" : "resize", c, secret, NULL, 0, error); if (r < 0) return r; @@ -1946,7 +2006,7 @@ int home_passwd(Home *h, return r; } - r = home_update_internal(h, "passwd", signed_c, merged_secret, error); + r = home_update_internal(h, "passwd", signed_c, merged_secret, NULL, 0, error); if (r < 0) return r; @@ -2003,7 +2063,7 @@ int home_lock(Home *h, sd_bus_error *error) { return sd_bus_error_setf(error, BUS_ERROR_HOME_BUSY, "An operation on home %s is currently being executed.", h->user_name); } - r = home_start_work(h, "lock", h->record, NULL); + r = home_start_work(h, "lock", h->record, NULL, NULL, 0); if (r < 0) return r; @@ -2018,7 +2078,7 @@ static int home_unlock_internal(Home *h, UserRecord *secret, HomeState for_state assert(secret); assert(IN_SET(for_state, HOME_UNLOCKING, HOME_UNLOCKING_FOR_ACQUIRE)); - r = home_start_work(h, "unlock", h->record, secret); + r = home_start_work(h, "unlock", h->record, secret, NULL, 0); if (r < 0) return r; diff --git a/src/home/homed-home.h b/src/home/homed-home.h index 6c069ab5f02..0f3d1ce3251 100644 --- a/src/home/homed-home.h +++ b/src/home/homed-home.h @@ -3,6 +3,7 @@ typedef struct Home Home; +#include "hashmap.h" #include "homed-manager.h" #include "homed-operation.h" #include "list.h" @@ -193,9 +194,9 @@ int home_fixate(Home *h, UserRecord *secret, sd_bus_error *error); int home_activate(Home *h, bool if_referenced, UserRecord *secret, sd_bus_error *error); int home_authenticate(Home *h, UserRecord *secret, sd_bus_error *error); int home_deactivate(Home *h, bool force, sd_bus_error *error); -int home_create(Home *h, UserRecord *secret, sd_bus_error *error); +int home_create(Home *h, UserRecord *secret, Hashmap *blobs, uint64_t flags, sd_bus_error *error); int home_remove(Home *h, sd_bus_error *error); -int home_update(Home *h, UserRecord *new_record, sd_bus_error *error); +int home_update(Home *h, UserRecord *new_record, Hashmap *blobs, uint64_t flags, sd_bus_error *error); int home_resize(Home *h, uint64_t disk_size, UserRecord *secret, bool automatic, sd_bus_error *error); int home_passwd(Home *h, UserRecord *new_secret, UserRecord *old_secret, sd_bus_error *error); int home_unregister(Home *h, sd_bus_error *error); diff --git a/src/home/homed-manager-bus.c b/src/home/homed-manager-bus.c index c8e232f4257..1e63012a06f 100644 --- a/src/home/homed-manager-bus.c +++ b/src/home/homed-manager-bus.c @@ -340,7 +340,7 @@ static int method_deactivate_home(sd_bus_message *message, void *userdata, sd_bu return generic_home_method(userdata, message, bus_home_method_deactivate, error); } -static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd_bus_error *error) { +static int validate_and_allocate_home(Manager *m, UserRecord *hr, Hashmap *blobs, Home **ret, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *signed_hr = NULL; bool signed_locally; Home *other; @@ -370,6 +370,15 @@ static int validate_and_allocate_home(Manager *m, UserRecord *hr, Home **ret, sd if (r != -ESRCH) return r; + if (blobs) { + const char *failed = NULL; + r = user_record_ensure_blob_manifest(hr, blobs, &failed); + if (r == -EINVAL) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Provided blob files do not correspond to blob manifest."); + if (r < 0) + return sd_bus_error_set_errnof(error, r, "Failed to generate hash for blob %s: %m", strnull(failed)); + } + r = manager_verify_user_record(m, hr); switch (r) { @@ -458,7 +467,7 @@ static int method_register_home( if (r == 0) return 1; /* Will call us back */ - r = validate_and_allocate_home(m, hr, &h, error); + r = validate_and_allocate_home(m, hr, NULL, &h, error); if (r < 0) return r; @@ -475,12 +484,11 @@ static int method_unregister_home(sd_bus_message *message, void *userdata, sd_bu return generic_home_method(userdata, message, bus_home_method_unregister, error); } -static int method_create_home( - sd_bus_message *message, - void *userdata, - sd_bus_error *error) { +static int method_create_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Manager *m = ASSERT_PTR(userdata); Home *h; int r; @@ -491,6 +499,18 @@ static int method_create_home( if (r < 0) return r; + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + if (flags != 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Provided flags are unsupported."); + } + r = bus_verify_polkit_async( message, "org.freedesktop.home1.create-home", @@ -502,11 +522,11 @@ static int method_create_home( if (r == 0) return 1; /* Will call us back */ - r = validate_and_allocate_home(m, hr, &h, error); + r = validate_and_allocate_home(m, hr, blobs, &h, error); if (r < 0) return r; - r = home_create(h, hr, error); + r = home_create(h, hr, blobs, flags, error); if (r < 0) goto fail; @@ -544,6 +564,8 @@ static int method_authenticate_home(sd_bus_message *message, void *userdata, sd_ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { _cleanup_(user_record_unrefp) UserRecord *hr = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; + uint64_t flags = 0; Manager *m = ASSERT_PTR(userdata); Home *h; int r; @@ -554,13 +576,23 @@ static int method_update_home(sd_bus_message *message, void *userdata, sd_bus_er if (r < 0) return r; + if (endswith(sd_bus_message_get_member(message), "Ex")) { + r = bus_message_read_blobs(message, &blobs, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + } + assert(hr->user_name); h = hashmap_get(m->homes_by_name, hr->user_name); if (!h) return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_HOME, "No home for user %s known", hr->user_name); - return bus_home_method_update_record(h, message, hr, error); + return bus_home_method_update_record(h, message, hr, blobs, flags, error); } static int method_resize_home(sd_bus_message *message, void *userdata, sd_bus_error *error) { @@ -769,6 +801,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_create_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("CreateHomeEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + method_create_home, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), /* Create $HOME for already registered JSON entry */ SD_BUS_METHOD_WITH_ARGS("RealizeHome", @@ -804,6 +841,11 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_NO_RESULT, method_update_home, SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), + SD_BUS_METHOD_WITH_ARGS("UpdateHomeEx", + SD_BUS_ARGS("s", user_record, "a{sh}", blobs, "t", flags), + SD_BUS_NO_RESULT, + method_update_home, + SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_SENSITIVE), SD_BUS_METHOD_WITH_ARGS("ResizeHome", SD_BUS_ARGS("s", user_name, "t", size, "s", secret), diff --git a/src/home/homework-blob.c b/src/home/homework-blob.c index b95a0dc7091..17cb7d6ce3c 100644 --- a/src/home/homework-blob.c +++ b/src/home/homework-blob.c @@ -241,3 +241,61 @@ int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled) { } return 0; } + +int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs) { + _cleanup_free_ char *fn = NULL; + _cleanup_close_ int base_dfd = -EBADF, dfd = -EBADF; + uint64_t total_size = 0; + const char *filename; + const void *v; + int r; + + assert(h); + + if (!blobs) /* Shortcut: If no blobs are passed from dbus, we have nothing to do. */ + return 0; + + base_dfd = open(home_system_blob_dir(), O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (base_dfd < 0) + return log_error_errno(errno, "Failed to open system blob base dir: %m"); + + if (hashmap_isempty(blobs)) { + /* Shortcut: If blobs was passed but empty, we can simply delete the contents + * of the directory. */ + r = rm_rf_at(base_dfd, h->user_name, REMOVE_PHYSICAL|REMOVE_MISSING_OK); + if (r < 0) + return log_error_errno(errno, "Failed to empty out system blob dir: %m"); + return 0; + } + + r = tempfn_random(h->user_name, NULL, &fn); + if (r < 0) + return r; + + dfd = open_mkdir_at(base_dfd, fn, O_EXCL|O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(errno, "Failed to create system blob dir: %m"); + + HASHMAP_FOREACH_KEY(v, filename, blobs) { + r = copy_one_blob(PTR_TO_FD(v), dfd, filename, &total_size, 0, h->blob_manifest); + if (r == -EFBIG) + break; + if (r < 0) { + log_error_errno(r, "Failed to copy %s into system blob dir: %m", filename); + goto fail; + } + } + + r = install_file(base_dfd, fn, base_dfd, h->user_name, INSTALL_REPLACE); + if (r < 0) { + log_error_errno(r, "Failed to move system blob dir into place: %m"); + goto fail; + } + + log_info("Replaced system blob directory."); + return 0; + +fail: + (void) rm_rf_at(base_dfd, fn, REMOVE_ROOT|REMOVE_PHYSICAL|REMOVE_MISSING_OK); + return r; +} diff --git a/src/home/homework-blob.h b/src/home/homework-blob.h index 7bf4f514b9f..fbe6c82cd4f 100644 --- a/src/home/homework-blob.h +++ b/src/home/homework-blob.h @@ -5,3 +5,5 @@ #include "user-record.h" int home_reconcile_blob_dirs(UserRecord *h, int root_fd, int reconciled); + +int home_apply_new_blob_dir(UserRecord *h, Hashmap *blobs); diff --git a/src/home/homework.c b/src/home/homework.c index 4575471041f..39e00514865 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -25,6 +25,7 @@ #include "memory-util.h" #include "missing_magic.h" #include "mount-util.h" +#include "parse-util.h" #include "path-util.h" #include "recovery-key.h" #include "rm-rf.h" @@ -1314,7 +1315,7 @@ static int determine_default_storage(UserStorage *ret) { return 0; } -static int home_create(UserRecord *h, UserRecord **ret_home) { +static int home_create(UserRecord *h, Hashmap *blobs, UserRecord **ret_home) { _cleanup_strv_free_erase_ char **effective_passwords = NULL; _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; @@ -1376,6 +1377,10 @@ static int home_create(UserRecord *h, UserRecord **ret_home) { if (!IN_SET(r, USER_TEST_ABSENT, USER_TEST_UNDEFINED, USER_TEST_MAYBE)) return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Image path %s already exists, refusing.", user_record_image_path(h)); + r = home_apply_new_blob_dir(h, blobs); + if (r < 0) + return r; + switch (user_record_storage(h)) { case USER_LUKS: @@ -1581,7 +1586,7 @@ static int home_validate_update(UserRecord *h, HomeSetup *setup, HomeSetupFlags return has_mount; /* return true if the home record is already active */ } -static int home_update(UserRecord *h, UserRecord **ret) { +static int home_update(UserRecord *h, Hashmap *blobs, UserRecord **ret) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL, *embedded_home = NULL; _cleanup_(home_setup_done) HomeSetup setup = HOME_SETUP_INIT; _cleanup_(password_cache_free) PasswordCache cache = {}; @@ -1600,6 +1605,10 @@ static int home_update(UserRecord *h, UserRecord **ret) { if (r < 0) return r; + r = home_apply_new_blob_dir(h, blobs); + if (r < 0) + return r; + r = home_setup(h, flags, &setup, &cache, &header_home); if (r < 0) return r; @@ -1867,10 +1876,12 @@ static int run(int argc, char *argv[]) { _cleanup_(user_record_unrefp) UserRecord *home = NULL, *new_home = NULL; _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_fclose_ FILE *opened_file = NULL; + _cleanup_hashmap_free_ Hashmap *blobs = NULL; unsigned line = 0, column = 0; - const char *json_path = NULL; + const char *json_path = NULL, *blob_filename; FILE *json_file; usec_t start; + JsonVariant *fdmap, *blob_fd_variant; int r; start = now(CLOCK_MONOTONIC); @@ -1901,6 +1912,48 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "[%s:%u:%u] Failed to parse JSON data: %m", json_path, line, column); + fdmap = json_variant_by_key(v, HOMEWORK_BLOB_FDMAP_FIELD); + if (fdmap) { + r = hashmap_ensure_allocated(&blobs, &blob_fd_hash_ops); + if (r < 0) + return log_oom(); + + JSON_VARIANT_OBJECT_FOREACH(blob_filename, blob_fd_variant, fdmap) { + _cleanup_free_ char *filename = NULL; + _cleanup_close_ int fd = -EBADF; + + assert(json_variant_is_integer(blob_fd_variant)); + assert(json_variant_integer(blob_fd_variant) >= 0); + assert(json_variant_integer(blob_fd_variant) <= INT_MAX - SD_LISTEN_FDS_START); + fd = SD_LISTEN_FDS_START + (int) json_variant_integer(blob_fd_variant); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *resolved = NULL; + r = fd_get_path(fd, &resolved); + log_debug("Got blob from daemon: %s (%d) → %s", + blob_filename, fd, resolved ?: STRERROR(r)); + } + + filename = strdup(blob_filename); + if (!filename) + return log_oom(); + + r = fd_cloexec(fd, true); + if (r < 0) + return log_error_errno(r, "Failed to enable O_CLOEXEC on blob %s: %m", filename); + + r = hashmap_put(blobs, filename, FD_TO_PTR(fd)); + if (r < 0) + return log_error_errno(r, "Failed to insert blob %s into map: %m", filename); + TAKE_PTR(filename); /* Ownership transfers to hashmap */ + TAKE_FD(fd); + } + + r = json_variant_filter(&v, STRV_MAKE(HOMEWORK_BLOB_FDMAP_FIELD)); + if (r < 0) + return log_error_errno(r, "Failed to strip internal fdmap from JSON: %m"); + } + home = user_record_new(); if (!home) return log_oom(); @@ -1944,11 +1997,11 @@ static int run(int argc, char *argv[]) { else if (streq(argv[1], "deactivate-force")) r = home_deactivate(home, true); else if (streq(argv[1], "create")) - r = home_create(home, &new_home); + r = home_create(home, blobs, &new_home); else if (streq(argv[1], "remove")) r = home_remove(home); else if (streq(argv[1], "update")) - r = home_update(home, &new_home); + r = home_update(home, blobs, &new_home); else if (streq(argv[1], "resize")) /* Resize on user request */ r = home_resize(home, false, &new_home); else if (streq(argv[1], "resize-auto")) /* Automatic resize */ diff --git a/src/home/org.freedesktop.home1.conf b/src/home/org.freedesktop.home1.conf index d2c4b9dd290..9ac34aea55f 100644 --- a/src/home/org.freedesktop.home1.conf +++ b/src/home/org.freedesktop.home1.conf @@ -77,6 +77,10 @@ send_interface="org.freedesktop.home1.Manager" send_member="CreateHome"/> + + @@ -97,6 +101,10 @@ send_interface="org.freedesktop.home1.Manager" send_member="UpdateHome"/> + + @@ -183,6 +191,10 @@ send_interface="org.freedesktop.home1.Home" send_member="Update"/> + + diff --git a/src/home/user-record-util.c b/src/home/user-record-util.c index b18a77d7407..3ae0883cb06 100644 --- a/src/home/user-record-util.c +++ b/src/home/user-record-util.c @@ -3,6 +3,7 @@ #include #include "errno-util.h" +#include "fd-util.h" #include "home-util.h" #include "id128-util.h" #include "libcrypt-util.h" @@ -10,6 +11,7 @@ #include "recovery-key.h" #include "mountpoint-util.h" #include "path-util.h" +#include "sha256.h" #include "stat-util.h" #include "user-record-util.h" #include "user-util.h" @@ -1396,6 +1398,9 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error) { return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Cannot manage custom blob directories."); } + if (json_variant_by_key(hr->json, HOMEWORK_BLOB_FDMAP_FIELD)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "User record contains unsafe internal fields."); + return 0; } @@ -1524,3 +1529,103 @@ int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight) { h->mask |= USER_RECORD_PER_MACHINE; return 0; } + +int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_hashmap_free_ Hashmap *manifest = NULL; + const char *filename; + void *key, *value; + uint64_t total_size = 0; + int r; + + assert(h); + assert(h->json); + assert(blobs); + assert(ret_failed); + + /* Ensures that blobManifest exists (possibly creating it using the + * contents of blobs), and that the set of keys in both hashmaps are + * exactly the same. If it fails to handle one blob file, the filename + * is put it ret_failed for nicer error reporting. ret_failed is a pointer + * to the same memory blobs uses to store its keys, so it is valid for + * as long as blobs is valid and the corresponding key isn't removed! */ + + if (h->blob_manifest) { + /* blobManifest already exists. In this case we verify + * that the sets of keys are equal and that's it */ + + HASHMAP_FOREACH_KEY(value, key, h->blob_manifest) + if (!hashmap_contains(blobs, key)) + return -EINVAL; + HASHMAP_FOREACH_KEY(value, key, blobs) + if (!hashmap_contains(h->blob_manifest, key)) + return -EINVAL; + + return 0; + } + + /* blobManifest doesn't exist, so we need to create it */ + + HASHMAP_FOREACH_KEY(value, filename, blobs) { + _cleanup_free_ char *filename_dup = NULL; + _cleanup_free_ uint8_t *hash = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *hash_json = NULL; + int fd = PTR_TO_FD(value); + off_t initial, size; + + *ret_failed = filename; + + filename_dup = strdup(filename); + if (!filename_dup) + return -ENOMEM; + + hash = malloc(SHA256_DIGEST_SIZE); + if (!hash) + return -ENOMEM; + + initial = lseek(fd, 0, SEEK_CUR); + if (initial < 0) + return -errno; + + r = sha256_fd(fd, BLOB_DIR_MAX_SIZE, hash); + if (r < 0) + return r; + + size = lseek(fd, 0, SEEK_CUR); + if (size < 0) + return -errno; + if (!DEC_SAFE(&size, initial)) + return -EOVERFLOW; + + if (!INC_SAFE(&total_size, size)) + total_size = UINT64_MAX; + if (total_size > BLOB_DIR_MAX_SIZE) + return -EFBIG; + + if (lseek(fd, initial, SEEK_SET) < 0) + return -errno; + + r = json_variant_new_hex(&hash_json, hash, SHA256_DIGEST_SIZE); + if (r < 0) + return r; + + r = hashmap_ensure_put(&manifest, &path_hash_ops_free_free, filename_dup, hash); + if (r < 0) + return r; + TAKE_PTR(filename_dup); /* Ownership transfers to hashmap */ + TAKE_PTR(hash); + + r = json_variant_set_field(&v, filename, hash_json); + if (r < 0) + return r; + + *ret_failed = NULL; + } + + r = json_variant_set_field_non_null(&h->json, "blobManifest", v); + if (r < 0) + return r; + + h->blob_manifest = TAKE_PTR(manifest); + return 0; +} diff --git a/src/home/user-record-util.h b/src/home/user-record-util.h index 508e2bdb8d9..1295a8e09a9 100644 --- a/src/home/user-record-util.h +++ b/src/home/user-record-util.h @@ -6,6 +6,11 @@ #include "user-record.h" #include "group-record.h" +/* We intentionally use snake_case instead of the usual camelCase here to further + * reduce the chance of collision with a field any legitimate user record may ever + * want to set. */ +#define HOMEWORK_BLOB_FDMAP_FIELD "__systemd_homework_internal_blob_fdmap" + int user_record_synthesize(UserRecord *h, const char *user_name, const char *realm, const char *image_path, UserStorage storage, uid_t uid, gid_t gid); int group_record_synthesize(GroupRecord *g, UserRecord *u); @@ -63,3 +68,5 @@ int user_record_is_supported(UserRecord *hr, sd_bus_error *error); bool user_record_shall_rebalance(UserRecord *h); int user_record_set_rebalance_weight(UserRecord *h, uint64_t weight); + +int user_record_ensure_blob_manifest(UserRecord *h, Hashmap *blobs, const char **ret_failed);