diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index dc4a69f729f..f39f1a8ca66 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -1491,12 +1491,12 @@ After=sys-subsystem-net-devices-ens1.device Mount options are comma-separated. and control whether to create a recursive or a regular bind mount. Defaults to . , - , and control ID mapping. + , and control ID mapping. - Using or requires support by the source filesystem - for user/group ID mapped mounts. Defaults to . With being the container's UID range - offset, being the length of the container's UID range, and being the - owner UID of the bind mount source inode on the host: + Using , or requires support + by the source filesystem for user/group ID mapped mounts. Defaults to . With + being the container's UID range offset, being the length of the + container's UID range, and being the owner UID of the bind mount source inode on the host: If is used, any user in the range @@ -1512,10 +1512,15 @@ After=sys-subsystem-net-devices-ens1.device If is used, the user seen from inside of the container is mapped to on the host. Other host users are mapped to inside the container. + + If is used, the owner of the target directory inside of the + container is mapped to on the host. Other host users are mapped to + inside the container. Whichever ID mapping option is used, the same mapping will be used for users and groups IDs. If - is used, the group owning the bind mounted directory will have no effect. + or are used, the group owning the bind mounted directory + will have no effect. Note that when this option is used in combination with , the resulting mount points will be owned by the nobody user. That's because the mount and its files and diff --git a/src/nspawn/nspawn-mount.c b/src/nspawn/nspawn-mount.c index 5f41c1c91fe..71efeb8572f 100644 --- a/src/nspawn/nspawn-mount.c +++ b/src/nspawn/nspawn-mount.c @@ -742,6 +742,8 @@ static int parse_mount_bind_options(const char *options, unsigned long *mount_fl new_idmapping = REMOUNT_IDMAPPING_NONE; else if (streq(word, "rootidmap")) new_idmapping = REMOUNT_IDMAPPING_HOST_OWNER; + else if (streq(word, "owneridmap")) + new_idmapping = REMOUNT_IDMAPPING_HOST_OWNER_TO_TARGET_OWNER; else return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid bind mount option: %s", word); @@ -759,6 +761,7 @@ static int mount_bind(const char *dest, CustomMount *m, uid_t uid_shift, uid_t u _cleanup_free_ char *mount_opts = NULL, *where = NULL; unsigned long mount_flags = MS_BIND | MS_REC; struct stat source_st, dest_st; + uid_t dest_uid = UID_INVALID; int r; RemountIdmapping idmapping = REMOUNT_IDMAPPING_NONE; @@ -787,6 +790,8 @@ static int mount_bind(const char *dest, CustomMount *m, uid_t uid_shift, uid_t u if (stat(where, &dest_st) < 0) return log_error_errno(errno, "Failed to stat %s: %m", where); + dest_uid = dest_st.st_uid; + if (S_ISDIR(source_st.st_mode) && !S_ISDIR(dest_st.st_mode)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot bind mount directory %s on file %s.", @@ -815,6 +820,8 @@ static int mount_bind(const char *dest, CustomMount *m, uid_t uid_shift, uid_t u if (chown(where, uid_shift, uid_shift) < 0) return log_error_errno(errno, "Failed to chown %s: %m", where); + + dest_uid = uid_shift; } r = mount_nofollow_verbose(LOG_ERR, m->source, where, NULL, mount_flags, mount_opts); @@ -828,7 +835,7 @@ static int mount_bind(const char *dest, CustomMount *m, uid_t uid_shift, uid_t u } if (idmapping != REMOUNT_IDMAPPING_NONE) { - r = remount_idmap(STRV_MAKE(where), uid_shift, uid_range, source_st.st_uid, idmapping); + r = remount_idmap(STRV_MAKE(where), uid_shift, uid_range, source_st.st_uid, dest_uid, idmapping); if (r < 0) return log_error_errno(r, "Failed to map ids for bind mount %s: %m", where); } diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 664637685bb..49b57c51683 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -3931,7 +3931,7 @@ static int outer_child( dirs[i] = NULL; - r = remount_idmap(dirs, arg_uid_shift, arg_uid_range, UID_INVALID, REMOUNT_IDMAPPING_HOST_ROOT); + r = remount_idmap(dirs, arg_uid_shift, arg_uid_range, UID_INVALID, UID_INVALID, REMOUNT_IDMAPPING_HOST_ROOT); if (r == -EINVAL || ERRNO_IS_NEG_NOT_SUPPORTED(r)) { /* This might fail because the kernel or file system doesn't support idmapping. We * can't really distinguish this nicely, nor do we have any guarantees about the diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index e699aa43e97..d7c705184ab 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2144,7 +2144,7 @@ int dissected_image_mount( if (userns_fd < 0 && need_user_mapping(uid_shift, uid_range) && FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_IDMAPPED)) { - my_userns_fd = make_userns(uid_shift, uid_range, UID_INVALID, REMOUNT_IDMAPPING_HOST_ROOT); + my_userns_fd = make_userns(uid_shift, uid_range, UID_INVALID, UID_INVALID, REMOUNT_IDMAPPING_HOST_ROOT); if (my_userns_fd < 0) return my_userns_fd; diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index 9bae21f88e5..bff1bf9f49c 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -1314,7 +1314,7 @@ int fd_make_mount_point(int fd) { return 1; } -int make_userns(uid_t uid_shift, uid_t uid_range, uid_t owner, RemountIdmapping idmapping) { +int make_userns(uid_t uid_shift, uid_t uid_range, uid_t source_owner, uid_t dest_owner, RemountIdmapping idmapping) { _cleanup_close_ int userns_fd = -EBADF; _cleanup_free_ char *line = NULL; @@ -1349,8 +1349,20 @@ int make_userns(uid_t uid_shift, uid_t uid_range, uid_t owner, RemountIdmapping if (idmapping == REMOUNT_IDMAPPING_HOST_OWNER) { /* Remap the owner of the bind mounted directory to the root user within the container. This * way every file written by root within the container to the bind-mounted directory will - * be owned by the original user. All other user will remain unmapped. */ - if (asprintf(&line, UID_FMT " " UID_FMT " " UID_FMT "\n", owner, uid_shift, 1u) < 0) + * be owned by the original user from the host. All other users will remain unmapped. */ + if (asprintf(&line, UID_FMT " " UID_FMT " " UID_FMT "\n", source_owner, uid_shift, 1u) < 0) + return log_oom_debug(); + } + + if (idmapping == REMOUNT_IDMAPPING_HOST_OWNER_TO_TARGET_OWNER) { + /* Remap the owner of the bind mounted directory to the owner of the target directory + * within the container. This way every file written by target directory owner within the + * container to the bind-mounted directory will be owned by the original host user. + * All other users will remain unmapped. */ + if (asprintf( + &line, + UID_FMT " " UID_FMT " " UID_FMT "\n", + source_owner, dest_owner, 1u) < 0) return log_oom_debug(); } @@ -1424,10 +1436,10 @@ int remount_idmap_fd( return 0; } -int remount_idmap(char **p, uid_t uid_shift, uid_t uid_range, uid_t owner, RemountIdmapping idmapping) { +int remount_idmap(char **p, uid_t uid_shift, uid_t uid_range, uid_t source_owner, uid_t dest_owner,RemountIdmapping idmapping) { _cleanup_close_ int userns_fd = -EBADF; - userns_fd = make_userns(uid_shift, uid_range, owner, idmapping); + userns_fd = make_userns(uid_shift, uid_range, source_owner, dest_owner, idmapping); if (userns_fd < 0) return userns_fd; diff --git a/src/shared/mount-util.h b/src/shared/mount-util.h index ef311049008..2f9f394ab0e 100644 --- a/src/shared/mount-util.h +++ b/src/shared/mount-util.h @@ -116,16 +116,19 @@ typedef enum RemountIdmapping { * certain security implications defaults to off, and requires explicit opt-in. */ REMOUNT_IDMAPPING_HOST_ROOT, /* Define a mapping from root user within the container to the owner of the bind mounted directory. - * This ensure no root-owned files will be written in a bind-mounted directory owned by a different + * This ensures no root-owned files will be written in a bind-mounted directory owned by a different * user. No other users are mapped. */ REMOUNT_IDMAPPING_HOST_OWNER, + /* Define a mapping from bind-target owner within the container to the host owner of the bind mounted + * directory. No other users are mapped. */ + REMOUNT_IDMAPPING_HOST_OWNER_TO_TARGET_OWNER, _REMOUNT_IDMAPPING_MAX, _REMOUNT_IDMAPPING_INVALID = -EINVAL, } RemountIdmapping; -int make_userns(uid_t uid_shift, uid_t uid_range, uid_t owner, RemountIdmapping idmapping); +int make_userns(uid_t uid_shift, uid_t uid_range, uid_t host_owner, uid_t dest_owner, RemountIdmapping idmapping); int remount_idmap_fd(char **p, int userns_fd); -int remount_idmap(char **p, uid_t uid_shift, uid_t uid_range, uid_t owner, RemountIdmapping idmapping); +int remount_idmap(char **p, uid_t uid_shift, uid_t uid_range, uid_t host_owner, uid_t dest_owner, RemountIdmapping idmapping); int bind_mount_submounts( const char *source, diff --git a/test/units/testsuite-13.nspawn.sh b/test/units/testsuite-13.nspawn.sh index b35c6c1e057..2e7a71f250b 100755 --- a/test/units/testsuite-13.nspawn.sh +++ b/test/units/testsuite-13.nspawn.sh @@ -622,6 +622,75 @@ testcase_rootidmap() { fi } +owneridmap_cleanup() { + local dir="${1:?}" + + mountpoint -q "$dir/bind" && umount "$dir/bind" + rm -fr "$dir" +} + +testcase_owneridmap() { + local root cmd permissions + local owner=1000 + + root="$(mktemp -d /var/lib/machines/testsuite-13.owneridmap-path.XXX)" + # Create ext4 image, as ext4 supports idmapped-mounts. + mkdir -p /tmp/owneridmap/bind + dd if=/dev/zero of=/tmp/owneridmap/ext4.img bs=4k count=2048 + mkfs.ext4 /tmp/owneridmap/ext4.img + mount /tmp/owneridmap/ext4.img /tmp/owneridmap/bind + trap "owneridmap_cleanup /tmp/owneridmap/" RETURN + + touch /tmp/owneridmap/bind/file + chown -R "$owner:$owner" /tmp/owneridmap/bind + + # Allow users to read and execute / in order to execute binaries + chmod o+rx "$root" + + create_dummy_container "$root" + + # --user= + # "Fake" getent passwd's bare minimum, so we don't have to pull it in + # with all the DSO shenanigans + cat >"$root/bin/getent" <<\EOF +#!/bin/bash + +if [[ $# -eq 0 ]]; then + : +elif [[ $1 == passwd ]]; then + echo "testuser:x:1010:1010:testuser:/:/bin/sh" +elif [[ $1 == initgroups ]]; then + echo "testuser" +fi +EOF + chmod +x "$root/bin/getent" + + mkdir -p "$root/home/testuser" + chown 1010:1010 "$root/home/testuser" + + cmd='PERMISSIONS=$(stat -c "%u:%g" /home/testuser/file); if [[ $PERMISSIONS != "1010:1010" ]]; then echo "*** wrong permissions: $PERMISSIONS"; return 1; fi; touch /home/testuser/other_file' + if ! SYSTEMD_LOG_TARGET=console \ + systemd-nspawn --register=no \ + --directory="$root" \ + -U \ + --user=testuser \ + --bind=/tmp/owneridmap/bind:/home/testuser:owneridmap \ + /usr/bin/bash -c "$cmd" |& tee nspawn.out; then + if grep -q "Failed to map ids for bind mount.*: Function not implemented" nspawn.out; then + echo "idmapped mounts are not supported, skipping the test..." + return 0 + fi + + return 1 + fi + + permissions=$(stat -c "%u:%g" /tmp/owneridmap/bind/other_file) + if [[ $permissions != "$owner:$owner" ]]; then + echo "*** wrong permissions: $permissions" + [[ "$IS_USERNS_SUPPORTED" == "yes" ]] && return 1 + fi +} + testcase_notification_socket() { # https://github.com/systemd/systemd/issues/4944 local root