diff --git a/TODO b/TODO index 8ea545ed46..18c38a09eb 100644 --- a/TODO +++ b/TODO @@ -185,6 +185,16 @@ Features: * start using STATX_SUBVOL in btrfs_is_subvol(). Also, make use of it generically, so that image discovery recognizes bcachefs subvols too. +* "systemd-export tar" should reuse the libarchive export code from systemd-dissect + --archive. + +* "systemd-import tar" should be moved to libarchive + +* foreign uid: + - add support to export-fs, import-fs, import-tar, export-tar + - add tool for deleting foreign UID held container images + - systemd-dissect should learn mappings, too, when doing mtree and such + * format-table: introduce new cell type for strings with ansi sequences in them. display them in regular output mode (via strip_tab_ansi()), but suppress them in json mode. diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index e3282793cd..4484e87831 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -841,6 +841,12 @@ host and container UIDs/GIDs are chosen identically it does provide process capability isolation, but may be useful if proper user namespacing with distinct UID maps is not possible. This option is not secure and must not be used to run untrusted code. + + If the parameter is managed, user namespacing is employed with + in managed mode, i.e. allocation of a UID range is delegated to + systemd-nsresourced.service8. This + mode is selected by default if invoked unprivileged, but can also be requested explicitly when + privileged. In this mode a 64K UID range is automatically picked. It is recommended to assign at least 65536 UIDs/GIDs to each container, so that the usable @@ -852,18 +858,23 @@ When user namespaces are used, the GID range assigned to each container is always chosen identical to the UID range. - In most cases, using is the recommended option as user - namespacing is required for security, and this option massively enhances container security while + In most cases, (or when privileged + , too) is the recommended option as user + namespacing is advised for security, and this option massively enhances container security while operating fully automatically in most cases. Note that the picked UID/GID range is not written to /etc/passwd or /etc/group. In fact, the allocation of the range is not stored persistently, - except in the file ownership of the files and directories of the container. + except possibly in the file ownership of the files and directories of the container, see + . - Note that when user namespacing is used file ownership on disk reflects this, and all of the container's - files and directories are owned by the container's effective user and group IDs. This means that copying files - from and to the container image requires correction of the numeric UID/GID values, according to the UID/GID - shift applied. + Note that when user namespacing is used without UID mapping (see below) file ownership on disk + reflects this, and all of the container's files and directories are owned by the container's + effective user and group IDs. This means that copying files from and to the container image requires + correction of the numeric UID/GID values, according to the UID/GID shift applied. + + Note that for fully unprivileged operation in managed mode, any directory + image should be ownd by the foreign UID range. @@ -875,8 +886,10 @@ chosen with , see above. Takes one of off (to leave the image as is), chown (to recursively chown() the container's directory tree as needed), map (in order to use transparent ID mapping - mounts) or auto for automatically using map where available and - chown where not. + mounts from UID 0 to the target UID range), foreign (the same, but from the + foreign UID range base) or auto for automatically using map or + foreign, where available and applicable and chown where + not. If chown is selected, all files and directories in the container's directory tree will be adjusted so that they are owned by the appropriate UIDs/GIDs selected for the container @@ -884,14 +897,19 @@ directory tree of the container. Besides actual file ownership, file ACLs are adjusted as well. - Typically map is the best choice, since it transparently maps UIDs/GIDs in - memory as needed without modifying the image, and without requiring an expensive recursive adjustment - operation. However, it is not available for all file systems, currently. + Typically foreign or map is the best choice, since it + transparently maps UIDs/GIDs in memory as needed without modifying the image, and without requiring + an expensive recursive adjustment operation. However, it is not available for all file systems, + currently. The option is implied if is used. This option has no effect if user namespacing is not used. + systemd-dissect1's + switch may be used to shift UID/GID ownership from or to the 0, foreign or + specific container UID/GID base outside of any systemd-nspawn invocation. + diff --git a/shell-completion/bash/systemd-nspawn b/shell-completion/bash/systemd-nspawn index 0a1761d110..e1829287f4 100644 --- a/shell-completion/bash/systemd-nspawn +++ b/shell-completion/bash/systemd-nspawn @@ -157,7 +157,7 @@ _systemd_nspawn() { comps='yes no' ;; --private-users) - comps='yes no pick' + comps='yes no pick identity managed' ;; --network-namespace-path) comps=$( compgen -A file -- "$cur" ) diff --git a/src/mountfsd/io.systemd.mount-file-system.policy b/src/mountfsd/io.systemd.mount-file-system.policy index 6a151eb437..78613bfdaf 100644 --- a/src/mountfsd/io.systemd.mount-file-system.policy +++ b/src/mountfsd/io.systemd.mount-file-system.policy @@ -67,4 +67,53 @@ io.systemd.mount-file-system.mount-image-privately + + + + + Allow mounting of directory + Authentication is required for an application to mount directory $(directory). + + auth_admin_keep + auth_admin_keep + yes + + + + + + Allow mounting of untrusted directory + Authentication is required for an application to mount directory $(directory) which is not owned by the user. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-directory + + + + + Allow private mounting of directory + Authentication is required for an application to privately mount directory $(directory). + + yes + yes + yes + + + + + Allow private mounting of untrusted directory + Authentication is required for an application to privately mount directory $(directory) which is not owned by the user. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-directory-privately + diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 31f2d47546..2fd610f5d6 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -1,5 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include +#if WANT_LINUX_FS_H +#include +#endif + #include "sd-daemon.h" #include "sd-varlink.h" @@ -15,12 +20,17 @@ #include "json-util.h" #include "main-func.h" #include "missing_loop.h" +#include "missing_mount.h" +#include "missing_syscall.h" #include "namespace-util.h" #include "nsresource.h" #include "nulstr-util.h" #include "os-util.h" #include "process-util.h" #include "stat-util.h" +#include "string-table.h" +#include "uid-classification.h" +#include "uid-range.h" #include "user-util.h" #include "varlink-io.systemd.MountFileSystem.h" #include "varlink-util.h" @@ -532,6 +542,349 @@ static int vl_method_mount_image( SD_JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(di->image_uuid), "imageUuid", SD_JSON_BUILD_UUID(di->image_uuid))); } +typedef enum MountMapMode { + MOUNT_MAP_AUTO = 0, /* determine automatically from image and caller */ + MOUNT_MAP_ROOT, /* map caller's UID to root in namespace (map 1 UID only) */ + MOUNT_MAP_FOREIGN, /* map foreign UID range to base in namespace (map 64K) */ + MOUNT_MAP_IDENTITY, /* apply identity mapping (map 64K) */ + _MOUNT_MAP_MODE_MAX, + _MOUNT_MAP_MODE_INVALID = -EINVAL, +} MountMapMode; + +static const char *const mount_map_mode_table[_MOUNT_MAP_MODE_MAX] = { + [MOUNT_MAP_AUTO] = "auto", + [MOUNT_MAP_ROOT] = "root", + [MOUNT_MAP_FOREIGN] = "foreign", + [MOUNT_MAP_IDENTITY] = "identity", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP(mount_map_mode, MountMapMode); + +typedef struct MountDirectoryParameters { + MountMapMode mode; + unsigned directory_fd_idx; + unsigned userns_fd_idx; + int read_only; +} MountDirectoryParameters; + +typedef enum DirectoryOwnership { + DIRECTORY_IS_ROOT_PEER_OWNED, /* This is returned if the directory is owned by the root user and the peer is root */ + DIRECTORY_IS_ROOT_OWNED, /* This is returned if the directory is owned by the root user (and the peer user is not root) */ + DIRECTORY_IS_PEER_OWNED, /* This is returned if the directory is owned by the peer user (who is not root) */ + DIRECTORY_IS_FOREIGN_OWNED, /* This is returned if the directory is owned by the foreign UID range */ + DIRECTORY_IS_OTHERWISE_OWNED, /* This is returned if the directory is owned by something else */ + _DIRECTORY_OWNERSHIP_MAX, + _DIRECTORY_OWNERSHIP_ERRNO_MAX = -ERRNO_MAX, /* Guarantee the whole negative errno range fits */ +} DirectoryOwnership; + +static MountMapMode default_mount_map_mode(DirectoryOwnership ownership) { + /* Derives a suitable mapping mode from the ownership of the base tree */ + + switch (ownership) { + case DIRECTORY_IS_PEER_OWNED: + return MOUNT_MAP_ROOT; /* Map the peer's UID to root in the container */ + + case DIRECTORY_IS_FOREIGN_OWNED: + return MOUNT_MAP_FOREIGN; /* Map the foreign UID range to the container's UID range */ + + case DIRECTORY_IS_ROOT_PEER_OWNED: + case DIRECTORY_IS_ROOT_OWNED: + case DIRECTORY_IS_OTHERWISE_OWNED: + return MOUNT_MAP_IDENTITY; /* Don't map */ + + default: + return _MOUNT_MAP_MODE_INVALID; + } +} + +static JSON_DISPATCH_ENUM_DEFINE(dispatch_mount_directory_mode, MountMapMode, mount_map_mode_from_string); + +static DirectoryOwnership validate_directory_fd(int fd, uid_t peer_uid) { + int r, fl; + + assert(fd >= 0); + + /* Checks if the specified directory fd looks sane. Returns a DirectoryOwnership that categorizes the + * ownership situation in comparison to the peer's UID. + * + * Note one key difference to image validation (as implemented above): for regular files if the + * client provided us with an open fd it implies the client has access, as well as what kind of + * access (i.e. ro or rw). But for directories this doesn't work the same way, as directories are + * always opened read-only only. Hence we use a different mechanism to validate access to them: we + * check if the directory is owned by the peer UID or by the foreign UID range (in the latter case + * one of the parent directories must be owned by the peer though). */ + + struct stat st; + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat() directory fd: %m"); + + r = stat_verify_directory(&st); + if (r < 0) + return r; + + fl = fd_verify_safe_flags_full(fd, O_DIRECTORY); + if (fl < 0) + return log_debug_errno(fl, "Directory file descriptor has unsafe flags set: %m"); + + if (st.st_uid == 0) { + if (peer_uid == 0) { + log_debug("Directory file descriptor points to root owned directory, who is also the peer."); + return DIRECTORY_IS_ROOT_PEER_OWNED; + } + log_debug("Directory file descriptor points to root owned directory."); + return DIRECTORY_IS_ROOT_OWNED; + } + if (st.st_uid == peer_uid) { + log_debug("Directory file descriptor points to peer owned directory."); + return DIRECTORY_IS_PEER_OWNED; + } + + /* For bind mounted directories we check if they are either owned by the client's UID, or by the + * foreign UID set, but in that case the parent directory must be owned by the client's UID, or some + * directory iteratively up the chain */ + + _cleanup_close_ int parent_fd = -EBADF; + unsigned n_level; + for (n_level = 0; n_level < 16; n_level++) { + /* Stop iteration if we find a directory up the tree that is neither owned by the user, nor is from the foreign UID range */ + if (!uid_is_foreign(st.st_uid) || !gid_is_foreign(st.st_gid)) { + log_debug("Directory file descriptor points to directory which itself or its parents is neither owned by foreign UID range nor by the user."); + return DIRECTORY_IS_OTHERWISE_OWNED; + } + + /* If the peer is root, then it doesn't matter if we find a parent owned by root, let's shortcut things. */ + if (peer_uid == 0) { + log_debug("Directory file descriptor is owned by foreign UID range, and peer is root."); + return DIRECTORY_IS_FOREIGN_OWNED; + } + + /* Go one level up */ + _cleanup_close_ int new_parent_fd = openat(fd, "..", O_DIRECTORY|O_PATH|O_CLOEXEC); + if (new_parent_fd < 0) + return log_debug_errno(errno, "Failed to open parent directory of directory file descriptor: %m"); + + struct stat new_st; + if (fstat(new_parent_fd, &new_st) < 0) + return log_debug_errno(errno, "Failed to stat parent directory of directory file descriptor: %m"); + + /* Safety check to see if we hit the root dir */ + if (stat_inode_same(&st, &new_st)) { + log_debug("Directory file descriptor is owned by foreign UID range, but didn't find parent directory that is owned by peer among ancestors."); + return DIRECTORY_IS_OTHERWISE_OWNED; + } + + if (new_st.st_uid == peer_uid) { /* Parent inode is owned by the peer. That's good! Everything's fine. */ + log_debug("Directory file descriptor is owned by foreign UID range, and ancestor is owned by peer."); + return DIRECTORY_IS_FOREIGN_OWNED; + } + + close_and_replace(parent_fd, new_parent_fd); + st = new_st; + } + + log_debug("Failed to find peer owned parent directory after %u levels, refusing.", n_level); + return DIRECTORY_IS_OTHERWISE_OWNED; +} + +static int vl_method_mount_directory( + sd_varlink *link, + sd_json_variant *parameters, + sd_varlink_method_flags_t flags, + void *userdata) { + + static const sd_json_dispatch_field dispatch_table[] = { + { "mode", SD_JSON_VARIANT_STRING, dispatch_mount_directory_mode, offsetof(MountDirectoryParameters, mode), 0 }, + { "directoryFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountDirectoryParameters, directory_fd_idx), SD_JSON_MANDATORY }, + { "userNamespaceFileDescriptor", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint, offsetof(MountDirectoryParameters, userns_fd_idx), 0 }, + { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MountDirectoryParameters, read_only), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + MountDirectoryParameters p = { + .mode = MOUNT_MAP_AUTO, + .directory_fd_idx = UINT_MAX, + .userns_fd_idx = UINT_MAX, + .read_only = -1, + }; + _cleanup_close_ int directory_fd = -EBADF, userns_fd = -EBADF; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + int r; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.directory_fd_idx == UINT_MAX) + return sd_varlink_error_invalid_parameter_name(link, "directoryFileDescriptor"); + + directory_fd = sd_varlink_peek_dup_fd(link, p.directory_fd_idx); + if (directory_fd < 0) + return log_debug_errno(directory_fd, "Failed to peek directory fd from client: %m"); + + if (p.userns_fd_idx != UINT_MAX) { + userns_fd = sd_varlink_peek_dup_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to peek user namespace fd from client: %m"); + } + + uid_t peer_uid; + r = sd_varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get client UID: %m"); + + DirectoryOwnership owned_by = validate_directory_fd(directory_fd, peer_uid); + if (owned_by < 0) + return owned_by; + + r = validate_userns(link, &userns_fd); + if (r != 0) + return r; + + /* If no mode is specified, pick sensible default */ + if (p.mode <= 0) { + p.mode = default_mount_map_mode(owned_by); + assert(p.mode > 0); + } + + _cleanup_free_ char *directory_path = NULL; + (void) fd_get_path(directory_fd, &directory_path); + + log_debug("Mounting '%s' with mapping mode: %s", strna(directory_path), mount_map_mode_to_string(p.mode)); + + const char *polkit_details[] = { + "read_only", one_zero(p.read_only > 0), + "directory", strna(directory_path), + NULL, + }; + + const char *polkit_action, *polkit_untrusted_action; + PolkitFlags polkit_flags; + if (userns_fd < 0) { + /* Mount into the host user namespace */ + polkit_action = "io.systemd.mount-file-system.mount-directory"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-directory"; + polkit_flags = 0; + } else { + /* Mount into a private user namespace */ + polkit_action = "io.systemd.mount-file-system.mount-directory-privately"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-directory-privately"; + + /* If polkit is not around, let's allow mounting authenticated images by default */ + polkit_flags = POLKIT_DEFAULT_ALLOW; + } + + /* We consider a directory "trusted" if it is owned by the peer or the foreign UID range */ + bool trusted_directory = IN_SET(owned_by, DIRECTORY_IS_ROOT_PEER_OWNED, DIRECTORY_IS_PEER_OWNED, DIRECTORY_IS_FOREIGN_OWNED); + + /* Let's definitely acquire the regular action privilege, for mounting properly signed images */ + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + trusted_directory ? polkit_action : polkit_untrusted_action, + polkit_details, + /* good_user= */ UID_INVALID, + trusted_directory ? polkit_flags : 0, + polkit_registry); + if (r <= 0) + return r; + + /* Generate the common dissection directory here. We are not going to use it, but the clients might, + * and they likely are unprivileged, hence cannot create it themselves. Hence let's just create it + * here, if it is missing. */ + r = get_common_dissect_directory(NULL); + if (r < 0) + return r; + + _cleanup_close_ int mount_fd = open_tree(directory_fd, "", OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH); + if (mount_fd < 0) + return log_debug_errno(errno, "Failed to issue open_tree() of provided directory '%s': %m", strna(directory_path)); + + if (p.read_only > 0 && mount_setattr( + mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = MOUNT_ATTR_RDONLY, + }, MOUNT_ATTR_SIZE_VER0) < 0) + return log_debug_errno(errno, "Failed to enable read-only mode: %m"); + + if (p.mode != MOUNT_MAP_IDENTITY) { + uid_t start; + + if (userns_fd >= 0) { + _cleanup_(uid_range_freep) UIDRange *uid_range_outside = NULL, *uid_range_inside = NULL, *gid_range_outside = NULL, *gid_range_inside = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &uid_range_outside); + if (r < 0) + return log_debug_errno(r, "Failed to load outside UID range of provided userns: %m"); + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_INSIDE, &uid_range_inside); + if (r < 0) + return log_debug_errno(r, "Failed to load inside UID range of provided userns: %m"); + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &gid_range_outside); + if (r < 0) + return log_debug_errno(r, "Failed to load outside GID range of provided userns: %m"); + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_INSIDE, &gid_range_inside); + if (r < 0) + return log_debug_errno(r, "Failed to load inside GID range of provided userns: %m"); + + /* Be very strict for now */ + if (!uid_range_equal(uid_range_outside, gid_range_outside) || + !uid_range_equal(uid_range_inside, gid_range_inside) || + uid_range_outside->n_entries != 1 || + uid_range_outside->entries[0].nr != 0x10000 || + uid_range_inside->n_entries != 1 || + uid_range_inside->entries[0].start != 0 || + uid_range_inside->entries[0].nr != 0x10000) + return sd_varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + start = uid_range_outside->entries[0].start; + } else + start = 0; + + _cleanup_free_ char *new_uid_map = NULL; + switch (p.mode) { + case MOUNT_MAP_ROOT: + r = strextendf(&new_uid_map, UID_FMT " " UID_FMT " " UID_FMT, + peer_uid, start, (uid_t) 1); + break; + case MOUNT_MAP_FOREIGN: + r = strextendf(&new_uid_map, UID_FMT " " UID_FMT " " UID_FMT, + (uid_t) FOREIGN_UID_MIN, start, (uid_t) 0x10000); + break; + default: + assert_not_reached(); + } + if (r < 0) + return r; + + _cleanup_close_ int idmap_userns_fd = userns_acquire(new_uid_map, new_uid_map); + if (idmap_userns_fd < 0) + return log_debug_errno(idmap_userns_fd, "Failed to acquire user namespace for id mapping: %m"); + + if (mount_setattr(mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = MOUNT_ATTR_IDMAP, + .userns_fd = idmap_userns_fd, + .propagation = MS_PRIVATE, + }, MOUNT_ATTR_SIZE_VER0) < 0) + return log_debug_errno(errno, "Failed to enable id mapping: %m"); + } + + if (userns_fd >= 0) { + r = nsresource_add_mount(userns_fd, mount_fd); + if (r < 0) + return r; + } + + int fd_idx = sd_varlink_push_fd(link, mount_fd); + if (fd_idx < 0) + return fd_idx; + + TAKE_FD(mount_fd); + + return sd_varlink_replybo( + link, + SD_JSON_BUILD_PAIR("mountFileDescriptor", SD_JSON_BUILD_INTEGER(fd_idx))); +} + static int process_connection(sd_varlink_server *server, int _fd) { _cleanup_close_ int fd = TAKE_FD(_fd); /* always take possession */ _cleanup_(sd_varlink_close_unrefp) sd_varlink *vl = NULL; @@ -601,7 +954,8 @@ static int run(int argc, char *argv[]) { r = sd_varlink_server_bind_method_many( server, - "io.systemd.MountFileSystem.MountImage", vl_method_mount_image); + "io.systemd.MountFileSystem.MountImage", vl_method_mount_image, + "io.systemd.MountFileSystem.MountDirectory", vl_method_mount_directory); if (r < 0) return log_error_errno(r, "Failed to bind methods: %m"); diff --git a/src/nspawn/nspawn-cgroup.c b/src/nspawn/nspawn-cgroup.c index 6d6a8a814b..4ee21c0779 100644 --- a/src/nspawn/nspawn-cgroup.c +++ b/src/nspawn/nspawn-cgroup.c @@ -119,7 +119,7 @@ int create_subcgroup( CGroupUnified unified_requested, uid_t uid_shift, int userns_fd, - bool privileged) { + UserNamespaceMode userns_mode) { _cleanup_free_ char *cgroup = NULL, *payload = NULL; CGroupMask supported; @@ -163,14 +163,14 @@ int create_subcgroup( if (!payload) return log_oom(); - if (privileged) + if (userns_mode != USER_NAMESPACE_MANAGED) r = cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, payload, pid); else r = cg_create(SYSTEMD_CGROUP_CONTROLLER, payload); if (r < 0) return log_error_errno(r, "Failed to create %s subcgroup: %m", payload); - if (privileged) { + if (userns_mode != USER_NAMESPACE_MANAGED) { _cleanup_free_ char *fs = NULL; r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, payload, NULL, &fs); if (r < 0) diff --git a/src/nspawn/nspawn-cgroup.h b/src/nspawn/nspawn-cgroup.h index 7e2cd53ddc..8f039ffb28 100644 --- a/src/nspawn/nspawn-cgroup.h +++ b/src/nspawn/nspawn-cgroup.h @@ -5,9 +5,10 @@ #include #include "cgroup-util.h" +#include "nspawn-settings.h" int sync_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift); -int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested, uid_t uid_shift, int userns_fd, bool privileged); +int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested, uid_t uid_shift, int userns_fd, UserNamespaceMode userns_mode); int mount_cgroups(const char *dest, CGroupUnified unified_requested, bool userns, uid_t uid_shift, uid_t uid_range, const char *selinux_apifs_context, bool use_cgns); int mount_systemd_cgroup_writable(const char *dest, CGroupUnified unified_requested); diff --git a/src/nspawn/nspawn-mount.c b/src/nspawn/nspawn-mount.c index 0f1fff375c..552d629a18 100644 --- a/src/nspawn/nspawn-mount.c +++ b/src/nspawn/nspawn-mount.c @@ -590,11 +590,11 @@ int mount_all(const char *dest, { "tmpfs", "/tmp", "tmpfs", "mode=01777" NESTED_TMPFS_LIMITS, MS_NOSUID|MS_NODEV|MS_STRICTATIME, MOUNT_FATAL|MOUNT_APPLY_TMPFS_TMP|MOUNT_MKDIR|MOUNT_USRQUOTA_GRACEFUL }, { "tmpfs", "/sys", "tmpfs", "mode=0555" TMPFS_LIMITS_SYS, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR|MOUNT_PRIVILEGED }, + MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR|MOUNT_UNMANAGED }, { "sysfs", "/sys", "sysfs", NULL, SYS_DEFAULT_MOUNT_FLAGS, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR|MOUNT_UNMANAGED }, /* skipped if above was mounted */ { "sysfs", "/sys", "sysfs", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_MKDIR|MOUNT_UNMANAGED }, /* skipped if above was mounted */ { "tmpfs", "/dev", "tmpfs", "mode=0755" TMPFS_LIMITS_PRIVATE_DEV, MS_NOSUID|MS_STRICTATIME, MOUNT_FATAL|MOUNT_MKDIR }, { "tmpfs", "/dev/shm", "tmpfs", "mode=01777" NESTED_TMPFS_LIMITS, MS_NOSUID|MS_NODEV|MS_STRICTATIME, @@ -617,9 +617,9 @@ int mount_all(const char *dest, { "/sys/fs/selinux", "/sys/fs/selinux", NULL, NULL, MS_BIND, MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* Bind mount first (mkdir/chown the mount point in case /sys/ is mounted as minimal skeleton tmpfs) */ { NULL, "/sys/fs/selinux", NULL, NULL, MS_BIND|MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV|MS_REMOUNT, - MOUNT_PRIVILEGED }, /* Then, make it r/o (don't mkdir/chown the mount point here, the previous entry already did that) */ + MOUNT_UNMANAGED|MOUNT_PRIVILEGED }, /* Then, make it r/o (don't mkdir/chown the mount point here, the previous entry already did that) */ { NULL, "/sys/fs/selinux", NULL, NULL, MS_PRIVATE, - MOUNT_PRIVILEGED }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ + MOUNT_UNMANAGED|MOUNT_PRIVILEGED }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ #endif }; @@ -628,6 +628,7 @@ int mount_all(const char *dest, bool ro = FLAGS_SET(mount_settings, MOUNT_APPLY_APIVFS_RO); bool in_userns = FLAGS_SET(mount_settings, MOUNT_IN_USERNS); bool tmpfs_tmp = FLAGS_SET(mount_settings, MOUNT_APPLY_TMPFS_TMP); + bool unmanaged = FLAGS_SET(mount_settings, MOUNT_UNMANAGED); bool privileged = FLAGS_SET(mount_settings, MOUNT_PRIVILEGED); int r; @@ -636,8 +637,9 @@ int mount_all(const char *dest, bool fatal = FLAGS_SET(m->mount_settings, MOUNT_FATAL); const char *o; - /* If we are not privileged but the entry is marked as privileged and to be mounted outside the user namespace, then skip it */ - if (!privileged && FLAGS_SET(m->mount_settings, MOUNT_PRIVILEGED) && !FLAGS_SET(m->mount_settings, MOUNT_IN_USERNS)) + /* If we are in managed user namespace mode but the entry is marked for mount outside of + * managed user namespace mode, and to be mounted outside the user namespace, then skip it */ + if (!unmanaged && FLAGS_SET(m->mount_settings, MOUNT_UNMANAGED) && !FLAGS_SET(m->mount_settings, MOUNT_IN_USERNS)) continue; if (in_userns != FLAGS_SET(m->mount_settings, MOUNT_IN_USERNS)) @@ -652,6 +654,9 @@ int mount_all(const char *dest, if (!tmpfs_tmp && FLAGS_SET(m->mount_settings, MOUNT_APPLY_TMPFS_TMP)) continue; + if (!privileged && FLAGS_SET(m->mount_settings, MOUNT_PRIVILEGED)) + continue; + r = chase(m->where, dest, CHASE_NONEXISTENT|CHASE_PREFIX_ROOT, &where, NULL); if (r < 0) return log_error_errno(r, "Failed to resolve %s%s: %m", strempty(dest), m->where); diff --git a/src/nspawn/nspawn-mount.h b/src/nspawn/nspawn-mount.h index 1f54b51625..87b3b91c43 100644 --- a/src/nspawn/nspawn-mount.h +++ b/src/nspawn/nspawn-mount.h @@ -20,8 +20,9 @@ typedef enum MountSettingsMask { MOUNT_TOUCH = 1 << 9, /* if set, touch file to mount over first */ MOUNT_PREFIX_ROOT = 1 << 10,/* if set, prefix the source path with the container's root directory */ MOUNT_FOLLOW_SYMLINKS = 1 << 11,/* if set, we'll follow symlinks for the mount target */ - MOUNT_PRIVILEGED = 1 << 12,/* if set, we'll only mount this in the outer child if we are running in privileged mode */ - MOUNT_USRQUOTA_GRACEFUL = 1 << 13,/* if set, append "usrquota" to mount options if kernel tmpfs supports that */ + MOUNT_UNMANAGED = 1 << 12,/* if set, we'll only mount this in the outer child if we are running in privileged mode */ + MOUNT_PRIVILEGED = 1 << 13,/* if set, we'll only mount this if we have full privileges */ + MOUNT_USRQUOTA_GRACEFUL = 1 << 14,/* if set, append "usrquota" to mount options if kernel tmpfs supports that */ } MountSettingsMask; typedef enum CustomMountType { diff --git a/src/nspawn/nspawn-settings.c b/src/nspawn/nspawn-settings.c index 7842d93c34..2d883e2196 100644 --- a/src/nspawn/nspawn-settings.c +++ b/src/nspawn/nspawn-settings.c @@ -930,10 +930,11 @@ DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(timezone_mode, TimezoneMode, TIMEZONE_AU DEFINE_CONFIG_PARSE_ENUM(config_parse_userns_ownership, user_namespace_ownership, UserNamespaceOwnership); static const char *const user_namespace_ownership_table[_USER_NAMESPACE_OWNERSHIP_MAX] = { - [USER_NAMESPACE_OWNERSHIP_OFF] = "off", - [USER_NAMESPACE_OWNERSHIP_CHOWN] = "chown", - [USER_NAMESPACE_OWNERSHIP_MAP] = "map", - [USER_NAMESPACE_OWNERSHIP_AUTO] = "auto", + [USER_NAMESPACE_OWNERSHIP_OFF] = "off", + [USER_NAMESPACE_OWNERSHIP_CHOWN] = "chown", + [USER_NAMESPACE_OWNERSHIP_MAP] = "map", + [USER_NAMESPACE_OWNERSHIP_FOREIGN] = "foreign", + [USER_NAMESPACE_OWNERSHIP_AUTO] = "auto", }; /* Note: while "yes" maps to "auto" here, we don't really document that, in order to make things clearer and less confusing to users. */ diff --git a/src/nspawn/nspawn-settings.h b/src/nspawn/nspawn-settings.h index 135b3dbb0a..0b30506391 100644 --- a/src/nspawn/nspawn-settings.h +++ b/src/nspawn/nspawn-settings.h @@ -29,14 +29,16 @@ typedef enum UserNamespaceMode { USER_NAMESPACE_NO, USER_NAMESPACE_FIXED, USER_NAMESPACE_PICK, + USER_NAMESPACE_MANAGED, _USER_NAMESPACE_MODE_MAX, _USER_NAMESPACE_MODE_INVALID = -EINVAL, } UserNamespaceMode; typedef enum UserNamespaceOwnership { - USER_NAMESPACE_OWNERSHIP_OFF, - USER_NAMESPACE_OWNERSHIP_CHOWN, - USER_NAMESPACE_OWNERSHIP_MAP, + USER_NAMESPACE_OWNERSHIP_OFF, /* do not change ownership */ + USER_NAMESPACE_OWNERSHIP_CHOWN, /* chown to target range */ + USER_NAMESPACE_OWNERSHIP_MAP, /* map from 0x00000000…0x0000FFFF range to target range */ + USER_NAMESPACE_OWNERSHIP_FOREIGN, /* map from 0x7FFE0000…0x7FFEFFFF range to target range */ USER_NAMESPACE_OWNERSHIP_AUTO, _USER_NAMESPACE_OWNERSHIP_MAX, _USER_NAMESPACE_OWNERSHIP_INVALID = -1, diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 5ab04b121a..4c054b2dbb 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -107,6 +107,7 @@ #include "sysctl-util.h" #include "terminal-util.h" #include "tmpfile-util.h" +#include "uid-classification.h" #include "umask-util.h" #include "unit-name.h" #include "user-util.h" @@ -139,7 +140,7 @@ static char *arg_hostname = NULL; /* The name the payload sees by default */ static const char *arg_selinux_context = NULL; static const char *arg_selinux_apifs_context = NULL; static char *arg_slice = NULL; -static bool arg_private_network = false; +static bool arg_private_network; /* initialized depending on arg_privileged in run() */ static bool arg_read_only = false; static StartMode arg_start_mode = START_PID1; static bool arg_ephemeral = false; @@ -197,7 +198,7 @@ static VolatileMode arg_volatile_mode = VOLATILE_NO; static ExposePort *arg_expose_ports = NULL; static char **arg_property = NULL; static sd_bus_message *arg_property_message = NULL; -static UserNamespaceMode arg_userns_mode = USER_NAMESPACE_NO; +static UserNamespaceMode arg_userns_mode; /* initialized depending on arg_privileged in run() */ static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U; static UserNamespaceOwnership arg_userns_ownership = _USER_NAMESPACE_OWNERSHIP_INVALID; static int arg_kill_signal = 0; @@ -369,7 +370,7 @@ static int help(void) { " the service unit nspawn is running in\n" "\n%3$sUser Namespacing:%4$s\n" " --private-users=no Run without user namespacing\n" - " --private-users=yes|pick|identity\n" + " --private-users=yes|pick|identity|managed\n" " Run within user namespace, autoselect UID/GID range\n" " --private-users=UIDBASE[:NUIDS]\n" " Similar, but with user configured UID/GID range\n" @@ -518,7 +519,7 @@ static int detect_unified_cgroup_hierarchy_from_environment(void) { static int detect_unified_cgroup_hierarchy_from_image(const char *directory) { int r; - if (!arg_privileged) { + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { /* We only support the unified mode when running unprivileged */ arg_unified_cgroup_hierarchy = CGROUP_UNIFIED_ALL; return 0; @@ -1257,6 +1258,11 @@ static int parse_argv(int argc, char *argv[]) { arg_userns_mode = USER_NAMESPACE_FIXED; arg_uid_shift = 0; arg_uid_range = UINT32_C(0x10000); + } else if (streq(optarg, "managed")) { + /* managed: User namespace on, and acquire it from systemd-nsresourced */ + arg_userns_mode = USER_NAMESPACE_MANAGED; + arg_uid_shift = UID_INVALID; + arg_uid_range = UINT32_C(0x10000); } else { /* anything else: User namespacing on, UID range is explicitly configured */ r = parse_userns_uid_range(optarg, &arg_uid_shift, &arg_uid_range); @@ -1271,9 +1277,8 @@ static int parse_argv(int argc, char *argv[]) { case 'U': if (userns_supported()) { - arg_userns_mode = USER_NAMESPACE_PICK; /* Note that arg_userns_ownership is - * implied by USER_NAMESPACE_PICK - * further down. */ + /* Note that arg_userns_ownership is implied by USER_NAMESPACE_PICK further down. */ + arg_userns_mode = arg_privileged ? USER_NAMESPACE_PICK : USER_NAMESPACE_MANAGED; arg_uid_shift = UID_INVALID; arg_uid_range = UINT32_C(0x10000); @@ -1656,14 +1661,23 @@ static int parse_argv(int argc, char *argv[]) { static int verify_arguments(void) { int r; - SET_FLAG(arg_mount_settings, MOUNT_PRIVILEGED, arg_privileged); + SET_FLAG(arg_mount_settings, MOUNT_UNMANAGED, arg_userns_mode != USER_NAMESPACE_MANAGED); - if (!arg_privileged) { - if (!arg_private_network) { - log_notice("Automatically implying --private-network, since mounting /sys/ in an unprivileged user namespaces requires network namespacing."); - arg_private_network = true; - } - } + /* We can mount selinuxfs only if we are privileged and can do so before userns. In managed mode we + * have to enter the userns earlier, hence cannot do that. */ + /* SET_FLAG(arg_mount_settings, MOUNT_PRIVILEGED, arg_privileged); */ + SET_FLAG(arg_mount_settings, MOUNT_PRIVILEGED, arg_userns_mode != USER_NAMESPACE_MANAGED); + + SET_FLAG(arg_mount_settings, MOUNT_USE_USERNS, arg_userns_mode != USER_NAMESPACE_NO); + + if (arg_private_network) + SET_FLAG(arg_mount_settings, MOUNT_APPLY_APIVFS_NETNS, arg_private_network); + + if (!arg_privileged && arg_userns_mode != USER_NAMESPACE_MANAGED) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unprivileged operation requires managed user namespaces, as otherwise no UID range can be acquired."); + + if (arg_userns_mode == USER_NAMESPACE_MANAGED && !arg_private_network) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Managed user namespace operation requires private networking, as otherwise /sys/ may not be mounted."); if (arg_start_mode == START_PID2 && arg_unified_cgroup_hierarchy == CGROUP_UNIFIED_UNKNOWN) { /* If we are running the stub init in the container, we don't need to look at what the init @@ -1684,12 +1698,6 @@ static int verify_arguments(void) { arg_unified_cgroup_hierarchy = CGROUP_UNIFIED_NONE; } - if (arg_userns_mode != USER_NAMESPACE_NO) - arg_mount_settings |= MOUNT_USE_USERNS; - - if (arg_private_network) - arg_mount_settings |= MOUNT_APPLY_APIVFS_NETNS; - if (!(arg_clone_ns_flags & CLONE_NEWPID) || !(arg_clone_ns_flags & CLONE_NEWUTS)) { arg_register = false; @@ -1699,8 +1707,7 @@ static int verify_arguments(void) { if (arg_userns_ownership < 0) arg_userns_ownership = - arg_userns_mode == USER_NAMESPACE_PICK ? USER_NAMESPACE_OWNERSHIP_AUTO : - USER_NAMESPACE_OWNERSHIP_OFF; + IN_SET(arg_userns_mode, USER_NAMESPACE_PICK, USER_NAMESPACE_MANAGED) ? USER_NAMESPACE_OWNERSHIP_AUTO : USER_NAMESPACE_OWNERSHIP_OFF; if (arg_start_mode == START_BOOT && arg_kill_signal <= 0) arg_kill_signal = SIGRTMIN+3; @@ -1809,10 +1816,18 @@ static int verify_network_interfaces_initialized(void) { return 0; } +static int in_child_chown(void) { + /* Returns true when chown()ing inodes we create inside the outer child is required. Basically, we + * need the chowning when we implement userns ourselves. If userns is off we don#t need to chown(), + * obviously. And if we are in managed mode we already entered the userns, and hence don#t need to + * manually chown either. */ + return IN_SET(arg_userns_mode, USER_NAMESPACE_PICK, USER_NAMESPACE_FIXED); +} + static int userns_chown_at(int fd, const char *fname, uid_t uid, gid_t gid, int flags) { assert(fd >= 0 || fd == AT_FDCWD); - if (arg_userns_mode == USER_NAMESPACE_NO) + if (!in_child_chown()) return 0; if (uid == UID_INVALID && gid == GID_INVALID) @@ -2295,18 +2310,24 @@ static int copy_devnode_one(const char *dest, const char *node, bool ignore_mkno if (r < 0) return log_error_errno(r, "Failed to create directory %s: %m", parent); - if (mknod(to, st.st_mode, st.st_rdev) < 0) { - r = -errno; /* Save the original error code. */ + r = RET_NERRNO(mknod(to, st.st_mode, st.st_rdev)); + if (r < 0) { /* Explicitly warn the user when /dev/ is already populated. */ if (r == -EEXIST) log_notice("%s/dev/ is pre-mounted and pre-populated. If a pre-mounted /dev/ is provided it needs to be an unpopulated file system.", dest); + /* If arg_uid_shift != 0, then we cannot fall back to use bind mount. */ - if (arg_uid_shift != 0) { + if (!(arg_userns_mode == USER_NAMESPACE_NO || + (arg_userns_mode == USER_NAMESPACE_FIXED && arg_uid_shift == 0))) { if (ignore_mknod_failure) { log_debug_errno(r, "Failed to mknod(%s), ignoring: %m", to); return 0; } - return log_error_errno(r, "Failed to mknod(%s): %m", to); + + if (arg_userns_mode != USER_NAMESPACE_MANAGED || !ERRNO_IS_NEG_PRIVILEGE(r)) + return log_error_errno(r, "Failed to mknod(%s): %m", to); + + log_debug_errno(r, "Failed to create device node '%s' and running in managed mode, resorting to bind mount: %m", to); } /* Some systems abusively restrict mknod but allow bind mounts. */ @@ -2402,7 +2423,7 @@ static int make_extra_nodes(const char *dest) { return 0; } -static int setup_pts(const char *dest) { +static int setup_pts(const char *dest, uid_t chown_uid) { _cleanup_free_ char *options = NULL; const char *p; int r; @@ -2411,13 +2432,13 @@ static int setup_pts(const char *dest) { if (arg_selinux_apifs_context) (void) asprintf(&options, "newinstance,ptmxmode=0666,mode=" STRINGIFY(TTY_MODE) ",gid=" GID_FMT ",context=\"%s\"", - arg_uid_shift + TTY_GID, + chown_uid + TTY_GID, arg_selinux_apifs_context); else #endif (void) asprintf(&options, "newinstance,ptmxmode=0666,mode=" STRINGIFY(TTY_MODE) ",gid=" GID_FMT, - arg_uid_shift + TTY_GID); + chown_uid + TTY_GID); if (!options) return log_oom(); @@ -2854,7 +2875,9 @@ static int reset_audit_loginuid(void) { if ((arg_clone_ns_flags & CLONE_NEWPID) == 0) return 0; - if (!arg_privileged) + /* if we are in managed userns mode, then we are already in our userns, hence we cannot reset the + * loginuid anyway, hence don't bother */ + if (arg_userns_mode == USER_NAMESPACE_MANAGED) return 0; r = read_virtual_file("/proc/self/loginuid", SIZE_MAX, &p, /* ret_size= */ NULL); @@ -2886,8 +2909,8 @@ static int mount_tunnel_dig(const char *root) { const char *p, *q; int r; - if (!arg_privileged) { - log_debug("Not digging mount tunnel, because running unprivileged."); + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + log_debug("Not digging mount tunnel, because running in managed user namespace mode."); return 0; } @@ -2919,8 +2942,8 @@ static int mount_tunnel_dig(const char *root) { static int mount_tunnel_open(void) { int r; - if (!arg_privileged) { - log_debug("Not opening up mount tunnel, because running unprivileged."); + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + log_debug("Not opening up mount tunnel, because running in managed user namespace mode."); return 0; } @@ -3267,6 +3290,12 @@ static int chase_and_update(char **p, unsigned flags) { static int determine_uid_shift(const char *directory) { assert(directory); + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + /* In managed mode we should already know the UID shift */ + assert(uid_is_valid(arg_uid_shift)); + return 0; + } + if (arg_userns_mode == USER_NAMESPACE_NO) { arg_uid_shift = 0; return 0; @@ -3447,7 +3476,7 @@ static int inner_child( if (!arg_network_namespace_path && arg_private_network) { _cleanup_close_ int netns_fd = -EBADF; - if (arg_privileged) + if (arg_userns_mode != USER_NAMESPACE_MANAGED) if (unshare(CLONE_NEWNET) < 0) return log_error_errno(errno, "Failed to unshare network namespace: %m"); @@ -3463,8 +3492,8 @@ static int inner_child( (void) barrier_place(barrier); /* #3 */ } - if (arg_privileged) { - r = mount_sysfs(NULL, arg_mount_settings); + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { + r = mount_sysfs(NULL, arg_mount_settings | MOUNT_IN_USERNS); if (r < 0) return r; } @@ -3817,8 +3846,8 @@ static int setup_unix_export_dir_outside(char **ret) { assert(ret); - if (!arg_privileged) { - log_debug("Not digging socket tunnel, because running unprivileged."); + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + log_debug("Not digging socket tunnel, because running in managed user namespace mode."); return 0; } @@ -3874,7 +3903,7 @@ static int setup_unix_export_host_inside(const char *directory, const char *unix assert(directory); - if (!arg_privileged) + if (arg_userns_mode == USER_NAMESPACE_MANAGED) return 0; assert(unix_export_path); @@ -3928,12 +3957,15 @@ static DissectImageFlags determine_dissect_image_flags(void) { DISSECT_IMAGE_PIN_PARTITION_DEVICES | (arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK|DISSECT_IMAGE_GROWFS) | DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | - (arg_console_mode == CONSOLE_INTERACTIVE ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0); + (arg_console_mode == CONSOLE_INTERACTIVE ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0) | + ((arg_userns_ownership == USER_NAMESPACE_OWNERSHIP_FOREIGN) ? DISSECT_IMAGE_FOREIGN_UID : + (arg_userns_ownership != USER_NAMESPACE_OWNERSHIP_AUTO) ? DISSECT_IMAGE_IDENTITY_UID : 0); } static int outer_child( Barrier *barrier, const char *directory, + int mount_fd, DissectedImage *dissected_image, int fd_outer_socket, int fd_inner_socket, @@ -3952,9 +3984,9 @@ static int outer_child( /* This is the "outer" child process, i.e the one forked off by the container manager itself. Its * namespace situation is: * - * - CLONE_NEWNS : already has its own (created by clone() if arg_privileged, or unshare() if !arg_unprivileged) - * - CLONE_NEWUSER : if arg_privileged: still in the host's - * if !arg_privileged: already has its own (created by nsresource_allocate_userns()->setns(userns_fd)) + * - CLONE_NEWUSER : if not in USER_NAMESPACE_MANAGED mode: still in the host's + * if USER_NAMESPACE_MANAGED mode: already has its own (created by nsresource_allocate_userns()->setns(userns_fd)) + * - CLONE_NEWNS : already has its own (created by clone() if not USER_NAMESPACE_MANAGED, or unshare() otherwise) * - CLONE_NEWPID : still in the host's * - CLONE_NEWUTS : still in the host's * - CLONE_NEWIPC : still in the host's @@ -3987,7 +4019,23 @@ static int outer_child( if (r < 0) return r; - if (dissected_image) { + /* Put the root dir into the target directory now. One of three mechanisms is provided: either we + * have a single mount fd (typically unprivileged --directory= mode) or we have a fully dissected + * image (--image= mode), or we have a regular path. */ + if (mount_fd >= 0) { + assert(arg_directory); + assert(!arg_image); + + if (move_mount(mount_fd, "", AT_FDCWD, directory, MOVE_MOUNT_F_EMPTY_PATH) < 0) + return log_error_errno(errno, "Failed to attach root directory: %m"); + + mount_fd = safe_close(mount_fd); + log_debug("Successfully attached root directory to '%s'.", directory); + + } else if (dissected_image) { + assert(!arg_directory); + assert(arg_image); + /* If we are operating on a disk image, then mount its root directory now, but leave out the * rest. We can read the UID shift from it if we need to. Further down we'll mount the rest, * but then with the uid shift known. That way we can mount VFAT file systems shifted to the @@ -4004,12 +4052,31 @@ static int outer_child( (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0)); if (r < 0) return r; + } else { + assert(arg_directory); + assert(!arg_image); + + r = mount_nofollow_verbose(LOG_ERR, arg_directory, directory, /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); + if (r < 0) + return r; } r = determine_uid_shift(directory); if (r < 0) return r; + /* If we do userns on our own, we need to chown() all files ourselves before. Otherwise, if userns is + * off or we are in managed mode we already have the userns applied, hence don't need to chown + * anything */ + uid_t chown_uid, chown_range; + if (in_child_chown()) { + chown_uid = arg_uid_shift; + chown_range = arg_uid_range; + } else { + chown_uid = 0; + chown_range = UINT32_C(0x10000); + } + if (arg_userns_mode != USER_NAMESPACE_NO) { _cleanup_close_ int mntns_fd = -EBADF; @@ -4041,33 +4108,15 @@ static int outer_child( if (l != sizeof(arg_uid_shift)) return log_error_errno(SYNTHETIC_ERRNO(EIO), "Short read while receiving UID shift."); + + if (in_child_chown()) + chown_uid = arg_uid_shift; } log_full(arg_quiet ? LOG_DEBUG : LOG_INFO, "Selected user namespace base " UID_FMT " and range " UID_FMT ".", arg_uid_shift, arg_uid_range); } - if (path_equal(directory, "/")) { - /* If the directory we shall boot is the host, let's operate on a bind mount at a different - * place, so that we can make changes to its mount structure (for example, to implement - * --volatile=) without this interfering with our ability to access files such as - * /etc/localtime to copy into the container. Note that we use a fixed place for this - * (instead of a temporary directory, since we are living in our own mount namespace here - * already, and thus don't need to be afraid of colliding with anyone else's mounts). */ - (void) mkdir_p("/run/systemd/nspawn-root", 0755); - - r = mount_nofollow_verbose(LOG_ERR, "/", "/run/systemd/nspawn-root", NULL, MS_BIND|MS_REC, NULL); - if (r < 0) - return r; - - directory = "/run/systemd/nspawn-root"; - } - - /* Make sure we always have a mount that we can move to root later on. */ - r = make_mount_point(directory); - if (r < 0) - return r; - /* So the whole tree is now MS_SLAVE, i.e. we'll still receive mount/umount events from the host * mount namespace. For the directory we are going to run our container let's turn this off, so that * we'll live in our own little world from now on, and propagation from the host may only happen via @@ -4086,7 +4135,7 @@ static int outer_child( r = setup_volatile_mode( directory, arg_volatile_mode, - arg_uid_shift, + chown_uid, arg_selinux_apifs_context); if (r < 0) return r; @@ -4094,8 +4143,8 @@ static int outer_child( r = bind_user_prepare( directory, arg_bind_user, - arg_uid_shift, - arg_uid_range, + chown_uid, + chown_range, &arg_custom_mounts, &arg_n_custom_mounts, &bind_user_context); if (r < 0) @@ -4126,17 +4175,47 @@ static int outer_child( directory, arg_custom_mounts, arg_n_custom_mounts, - arg_uid_shift, - arg_uid_range, + chown_uid, + chown_range, arg_selinux_apifs_context, MOUNT_ROOT_ONLY); if (r < 0) return r; - if (arg_userns_mode != USER_NAMESPACE_NO && - IN_SET(arg_userns_ownership, USER_NAMESPACE_OWNERSHIP_MAP, USER_NAMESPACE_OWNERSHIP_AUTO) && - arg_uid_shift != 0) { + if (!IN_SET(arg_userns_mode, USER_NAMESPACE_NO, USER_NAMESPACE_MANAGED) && + IN_SET(arg_userns_ownership, USER_NAMESPACE_OWNERSHIP_MAP, USER_NAMESPACE_OWNERSHIP_FOREIGN, USER_NAMESPACE_OWNERSHIP_AUTO) && + chown_uid != 0) { _cleanup_strv_free_ char **dirs = NULL; + RemountIdmapping mapping; + + switch (arg_userns_ownership) { + case USER_NAMESPACE_OWNERSHIP_MAP: + mapping = REMOUNT_IDMAPPING_HOST_ROOT; + break; + + case USER_NAMESPACE_OWNERSHIP_FOREIGN: + mapping = REMOUNT_IDMAPPING_FOREIGN_WITH_HOST_ROOT; + break; + + case USER_NAMESPACE_OWNERSHIP_AUTO: { + struct stat st; + + if (lstat(directory, &st) < 0) + return log_error_errno(errno, "Failed to stat() container root directory '%s': %m", directory); + + r = stat_verify_directory(&st); + if (r < 0) + return log_error_errno(r, "Container root directory '%s' is not a directory: %m", directory); + + mapping = uid_is_foreign(st.st_uid) ? + REMOUNT_IDMAPPING_FOREIGN_WITH_HOST_ROOT : + REMOUNT_IDMAPPING_HOST_ROOT; + break; + } + + default: + assert_not_reached(); + } if (arg_volatile_mode != VOLATILE_YES) { r = strv_extend(&dirs, directory); @@ -4155,7 +4234,13 @@ static int outer_child( return log_oom(); } - r = remount_idmap(dirs, arg_uid_shift, arg_uid_range, UID_INVALID, UID_INVALID, REMOUNT_IDMAPPING_HOST_ROOT); + r = remount_idmap( + dirs, + chown_uid, + chown_range, + /* host_owner= */ UID_INVALID, + /* dest_owner= */ UID_INVALID, + mapping); 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 @@ -4177,7 +4262,7 @@ static int outer_child( r = setup_volatile_mode_after_remount_idmap( directory, arg_volatile_mode, - arg_uid_shift, + chown_uid, arg_selinux_apifs_context); if (r < 0) return r; @@ -4187,8 +4272,8 @@ static int outer_child( r = dissected_image_mount_and_warn( dissected_image, directory, - arg_uid_shift, - arg_uid_range, + chown_uid, + chown_range, /* userns_fd= */ -EBADF, determine_dissect_image_flags()| DISSECT_IMAGE_MOUNT_NON_ROOT_ONLY| @@ -4212,11 +4297,11 @@ static int outer_child( "Short write while sending cgroup mode."); } - r = recursive_chown(directory, arg_uid_shift, arg_uid_range); + r = recursive_chown(directory, chown_uid, chown_range); if (r < 0) return r; - r = base_filesystem_create(directory, arg_uid_shift, (gid_t) arg_uid_shift); + r = base_filesystem_create(directory, chown_uid, (gid_t) chown_uid); if (r < 0) return r; @@ -4229,7 +4314,7 @@ static int outer_child( r = mount_all(directory, arg_mount_settings, - arg_uid_shift, + chown_uid, arg_selinux_apifs_context); if (r < 0) return r; @@ -4247,16 +4332,16 @@ static int outer_child( if (r < 0) return r; - (void) dev_setup(directory, arg_uid_shift, arg_uid_shift); + (void) dev_setup(directory, chown_uid, chown_uid); p = prefix_roota(directory, "/run/host"); - (void) make_inaccessible_nodes(p, arg_uid_shift, arg_uid_shift); + (void) make_inaccessible_nodes(p, chown_uid, chown_uid); r = setup_unix_export_host_inside(directory, unix_export_path); if (r < 0) return r; - r = setup_pts(directory); + r = setup_pts(directory, chown_uid); if (r < 0) return r; @@ -4280,8 +4365,8 @@ static int outer_child( directory, arg_custom_mounts, arg_n_custom_mounts, - arg_uid_shift, - arg_uid_range, + chown_uid, + chown_range, arg_selinux_apifs_context, MOUNT_NON_ROOT_ONLY); if (r < 0) @@ -4316,8 +4401,8 @@ static int outer_child( directory, arg_unified_cgroup_hierarchy, arg_userns_mode != USER_NAMESPACE_NO, - arg_uid_shift, - arg_uid_range, + chown_uid, + chown_range, arg_selinux_apifs_context, false); if (r < 0) @@ -4333,7 +4418,7 @@ static int outer_child( * (and fork for it) for which we then mount sysfs/procfs, and only then switch root. */ _cleanup_close_ int notify_fd = -EBADF; - if (arg_privileged) { + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { /* Mark everything as shared so our mounts get propagated down. This is required to make new * bind mounts available in systemd services inside the container that create a new mount * namespace. See https://github.com/systemd/systemd/issues/3860 Further submounts (such as @@ -4376,8 +4461,8 @@ static int outer_child( pid = raw_clone(SIGCHLD|CLONE_NEWNS| arg_clone_ns_flags | - (arg_userns_mode != USER_NAMESPACE_NO ? CLONE_NEWUSER : 0) | - ((arg_private_network && !arg_privileged) ? CLONE_NEWNET : 0)); + (IN_SET(arg_userns_mode, USER_NAMESPACE_FIXED, USER_NAMESPACE_PICK) ? CLONE_NEWUSER : 0) | + ((arg_private_network && arg_userns_mode == USER_NAMESPACE_MANAGED) ? CLONE_NEWNET : 0)); if (pid < 0) return log_error_errno(errno, "Failed to fork inner child: %m"); if (pid == 0) { @@ -4396,9 +4481,10 @@ static int outer_child( return log_error_errno(r, "Failed to join network namespace: %m"); } - if (!arg_privileged) { - /* In unprivileged operation, sysfs + procfs are special, we'll have to mount them - * inside the inner namespaces, but before we switch root. Hence do so here. */ + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + /* In managed usernamespace operation, sysfs + procfs are special, we'll have to + * mount them inside the inner namespaces, but before we switch root. Hence do so + * here. */ _cleanup_free_ char *j = path_join(directory, "/proc"); if (!j) return log_oom(); @@ -5161,6 +5247,8 @@ static int load_oci_bundle(void) { } static int run_container( + const char *directory, + int mount_fd, DissectedImage *dissected_image, int userns_fd, FDSet *fds, @@ -5248,9 +5336,8 @@ static int run_container( "Path %s doesn't refer to a network namespace, refusing.", arg_network_namespace_path); } - if (arg_privileged) { + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { assert(userns_fd < 0); - /* If we have no user namespace then we'll clone and create a new mount namespace right-away. */ *pid = raw_clone(SIGCHLD|CLONE_NEWNS); @@ -5260,7 +5347,6 @@ static int run_container( ", do you have namespace support enabled in your kernel? (You need UTS, IPC, PID and NET namespacing built in)" : ""); } else { assert(userns_fd >= 0); - /* If we have a user namespace then we'll clone() first, and then join the user namespace, * and then open the mount namespace, so that it is owned by the user namespace */ @@ -5298,7 +5384,8 @@ static int run_container( (void) reset_signal_mask(); r = outer_child(&barrier, - arg_directory, + directory, + mount_fd, dissected_image, fd_outer_socket_pair[1], fd_inner_socket_pair[1], @@ -5416,9 +5503,11 @@ static int run_container( if (!barrier_place_and_sync(&barrier)) /* #1 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Child died too early."); - r = setup_uid_map(*pid, bind_user_uid, n_bind_user_uid); - if (r < 0) - return r; + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { + r = setup_uid_map(*pid, bind_user_uid, n_bind_user_uid); + if (r < 0) + return r; + } (void) barrier_place(&barrier); /* #2 */ } @@ -5442,7 +5531,7 @@ static int run_container( return r; if (arg_network_veth) { - if (arg_privileged) { + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { r = setup_veth(arg_machine, *pid, veth_name, arg_network_bridge || arg_network_zone, &arg_network_provided_mac); if (r < 0) @@ -5580,7 +5669,7 @@ static int run_container( arg_unified_cgroup_hierarchy, arg_uid_shift, userns_fd, - arg_privileged); + arg_userns_mode); if (r < 0) return r; @@ -5622,7 +5711,7 @@ static int run_container( if (!barrier_sync(&barrier)) /* #5.1 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Child died too early."); - if (arg_userns_mode != USER_NAMESPACE_NO) { + if (!IN_SET(arg_userns_mode, USER_NAMESPACE_NO, USER_NAMESPACE_MANAGED)) { r = wipe_fully_visible_api_fs(mntns_fd); if (r < 0) return r; @@ -5749,7 +5838,7 @@ static int run_container( fd_kmsg_fifo = safe_close(fd_kmsg_fifo); - if (arg_private_network && arg_privileged) { + if (arg_private_network && arg_userns_mode != USER_NAMESPACE_MANAGED) { r = move_back_network_interfaces(child_netns_fd, arg_network_interfaces); if (r < 0) return r; @@ -5914,15 +6003,25 @@ static int cant_be_in_netns(void) { return 0; } +static void initialize_defaults(void) { + arg_privileged = getuid() == 0; + + /* If running unprivileged default to systemd-nsresourced operation */ + arg_userns_mode = arg_privileged ? USER_NAMESPACE_NO : USER_NAMESPACE_MANAGED; + + /* Imply private networking for unprivileged operation, since kernel otherwise refuses mounting sysfs */ + arg_private_network = !arg_privileged; +} + static int run(int argc, char *argv[]) { - bool remove_directory = false, remove_image = false, veth_created = false, remove_tmprootdir = false; - _cleanup_close_ int master = -EBADF, userns_fd = -EBADF; + bool remove_directory = false, remove_image = false, veth_created = false; + _cleanup_close_ int master = -EBADF, userns_fd = -EBADF, mount_fd = -EBADF; _cleanup_fdset_free_ FDSet *fds = NULL; int r, n_fd_passed, ret = EXIT_SUCCESS; char veth_name[IFNAMSIZ] = ""; struct ExposeArgs expose_args = {}; _cleanup_(release_lock_file) LockFile tree_global_lock = LOCK_FILE_INIT, tree_local_lock = LOCK_FILE_INIT; - char tmprootdir[] = "/tmp/nspawn-root-XXXXXX"; + _cleanup_(rmdir_and_freep) char *rootdir = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_(fw_ctx_freep) FirewallContext *fw_ctx = NULL; @@ -5930,7 +6029,7 @@ static int run(int argc, char *argv[]) { log_setup(); - arg_privileged = getuid() == 0; + initialize_defaults(); r = parse_argv(argc, argv); if (r <= 0) @@ -5987,14 +6086,14 @@ static int run(int argc, char *argv[]) { /* Reapply environment settings. */ (void) detect_unified_cgroup_hierarchy_from_environment(); - if (!arg_privileged) { + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { r = cg_all_unified(); if (r < 0) { log_error_errno(r, "Failed to determine if we are in unified cgroupv2 mode: %m"); goto finish; } if (r == 0) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unprivileged operation only supported in unified cgroupv2 mode."); + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Managed user namespace operation only supported in unified cgroupv2 mode."); } /* Ignore SIGPIPE here, because we use splice() on the ptyfwd stuff and that will generate SIGPIPE if @@ -6023,14 +6122,33 @@ static int run(int argc, char *argv[]) { if (arg_console_mode == CONSOLE_PIPE) /* if we pass STDERR on to the container, don't add our own logs into it too */ arg_quiet = true; - if (arg_directory) { - assert(!arg_image); + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + /* Let's allocate a 64K userns first, if managed mode is chosen */ - if (!arg_privileged) { - r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Invoking container from plain directory tree is currently not supported if called without privileges."); + _cleanup_free_ char *userns_name = NULL; + if (asprintf(&userns_name, "nspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0) { + r = log_oom(); goto finish; } + userns_fd = nsresource_allocate_userns(userns_name, UINT64_C(0x10000)); + if (userns_fd < 0) { + r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); + goto finish; + } + + r = userns_get_base_uid(userns_fd, &arg_uid_shift, /* ret_gid= */ NULL); + if (r < 0) { + log_error_errno(r, "Failed to determine UID shift from userns: %m"); + goto finish; + } + + arg_uid_range = UINT32_C(0x10000); + } + + if (arg_directory) { + assert(!arg_image); + /* Safety precaution: let's not allow running images from the live host OS image, as long as * /var from the host will propagate into container dynamically (because bad things happen if * two systems write to the same /var). Let's allow it for the special cases where /var is @@ -6200,6 +6318,15 @@ static int run(int argc, char *argv[]) { } } + if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + r = mountfsd_mount_directory( + arg_directory, + userns_fd, + determine_dissect_image_flags(), + &mount_fd); + if (r < 0) + goto finish; + } } else { DissectImageFlags dissect_image_flags = determine_dissect_image_flags(); @@ -6274,20 +6401,7 @@ static int run(int argc, char *argv[]) { dissect_image_flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; } - if (!mkdtemp(tmprootdir)) { - r = log_error_errno(errno, "Failed to create temporary directory: %m"); - goto finish; - } - - remove_tmprootdir = true; - - arg_directory = strdup(tmprootdir); - if (!arg_directory) { - r = log_oom(); - goto finish; - } - - if (arg_privileged) { + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { r = loop_device_make_by_path( arg_image, arg_read_only ? O_RDONLY : O_RDWR, @@ -6339,19 +6453,6 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; } else { - _cleanup_free_ char *userns_name = strjoin("nspawn-", arg_machine); - if (!userns_name) { - r = log_oom(); - goto finish; - } - - /* if we are unprivileged, let's allocate a 64K userns first */ - userns_fd = nsresource_allocate_userns(userns_name, UINT64_C(0x10000)); - if (userns_fd < 0) { - r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); - goto finish; - } - r = mountfsd_mount_image( arg_image, userns_fd, @@ -6370,7 +6471,14 @@ static int run(int argc, char *argv[]) { arg_architecture = dissected_image_architecture(dissected_image); } - r = custom_mount_prepare_all(arg_directory, arg_custom_mounts, arg_n_custom_mounts); + /* Create a temporary place to mount stuff. */ + r = mkdtemp_malloc("/tmp/nspawn-root-XXXXXX", &rootdir); + if (r < 0) { + log_error_errno(r, "Failed to create temporary directory: %m"); + goto finish; + } + + r = custom_mount_prepare_all(rootdir, arg_custom_mounts, arg_n_custom_mounts); if (r < 0) goto finish; @@ -6405,6 +6513,8 @@ static int run(int argc, char *argv[]) { } for (;;) { r = run_container( + rootdir, + mount_fd, dissected_image, userns_fd, fds, @@ -6447,12 +6557,7 @@ finish: log_warning_errno(errno, "Can't remove image file '%s', ignoring: %m", arg_image); } - if (remove_tmprootdir) { - if (rmdir(tmprootdir) < 0) - log_debug_errno(errno, "Can't remove temporary root directory '%s', ignoring: %m", tmprootdir); - } - - if (arg_machine && arg_privileged) { + if (arg_machine && arg_userns_mode != USER_NAMESPACE_MANAGED) { const char *p; p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); @@ -6466,7 +6571,7 @@ finish: expose_port_flush(&fw_ctx, arg_expose_ports, AF_INET, &expose_args.address4); expose_port_flush(&fw_ctx, arg_expose_ports, AF_INET6, &expose_args.address6); - if (arg_privileged) { + if (arg_userns_mode != USER_NAMESPACE_MANAGED) { if (veth_created) (void) remove_veth_links(veth_name, arg_network_veth_extra); (void) remove_bridge(arg_network_zone); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index ce1fa3a3d4..e4ae894782 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2170,6 +2170,9 @@ int dissected_image_mount( assert(m); + if (FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID)) /* For image based mounts we currently require an identity mapping */ + return -EOPNOTSUPP; + /* If 'where' is NULL then we'll use the new mount API to create fsmount() fds for the mounts and * store them in DissectedPartition.fsmount_fd. * @@ -4409,3 +4412,78 @@ int mountfsd_mount_image( return -EOPNOTSUPP; #endif } + +int mountfsd_mount_directory( + const char *path, + int userns_fd, + DissectImageFlags flags, + int *ret_mount_fd) { + + int r; + + /* Pick one identity, not both, that makes no sense. */ + assert(!FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID|DISSECT_IMAGE_IDENTITY_UID)); + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = sd_varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); + + r = sd_varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + + _cleanup_close_ int directory_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (directory_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + r = sd_varlink_push_dup_fd(vl, directory_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + + if (userns_fd >= 0) { + r = sd_varlink_push_dup_fd(vl, userns_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + } + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + vl, + "io.systemd.MountFileSystem.MountDirectory", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_UNSIGNED("directoryFileDescriptor", 0), + SD_JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(1)), + SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY)), + SD_JSON_BUILD_PAIR_STRING("mode", FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID) ? "foreign" : + FLAGS_SET(flags, DISSECT_IMAGE_IDENTITY_UID) ? "identity" : "auto"), + SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))); + if (r < 0) + return log_error_errno(r, "Failed to call MountDirectory() varlink call: %m"); + if (!isempty(error_id)) + return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to call MountDirectory() varlink call: %s", error_id); + + + static const sd_json_dispatch_field dispatch_table[] = { + { "mountFileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, 0, SD_JSON_MANDATORY }, + {} + }; + + unsigned fsmount_fd_idx = UINT_MAX; + r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &fsmount_fd_idx); + if (r < 0) + return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + + _cleanup_close_ int fsmount_fd = sd_varlink_take_fd(vl, fsmount_fd_idx); + if (fsmount_fd < 0) + return log_error_errno(fsmount_fd, "Failed to take mount fd from Varlink connection: %m"); + + *ret_mount_fd = TAKE_FD(fsmount_fd); + return 0; +} diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 2c118b97ef..c001915e89 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -89,6 +89,8 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_TRY_ATOMIC_MOUNT_EXCHANGE = 1 << 25, /* Try to mount the image beneath the specified mountpoint, rather than on top of it, and then umount the top */ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY = 1 << 26, /* Allow userspace verity keyring in /etc/verity.d/ and related dirs */ DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH = 1 << 27, /* Allow interactive authorization when going through mountfsd */ + DISSECT_IMAGE_FOREIGN_UID = 1 << 28, /* Request a foreign UID range mapping */ + DISSECT_IMAGE_IDENTITY_UID = 1 << 29, /* Explicitly request an identity UID range mapping */ } DissectImageFlags; struct DissectedImage { @@ -244,3 +246,4 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); int mountfsd_mount_image(const char *path, int userns_fd, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index c7a9984528..9e2d70b30a 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -1359,9 +1359,15 @@ int fd_make_mount_point(int fd) { return 1; } -int make_userns(uid_t uid_shift, uid_t uid_range, uid_t source_owner, uid_t dest_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; + uid_t source_base = 0; /* Allocates a userns file descriptor with the mapping we need. For this we'll fork off a child * process whose only purpose is to give us a new user namespace. It's killed when we got it. */ @@ -1369,8 +1375,18 @@ int make_userns(uid_t uid_shift, uid_t uid_range, uid_t source_owner, uid_t dest if (!userns_shift_range_valid(uid_shift, uid_range)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid UID range for user namespace."); - if (IN_SET(idmapping, REMOUNT_IDMAPPING_NONE, REMOUNT_IDMAPPING_HOST_ROOT)) { - if (asprintf(&line, UID_FMT " " UID_FMT " " UID_FMT "\n", 0u, uid_shift, uid_range) < 0) + switch (idmapping) { + + case REMOUNT_IDMAPPING_FOREIGN_WITH_HOST_ROOT: + source_base = FOREIGN_UID_BASE; + _fallthrough_; + + case REMOUNT_IDMAPPING_NONE: + case REMOUNT_IDMAPPING_HOST_ROOT: + + if (asprintf(&line, + UID_FMT " " UID_FMT " " UID_FMT "\n", + source_base, uid_shift, uid_range) < 0) return log_oom_debug(); /* If requested we'll include an entry in the mapping so that the host root user can make @@ -1387,28 +1403,34 @@ int make_userns(uid_t uid_shift, uid_t uid_range, uid_t source_owner, uid_t dest if (idmapping == REMOUNT_IDMAPPING_HOST_ROOT) if (strextendf(&line, UID_FMT " " UID_FMT " " UID_FMT "\n", - UID_MAPPED_ROOT, 0u, 1u) < 0) + UID_MAPPED_ROOT, (uid_t) 0u, (uid_t) 1u) < 0) return log_oom_debug(); - } - if (idmapping == REMOUNT_IDMAPPING_HOST_OWNER) { + break; + + case 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 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) + if (asprintf(&line, + UID_FMT " " UID_FMT " " UID_FMT "\n", + source_owner, uid_shift, (uid_t) 1u) < 0) return log_oom_debug(); - } + break; - if (idmapping == REMOUNT_IDMAPPING_HOST_OWNER_TO_TARGET_OWNER) { + case 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, + if (asprintf(&line, UID_FMT " " UID_FMT " " UID_FMT "\n", - source_owner, dest_owner, 1u) < 0) + source_owner, dest_owner, (uid_t) 1u) < 0) return log_oom_debug(); + break; + + default: + assert_not_reached(); } /* We always assign the same UID and GID ranges */ diff --git a/src/shared/mount-util.h b/src/shared/mount-util.h index 3235a3cc81..895fe2b2cd 100644 --- a/src/shared/mount-util.h +++ b/src/shared/mount-util.h @@ -156,6 +156,9 @@ typedef enum RemountIdmapping { * to add inodes to file systems mapped this way should set this flag, but given it comes with * certain security implications defaults to off, and requires explicit opt-in. */ REMOUNT_IDMAPPING_HOST_ROOT, + /* Much like REMOUNT_IDMAPPING_HOST_ROOT, but the source mapping is not from 0…65535 but from the + * foreign UID range. */ + REMOUNT_IDMAPPING_FOREIGN_WITH_HOST_ROOT, /* Define a mapping from root user within the container to the owner of the bind mounted directory. * This ensures no root-owned files will be written in a bind-mounted directory owned by a different * user. No other users are mapped. */ diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c index 43b812b0d2..78e7ce06ab 100644 --- a/src/shared/varlink-io.systemd.MountFileSystem.c +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -49,6 +49,31 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_OUTPUT(imageName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(imageUuid, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); +static SD_VARLINK_DEFINE_ENUM_TYPE( + MountMapMode, + SD_VARLINK_FIELD_COMMENT("Map the caller's UID to root in the user namespace, do not map anything else."), + SD_VARLINK_DEFINE_ENUM_VALUE(root), + SD_VARLINK_FIELD_COMMENT("Map the foreign UID range to the base UID range in the user namespace (i.e. UID zero and above), covering 64K users."), + SD_VARLINK_DEFINE_ENUM_VALUE(foreign), + SD_VARLINK_FIELD_COMMENT("Apply an identity (1:1) mapping, but limit it to 64K users."), + SD_VARLINK_DEFINE_ENUM_VALUE(identity), + SD_VARLINK_FIELD_COMMENT("Determine automatically based on provided directory and caller."), + SD_VARLINK_DEFINE_ENUM_VALUE(auto)); + +static SD_VARLINK_DEFINE_METHOD( + MountDirectory, + SD_VARLINK_FIELD_COMMENT("Directory file descriptor of the directory to assign to the user namespace. Must be a regular, i.e. non-O_PATH file descriptor."), + SD_VARLINK_DEFINE_INPUT(directoryFileDescriptor, SD_VARLINK_INT, 0), + SD_VARLINK_FIELD_COMMENT("File descriptor to the user namespace to assign this directory to. If not specified uses the host user namespace."), + SD_VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Whether to mark the resulting mount file descriptor as read-only. If not specified defaults to false."), + SD_VARLINK_DEFINE_INPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Which kinda of UID/GID mapping to apply to the resulting mount file descriptor."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(mode, MountMapMode, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT, + SD_VARLINK_FIELD_COMMENT("The freshly allocated mount file descriptor for the mount."), + SD_VARLINK_DEFINE_OUTPUT(mountFileDescriptor, SD_VARLINK_INT, 0)); + static SD_VARLINK_DEFINE_ERROR(IncompatibleImage); static SD_VARLINK_DEFINE_ERROR(MultipleRootPartitionsFound); static SD_VARLINK_DEFINE_ERROR(RootPartitionNotFound); @@ -59,9 +84,17 @@ static SD_VARLINK_DEFINE_ERROR(VerityFailure); SD_VARLINK_DEFINE_INTERFACE( io_systemd_MountFileSystem, "io.systemd.MountFileSystem", + SD_VARLINK_INTERFACE_COMMENT("APIs for unpriviliged mounting."), + SD_VARLINK_SYMBOL_COMMENT("Encodes the designated purpose of a partition."), &vl_type_PartitionDesignator, + SD_VARLINK_SYMBOL_COMMENT("Information about a specific partition."), &vl_type_PartitionInfo, + SD_VARLINK_SYMBOL_COMMENT("Selects the type of UID/GID mapping to apply."), + &vl_type_MountMapMode, + SD_VARLINK_SYMBOL_COMMENT("Takes a disk image file descriptor as input, returns a set of mount file descriptors for it."), &vl_method_MountImage, + SD_VARLINK_SYMBOL_COMMENT("Takes a directory file descriptor as input, returns a mount file descriptor."), + &vl_method_MountDirectory, &vl_error_IncompatibleImage, &vl_error_MultipleRootPartitionsFound, &vl_error_RootPartitionNotFound, diff --git a/test/units/TEST-13-NSPAWN.nspawn.sh b/test/units/TEST-13-NSPAWN.nspawn.sh index cd37f4c65e..076e94c7b1 100755 --- a/test/units/TEST-13-NSPAWN.nspawn.sh +++ b/test/units/TEST-13-NSPAWN.nspawn.sh @@ -916,7 +916,7 @@ matrix_run_one() { --boot; then [[ "$IS_USERNS_SUPPORTED" == "yes" && "$api_vfs_writable" == "network" ]] && return 1 else - [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" = "network" ]] && return 1 + [[ "$IS_USERNS_SUPPORTED" == "no" && "$api_vfs_writable" == "network" ]] && return 1 fi if SYSTEMD_NSPAWN_UNIFIED_HIERARCHY="$cgroupsv2" SYSTEMD_NSPAWN_USE_CGNS="$use_cgns" SYSTEMD_NSPAWN_API_VFS_WRITABLE="$api_vfs_writable" \ @@ -1282,4 +1282,42 @@ testcase_dev_net_tun() { rm -fr "$root" } +testcase_unpriv_dir() { + if ! can_do_rootless_nspawn; then + echo "Skipping rootless test..." + return 0 + fi + + root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.unpriv.XXX)" + create_dummy_container "$root" + + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=no bash -c 'echo foobar')" "foobar" + + # Use an image owned by some freshly acquired container user + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=pick --private-users-ownership=chown bash -c 'echo foobar')" "foobar" + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=yes --private-users-ownership=chown bash -c 'echo foobar')" "foobar" + + # Now move back to root owned, and try to use fs idmapping + systemd-dissect --shift "$root" 0 + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=no --private-users-ownership=no bash -c 'echo foobar')" "foobar" + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=pick --private-users-ownership=map bash -c 'echo foobar')" "foobar" + + # Use an image owned by the foreign UID range first via direct mapping, and than via the managed uid logic + systemd-dissect --shift "$root" foreign + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=pick --private-users-ownership=foreign bash -c 'echo foobar')" "foobar" + assert_eq "$(systemd-nspawn --pipe --register=no -D "$root" --private-users=managed --private-network bash -c 'echo foobar')" "foobar" + + # Test unprivileged operation + chown testuser:testuser "$root/.." + + ls -al "/var/lib/machines" + ls -al "$root" + + assert_eq "$(run0 --pipe -u testuser systemd-nspawn --pipe --register=no -D "$root" --private-users=managed --private-network bash -c 'echo foobar')" "foobar" + assert_eq "$(run0 --pipe -u testuser systemd-nspawn --pipe --register=no -D "$root" --private-network bash -c 'echo foobar')" "foobar" + chown root:root "$root/.." + + rm -rf "$root" +} + run_testcases diff --git a/test/units/TEST-74-AUX-UTILS.userdbctl.sh b/test/units/TEST-74-AUX-UTILS.userdbctl.sh new file mode 100755 index 0000000000..b0b7472d61 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.userdbctl.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Root +userdbctl user root +userdbctl user 0 + +# Nobody +userdbctl user 65534 + +# The 16bit and 32bit -1 user cannot exist +(! userdbctl user 65535) +(! userdbctl user 4294967295) + +userdbctl user foreign-0 +userdbctl user 2147352576 +userdbctl user foreign-1 +userdbctl user 2147352577 +userdbctl user foreign-65534 +userdbctl user 2147418110 +(! userdbctl user foreign-65535) +(! userdbctl user 2147418111) +(! userdbctl user foreign-65536) +(! userdbctl user 2147418112) + +assert_eq "$(userdbctl user root -j | jq .uid)" 0 +assert_eq "$(userdbctl user foreign-0 -j | jq .uid)" 2147352576 +assert_eq "$(userdbctl user foreign-1 -j | jq .uid)" 2147352577 +assert_eq "$(userdbctl user foreign-65534 -j | jq .uid)" 2147418110 + +assert_eq "$(userdbctl user 0 -j | jq -r .userName)" root +assert_eq "$(userdbctl user 2147352576 -j | jq -r .userName)" foreign-0 +assert_eq "$(userdbctl user 2147352577 -j | jq -r .userName)" foreign-1 +assert_eq "$(userdbctl user 2147418110 -j | jq -r .userName)" foreign-65534 diff --git a/units/systemd-mountfsd.service.in b/units/systemd-mountfsd.service.in index ef413684c3..00a0827fbc 100644 --- a/units/systemd-mountfsd.service.in +++ b/units/systemd-mountfsd.service.in @@ -25,13 +25,7 @@ LimitNOFILE={{HIGH_RLIMIT_NOFILE}} LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes -ProtectProc=invisible -ProtectControlGroups=yes -ProtectHome=yes ProtectHostname=yes -ProtectKernelLogs=yes -ProtectKernelModules=yes -ProtectSystem=strict RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 RestrictRealtime=yes RestrictSUIDSGID=yes