diff --git a/src/home/home-util.h b/src/home/home-util.h index fba1c7d8f1d..f7bf637dd2e 100644 --- a/src/home/home-util.h +++ b/src/home/home-util.h @@ -8,6 +8,10 @@ #include "time-util.h" #include "user-record.h" +/* See https://systemd.io/UIDS-GIDS for details how this range fits into the rest of the world */ +#define HOME_UID_MIN 60001 +#define HOME_UID_MAX 60513 + 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-manager.h b/src/home/homed-manager.h index 851b302f593..f5659636c38 100644 --- a/src/home/homed-manager.h +++ b/src/home/homed-manager.h @@ -13,9 +13,6 @@ typedef struct Manager Manager; #include "homed-home.h" #include "varlink.h" -#define HOME_UID_MIN 60001 -#define HOME_UID_MAX 60513 - struct Manager { sd_event *event; sd_bus *bus; diff --git a/src/home/homework-directory.c b/src/home/homework-directory.c index 8f2f512b1bb..65575713617 100644 --- a/src/home/homework-directory.c +++ b/src/home/homework-directory.c @@ -5,6 +5,7 @@ #include "btrfs-util.h" #include "fd-util.h" #include "homework-directory.h" +#include "homework-mount.h" #include "homework-quota.h" #include "mkdir.h" #include "mount-util.h" @@ -12,12 +13,43 @@ #include "rm-rf.h" #include "tmpfile-util.h" #include "umask-util.h" +#include "user-util.h" int home_setup_directory(UserRecord *h, HomeSetup *setup) { - assert(h); - assert(setup); + const char *ip; + int r; - setup->root_fd = open(user_record_image_path(h), O_RDONLY|O_CLOEXEC|O_DIRECTORY); + assert(h); + assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME)); + assert(setup); + assert(!setup->undo_mount); + assert(setup->root_fd < 0); + + /* We'll bind mount the image directory to a new mount point where we'll start adjusting it. Only + * once that's complete we'll move the thing to its final place eventually. */ + r = home_unshare_and_mkdir(); + if (r < 0) + return r; + + assert_se(ip = user_record_image_path(h)); + + r = mount_follow_verbose(LOG_ERR, ip, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND, NULL); + if (r < 0) + return r; + + setup->undo_mount = true; + + /* Turn off any form of propagation for this */ + r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_PRIVATE, NULL); + if (r < 0) + return r; + + /* Adjust MS_SUID and similar flags */ + r = mount_nofollow_verbose(LOG_ERR, NULL, HOME_RUNTIME_WORK_DIR, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); + if (r < 0) + return r; + + setup->root_fd = open(HOME_RUNTIME_WORK_DIR, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); if (setup->root_fd < 0) return log_error_errno(errno, "Failed to open home directory: %m"); @@ -31,7 +63,7 @@ int home_activate_directory( UserRecord **ret_home) { _cleanup_(user_record_unrefp) UserRecord *new_home = NULL, *header_home = NULL; - const char *hdo, *hd, *ipo, *ip; + const char *hd, *hdo; int r; assert(h); @@ -39,9 +71,6 @@ int home_activate_directory( assert(setup); assert(ret_home); - assert_se(ipo = user_record_image_path(h)); - ip = strdupa_safe(ipo); /* copy out, since reconciliation might cause changing of the field */ - assert_se(hdo = user_record_home_directory(h)); hd = strdupa_safe(hdo); @@ -53,24 +82,19 @@ int home_activate_directory( if (r < 0) return r; - setup->root_fd = safe_close(setup->root_fd); - - /* Create mount point to mount over if necessary */ - if (!path_equal(ip, hd)) - (void) mkdir_p(hd, 0700); - - /* Create a mount point (even if the directory is already placed correctly), as a way to indicate - * this mount point is now "activated". Moreover, we want to set per-user - * MS_NOSUID/MS_NOEXEC/MS_NODEV. */ - r = mount_nofollow_verbose(LOG_ERR, ip, hd, NULL, MS_BIND, NULL); + r = home_extend_embedded_identity(new_home, h, setup); if (r < 0) return r; - r = mount_nofollow_verbose(LOG_ERR, NULL, hd, NULL, MS_BIND|MS_REMOUNT|user_record_mount_flags(h), NULL); - if (r < 0) { - (void) umount_verbose(LOG_ERR, hd, UMOUNT_NOFOLLOW); + /* Close fd to private mount before moving mount */ + setup->root_fd = safe_close(setup->root_fd); + + /* We are now done with everything, move the mount into place */ + r = home_move_mount(NULL, hd); + if (r < 0) return r; - } + + setup->undo_mount = false; setup->do_drop_caches = false; @@ -80,16 +104,18 @@ int home_activate_directory( return 0; } -int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { +int home_create_directory_or_subvolume(UserRecord *h, HomeSetup *setup, UserRecord **ret_home) { _cleanup_(rm_rf_subvolume_and_freep) char *temporary = NULL; _cleanup_(user_record_unrefp) UserRecord *new_home = NULL; - _cleanup_close_ int root_fd = -1; + _cleanup_close_ int mount_fd = -1; _cleanup_free_ char *d = NULL; + bool is_subvolume = false; const char *ip; int r; assert(h); assert(IN_SET(user_record_storage(h), USER_DIRECTORY, USER_SUBVOLUME)); + assert(setup); assert(ret_home); assert_se(ip = user_record_image_path(h)); @@ -108,6 +134,7 @@ int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { if (r >= 0) { log_info("Subvolume created."); + is_subvolume = true; if (h->disk_size != UINT64_MAX) { @@ -149,15 +176,44 @@ int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { temporary = TAKE_PTR(d); /* Needs to be destroyed now */ - root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); - if (root_fd < 0) - return log_error_errno(errno, "Failed to open temporary home directory: %m"); - - r = home_populate(h, root_fd); + /* Let's decouple namespaces now, so that we can possibly mount a UID map mount into + * /run/systemd/user-home-mount/ that noone will see but us. */ + r = home_unshare_and_mkdir(); if (r < 0) return r; - r = home_sync_and_statfs(root_fd, NULL); + setup->root_fd = open(temporary, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); + if (setup->root_fd < 0) + return log_error_errno(errno, "Failed to open temporary home directory: %m"); + + /* Try to apply a UID shift, so that the directory is actually owned by "nobody", and is only mapped + * to the proper UID while active. — Well, that's at least the theory. Unfortunately, only btrfs does + * per-subvolume quota. The others do per-uid quota. Which means mapping all home directories to the + * same UID of "nobody" makes quota impossible. Hence unless we actually managed to create a btrfs + * subvolume for this user we'll map the user's UID to itself. Now you might ask: why bother mapping + * at all? It's because we want to restrict the UIDs used on the home directory: we leave all other + * UIDs of the homed UID range unmapped, thus making them unavailable to programs accessing the + * mount. */ + r = home_shift_uid(setup->root_fd, HOME_RUNTIME_WORK_DIR, is_subvolume ? UID_NOBODY : h->uid, h->uid, &mount_fd); + if (r > 0) + setup->undo_mount = true; /* If uidmaps worked we have a mount to undo again */ + + if (mount_fd >= 0) { + /* If we have established a new mount, then we can use that as new root fd to our home directory. */ + safe_close(setup->root_fd); + + setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(setup->root_fd, "Unable to convert mount fd into proper directory fd: %m"); + + mount_fd = safe_close(mount_fd); + } + + r = home_populate(h, setup->root_fd); + if (r < 0) + return r; + + r = home_sync_and_statfs(setup->root_fd, NULL); if (r < 0) return r; @@ -182,6 +238,17 @@ int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home) { if (r < 0) return log_error_errno(r, "Failed to add binding to record: %m"); + setup->root_fd = safe_close(setup->root_fd); + + /* Unmount mapped mount before we move the dir into place */ + if (setup->undo_mount) { + r = umount_verbose(LOG_ERR, HOME_RUNTIME_WORK_DIR, UMOUNT_NOFOLLOW); + if (r < 0) + return r; + + setup->undo_mount = false; + } + if (rename(temporary, ip) < 0) return log_error_errno(errno, "Failed to rename %s to %s: %m", temporary, ip); @@ -216,6 +283,10 @@ int home_resize_directory( if (r < 0) return r; + r = home_maybe_shift_uid(h, setup); + if (r < 0) + return r; + r = home_update_quota_auto(h, NULL); if (ERRNO_IS_NOT_SUPPORTED(r)) return -ESOCKTNOSUPPORT; /* make recognizable */ diff --git a/src/home/homework-directory.h b/src/home/homework-directory.h index 98b18047748..92cc755546c 100644 --- a/src/home/homework-directory.h +++ b/src/home/homework-directory.h @@ -6,5 +6,5 @@ int home_setup_directory(UserRecord *h, HomeSetup *setup); int home_activate_directory(UserRecord *h, HomeSetup *setup, PasswordCache *cache, UserRecord **ret_home); -int home_create_directory_or_subvolume(UserRecord *h, UserRecord **ret_home); +int home_create_directory_or_subvolume(UserRecord *h, HomeSetup *setup, UserRecord **ret_home); int home_resize_directory(UserRecord *h, HomeSetupFlags flags, PasswordCache *cache, HomeSetup *setup, UserRecord **ret_home); diff --git a/src/home/homework-mount.c b/src/home/homework-mount.c index 47345eb8391..7ab503fb7ac 100644 --- a/src/home/homework-mount.c +++ b/src/home/homework-mount.c @@ -2,14 +2,22 @@ #include #include +#include #include "alloc-util.h" +#include "fd-util.h" +#include "format-util.h" +#include "home-util.h" #include "homework-mount.h" #include "homework.h" +#include "missing_mount.h" +#include "missing_syscall.h" #include "mkdir.h" #include "mount-util.h" +#include "namespace-util.h" #include "path-util.h" #include "string-util.h" +#include "user-util.h" static const char *mount_options_for_fstype(const char *fstype) { if (streq(fstype, "ext4")) @@ -110,3 +118,139 @@ int home_move_mount(const char *user_name_and_realm, const char *target) { log_info("Moving to final mount point %s completed.", target); return 0; } + +static int append_identity_range(char **text, uid_t start, uid_t next_start, uid_t exclude) { + /* Creates an identity range ranging from 'start' to 'next_start-1'. Excludes the UID specified by 'exclude' if + * it is in that range. */ + + assert(text); + + if (next_start <= start) /* Empty range? */ + return 0; + + if (exclude < start || exclude >= next_start) /* UID to exclude it outside of the range? */ + return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start, start, next_start - start); + + if (start == exclude && next_start == exclude + 1) /* The only UID in the range is the one to exclude? */ + return 0; + + if (exclude == start) /* UID to exclude at beginning of range? */ + return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start+1, start+1, next_start - start - 1); + + if (exclude == next_start - 1) /* UID to exclude at end of range? */ + return strextendf(text, UID_FMT " " UID_FMT " " UID_FMT "\n", start, start, next_start - start - 1); + + return strextendf(text, + UID_FMT " " UID_FMT " " UID_FMT "\n" + UID_FMT " " UID_FMT " " UID_FMT "\n", + start, start, exclude - start, + exclude + 1, exclude + 1, next_start - exclude - 1); +} + +static int make_userns(uid_t stored_uid, uid_t exposed_uid) { + _cleanup_free_ char *text = NULL; + _cleanup_close_ int userns_fd = -1; + int r; + + assert(uid_is_valid(stored_uid)); + assert(uid_is_valid(exposed_uid)); + + assert_cc(HOME_UID_MIN <= HOME_UID_MAX); + assert_cc(HOME_UID_MAX < UID_NOBODY); + + /* Map everything below the homed UID range to itself (except for the UID we actually care about if + * it is inside this range) */ + r = append_identity_range(&text, 0, HOME_UID_MIN, stored_uid); + if (r < 0) + return log_oom(); + + /* Now map the UID we are doing this for to the target UID. */ + r = strextendf(&text, UID_FMT " " UID_FMT " " UID_FMT "\n", stored_uid, exposed_uid, 1); + if (r < 0) + return log_oom(); + + /* Map everything above the homed UID range to itself (again, excluding the UID we actually care + * about if it is in that range). Also we leave "nobody" itself excluded) */ + r = append_identity_range(&text, HOME_UID_MAX, UID_NOBODY, stored_uid); + if (r < 0) + return log_oom(); + + /* Leave everything else unmapped, starting from UID_NOBODY itself. Specifically, this means the + * whole space outside of 16bit remains unmapped */ + + log_debug("Creating userns with mapping:\n%s", text); + + userns_fd = userns_acquire(text, text); /* same uid + gid mapping */ + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to allocate user namespace: %m"); + + return TAKE_FD(userns_fd); +} + +int home_shift_uid(int dir_fd, const char *target, uid_t stored_uid, uid_t exposed_uid, int *ret_mount_fd) { + _cleanup_close_ int mount_fd = -1, userns_fd = -1; + int r; + + assert(dir_fd >= 0); + assert(uid_is_valid(stored_uid)); + assert(uid_is_valid(exposed_uid)); + + /* Let's try to set up a UID mapping for this directory. This is called when first creating a home + * directory or when activating it again. We do this as optimization only, to avoid having to + * recursively chown() things on each activation. If the kernel or file system doesn't support this + * scheme we'll handle this gracefully, and not do anything, so that the later recursive chown()ing + * then fixes up things for us. Note that the chown()ing is smart enough to skip things if they look + * alright already. + * + * Note that this always creates a new mount (i.e. we use OPEN_TREE_CLONE), since applying idmaps is + * not allowed once the mount is put in place. */ + + mount_fd = open_tree(dir_fd, "", AT_EMPTY_PATH | OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (mount_fd < 0) { + if (ERRNO_IS_NOT_SUPPORTED(errno)) { + log_debug_errno(errno, "The open_tree() syscall is not supported, not setting up UID shift mount: %m"); + + if (ret_mount_fd) + *ret_mount_fd = -1; + + return 0; + } + + return log_error_errno(errno, "Failed to open tree of home directory: %m"); + } + + userns_fd = make_userns(stored_uid, exposed_uid); + if (userns_fd < 0) + return userns_fd; + + /* Set the user namespace mapping attribute on the cloned mount point */ + if (mount_setattr(mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = MOUNT_ATTR_IDMAP, + .userns_fd = userns_fd, + }, MOUNT_ATTR_SIZE_VER0) < 0) { + + if (ERRNO_IS_NOT_SUPPORTED(errno) || errno == EINVAL) { /* EINVAL is documented in mount_attr() as fs doesn't support idmapping */ + log_debug_errno(errno, "UID/GID mapping for shifted mount not available, not setting it up: %m"); + + if (ret_mount_fd) + *ret_mount_fd = -1; + + return 0; + } + + return log_error_errno(errno, "Failed to apply UID/GID mapping: %m"); + } + + if (target) + r = move_mount(mount_fd, "", AT_FDCWD, target, MOVE_MOUNT_F_EMPTY_PATH); + else + r = move_mount(mount_fd, "", dir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH); + if (r < 0) + return log_error_errno(errno, "Failed to apply UID/GID map: %m"); + + if (ret_mount_fd) + *ret_mount_fd = TAKE_FD(mount_fd); + + return 1; +} diff --git a/src/home/homework-mount.h b/src/home/homework-mount.h index 893ecdc5869..c9190ae7228 100644 --- a/src/home/homework-mount.h +++ b/src/home/homework-mount.h @@ -7,3 +7,4 @@ int home_mount_node(const char *node, const char *fstype, bool discard, unsigned int home_unshare_and_mkdir(void); int home_unshare_and_mount(const char *node, const char *fstype, bool discard, unsigned long flags); int home_move_mount(const char *user_name_and_realm, const char *target); +int home_shift_uid(int dir_fd, const char *target, uid_t stored_uid, uid_t exposed_uid, int *ret_mount_fd); diff --git a/src/home/homework.c b/src/home/homework.c index f0ef2def7c5..cfc0c945def 100644 --- a/src/home/homework.c +++ b/src/home/homework.c @@ -716,6 +716,38 @@ static int chown_recursive_directory(int root_fd, uid_t uid) { return 0; } +int home_maybe_shift_uid( + UserRecord *h, + HomeSetup *setup) { + + _cleanup_close_ int mount_fd = -1; + struct stat st; + + assert(h); + assert(setup); + assert(setup->root_fd >= 0); + + if (fstat(setup->root_fd, &st) < 0) + return log_error_errno(errno, "Failed to stat() home directory: %m"); + + /* Let's shift UIDs of this mount. Hopefully this makes the later chowning unnecessary. (Note that we + * also prefer to do UID mapping even if the UID already matches our goal UID. That's because we want + * to leave UIDs in the homed managed range unmapped.) */ + (void) home_shift_uid(setup->root_fd, NULL, st.st_uid, h->uid, &mount_fd); + + /* If this worked, then we'll have a reference to the mount now, which we can also use like an O_PATH + * fd to the new dir. Let's convert it into a proper O_DIRECTORY fd. */ + if (mount_fd >= 0) { + safe_close(setup->root_fd); + + setup->root_fd = fd_reopen(mount_fd, O_RDONLY|O_CLOEXEC|O_DIRECTORY); + if (setup->root_fd < 0) + return log_error_errno(setup->root_fd, "Failed to convert mount fd into regular directory fd: %m"); + } + + return 0; +} + int home_refresh( UserRecord *h, HomeSetup *setup, @@ -738,6 +770,10 @@ int home_refresh( if (r < 0) return r; + r = home_maybe_shift_uid(h, setup); + if (r < 0) + return r; + r = home_store_header_identity_luks(new_home, setup, header_home); if (r < 0) return r; @@ -1232,7 +1268,7 @@ static int home_create(UserRecord *h, UserRecord **ret_home) { case USER_DIRECTORY: case USER_SUBVOLUME: - r = home_create_directory_or_subvolume(h, &new_home); + r = home_create_directory_or_subvolume(h, &setup, &new_home); break; case USER_FSCRYPT: diff --git a/src/home/homework.h b/src/home/homework.h index 7bd31b5cead..1fa5a1e37a5 100644 --- a/src/home/homework.h +++ b/src/home/homework.h @@ -74,6 +74,7 @@ int home_setup(UserRecord *h, HomeSetupFlags flags, PasswordCache *cache, HomeSe int home_refresh(UserRecord *h, HomeSetup *setup, UserRecord *header_home, PasswordCache *cache, struct statfs *ret_statfs, UserRecord **ret_new_home); +int home_maybe_shift_uid(UserRecord *h, HomeSetup *setup); int home_populate(UserRecord *h, int dir_fd); int home_load_embedded_identity(UserRecord *h, int root_fd, UserRecord *header_home, UserReconcileMode mode, PasswordCache *cache, UserRecord **ret_embedded_home, UserRecord **ret_new_home);