homework: Handle Update & Create w/ blob dir

Introduces new extended variants of the various incarnations of
Create and Update, which take a map of filenames to FDs. This map is
then used to populate the bulk directory.

FDs are used to prevent the client from abusing homed's blob directory
permissions (everything is made world-readable by homed) to open files
that they normally aren't allowed to open. Passing along an FD ensures
that the client has read access to the file it wants homed to make
world-readable.

Internally, homework uses the map to overwrite the system blob dir.
Later, homework's existing blob dir reconciliation logic will propagate
the new contents from the system blob dir into the embedded blob
dir
This commit is contained in:
Adrian Vovk 2024-01-09 22:06:35 -05:00 committed by Luca Boccassi
parent 17ac40e4cd
commit a4d72746c7
16 changed files with 534 additions and 49 deletions

View file

@ -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 {
<variablelist class="dbus-method" generated="True" extra-ref="CreateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="CreateHomeEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RealizeHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="RemoveHome()"/>
@ -161,6 +169,8 @@ node /org/freedesktop/home1 {
<variablelist class="dbus-method" generated="True" extra-ref="UpdateHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UpdateHomeEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ResizeHome()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ChangePasswordHome()"/>
@ -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.</para>
<para><function>CreateHomeEx()</function> is like <function>CreateHome()</function>, but it allows the
home directory to be created with a pre-populated blob directory (see
<ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob Directories</ulink> for more info).
This can be done via the dictionary passed as the <varname>blobs</varname> 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 <literal>O_PATH</literal>; 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 <literal>blobManifest</literal>
field it will be enforced; otherwise, a <literal>blobManifest</literal> field will be generated and inserted
into the record. The <varname>flags</varname> argument may be used for future expansion, but for now
pass 0.</para>
<para><function>RealizeHome()</function> creates a home directory whose user record is already
registered locally. This takes a user name plus a user record consisting only of the
<literal>secret</literal> section. Invoking <function>RegisterHome()</function> followed by
@ -308,6 +331,14 @@ node /org/freedesktop/home1 {
as the storage resized using <function>ResizeHome()</function>. This method is equivalent to
<function>Update()</function> on the <classname>org.freedesktop.home1.Home</classname> interface.</para>
<para><function>UpdateHomeEx()</function> is like <function>UpdateHome()</function>, but it allows for
changes to the blob directory (see <ulink url="https://systemd.io/USER_RECORD_BLOB_DIRS">User Record Blob
Directories</ulink> for more info). The <varname>blobs</varname> argument works in the same way as
<function>CreateHomeEx()</function>, so check there for details. The new blob directory contents passed into
this method will completely replace the user's existing blob directory. The <varname>flags</varname> argument
may be used for future expansion, but for now pass 0. This method is equivalent to <function>UpdateEx()</function>
on the <classname>org.freedesktop.home1.Home</classname> interface.</para>
<para><function>ResizeHome()</function> 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 <literal>secret</literal> section
as argument. If the size is specified as <constant>UINT64_MAX</constant> 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 {
<variablelist class="dbus-method" generated="True" extra-ref="Update()"/>
<variablelist class="dbus-method" generated="True" extra-ref="UpdateEx()"/>
<variablelist class="dbus-method" generated="True" extra-ref="Resize()"/>
<variablelist class="dbus-method" generated="True" extra-ref="ChangePassword()"/>
@ -540,11 +576,11 @@ node /org/freedesktop/home1/home {
<para><function>Activate()</function>, <function>ActivateIfReferenced()</function>,
<function>Deactivate()</function>, <function>Unregister()</function>, <function>Realize()</function>,
<function>Remove()</function>, <function>Fixate()</function>, <function>Authenticate()</function>,
<function>Update()</function>, <function>Resize()</function>, <function>ChangePassword()</function>,
<function>Lock()</function>, <function>Unlock()</function>, <function>Acquire()</function>,
<function>Ref()</function>, <function>RefUnrestricted()</function>, <function>Release()</function>,
<function>InhibitSuspend()</function> operate like their matching counterparts on the
<classname>org.freedesktop.home1.Manager</classname> interface (see above). The main difference is that
<function>Update()</function>, <function>UpdateEx()</function>, <function>Resize()</function>,
<function>ChangePassword()</function>, <function>Lock()</function>, <function>Unlock()</function>,
<function>Acquire()</function>, <function>Ref()</function>, <function>RefUnrestricted()</function>,
<function>Release()</function>, <function>InhibitSuspend()</function> operate like their matching counterparts
on the <classname>org.freedesktop.home1.Manager</classname> 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 {
<title>History</title>
<refsect2>
<title>The Manager Object</title>
<para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function> wer added in version 256.</para>
<para><function>InhibitSuspendHome()</function>, <function>ActivateHomeIfReferenced()</function>, <function>RefHomeUnrestricted()</function>,
<function>CreateHomeEx()</function>, and <function>UpdateHomeEx()</function> were added in version 256.</para>
</refsect2>
<refsect2>
<title>Home Objects</title>
<para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function> and <function>RefUnrestricted()</function> were added in version 256.</para>
<para><function>InhibitSuspend()</function>, <function>ActivateIfReferenced()</function>, <function>RefUnrestricted()</function>, and
<function>UpdateEx()</function> were added in version 256.</para>
</refsect2>
</refsect1>

View file

@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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),

View file

@ -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;
}

View file

@ -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);

View file

@ -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 */

View file

@ -77,6 +77,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="CreateHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="CreateHomeEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="RealizeHome"/>
@ -97,6 +101,10 @@
send_interface="org.freedesktop.home1.Manager"
send_member="UpdateHome"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="UpdateHomeEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Manager"
send_member="ResizeHome"/>
@ -183,6 +191,10 @@
send_interface="org.freedesktop.home1.Home"
send_member="Update"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="UpdateEx"/>
<allow send_destination="org.freedesktop.home1"
send_interface="org.freedesktop.home1.Home"
send_member="Resize"/>

View file

@ -3,6 +3,7 @@
#include <sys/xattr.h>
#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;
}

View file

@ -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);