diff --git a/TODO b/TODO index 29a1a8e7c4..7bff644109 100644 --- a/TODO +++ b/TODO @@ -268,6 +268,18 @@ Features: * use udev rule networkd ownership property to take ownership of network interfaces nspawn creates +* mountfsd/nsresourced + - userdb: maybe allow callers to map one uid to their own uid + - bpflsm: allow writes if resulting UID on disk would be userns' owner UID + - make encrypted DDIs work (password…) + - add API for creating a new file system from scratch (together with some + dm-integrity/HMAC key). Should probably work using systemd-repart (access + via varlink). + - add api to make an existing file "trusted" via dm-integry/HMAC key + - port: portabled + - port: tmpfiles, sysusers and similar + - lets see if we can make runtime bind mounts into unpriv nspawn work + * add a kernel cmdline switch (and cred?) for marking a system to be "headless", in which case we never open /dev/console for reading, only for writing. This would then mean: systemd-firstboot would process creds but not diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 449e284a91..3585ab0123 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -676,3 +676,21 @@ Tools using the Varlink protocol (such as `varlinkctl`) or sd-bus (such as service. Takes a file system path: if specified the tool will listen on an `AF_UNIX` stream socket on the specified path in addition to whatever else it would listen on. + +`systemd-mountfsd`: + +* `$SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES` – takes a boolean argument. If true + disk images from the usual disk image directories (`/var/lib/machines/`, + `/var/lib/confexts/`, …) will be considered "trusted", i.e. are validated + with a more relaxed image policy (typically not requiring Verity signature + checking) than those from other directories (where Verity signature checks + are mandatory). If false all images are treated the same, regardless if + placed in the usual disk image directories or elsewhere. If not set defaults + to a compile time setting. + +* `$SYSTEMD_MOUNTFSD_IMAGE_POLICY_TRUSTED`, + `$SYSTEMD_MOUNTFSD_IMAGE_POLICY_UNTRUSTED` – the default image policy to + apply to trusted and untrusted disk images. An image is considered trusted if + placed in a trusted disk image directory (see above), or if suitable polkit + authentication was acquired. See `systemd.image-policy(7)` for the valid + syntax for image policy strings. diff --git a/man/rules/meson.build b/man/rules/meson.build index 1ca5b105b3..cf2be55bbd 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -999,6 +999,7 @@ manpages = [ ['systemd-measure', '1', [], 'HAVE_TPM2 HAVE_BLKID HAVE_OPENSSL'], ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], + ['systemd-mountfsd.service', '8', ['systemd-mountfsd'], 'ENABLE_MOUNTFSD'], ['systemd-network-generator.service', '8', ['systemd-network-generator'], ''], ['systemd-networkd-wait-online.service', '8', @@ -1007,6 +1008,10 @@ manpages = [ ['systemd-networkd.service', '8', ['systemd-networkd'], 'ENABLE_NETWORKD'], ['systemd-notify', '1', [], ''], ['systemd-nspawn', '1', [], ''], + ['systemd-nsresourced.service', + '8', + ['systemd-nsresourced'], + 'ENABLE_NSRESOURCED'], ['systemd-oomd.service', '8', ['systemd-oomd'], 'ENABLE_OOMD'], ['systemd-path', '1', [], ''], ['systemd-pcrlock', diff --git a/man/systemd-mountfsd.service.xml b/man/systemd-mountfsd.service.xml new file mode 100644 index 0000000000..25c1568f33 --- /dev/null +++ b/man/systemd-mountfsd.service.xml @@ -0,0 +1,70 @@ + + + + + + + + systemd-mountfsd.service + systemd + + + + systemd-mountfsd.service + 8 + + + + systemd-mountfsd.service + systemd-mountfsd + Disk Image File System Mount Service + + + + systemd-mountfsd.service + /usr/lib/systemd/systemd-mountfsd + + + + Description + + systemd-mountfsd is a system service that dissects disk images, and returns mount + file descriptors for the file systems contained therein to clients, via a Varlink IPC API. + + The disk images provided must contain a raw file system image or must follow the Discoverable + Partitions Specification. Before mounting any file systems authenticity of the disk image is + established in one or a combination of the following ways: + + + If the disk image is located in a regular file in one of the directories + /var/lib/machines/, /var/lib/portables/, + /var/lib/extensions/, /var/lib/confexts/ or their + counterparts in the /etc/, /run/, + /usr/lib/ it is assumed to be trusted. + + If the disk image contains a Verity enabled disk image, along with a signature + partition with a key in the kernel keyring or in /etc/verity.d/ (and related + directories) the disk image is considered trusted. + + + This service provides one Varlink service: + io.systemd.MountFileSystem which accepts a file descriptor to a regular file or + block device, and returns a number of file descriptors referring to an fsmount() + file descriptor the client may then attach to a path of their choice. + + The returned mounts are automatically allowlisted in the per-user-namespace allowlist maintained by + systemd-nsresourced.service8. + + The file systems are automatically fsck'ed before mounting. + + + + See Also + + systemd1, + systemd-nsresourced.service8 + + + diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index 4b9c252784..c7359f298a 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -118,6 +118,28 @@ keeps track of running containers, and provides programming interfaces to interact with them. + + Unprivileged Operation + + systemd-nspawn may be invoked with or without privileges. The full functionality + is currently only available when invoked with privileges. When invoked without privileges, various + limitations apply, including, but not limited to: + + + Only disk image based containers are supported (i.e. ). + Directory based ones (i.e. ) are not supported. + + Machine registration via is not supported. + + Only and networking modes are supported. + + + When running in unprivileged mode, some needed functionality is provided via + systemd-mountfsd.service8 + and + systemd-nsresourced.service8 + + Options @@ -1910,6 +1932,8 @@ After=sys-subsystem-net-devices-ens1.device systemd.slice5 machinectl1 importctl1 + systemd-mountfsd.service8 + systemd-nsresourced.service8 btrfs8 diff --git a/man/systemd-nsresourced.service.xml b/man/systemd-nsresourced.service.xml new file mode 100644 index 0000000000..d0a561e492 --- /dev/null +++ b/man/systemd-nsresourced.service.xml @@ -0,0 +1,81 @@ + + + + + + + + systemd-nsresourced.service + systemd + + + + systemd-nsresourced.service + 8 + + + + systemd-nsresourced.service + systemd-nsresourced + User Namespace Resource Delegation Service + + + + systemd-nsresourced.service + /usr/lib/systemd/systemd-nsresourced + + + + Description + + systemd-nsresourced is a system service that permits transient delegation of a a + UID/GID range to a user namespace (see user_namespaces7) + allocated by a client, via a Varlink IPC API. + + Unprivileged clients may allocate a user namespace, and then request a UID/GID range to be assigned + to it via this service. The user namespace may then be used to run containers and other sandboxes, and/or + apply it to an id-mapped mount. + + Allocations of UIDs/GIDs this way are transient: when a user namespace goes away, its UID/GID range + is returned to the pool of available ranges. In order to ensure that clients cannot gain persistency in + their transient UID/GID range a BPF-LSM based policy is enforced that ensures that user namespaces set up + this way can only write to file systems they allocate themselves or that are explicitly allowlisted via + systemd-nsresourced. + + systemd-nsresourced automatically ensures that any registered UID ranges show up + in the system's NSS database via the User/Group Record + Lookup API via Varlink. + + Currently, only UID/GID ranges consisting of either exactly 1 or exactly 65536 UIDs/GIDs can be + registered with this service. Moreover, UIDs and GIDs are always allocated together, and + symmetrically. + + The service provides API calls to allowlist mounts (referenced via their mount file descriptors as + per Linux fsmount() API), to pass ownership of a cgroup subtree to the user + namespace and to delegate a virtual Ethernet device pair to the user namespace. When used in combination + this is sufficient to implement fully unprivileged container environments, as implemented by + systemd-nspawn1, fully + unprivileged RootImage= (see + systemd.exec5) or + fully unprivileged disk image tools such as + systemd-dissect1. + + This service provides one Varlink service: + io.systemd.NamespaceResource allows registering user namespaces, and assign mounts, + cgroups and network interfaces to it. + + + + See Also + + systemd1, + systemd-mountfsd.service8, + systemd-nspawn1, + systemd.exec5, + systemd-dissect1, + user_namespaces7 + + + diff --git a/meson.build b/meson.build index 1365ddc978..1a160960cc 100644 --- a/meson.build +++ b/meson.build @@ -272,6 +272,8 @@ conf.set_quoted('SYSTEMD_TEST_DATA', testdata_dir) conf.set_quoted('SYSTEMD_TTY_ASK_PASSWORD_AGENT_BINARY_PATH', bindir / 'systemd-tty-ask-password-agent') conf.set_quoted('SYSTEMD_UPDATE_HELPER_PATH', libexecdir / 'systemd-update-helper') conf.set_quoted('SYSTEMD_USERWORK_PATH', libexecdir / 'systemd-userwork') +conf.set_quoted('SYSTEMD_MOUNTWORK_PATH', libexecdir / 'systemd-mountwork') +conf.set_quoted('SYSTEMD_NSRESOURCEWORK_PATH', libexecdir / 'systemd-nsresourcework') conf.set_quoted('SYSTEMD_VERITYSETUP_PATH', libexecdir / 'systemd-veritysetup') conf.set_quoted('SYSTEM_CONFIG_UNIT_DIR', pkgsysconfdir / 'system') conf.set_quoted('SYSTEM_DATA_UNIT_DIR', systemunitdir) @@ -1597,6 +1599,8 @@ conf.set10('ENABLE_REMOTE', have) feature = get_option('vmspawn').disable_auto_if(conf.get('BUILD_MODE_DEVELOPER') == 0) conf.set10('ENABLE_VMSPAWN', feature.allowed()) +conf.set10('DEFAULT_MOUNTFSD_TRUSTED_DIRECTORIES', get_option('default-mountfsd-trusted-directories')) + foreach term : ['analyze', 'backlight', 'binfmt', @@ -1617,8 +1621,10 @@ foreach term : ['analyze', 'localed', 'logind', 'machined', + 'mountfsd', 'networkd', 'nscd', + 'nsresourced', 'nss-myhostname', 'nss-systemd', 'oomd', @@ -1839,6 +1845,79 @@ conf.set10('ENABLE_UKIFY', want_ukify) ##################################################################### check_efi_alignment_py = find_program('tools/check-efi-alignment.py') + +##################################################################### + +use_provided_vmlinux_h = false +use_generated_vmlinux_h = false +provided_vmlinux_h_path = get_option('vmlinux-h-path') + +# For the more complex BPF programs we really want a vmlinux.h (which is arch +# specific, but only somewhat bound to kernel version). Ideally the kernel +# development headers would ship that, but right now they don't. Hence address +# this in two ways: +# +# 1. Provide a vmlinux.h at build time +# 2. Generate the file on the fly where possible (which requires /sys/ to be mounted) +# +# We generally prefer the former (to support reproducible builds), but will +# fallback to the latter. + +if conf.get('BPF_FRAMEWORK') == 1 + enable_vmlinux_h = get_option('vmlinux-h') + + if enable_vmlinux_h == 'auto' + if provided_vmlinux_h_path != '' + use_provided_vmlinux_h = true + elif fs.exists('/sys/kernel/btf/vmlinux') and \ + bpftool.found() and \ + (host_machine.cpu_family() == build_machine.cpu_family()) and \ + host_machine.cpu_family() in ['x86_64', 'aarch64'] + + # We will only generate a vmlinux.h from the running + # kernel if the host and build machine are of the same + # family. Also for now we focus on x86_64 and aarch64, + # since other archs don't seem to be ready yet. + + use_generated_vmlinux_h = true + endif + elif enable_vmlinux_h == 'provided' + use_provided_vmlinux_h = true + elif enable_vmlinux_h == 'generated' + if not fs.exists('/sys/kernel/btf/vmlinux') + error('BTF data from kernel not available (/sys/kernel/btf/vmlinux missing), cannot generate vmlinux.h, but was asked to.') + endif + if not bpftool.found() + error('bpftool not available, cannot generate vmlinux.h, but was asked to.') + endif + use_generated_vmlinux_h = true + endif +endif + +if use_provided_vmlinux_h + if not fs.exists(provided_vmlinux_h_path) + error('Path to provided vmlinux.h does not exist.') + endif + vmlinux_h_dependency = [] + bpf_o_unstripped_cmd += ['-I' + fs.parent(provided_vmlinux_h_path)] + message('Using provided @0@'.format(provided_vmlinux_h_path)) +elif use_generated_vmlinux_h + vmlinux_h_dependency = custom_target( + 'vmlinux.h', + output: 'vmlinux.h', + command : [ bpftool, 'btf', 'dump', 'file', '/sys/kernel/btf/vmlinux', 'format', 'c' ], + capture : true) + + bpf_o_unstripped_cmd += ['-I' + fs.parent(vmlinux_h_dependency.full_path())] + message('Using generated @0@'.format(vmlinux_h_dependency.full_path())) +else + message('Using neither provided nor generated vmlinux.h, some features will not be available.') +endif + +conf.set10('HAVE_VMLINUX_H', use_provided_vmlinux_h or use_generated_vmlinux_h) + +##################################################################### + check_version_history_py = find_program('tools/check-version-history.py') elf2efi_py = find_program('tools/elf2efi.py') export_dbus_interfaces_py = find_program('tools/dbus_exporter.py') @@ -2201,11 +2280,13 @@ subdir('src/locale') subdir('src/login') subdir('src/machine') subdir('src/machine-id-setup') +subdir('src/mountfsd') subdir('src/modules-load') subdir('src/mount') subdir('src/network') subdir('src/notify') subdir('src/nspawn') +subdir('src/nsresourced') subdir('src/nss-myhostname') subdir('src/nss-mymachines') subdir('src/nss-resolve') diff --git a/meson_options.txt b/meson_options.txt index af9a006563..41a524b0dc 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -124,6 +124,8 @@ option('portabled', type : 'boolean', description : 'install the systemd-portabled stack') option('sysext', type : 'boolean', description : 'install the systemd-sysext stack') +option('mountfsd', type : 'boolean', + description : 'install the systemd-mountfsd stack') option('userdb', type : 'boolean', description : 'install the systemd-userdbd stack') option('homed', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, @@ -140,6 +142,8 @@ option('remote', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : description : 'support for "journal over the network"') option('create-log-dirs', type : 'boolean', description : 'create /var/log/journal{,/remote}') +option('nsresourced', type : 'boolean', + description : 'install the systemd-nsresourced stack') option('nss-myhostname', type : 'boolean', description : 'install nss-myhostname module') option('nss-mymachines', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, @@ -519,6 +523,13 @@ option('analyze', type: 'boolean', value: true, description : 'install systemd-analyze') option('bpf-compiler', type : 'combo', choices : ['clang', 'gcc'], - description: 'compiler used to build BPF programs') + description : 'compiler used to build BPF programs') option('bpf-framework', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, - description: 'build BPF programs from source code in restricted C') + description : 'build BPF programs from source code in restricted C') +option('vmlinux-h', type : 'combo', choices : ['auto', 'provided', 'generated', 'disabled'], + description : 'which vmlinux.h to use') +option('vmlinux-h-path', type : 'string', value : '', + description : 'path to vmlinux.h to use') + +option('default-mountfsd-trusted-directories', type : 'boolean', value: false, + description : 'controls whether mountfsd should apply a relaxed policy on DDIs in system DDI directories') diff --git a/network/80-namespace-ns.network b/network/80-namespace-ns.network new file mode 100644 index 0000000000..8a84de9ea7 --- /dev/null +++ b/network/80-namespace-ns.network @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT-0 +# +# This config file is installed as part of systemd. +# It may be freely copied and edited (following the MIT No Attribution license). +# +# To make local modifications, one of the following methods may be used: +# 1. add a drop-in file that extends this file by creating the +# /etc/systemd/network/80-namespace-ns.network.d/ directory and creating a +# new .conf file there. +# 2. copy this file into /etc/systemd/network or one of the other paths checked +# by systemd-networkd and edit it there. +# This file should not be edited in place, because it'll be overwritten on upgrades. + +# This network file matches the host-side of the virtual Ethernet link +# created by systemd-nsresourced's network support. See systemd-nsresourced(1) for +# details. + +[Match] +Kind=veth +Name=ns-* + +[Network] +# Default to using a /28 prefix, giving up to 13 addresses per namespace +Address=0.0.0.0/28 +LinkLocalAddressing=yes +DHCPServer=yes +IPMasquerade=both +LLDP=yes +EmitLLDP=customer-bridge +IPv6AcceptRA=no +IPv6SendRA=yes diff --git a/network/meson.build b/network/meson.build index 9df0bea760..a1a87272f7 100644 --- a/network/meson.build +++ b/network/meson.build @@ -11,6 +11,7 @@ if conf.get('ENABLE_NETWORKD') == 1 '80-container-ve.link', '80-container-vz.network', '80-container-vz.link', + '80-namespace-ns.network', '80-vm-vt.network', '80-vm-vt.link', '80-wifi-adhoc.network', diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index 5f1c5b072f..7b7c20a020 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -23,9 +23,11 @@ enable systemd-homed.service enable systemd-homed-activate.service enable systemd-homed-firstboot.service enable systemd-journald-audit.socket +enable systemd-mountfsd.socket enable systemd-network-generator.service enable systemd-networkd.service enable systemd-networkd-wait-online.service +enable systemd-nsresourced.socket enable systemd-pstore.service enable systemd-resolved.service enable systemd-sysext.service diff --git a/src/basic/cgroup-util.c b/src/basic/cgroup-util.c index 6991460f86..8303b37684 100644 --- a/src/basic/cgroup-util.c +++ b/src/basic/cgroup-util.c @@ -22,6 +22,7 @@ #include "log.h" #include "login-util.h" #include "macro.h" +#include "missing_fs.h" #include "missing_magic.h" #include "missing_threads.h" #include "mkdir.h" @@ -39,6 +40,38 @@ #include "user-util.h" #include "xattr-util.h" +int cg_path_open(const char *controller, const char *path) { + _cleanup_free_ char *fs = NULL; + int r; + + r = cg_get_path(controller, path, /* item=*/ NULL, &fs); + if (r < 0) + return r; + + return RET_NERRNO(open(fs, O_DIRECTORY|O_CLOEXEC)); +} + +int cg_cgroupid_open(int cgroupfs_fd, uint64_t id) { + _cleanup_close_ int fsfd = -EBADF; + + if (cgroupfs_fd < 0) { + fsfd = open("/sys/fs/cgroup", O_CLOEXEC|O_DIRECTORY); + if (fsfd < 0) + return -errno; + + cgroupfs_fd = fsfd; + } + + cg_file_handle fh = CG_FILE_HANDLE_INIT; + CG_FILE_HANDLE_CGROUPID(fh) = id; + + int fd = open_by_handle_at(cgroupfs_fd, &fh.file_handle, O_DIRECTORY|O_CLOEXEC); + if (fd < 0) + return -errno; + + return fd; +} + static int cg_enumerate_items(const char *controller, const char *path, FILE **ret, const char *item) { _cleanup_free_ char *fs = NULL; FILE *f; @@ -1404,7 +1437,7 @@ int cg_pid_get_machine_name(pid_t pid, char **ret_machine) { int cg_path_get_cgroupid(const char *path, uint64_t *ret) { cg_file_handle fh = CG_FILE_HANDLE_INIT; - int mnt_id = -1; + int mnt_id; assert(path); assert(ret); @@ -1418,6 +1451,20 @@ int cg_path_get_cgroupid(const char *path, uint64_t *ret) { return 0; } +int cg_fd_get_cgroupid(int fd, uint64_t *ret) { + cg_file_handle fh = CG_FILE_HANDLE_INIT; + int mnt_id = -1; + + assert(fd >= 0); + assert(ret); + + if (name_to_handle_at(fd, "", &fh.file_handle, &mnt_id, AT_EMPTY_PATH) < 0) + return -errno; + + *ret = CG_FILE_HANDLE_CGROUPID(fh); + return 0; +} + int cg_path_get_session(const char *path, char **ret_session) { _cleanup_free_ char *unit = NULL; char *start, *end; diff --git a/src/basic/cgroup-util.h b/src/basic/cgroup-util.h index 244f3b657b..9ad6dd8eb6 100644 --- a/src/basic/cgroup-util.h +++ b/src/basic/cgroup-util.h @@ -180,6 +180,9 @@ typedef enum CGroupUnified { * generate paths with multiple adjacent / removed. */ +int cg_path_open(const char *controller, const char *path); +int cg_cgroupid_open(int fsfd, uint64_t id); + int cg_enumerate_processes(const char *controller, const char *path, FILE **ret); int cg_read_pid(FILE *f, pid_t *ret); int cg_read_pidref(FILE *f, PidRef *ret); @@ -267,6 +270,7 @@ int cg_is_empty_recursive(const char *controller, const char *path); int cg_get_root_path(char **path); int cg_path_get_cgroupid(const char *path, uint64_t *ret); +int cg_fd_get_cgroupid(int fd, uint64_t *ret); int cg_path_get_session(const char *path, char **ret_session); int cg_path_get_owner_uid(const char *path, uid_t *ret_uid); int cg_path_get_unit(const char *path, char **ret_unit); @@ -352,5 +356,10 @@ typedef union { uint8_t space[offsetof(struct file_handle, f_handle) + sizeof(uint64_t)]; } cg_file_handle; -#define CG_FILE_HANDLE_INIT { .file_handle.handle_bytes = sizeof(uint64_t) } +#define CG_FILE_HANDLE_INIT \ + (cg_file_handle) { \ + .file_handle.handle_bytes = sizeof(uint64_t), \ + .file_handle.handle_type = FILEID_KERNFS, \ + } + #define CG_FILE_HANDLE_CGROUPID(fh) (*(uint64_t*) (fh).file_handle.f_handle) diff --git a/src/basic/lock-util.h b/src/basic/lock-util.h index 91b332f803..8fb4757968 100644 --- a/src/basic/lock-util.h +++ b/src/basic/lock-util.h @@ -17,7 +17,7 @@ static inline int make_lock_file(const char *p, int operation, LockFile *ret) { int make_lock_file_for(const char *p, int operation, LockFile *ret); void release_lock_file(LockFile *f); -#define LOCK_FILE_INIT { .dir_fd = -EBADF, .fd = -EBADF } +#define LOCK_FILE_INIT (LockFile) { .dir_fd = -EBADF, .fd = -EBADF } /* POSIX locks with the same interface as flock(). */ int posix_lock(int fd, int operation); diff --git a/src/basic/missing_fs.h b/src/basic/missing_fs.h index ee70ae08db..d97b190131 100644 --- a/src/basic/missing_fs.h +++ b/src/basic/missing_fs.h @@ -111,3 +111,8 @@ assert_cc(FS_PROJINHERIT_FL == 0x20000000); #else assert_cc(FS_KEY_DESCRIPTOR_SIZE == 8); #endif + +/* linux/exportfs.h */ +#ifndef FILEID_KERNFS +#define FILEID_KERNFS 0xfe +#endif diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index c537c0f9b7..5b4e43f921 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -34,6 +34,14 @@ const struct namespace_info namespace_info[] = { #define pid_namespace_path(pid, type) procfs_file_alloca(pid, namespace_info[type].proc_path) +static NamespaceType clone_flag_to_namespace_type(unsigned long clone_flag) { + for (NamespaceType t = 0; t < _NAMESPACE_TYPE_MAX; t++) + if (((namespace_info[t].clone_flag ^ clone_flag) & (CLONE_NEWCGROUP|CLONE_NEWIPC|CLONE_NEWNET|CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWUTS|CLONE_NEWTIME)) == 0) + return t; + + return _NAMESPACE_TYPE_INVALID; +} + int namespace_open( pid_t pid, int *ret_pidns_fd, @@ -214,6 +222,88 @@ int detach_mount_namespace(void) { return 0; } +int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid) { + int r; + + /* Tried detach_mount_namespace() first. If that doesn't work due to permissions, opens up an + * unprivileged user namespace with a mapping of the originating UID/GID to the specified target + * UID/GID. Then, tries detach_mount_namespace() again. + * + * Or in other words: tries much harder to get a mount namespace, making use of unprivileged user + * namespaces if need be. + * + * Note that after this function completed: + * + * → if we had privs, afterwards uids/gids on files and processes are as before + * + * → if we had no privs, our own id and all our files will show up owned by target_uid/target_gid, + * and everything else owned by nobody. + * + * Yes, that's quite a difference. */ + + if (!uid_is_valid(target_uid)) + return -EINVAL; + if (!gid_is_valid(target_gid)) + return -EINVAL; + + r = detach_mount_namespace(); + if (r != -EPERM) + return r; + + if (unshare(CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to acquire user namespace: %m"); + + r = write_string_filef("/proc/self/uid_map", 0, + UID_FMT " " UID_FMT " 1\n", target_uid, getuid()); + if (r < 0) + return log_debug_errno(r, "Failed to write uid map: %m"); + + r = write_string_file("/proc/self/setgroups", "deny", 0); + if (r < 0) + return log_debug_errno(r, "Failed to write setgroups file: %m"); + + r = write_string_filef("/proc/self/gid_map", 0, + GID_FMT " " GID_FMT " 1\n", target_gid, getgid()); + if (r < 0) + return log_debug_errno(r, "Failed to write gid map: %m"); + + return detach_mount_namespace(); +} + +int detach_mount_namespace_userns(int userns_fd) { + int r; + + assert(userns_fd >= 0); + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to join user namespace: %m"); + + r = reset_uid_gid(); + if (r < 0) + return log_debug_errno(r, "Failed to become root in user namespace: %m"); + + return detach_mount_namespace(); +} + +int userns_acquire_empty(void) { + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_close_ int userns_fd = -EBADF; + int r; + + r = safe_fork("(sd-mkuserns)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL|FORK_NEW_USERNS, &pid); + if (r < 0) + return r; + if (r == 0) + /* Child. We do nothing here, just freeze until somebody kills us. */ + freeze(); + + r = namespace_open(pid, NULL, NULL, NULL, &userns_fd, NULL); + if (r < 0) + return log_error_errno(r, "Failed to open userns fd: %m"); + + return TAKE_FD(userns_fd); +} + int userns_acquire(const char *uid_map, const char *gid_map) { char path[STRLEN("/proc//uid_map") + DECIMAL_STR_MAX(pid_t) + 1]; _cleanup_(sigkill_waitp) pid_t pid = 0; @@ -344,3 +434,50 @@ int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_r return 0; } + +int namespace_open_by_type(NamespaceType type) { + const char *p; + int fd; + + assert(type >= 0); + assert(type < _NAMESPACE_TYPE_MAX); + + p = pid_namespace_path(0, type); + + fd = RET_NERRNO(open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC)); + if (fd == -ENOENT && proc_mounted() == 0) + return -ENOSYS; + + return fd; +} + +int is_our_namespace(int fd, NamespaceType request_type) { + int clone_flag; + + assert(fd >= 0); + + clone_flag = ioctl(fd, NS_GET_NSTYPE); + if (clone_flag < 0) + return -errno; + + NamespaceType found_type = clone_flag_to_namespace_type(clone_flag); + if (found_type < 0) + return -EBADF; /* Uh? Unknown namespace type? */ + + if (request_type >= 0 && request_type != found_type) /* It's a namespace, but not of the right type? */ + return -EUCLEAN; + + struct stat st_fd, st_ours; + if (fstat(fd, &st_fd) < 0) + return -errno; + + const char *p = pid_namespace_path(0, found_type); + if (stat(p, &st_ours) < 0) { + if (errno == ENOENT) + return proc_mounted() == 0 ? -ENOSYS : -ENOENT; + + return -errno; + } + + return stat_inode_same(&st_ours, &st_fd); +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index 34cbec3f6e..545952a5d1 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -34,6 +34,8 @@ int namespace_enter(int pidns_fd, int mntns_fd, int netns_fd, int userns_fd, int int fd_is_ns(int fd, unsigned long nsflag); int detach_mount_namespace(void); +int detach_mount_namespace_harder(uid_t target_uid, gid_t target_gid); +int detach_mount_namespace_userns(int userns_fd); static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { /* Checks that the specified userns range makes sense, i.e. contains at least one UID, and the end @@ -50,8 +52,15 @@ static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { return true; } +int userns_acquire_empty(void); int userns_acquire(const char *uid_map, const char *gid_map); + int netns_acquire(void); + int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type); int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); + +int namespace_open_by_type(NamespaceType type); + +int is_our_namespace(int fd, NamespaceType type); diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 90eb0cd911..4fc79c07c2 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -10,6 +10,7 @@ #include "format-util.h" #include "macro.h" #include "path-util.h" +#include "process-util.h" #include "sort-util.h" #include "stat-util.h" #include "uid-range.h" @@ -204,7 +205,7 @@ int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_rang return 0; } -int uid_range_load_userns(UIDRange **ret, const char *path) { +int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode mode) { _cleanup_(uid_range_freep) UIDRange *range = NULL; _cleanup_fclose_ FILE *f = NULL; int r; @@ -216,9 +217,11 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { * To simplify things this will modify the passed array in case of later failure. */ assert(ret); + assert(mode >= 0); + assert(mode < _UID_RANGE_USERNS_MODE_MAX); if (!path) - path = "/proc/self/uid_map"; + path = IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "/proc/self/uid_map" : "/proc/self/gid_map"; f = fopen(path, "re"); if (!f) { @@ -243,7 +246,11 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { if (r < 0) return r; - r = uid_range_add_internal(&range, uid_base, uid_range, /* coalesce = */ false); + r = uid_range_add_internal( + &range, + IN_SET(mode, UID_RANGE_USERNS_INSIDE, GID_RANGE_USERNS_INSIDE) ? uid_base : uid_shift, + uid_range, + /* coalesce = */ false); if (r < 0) return r; } @@ -253,3 +260,103 @@ int uid_range_load_userns(UIDRange **ret, const char *path) { *ret = TAKE_PTR(range); return 0; } + +int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret) { + _cleanup_(close_pairp) int pfd[2] = EBADF_PAIR; + _cleanup_(sigkill_waitp) pid_t pid = 0; + ssize_t n; + char x; + int r; + + assert(userns_fd >= 0); + assert(mode >= 0); + assert(mode < _UID_RANGE_USERNS_MODE_MAX); + assert(ret); + + if (pipe2(pfd, O_CLOEXEC) < 0) + return -errno; + + r = safe_fork_full( + "(sd-mkuserns)", + /* stdio_fds= */ NULL, + (int[]) { pfd[1], userns_fd }, 2, + FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL, + &pid); + if (r < 0) + return r; + if (r == 0) { + /* Child. */ + + if (setns(userns_fd, CLONE_NEWUSER) < 0) { + log_debug_errno(errno, "Failed to join userns: %m"); + _exit(EXIT_FAILURE); + } + + userns_fd = safe_close(userns_fd); + + n = write(pfd[1], &(const char) { 'x' }, 1); + if (n < 0) { + log_debug_errno(errno, "Failed to write to fifo: %m"); + _exit(EXIT_FAILURE); + } + assert(n == 1); + + freeze(); + } + + pfd[1] = safe_close(pfd[1]); + + n = read(pfd[0], &x, 1); + if (n < 0) + return -errno; + if (n == 0) + return -EPROTO; + assert(n == 1); + assert(x == 'x'); + + const char *p = procfs_file_alloca( + pid, + IN_SET(mode, UID_RANGE_USERNS_INSIDE, UID_RANGE_USERNS_OUTSIDE) ? "uid_map" : "gid_map"); + + return uid_range_load_userns(ret, p, mode); +} + +bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr) { + + if (!range) + return false; + + /* Avoid overflow */ + if (start > UINT32_MAX - nr) + nr = UINT32_MAX - start; + + if (nr == 0) + return false; + + FOREACH_ARRAY(entry, range->entries, range->n_entries) + if (start < entry->start + entry->nr && + start + nr >= entry->start) + return true; + + return false; +} + +bool uid_range_equal(const UIDRange *a, const UIDRange *b) { + if (a == b) + return true; + + if (!a || !b) + return false; + + if (a->n_entries != b->n_entries) + return false; + + for (size_t i = 0; i < a->n_entries; i++) { + if (a->entries[i].start != b->entries[i].start) + return false; + if (a->entries[i].nr != b->entries[i].nr) + return false; + } + + return true; +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index 5c4aac3726..48dfebbf02 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -33,4 +33,46 @@ static inline bool uid_range_contains(const UIDRange *range, uid_t uid) { int uid_map_read_one(FILE *f, uid_t *ret_base, uid_t *ret_shift, uid_t *ret_range); -int uid_range_load_userns(UIDRange **ret, const char *path); +static inline size_t uid_range_entries(const UIDRange *range) { + return range ? range->n_entries : 0; +} + +static inline unsigned uid_range_size(const UIDRange *range) { + if (!range) + return 0; + + unsigned n = 0; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + n += e->nr; + + return n; +} + +static inline bool uid_range_is_empty(const UIDRange *range) { + + if (!range) + return true; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + if (e->nr > 0) + return false; + + return true; +} + +bool uid_range_equal(const UIDRange *a, const UIDRange *b); + +typedef enum UIDRangeUsernsMode { + UID_RANGE_USERNS_INSIDE, + UID_RANGE_USERNS_OUTSIDE, + GID_RANGE_USERNS_INSIDE, + GID_RANGE_USERNS_OUTSIDE, + _UID_RANGE_USERNS_MODE_MAX, + _UID_RANGE_USERNS_MODE_INVALID = -EINVAL, +} UIDRangeUsernsMode; + +int uid_range_load_userns(UIDRange **ret, const char *path, UIDRangeUsernsMode mode); +int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange **ret); + +bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); diff --git a/src/core/manager.c b/src/core/manager.c index 0103c582b5..b333be9afd 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -4805,7 +4805,7 @@ static int short_uid_range(const char *path) { /* Taint systemd if we the UID range assigned to this environment doesn't at least cover 0…65534, * i.e. from root to nobody. */ - r = uid_range_load_userns(&p, path); + r = uid_range_load_userns(&p, path, UID_RANGE_USERNS_INSIDE); if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) return false; if (r < 0) diff --git a/src/core/namespace.c b/src/core/namespace.c index 855ad0de89..3ade7913c7 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2114,6 +2114,7 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; _cleanup_strv_free_ char **hierarchies = NULL; _cleanup_(mount_list_done) MountList ml = {}; + _cleanup_close_ int userns_fd = -EBADF; bool require_prefix = false; const char *root; DissectImageFlags dissect_image_flags = @@ -2150,40 +2151,57 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); - r = loop_device_make_by_path( - p->root_image, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */, - /* sector_size= */ UINT32_MAX, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, - LOCK_SH, - &loop_device); - if (r < 0) - return log_debug_errno(r, "Failed to create loop device for root image: %m"); + if (p->runtime_scope == RUNTIME_SCOPE_SYSTEM) { + /* In system mode we mount directly */ - r = dissect_loop_device( - loop_device, - p->verity, - p->root_image_options, - p->root_image_policy, - dissect_image_flags, - &dissected_image); - if (r < 0) - return log_debug_errno(r, "Failed to dissect image: %m"); + r = loop_device_make_by_path( + p->root_image, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means writable if possible, read-only as fallback */, + /* sector_size= */ UINT32_MAX, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop_device); + if (r < 0) + return log_debug_errno(r, "Failed to create loop device for root image: %m"); - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop_device->fd, - p->verity); - if (r < 0) - return r; + r = dissect_loop_device( + loop_device, + p->verity, + p->root_image_options, + p->root_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_decrypt( - dissected_image, - NULL, - p->verity, - dissect_image_flags); - if (r < 0) - return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop_device->fd, + p->verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + dissected_image, + NULL, + p->verity, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to decrypt dissected image: %m"); + } else { + userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); + + r = mountfsd_mount_image( + p->root_image, + userns_fd, + p->root_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + return r; + } } if (p->root_directory) @@ -2547,16 +2565,18 @@ int setup_namespace(const NamespaceParameters *p, char **error_path) { root, /* uid_shift= */ UID_INVALID, /* uid_range= */ UID_INVALID, - /* userns_fd= */ -EBADF, + userns_fd, dissect_image_flags); if (r < 0) return log_debug_errno(r, "Failed to mount root image: %m"); /* Now release the block device lock, so that udevd is free to call BLKRRPART on the device * if it likes. */ - r = loop_device_flock(loop_device, LOCK_UN); - if (r < 0) - return log_debug_errno(r, "Failed to release lock on loopback block device: %m"); + if (loop_device) { + r = loop_device_flock(loop_device, LOCK_UN); + if (r < 0) + return log_debug_errno(r, "Failed to release lock on loopback block device: %m"); + } r = dissected_image_relinquish(dissected_image); if (r < 0) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index e4cc536092..d68c29cbf1 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -31,10 +31,12 @@ #include "log.h" #include "loop-util.h" #include "main-func.h" +#include "missing_syscall.h" #include "mkdir.h" #include "mount-util.h" #include "mountpoint-util.h" #include "namespace-util.h" +#include "nsresource.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -91,6 +93,7 @@ static char **arg_argv = NULL; static char *arg_loop_ref = NULL; static ImagePolicy *arg_image_policy = NULL; static bool arg_mtree_hash = true; +static bool arg_via_service = false; STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); @@ -704,6 +707,18 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } + r = getenv_bool("SYSTEMD_USE_MOUNTFSD"); + if (r < 0) { + if (r != -ENXIO) + return log_error_errno(r, "Failed to parse $SYSTEMD_USE_MOUNTFSD: %m"); + } else + arg_via_service = r; + + if (!IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_DISCOVER, ACTION_VALIDATE) && geteuid() != 0) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be root."); + + SET_FLAG(arg_flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH, isatty(STDIN_FILENO)); + return 1; } @@ -841,7 +856,11 @@ static int get_extension_scopes(DissectedImage *m, ImageClass class, char ***ret return 1; } -static int action_dissect(DissectedImage *m, LoopDevice *d) { +static int action_dissect( + DissectedImage *m, + LoopDevice *d, + int userns_fd) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; _cleanup_(table_unrefp) Table *t = NULL; _cleanup_free_ char *bn = NULL; @@ -849,7 +868,6 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { int r; assert(m); - assert(d); r = path_extract_filename(arg_image, &bn); if (r < 0) @@ -875,7 +893,7 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) { fflush(stdout); } - r = dissected_image_acquire_metadata(m, 0); + r = dissected_image_acquire_metadata(m, userns_fd, /* extra_flags= */ 0); if (r == -ENXIO) return log_error_errno(r, "No root partition discovered."); if (r == -EUCLEAN) @@ -1087,7 +1105,6 @@ static int action_mount(DissectedImage *m, LoopDevice *d) { int r; assert(m); - assert(d); assert(arg_action == ACTION_MOUNT); r = dissected_image_mount_and_warn( @@ -1100,9 +1117,11 @@ static int action_mount(DissectedImage *m, LoopDevice *d) { if (r < 0) return r; - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } r = dissected_image_relinquish(m); if (r < 0) @@ -1390,8 +1409,8 @@ static int archive_item( } #endif -static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d) { - _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; +static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d, int userns_fd) { + _cleanup_(umount_and_freep) char *mounted_dir = NULL; _cleanup_free_ char *t = NULL; const char *root; int r; @@ -1400,12 +1419,16 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD if (arg_image) { assert(m); - assert(d); - r = detach_mount_namespace(); + if (userns_fd < 0) + r = detach_mount_namespace_harder(0, 0); + else + r = detach_mount_namespace_userns(userns_fd); if (r < 0) return log_error_errno(r, "Failed to detach mount namespace: %m"); + /* Create a place we can mount things onto soon. We use a fixed path shared by all invocations. Given + * the mounts are done in a mount namespace there's not going to be a collision here */ r = get_common_dissect_directory(&t); if (r < 0) return log_error_errno(r, "Failed generate private mount directory: %m"); @@ -1422,9 +1445,11 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD mounted_dir = TAKE_PTR(t); - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } r = dissected_image_relinquish(m); if (r < 0) @@ -1433,6 +1458,8 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD root = mounted_dir ?: arg_root; + dissected_image_close(m); + switch (arg_action) { case ACTION_COPY_FROM: { @@ -1716,7 +1743,6 @@ static int action_with(DissectedImage *m, LoopDevice *d) { int r, rcode; assert(m); - assert(d); assert(arg_action == ACTION_WITH); r = tempfn_random_child(NULL, program_invocation_short_name, &temp); @@ -1745,9 +1771,11 @@ static int action_with(DissectedImage *m, LoopDevice *d) { if (r < 0) return log_error_errno(r, "Failed to relinquish DM and loopback block devices: %m"); - r = loop_device_flock(d, LOCK_UN); - if (r < 0) - return log_error_errno(r, "Failed to unlock loopback block device: %m"); + if (d) { + r = loop_device_flock(d, LOCK_UN); + if (r < 0) + return log_error_errno(r, "Failed to unlock loopback block device: %m"); + } rcode = safe_fork("(with)", FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_WAIT, NULL); if (rcode == 0) { @@ -1788,14 +1816,16 @@ static int action_with(DissectedImage *m, LoopDevice *d) { } /* Let's manually detach everything, to make things synchronous */ - r = loop_device_flock(d, LOCK_SH); - if (r < 0) - log_warning_errno(r, "Failed to lock loopback block device, ignoring: %m"); + if (d) { + r = loop_device_flock(d, LOCK_SH); + if (r < 0) + log_warning_errno(r, "Failed to lock loopback block device, ignoring: %m"); + } r = umount_recursive(mounted_dir, 0); if (r < 0) log_warning_errno(r, "Failed to unmount '%s', ignoring: %m", mounted_dir); - else + else if (d) loop_device_unrelinquish(d); /* Let's try to destroy the loopback device */ created_dir = TAKE_PTR(mounted_dir); @@ -1982,8 +2012,8 @@ static int action_validate(void) { static int run(int argc, char *argv[]) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_(loop_device_unrefp) LoopDevice *d = NULL; - uint32_t loop_flags; - int open_flags, r; + _cleanup_close_ int userns_fd = -EBADF; + int r; log_setup(); @@ -2015,7 +2045,7 @@ static int run(int argc, char *argv[]) { return action_discover(); default: - /* All other actions need the image dissected */ + /* All other actions need the image dissected (except for ACTION_VALIDATE, see below) */ break; } @@ -2031,50 +2061,92 @@ static int run(int argc, char *argv[]) { * hence if there's external Verity data * available we turn off partition table * support */ + } - if (arg_action == ACTION_VALIDATE) - return action_validate(); + if (arg_action == ACTION_VALIDATE) + return action_validate(); - open_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR; - loop_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN; - if (arg_in_memory) - r = loop_device_make_by_path_memory(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); - else - r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); - if (r < 0) - return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); + if (arg_image) { + /* First try locally, if we are allowed to */ + if (!arg_via_service) { + uint32_t loop_flags; + int open_flags; - if (arg_loop_ref) { - r = loop_device_set_filename(d, arg_loop_ref); - if (r < 0) - log_warning_errno(r, "Failed to set loop reference string to '%s', ignoring: %m", arg_loop_ref); + open_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : O_RDWR; + loop_flags = FLAGS_SET(arg_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN; + + if (arg_in_memory) + r = loop_device_make_by_path_memory(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); + else + r = loop_device_make_by_path(arg_image, open_flags, /* sector_size= */ UINT32_MAX, loop_flags, LOCK_SH, &d); + if (r < 0) { + if (!ERRNO_IS_PRIVILEGE(r) || !IN_SET(arg_action, ACTION_DISSECT, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO)) + return log_error_errno(r, "Failed to set up loopback device for %s: %m", arg_image); + + log_debug_errno(r, "Lacking permissions to set up loopback block device for %s, using service: %m", arg_image); + arg_via_service = true; + } else { + if (arg_loop_ref) { + r = loop_device_set_filename(d, arg_loop_ref); + if (r < 0) + log_warning_errno(r, "Failed to set loop reference string to '%s', ignoring: %m", arg_loop_ref); + } + + r = dissect_loop_device_and_warn( + d, + &arg_verity_settings, + /* mount_options= */ NULL, + arg_image_policy, + arg_flags, + &m); + if (r < 0) + return r; + + if (arg_action == ACTION_ATTACH) + return action_attach(m, d); + + r = dissected_image_load_verity_sig_partition( + m, + d->fd, + &arg_verity_settings); + if (r < 0) + return log_error_errno(r, "Failed to load verity signature partition: %m"); + + if (arg_action != ACTION_DISSECT) { + r = dissected_image_decrypt_interactively( + m, NULL, + &arg_verity_settings, + arg_flags); + if (r < 0) + return r; + } + } } - r = dissect_loop_device_and_warn( - d, - &arg_verity_settings, - /* mount_options= */ NULL, - arg_image_policy, - arg_flags, - &m); - if (r < 0) - return r; + /* Try via service */ + if (arg_via_service) { + if (arg_in_memory) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--in-memory= not supported when operating via systemd-mountfsd."); - if (arg_action == ACTION_ATTACH) - return action_attach(m, d); + if (arg_loop_ref) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--loop-ref= not supported when operating via systemd-mountfsd."); - r = dissected_image_load_verity_sig_partition( - m, - d->fd, - &arg_verity_settings); - if (r < 0) - return log_error_errno(r, "Failed to load verity signature partition: %m"); + if (verity_settings_set(&arg_verity_settings)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Externally configured verity settings not supported when operating via systemd-mountfsd."); - if (arg_action != ACTION_DISSECT) { - r = dissected_image_decrypt_interactively( - m, NULL, - &arg_verity_settings, - arg_flags); + /* Don't run things in private userns, if the mount shall be attached to the host */ + if (!IN_SET(arg_action, ACTION_MOUNT, ACTION_WITH)) { + userns_fd = nsresource_allocate_userns(/* name= */ NULL, UINT64_C(0x10000)); /* allocate 64K users by default */ + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); + } + + r = mountfsd_mount_image( + arg_image, + userns_fd, + arg_image_policy, + arg_flags, + &m); if (r < 0) return r; } @@ -2083,7 +2155,7 @@ static int run(int argc, char *argv[]) { switch (arg_action) { case ACTION_DISSECT: - return action_dissect(m, d); + return action_dissect(m, d, userns_fd); case ACTION_MOUNT: return action_mount(m, d); @@ -2093,7 +2165,7 @@ static int run(int argc, char *argv[]) { case ACTION_COPY_FROM: case ACTION_COPY_TO: case ACTION_MAKE_ARCHIVE: - return action_list_or_mtree_or_copy_or_make_archive(m, d); + return action_list_or_mtree_or_copy_or_make_archive(m, d, userns_fd); case ACTION_WITH: return action_with(m, d); diff --git a/src/libsystemd/sd-device/device-monitor.c b/src/libsystemd/sd-device/device-monitor.c index 018a68d1db..408cd17581 100644 --- a/src/libsystemd/sd-device/device-monitor.c +++ b/src/libsystemd/sd-device/device-monitor.c @@ -473,7 +473,7 @@ static bool check_sender_uid(sd_device_monitor *m, uid_t uid) { return true; if (!m->mapped_userns_uid_range) { - r = uid_range_load_userns(&m->mapped_userns_uid_range, NULL); + r = uid_range_load_userns(&m->mapped_userns_uid_range, NULL, UID_RANGE_USERNS_INSIDE); if (r < 0) log_monitor_errno(m, r, "Failed to load UID ranges mapped to the current user namespace, ignoring: %m"); } diff --git a/src/mountfsd/io.systemd.mount-file-system.policy b/src/mountfsd/io.systemd.mount-file-system.policy new file mode 100644 index 0000000000..6a151eb437 --- /dev/null +++ b/src/mountfsd/io.systemd.mount-file-system.policy @@ -0,0 +1,70 @@ + + + + + + + + The systemd Project + https://systemd.io + + + + + Allow mounting of file system image + Authentication is required for an application to mount a file system image. + + auth_admin_keep + auth_admin_keep + yes + + + + + + Allow mounting of untrusted file system image + Authentication is required for an application to mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-image + + + + + Allow private mounting of trusted file system image + Authentication is required for an application to privately mount a file system image or an image whose cryptographic signature is recognized. + + yes + yes + yes + + + + + Allow private mounting of untrusted file system image + Authentication is required for an application to privately mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized. + + auth_admin + auth_admin + auth_admin + + + io.systemd.mount-file-system.mount-image-privately + + diff --git a/src/mountfsd/meson.build b/src/mountfsd/meson.build new file mode 100644 index 0000000000..3689d2af27 --- /dev/null +++ b/src/mountfsd/meson.build @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +systemd_mountwork_sources = files( + 'mountwork.c', +) + +systemd_mountfsd_sources = files( + 'mountfsd.c', + 'mountfsd-manager.c', +) + +executables += [ + libexec_template + { + 'name' : 'systemd-mountfsd', + 'conditions' : ['ENABLE_MOUNTFSD'], + 'sources' : systemd_mountfsd_sources, + }, + libexec_template + { + 'name' : 'systemd-mountwork', + 'conditions' : ['ENABLE_MOUNTFSD'], + 'sources' : systemd_mountwork_sources, + 'link_with' : common_libs, + 'dependencies' : common_deps, + }, +] + +install_data('io.systemd.mount-file-system.policy', + install_dir : polkitpolicydir) diff --git a/src/mountfsd/mountfsd-manager.c b/src/mountfsd/mountfsd-manager.c new file mode 100644 index 0000000000..b05c6e8493 --- /dev/null +++ b/src/mountfsd/mountfsd-manager.c @@ -0,0 +1,277 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#include "sd-daemon.h" + +#include "build-path.h" +#include "common-signal.h" +#include "env-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "mountfsd-manager.h" +#include "process-util.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "umask-util.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int on_worker_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + assert_se(!set_remove(m->workers_dynamic, s) != !set_remove(m->workers_fixed, s)); + sd_event_source_disable_unref(s); + + if (si->si_code == CLD_EXITED) { + if (si->si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", si->si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", si->si_pid, si->si_status); + } else if (si->si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else if (si->si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else + log_warning("Got unexpected exit code via SIGCHLD, ignoring."); + + (void) start_workers(m, /* explicit_request= */ false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + (void) start_workers(m, /* explicit_request= */ true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + event_source_hash_ops, + sd_event_source, + (void (*)(const sd_event_source*, struct siphash*)) trivial_hash_func, + (int (*)(const sd_event_source*, const sd_event_source*)) trivial_compare_func, + sd_event_source_disable_unref); + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -EBADF, + .worker_ratelimit = { + .interval = 5 * USEC_PER_SEC, + .burst = 50, + }, + }; + + r = sd_event_new(&m->event); + if (r < 0) + return r; + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_event_set_watchdog(m->event, true); + if (r < 0) + log_debug_errno(r, "Failed to enable watchdog handling, ignoring: %m"); + + r = sd_event_add_signal(m->event, NULL, SIGUSR2|SD_EVENT_SIGNAL_PROCMASK, on_sigusr2, m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + /* Note: we rely on PR_DEATHSIG to kill the workers for us */ + + sd_event_unref(m->event); + + return mfree(m); +} + +static int start_one_worker(Manager *m) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source = NULL; + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < MOUNTFS_WORKERS_MIN; + + r = safe_fork_full( + "(sd-worker)", + /* stdio_fds= */ NULL, + &m->listen_fd, 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_CLOSE_ALL_FDS, + &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("MOUNTFS_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $MOUNTFS_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + + r = setenv_systemd_log_level(); + if (r < 0) { + log_error_errno(r, "Failed to set $SYSTEMD_LOG_LEVEL: %m"); + _exit(EXIT_FAILURE); + } + + r = invoke_callout_binary(SYSTEMD_MOUNTWORK_PATH, STRV_MAKE("systemd-mountwork", "xxxxxxxxxxxxxxxx")); /* With some extra space rename_process() can make use of */ + log_error_errno(r, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(m->event, &source, pid, WEXITED, on_worker_exit, m); + if (r < 0) + return log_error_errno(r, "Failed to watch child " PID_FMT ": %m", pid); + + r = set_ensure_put( + fixed ? &m->workers_fixed : &m->workers_dynamic, + &event_source_hash_ops, + source); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + TAKE_PTR(source); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + + log_debug("%zu workers running.", n); + + if (n >= MOUNTFS_WORKERS_MIN && (!explicit_request || n >= MOUNTFS_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + /* If we keep starting workers too often, let's fail the whole daemon, something is wrong */ + sd_event_exit(m->event, EXIT_FAILURE); + + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong."); + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +int manager_startup(Manager *m) { + int n; + + assert(m); + assert(m->listen_fd < 0); + + n = sd_listen_fds(false); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + if (n > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n); + if (n == 1) + m->listen_fd = SD_LISTEN_FDS_START; + else { + static const union sockaddr_union sockaddr = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/io.systemd.MountFileSystem", + }; + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + } + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, TIMEVAL_STORE(LISTEN_TIMEOUT_USEC), sizeof(struct timeval)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + return start_workers(m, /* explicit_request= */ false); +} diff --git a/src/mountfsd/mountfsd-manager.h b/src/mountfsd/mountfsd-manager.h new file mode 100644 index 0000000000..6bfbddcd17 --- /dev/null +++ b/src/mountfsd/mountfsd-manager.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "ratelimit.h" + +#define MOUNTFS_WORKERS_MIN 3 +#define MOUNTFS_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…MOUNTFS_WORKERS_MIN */ + Set *workers_dynamic; /* Workers MOUNTFS_WORKERS_MIN+1…MOUNTFS_WORKERS_MAX */ + + int listen_fd; + + RateLimit worker_ratelimit; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/mountfsd/mountfsd.c b/src/mountfsd/mountfsd.c new file mode 100644 index 0000000000..6073bd548e --- /dev/null +++ b/src/mountfsd/mountfsd.c @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "daemon-util.h" +#include "log.h" +#include "main-func.h" +#include "mountfsd-manager.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c new file mode 100644 index 0000000000..f598596215 --- /dev/null +++ b/src/mountfsd/mountwork.c @@ -0,0 +1,703 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-daemon.h" + +#include "argv-util.h" +#include "bus-polkit.h" +#include "chase.h" +#include "discover-image.h" +#include "dissect-image.h" +#include "env-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "io-util.h" +#include "main-func.h" +#include "missing_loop.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 "user-util.h" +#include "varlink.h" +#include "varlink-io.systemd.MountFileSystem.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) + +static const ImagePolicy image_policy_untrusted = { + .n_policies = 2, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +static int json_dispatch_image_policy(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + _cleanup_(image_policy_freep) ImagePolicy *q = NULL; + ImagePolicy **p = ASSERT_PTR(userdata); + int r; + + assert(p); + + if (json_variant_is_null(variant)) { + *p = image_policy_free(*p); + return 0; + } + + if (!json_variant_is_string(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name)); + + r = image_policy_from_string(json_variant_string(variant), &q); + if (r < 0) + return json_log(variant, flags, r, "JSON field '%s' is not a valid image policy.", strna(name)); + + image_policy_free(*p); + *p = TAKE_PTR(q); + return 0; +} + +typedef struct MountImageParameters { + unsigned image_fd_idx; + unsigned userns_fd_idx; + int read_only; + int growfs; + char *password; + ImagePolicy *image_policy; +} MountImageParameters; + +static void mount_image_parameters_done(MountImageParameters *p) { + assert(p); + + p->password = erase_and_free(p->password); + p->image_policy = image_policy_free(p->image_policy); +} + +static int validate_image_fd(int fd, MountImageParameters *p) { + int r, fl; + + assert(fd >= 0); + assert(p); + + r = fd_verify_regular(fd); + if (r < 0) + return r; + + fl = fd_verify_safe_flags(fd); + if (fl < 0) + return log_debug_errno(fl, "Image file descriptor has unsafe flags set: %m"); + + switch (fl & O_ACCMODE) { + + case O_RDONLY: + p->read_only = true; + break; + + case O_RDWR: + break; + + default: + return -EBADF; + } + + return 0; +} + +static int verify_trusted_image_fd_by_path(int fd) { + _cleanup_free_ char *p = NULL; + struct stat sta; + int r; + + assert(fd >= 0); + + r = secure_getenv_bool("SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES"); + if (r == -ENXIO) { + if (!DEFAULT_MOUNTFSD_TRUSTED_DIRECTORIES) { + log_debug("Trusted directory mechanism disabled at compile time."); + return false; + } + } else if (r < 0) { + log_debug_errno(r, "Failed to parse $SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES environment variable, not trusting any image."); + return false; + } else if (!r) { + log_debug("Trusted directory mechanism disabled via $SYSTEMD_MOUNTFSD_TRUSTED_DIRECTORIES environment variable."); + return false; + } + + r = fd_get_path(fd, &p); + if (r < 0) + return log_debug_errno(r, "Failed to get path of passed image file descriptor: %m"); + if (fstat(fd, &sta) < 0) + return log_debug_errno(errno, "Failed to stat() passed image file descriptor: %m"); + + log_debug("Checking if image '%s' is in trusted directories.", p); + + for (ImageClass c = 0; c < _IMAGE_CLASS_MAX; c++) + NULSTR_FOREACH(s, image_search_path[c]) { + _cleanup_close_ int dir_fd = -EBADF, inode_fd = -EBADF; + _cleanup_free_ char *q = NULL; + struct stat stb; + const char *e; + + r = chase(s, NULL, CHASE_SAFE, &q, &dir_fd); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to resolve search path '%s', ignoring: %m", s); + continue; + } + + /* Check that the inode refers to a file immediately inside the image directory, + * i.e. not the image directory itself, and nothing further down the tree */ + e = path_startswith(p, q); + if (isempty(e)) + continue; + + e += strspn(e, "/"); + if (!filename_is_valid(e)) + continue; + + r = chaseat(dir_fd, e, CHASE_SAFE, NULL, &inode_fd); + if (r < 0) + return log_error_errno(r, "Couldn't verify that specified image '%s' is in search path '%s': %m", p, s); + + if (fstat(inode_fd, &stb) < 0) + return log_error_errno(errno, "Failed to stat image file '%s/%s': %m", q, e); + + if (stat_inode_same(&sta, &stb)) { + log_debug("Image '%s' is *in* trusted directories.", p); + return true; /* Yay */ + } + } + + log_debug("Image '%s' is *not* in trusted directories.", p); + return false; +} + +static int determine_image_policy( + int image_fd, + bool trusted, + ImagePolicy *client_policy, + ImagePolicy **ret) { + + _cleanup_(image_policy_freep) ImagePolicy *envvar_policy = NULL; + const ImagePolicy *default_policy; + const char *envvar, *e; + int r; + + assert(image_fd >= 0); + assert(ret); + + if (trusted) { + envvar = "SYSTEMD_MOUNTFSD_IMAGE_POLICY_TRUSTED"; + default_policy = &image_policy_allow; + } else { + envvar = "SYSTEMD_MOUNTFSD_IMAGE_POLICY_UNTRUSTED"; + default_policy = &image_policy_untrusted; + } + + e = secure_getenv(envvar); + if (e) { + r = image_policy_from_string(e, &envvar_policy); + if (r < 0) + return log_error_errno(r, "Failed to parse image policy supplied via $%s: %m", envvar); + + default_policy = envvar_policy; + } + + return image_policy_intersect(default_policy, client_policy, ret); +} + +static int validate_userns(Varlink *link, int *userns_fd) { + int r; + + assert(link); + assert(userns_fd); + + if (*userns_fd < 0) + return 0; + + r = fd_verify_safe_flags(*userns_fd); + if (r < 0) + return log_debug_errno(r, "User namespace file descriptor has unsafe flags set: %m"); + + r = fd_is_ns(*userns_fd, CLONE_NEWUSER); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Our own host user namespace? Then close the fd, and handle it as if none was specified. */ + r = is_our_namespace(*userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to determine if user namespace provided by client is our own."); + if (r > 0) { + log_debug("User namespace provided by client is our own."); + *userns_fd = safe_close(*userns_fd); + } + + return 0; +} + +static int vl_method_mount_image( + Varlink *link, + JsonVariant *parameters, + VarlinkMethodFlags flags, + void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "imageFileDescriptor", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MountImageParameters, image_fd_idx), JSON_MANDATORY }, + { "userNamespaceFileDescriptor", JSON_VARIANT_UNSIGNED, json_dispatch_uint, offsetof(MountImageParameters, userns_fd_idx), 0 }, + { "readOnly", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MountImageParameters, read_only), 0 }, + { "growFileSystems", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MountImageParameters, growfs), 0 }, + { "password", JSON_VARIANT_STRING, json_dispatch_string, offsetof(MountImageParameters, password), 0 }, + { "imagePolicy", JSON_VARIANT_STRING, json_dispatch_image_policy, offsetof(MountImageParameters, image_policy), 0 }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + _cleanup_(mount_image_parameters_done) MountImageParameters p = { + .image_fd_idx = UINT_MAX, + .userns_fd_idx = UINT_MAX, + .read_only = -1, + .growfs = -1, + }; + _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *aj = NULL; + _cleanup_close_ int image_fd = -EBADF, userns_fd = -EBADF; + _cleanup_(image_policy_freep) ImagePolicy *use_policy = NULL; + Hashmap **polkit_registry = ASSERT_PTR(userdata); + _cleanup_free_ char *ps = NULL; + bool image_is_trusted = false; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + json_variant_sensitive(parameters); /* might contain passwords */ + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get client UID: %m"); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (p.image_fd_idx != UINT_MAX) { + image_fd = varlink_peek_dup_fd(link, p.image_fd_idx); + if (image_fd < 0) + return log_debug_errno(image_fd, "Failed to peek image fd from client: %m"); + } + + if (p.userns_fd_idx != UINT_MAX) { + userns_fd = 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"); + } + + r = validate_image_fd(image_fd, &p); + if (r < 0) + return r; + + r = validate_userns(link, &userns_fd); + if (r != 0) + return r; + + r = verify_trusted_image_fd_by_path(image_fd); + if (r < 0) + return r; + image_is_trusted = r; + + const char *polkit_details[] = { + "read_only", one_zero(p.read_only > 0), + 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-image"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-image"; + polkit_flags = 0; + } else { + /* Mount into a private user namespace */ + polkit_action = "io.systemd.mount-file-system.mount-image-privately"; + polkit_untrusted_action = "io.systemd.mount-file-system.mount-untrusted-image-privately"; + + /* If polkit is not around, let's allow mounting authenticated images by default */ + polkit_flags = POLKIT_DEFAULT_ALLOW; + } + + /* Let's definitely acquire the regular action privilege, for mounting properly signed images */ + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + polkit_action, + polkit_details, + /* good_user= */ UID_INVALID, + polkit_flags, + polkit_registry); + if (r <= 0) + return r; + + /* Generate the commmon 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 jsut create it + * here, if it is missing. */ + r = get_common_dissect_directory(NULL); + if (r < 0) + return r; + + r = loop_device_make( + image_fd, + p.read_only == 0 ? O_RDONLY : O_RDWR, + 0, + UINT64_MAX, + UINT32_MAX, + LO_FLAGS_PARTSCAN, + LOCK_EX, + &loop); + if (r < 0) + return r; + + DissectImageFlags dissect_flags = + (p.read_only == 0 ? DISSECT_IMAGE_READ_ONLY : 0) | + (p.growfs != 0 ? DISSECT_IMAGE_GROWFS : 0) | + DISSECT_IMAGE_DISCARD_ANY | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES | + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + + /* Let's see if we have acquired the privilege to mount untrusted images already */ + bool polkit_have_untrusted_action = + varlink_has_polkit_action(link, polkit_untrusted_action, polkit_details, polkit_registry); + + for (;;) { + use_policy = image_policy_free(use_policy); + ps = mfree(ps); + + /* We use the image policy for trusted images if either the path is below a trusted + * directory, or if we have already acquired a PK authentication that tells us that untrusted + * images are OK */ + bool use_trusted_policy = + image_is_trusted || + polkit_have_untrusted_action; + + r = determine_image_policy( + image_fd, + use_trusted_policy, + p.image_policy, + &use_policy); + if (r < 0) + return r; + + r = image_policy_to_string(use_policy, /* simplify= */ true, &ps); + if (r < 0) + return r; + + log_debug("Using image policy: %s", ps); + + r = dissect_loop_device( + loop, + &verity, + /* mount_options= */ NULL, + use_policy, + dissect_flags, + &di); + if (r == -ENOPKG) + return varlink_error(link, "io.systemd.MountFileSystem.IncompatibleImage", NULL); + if (r == -ENOTUNIQ) + return varlink_error(link, "io.systemd.MountFileSystem.MultipleRootPartitionsFound", NULL); + if (r == -ENXIO) + return varlink_error(link, "io.systemd.MountFileSystem.RootPartitionNotFound", NULL); + if (r == -ERFKILL) { + /* The image policy refused this, let's retry after trying to get PolicyKit */ + + if (!polkit_have_untrusted_action) { + log_debug("Denied by image policy. Trying a stronger polkit authentication before continuing."); + r = varlink_verify_polkit_async_full( + link, + /* bus= */ NULL, + polkit_untrusted_action, + polkit_details, + /* good_user= */ UID_INVALID, + /* flags= */ 0, /* NB: the image cannot be authenticated, hence unless PK is around to allow this anyway, fail! */ + polkit_registry); + if (r <= 0 && !ERRNO_IS_NEG_PRIVILEGE(r)) + return r; + if (r > 0) { + /* Try again, now that we know the client has enough privileges. */ + log_debug("Denied by image policy, retrying after polkit authentication."); + polkit_have_untrusted_action = true; + continue; + } + } + + return varlink_error(link, "io.systemd.MountFileSystem.DeniedByImagePolicy", NULL); + } + if (r < 0) + return r; + + /* Success */ + break; + } + + r = dissected_image_load_verity_sig_partition( + di, + loop->fd, + &verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + di, + p.password, + &verity, + dissect_flags); + if (r == -ENOKEY) /* new dm-verity userspace returns ENOKEY if the dm-verity signature key is not in + * key chain. That's great. */ + return varlink_error(link, "io.systemd.MountFileSystem.KeyNotFound", NULL); + if (r == -EBUSY) /* DM kernel subsystem is shit with returning useful errors hence we keep retrying + * under the assumption that some errors are transitional. Which the errors might + * not actually be. After all retries failed we return EBUSY. Let's turn that into a + * generic Verity error. It's not very helpful, could mean anything, but at least it + * gives client a clear idea that this has to do with Verity. */ + return varlink_error(link, "io.systemd.MountFileSystem.VerityFailure", NULL); + if (r < 0) + return r; + + r = dissected_image_mount( + di, + /* where= */ NULL, + /* uid_shift= */ UID_INVALID, + /* uid_range= */ UID_INVALID, + userns_fd, + dissect_flags); + if (r < 0) + return r; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + _cleanup_(json_variant_unrefp) JsonVariant *pj = NULL; + DissectedPartition *pp = di->partitions + d; + int fd_idx; + + if (!pp->found) + continue; + + if (pp->fsmount_fd < 0) + continue; + + if (userns_fd >= 0) { + r = nsresource_add_mount(userns_fd, pp->fsmount_fd); + if (r < 0) + return r; + } + + fd_idx = varlink_push_fd(link, pp->fsmount_fd); + if (fd_idx < 0) + return fd_idx; + + TAKE_FD(pp->fsmount_fd); + + r = json_build(&pj, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("designator", JSON_BUILD_STRING(partition_designator_to_string(d))), + JSON_BUILD_PAIR("writable", JSON_BUILD_BOOLEAN(pp->rw)), + JSON_BUILD_PAIR("growFileSystem", JSON_BUILD_BOOLEAN(pp->growfs)), + JSON_BUILD_PAIR_CONDITION(pp->partno > 0, "partitionNumber", JSON_BUILD_INTEGER(pp->partno)), + JSON_BUILD_PAIR_CONDITION(pp->architecture > 0, "architecture", JSON_BUILD_STRING(architecture_to_string(pp->architecture))), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(pp->uuid), "partitionUuid", JSON_BUILD_UUID(pp->uuid)), + JSON_BUILD_PAIR("fileSystemType", JSON_BUILD_STRING(dissected_partition_fstype(pp))), + JSON_BUILD_PAIR_CONDITION(pp->label, "partitionLabel", JSON_BUILD_STRING(pp->label)), + JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(pp->size)), + JSON_BUILD_PAIR("offset", JSON_BUILD_INTEGER(pp->offset)), + JSON_BUILD_PAIR("mountFileDescriptor", JSON_BUILD_INTEGER(fd_idx)))); + if (r < 0) + return r; + + r = json_variant_append_array(&aj, pj); + if (r < 0) + return r; + } + + loop_device_relinquish(loop); + + r = varlink_replyb(link, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("partitions", JSON_BUILD_VARIANT(aj)), + JSON_BUILD_PAIR("imagePolicy", JSON_BUILD_STRING(ps)), + JSON_BUILD_PAIR("imageSize", JSON_BUILD_INTEGER(di->image_size)), + JSON_BUILD_PAIR("sectorSize", JSON_BUILD_INTEGER(di->sector_size)), + JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(di->image_uuid), "imageUuid", JSON_BUILD_UUID(di->image_uuid)))); + if (r < 0) + return r; + + return r; +} + +static int process_connection(VarlinkServer *server, int _fd) { + _cleanup_close_ int fd = TAKE_FD(_fd); /* always take possesion */ + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + int r; + + r = sd_event_new(&event); + if (r < 0) + return r; + + r = varlink_server_attach_event(server, event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach Varlink server to event loop: %m"); + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) + return log_error_errno(r, "Failed to add connection: %m"); + + TAKE_FD(fd); + vl = varlink_ref(vl); + + r = varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for read: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for write: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + r = varlink_server_detach_event(server); + if (r < 0) + return log_error_errno(r, "Failed to detach Varlink server from event loop: %m"); + + return 0; +} + +static int run(int argc, char *argv[]) { + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL; + _cleanup_(pidref_done) PidRef parent = PIDREF_NULL; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup(); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, VARLINK_SERVER_INHERIT_USERDATA); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_add_interface(server, &vl_interface_io_systemd_MountFileSystem); + if (r < 0) + return log_error_errno(r, "Failed to add MountFileSystem interface to varlink server: %m"); + + r = varlink_server_bind_method_many( + server, + "io.systemd.MountFileSystem.MountImage",vl_method_mount_image); + if (r < 0) + return log_error_errno(r, "Failed to bind methods: %m"); + + varlink_server_set_userdata(server, &polkit_registry); + + r = varlink_server_set_exit_on_idle(server, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit-on-idle mode: %m"); + + r = getenv_bool("MOUNTFS_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse MOUNTFSD_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + r = pidref_set_parent(&parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire pidfd of parent process: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -EBADF; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + log_debug("Exiting worker, ran for %s, that's enough.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + log_debug("Exiting worker, been idle for %s.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-mountwork: waiting..."); + fd = RET_NERRNO(accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC)); + (void) rename_process("systemd-mountwork: processing..."); + + if (fd == -EAGAIN) + continue; /* The listening socket has SO_RECVTIMEO set, hence a timeout is expected + * after a while, let's check if it's time to exit though. */ + if (fd == -EINTR) + continue; /* Might be that somebody attached via strace, let's just continue in that + * case */ + if (fd < 0) + return log_error_errno(fd, "Failed to accept() from listening socket: %m"); + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + r = fd_wait_for_event(listen_fd, POLLIN, 0); + if (r < 0) + return log_error_errno(r, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(r, POLLIN)) { + r = pidref_kill(&parent, SIGUSR2); + if (r == -ESRCH) + return log_error_errno(r, "Parent already died?"); + if (r < 0) + return log_error_errno(r, "Failed to send SIGUSR2 signal to parent. %m"); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nspawn/nspawn-cgroup.c b/src/nspawn/nspawn-cgroup.c index c4a784fd64..4f28b4a225 100644 --- a/src/nspawn/nspawn-cgroup.c +++ b/src/nspawn/nspawn-cgroup.c @@ -13,6 +13,7 @@ #include "mountpoint-util.h" #include "nspawn-cgroup.h" #include "nspawn-mount.h" +#include "nsresource.h" #include "path-util.h" #include "rm-rf.h" #include "string-util.h" @@ -46,38 +47,6 @@ static int chown_cgroup_path(const char *path, uid_t uid_shift) { return 0; } -int chown_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift) { - _cleanup_free_ char *path = NULL, *fs = NULL; - int r; - - r = cg_pid_get_path(NULL, pid, &path); - if (r < 0) - return log_error_errno(r, "Failed to get container cgroup path: %m"); - - r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, path, NULL, &fs); - if (r < 0) - return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); - - r = chown_cgroup_path(fs, uid_shift); - if (r < 0) - return log_error_errno(r, "Failed to chown() cgroup %s: %m", fs); - - if (unified_requested == CGROUP_UNIFIED_SYSTEMD || (unified_requested == CGROUP_UNIFIED_NONE && cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER) > 0)) { - _cleanup_free_ char *lfs = NULL; - /* Always propagate access rights from unified to legacy controller */ - - r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER_LEGACY, path, NULL, &lfs); - if (r < 0) - return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); - - r = chown_cgroup_path(lfs, uid_shift); - if (r < 0) - return log_error_errno(r, "Failed to chown() cgroup %s: %m", lfs); - } - - return 0; -} - int sync_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift) { _cleanup_free_ char *cgroup = NULL; char tree[] = "/tmp/unifiedXXXXXX", pid_string[DECIMAL_STR_MAX(pid) + 1]; @@ -142,7 +111,14 @@ finish: return r; } -int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested) { +int create_subcgroup( + pid_t pid, + bool keep_unit, + CGroupUnified unified_requested, + uid_t uid_shift, + int userns_fd, + bool privileged) { + _cleanup_free_ char *cgroup = NULL, *payload = NULL; CGroupMask supported; char *e; @@ -185,13 +161,54 @@ int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested) if (!payload) return log_oom(); - r = cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, payload, pid); + if (privileged) + 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) { + _cleanup_free_ char *fs = NULL; + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, payload, NULL, &fs); + if (r < 0) + return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); + + r = chown_cgroup_path(fs, uid_shift); + if (r < 0) + return log_error_errno(r, "Failed to chown() cgroup %s: %m", fs); + + } else if (userns_fd >= 0) { + _cleanup_close_ int cgroup_fd = -EBADF; + + cgroup_fd = cg_path_open(SYSTEMD_CGROUP_CONTROLLER, payload); + if (cgroup_fd < 0) + return log_error_errno(cgroup_fd, "Failed to open cgroup %s: %m", payload); + + r = cg_fd_attach(cgroup_fd, pid); + if (r < 0) + return log_error_errno(r, "Failed to add process " PID_FMT " to cgroup %s: %m", pid, payload); + + r = nsresource_add_cgroup(userns_fd, cgroup_fd); + if (r < 0) + return log_error_errno(r, "Failed to add cgroup %s to userns: %m", payload); + } + + if (unified_requested == CGROUP_UNIFIED_SYSTEMD || (unified_requested == CGROUP_UNIFIED_NONE && cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER) > 0)) { + _cleanup_free_ char *lfs = NULL; + /* Always propagate access rights from unified to legacy controller */ + + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER_LEGACY, payload, NULL, &lfs); + if (r < 0) + return log_error_errno(r, "Failed to get file system path for container cgroup: %m"); + + r = chown_cgroup_path(lfs, uid_shift); + if (r < 0) + return log_error_errno(r, "Failed to chown() cgroup %s: %m", lfs); + } + if (keep_unit) { _cleanup_free_ char *supervisor = NULL; - supervisor = path_join(cgroup, "supervisor"); if (!supervisor) return log_oom(); diff --git a/src/nspawn/nspawn-cgroup.h b/src/nspawn/nspawn-cgroup.h index 3f5ba622d8..7e2cd53ddc 100644 --- a/src/nspawn/nspawn-cgroup.h +++ b/src/nspawn/nspawn-cgroup.h @@ -6,9 +6,8 @@ #include "cgroup-util.h" -int chown_cgroup(pid_t pid, CGroupUnified unified_requested, uid_t uid_shift); 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); +int create_subcgroup(pid_t pid, bool keep_unit, CGroupUnified unified_requested, uid_t uid_shift, int userns_fd, bool privileged); 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 e94ffd799e..c2bd4f6c30 100644 --- a/src/nspawn/nspawn-mount.c +++ b/src/nspawn/nspawn-mount.c @@ -444,22 +444,38 @@ int tmpfs_patch_options( } int mount_sysfs(const char *dest, MountSettingsMask mount_settings) { - const char *full, *top; - int r; + _cleanup_free_ char *top = NULL, *full = NULL;; unsigned long extra_flags = 0; + int r; - top = prefix_roota(dest, "/sys"); - r = path_is_fs_type(top, SYSFS_MAGIC); + top = path_join(dest, "/sys"); + if (!top) + return log_oom(); + + r = path_is_mount_point(top); if (r < 0) - return log_error_errno(r, "Failed to determine filesystem type of %s: %m", top); - /* /sys might already be mounted as sysfs by the outer child in the - * !netns case. In this case, it's all good. Don't touch it because we - * don't have the right to do so, see https://github.com/systemd/systemd/issues/1555. - */ - if (r > 0) - return 0; + return log_error_errno(r, "Failed to determine if '%s' is a mountpoint: %m", top); + if (r == 0) { + /* If this is not a mount point yet, then mount a tmpfs there */ + r = mount_nofollow_verbose(LOG_ERR, "tmpfs", top, "tmpfs", MS_NOSUID|MS_NOEXEC|MS_NODEV, "mode=0555" TMPFS_LIMITS_SYS); + if (r < 0) + return r; + } else { + r = path_is_fs_type(top, SYSFS_MAGIC); + if (r < 0) + return log_error_errno(r, "Failed to determine filesystem type of %s: %m", top); - full = prefix_roota(top, "/full"); + /* /sys/ might already be mounted as sysfs by the outer child in the !netns case. In this case, it's + * all good. Don't touch it because we don't have the right to do so, see + * https://github.com/systemd/systemd/issues/1555. + */ + if (r > 0) + return 0; + } + + full = path_join(top, "/full"); + if (!full) + return log_oom(); (void) mkdir(full, 0755); @@ -501,10 +517,11 @@ int mount_sysfs(const char *dest, MountSettingsMask mount_settings) { if (rmdir(full) < 0) return log_error_errno(errno, "Failed to remove %s: %m", full); - /* Create mountpoint for cgroups. Otherwise we are not allowed since we - * remount /sys read-only. - */ - const char *x = prefix_roota(top, "/fs/cgroup"); + /* Create mountpoint for cgroups. Otherwise we are not allowed since we remount /sys/ read-only. */ + _cleanup_free_ char *x = path_join(top, "/fs/cgroup"); + if (!x) + return log_oom(); + (void) mkdir_p(x, 0755); return mount_nofollow_verbose(LOG_ERR, NULL, top, NULL, @@ -541,7 +558,7 @@ int mount_all(const char *dest, } MountPoint; static const MountPoint mount_table[] = { - /* First we list inner child mounts (i.e. mounts applied *after* entering user namespacing) */ + /* First we list inner child mounts (i.e. mounts applied *after* entering user namespacing when we are privileged) */ { "proc", "/proc", "proc", NULL, PROC_DEFAULT_MOUNT_FLAGS, MOUNT_FATAL|MOUNT_IN_USERNS|MOUNT_MKDIR|MOUNT_FOLLOW_SYMLINKS }, /* we follow symlinks here since not following them requires /proc/ already being mounted, which we don't have here. */ @@ -575,15 +592,15 @@ int mount_all(const char *dest, { "mqueue", "/dev/mqueue", "mqueue", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, MOUNT_IN_USERNS|MOUNT_MKDIR }, - /* Then we list outer child mounts (i.e. mounts applied *before* entering user namespacing) */ + /* Then we list outer child mounts (i.e. mounts applied *before* entering user namespacing when we are privileged) */ { "tmpfs", "/tmp", "tmpfs", "mode=01777" NESTED_TMPFS_LIMITS, MS_NOSUID|MS_NODEV|MS_STRICTATIME, MOUNT_FATAL|MOUNT_APPLY_TMPFS_TMP|MOUNT_MKDIR }, { "tmpfs", "/sys", "tmpfs", "mode=0555" TMPFS_LIMITS_SYS, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR }, + MOUNT_FATAL|MOUNT_APPLY_APIVFS_NETNS|MOUNT_MKDIR|MOUNT_PRIVILEGED }, { "sysfs", "/sys", "sysfs", NULL, SYS_DEFAULT_MOUNT_FLAGS, - MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_APPLY_APIVFS_RO|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* skipped if above was mounted */ { "sysfs", "/sys", "sysfs", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, - MOUNT_FATAL|MOUNT_MKDIR }, /* skipped if above was mounted */ + MOUNT_FATAL|MOUNT_MKDIR|MOUNT_PRIVILEGED }, /* 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, @@ -604,11 +621,11 @@ int mount_all(const char *dest, MOUNT_FATAL|MOUNT_IN_USERNS }, #if HAVE_SELINUX { "/sys/fs/selinux", "/sys/fs/selinux", NULL, NULL, MS_BIND, - MOUNT_MKDIR }, /* Bind mount first (mkdir/chown the mount point in case /sys/ is mounted as minimal skeleton tmpfs) */ + 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, - 0 }, /* Then, make it r/o (don't mkdir/chown the mount point here, the previous entry already did that) */ + 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, - 0 }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ + MOUNT_PRIVILEGED }, /* Turn off propagation (we only want that for the mount propagation tunnel dir) */ #endif }; @@ -617,6 +634,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 privileged = FLAGS_SET(mount_settings, MOUNT_PRIVILEGED); int r; for (size_t k = 0; k < ELEMENTSOF(mount_table); k++) { @@ -624,6 +642,10 @@ int mount_all(const char *dest, bool fatal = FLAGS_SET(mount_table[k].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(mount_table[k].mount_settings, MOUNT_PRIVILEGED) && !FLAGS_SET(mount_table[k].mount_settings, MOUNT_IN_USERNS)) + continue; + if (in_userns != FLAGS_SET(mount_table[k].mount_settings, MOUNT_IN_USERNS)) continue; diff --git a/src/nspawn/nspawn-mount.h b/src/nspawn/nspawn-mount.h index bf5e47dce4..9112f24b94 100644 --- a/src/nspawn/nspawn-mount.h +++ b/src/nspawn/nspawn-mount.h @@ -20,6 +20,7 @@ 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 in the outer child if we are running in privileged mode */ } MountSettingsMask; typedef enum CustomMountType { diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3d0a9a73e3..7edb45dd17 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -84,6 +84,7 @@ #include "nspawn-stub-pid1.h" #include "nspawn-util.h" #include "nspawn.h" +#include "nsresource.h" #include "nulstr-util.h" #include "os-util.h" #include "pager.h" @@ -237,6 +238,7 @@ static char *arg_settings_filename = NULL; static Architecture arg_architecture = _ARCHITECTURE_INVALID; static ImagePolicy *arg_image_policy = NULL; static char *arg_background = NULL; +static bool arg_privileged = false; STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_template, freep); @@ -518,6 +520,12 @@ 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) { + /* We only support the unified mode when running unprivileged */ + arg_unified_cgroup_hierarchy = CGROUP_UNIFIED_ALL; + return 0; + } + /* Let's inherit the mode to use from the host system, but let's take into consideration what systemd * in the image actually supports. */ r = cg_all_unified(); @@ -619,7 +627,6 @@ static int parse_mount_settings_env(void) { e = getenv("SYSTEMD_NSPAWN_API_VFS_WRITABLE"); if (streq_ptr(e, "network")) arg_mount_settings |= MOUNT_APPLY_APIVFS_RO|MOUNT_APPLY_APIVFS_NETNS; - else if (e) { r = parse_boolean(e); if (r < 0) @@ -1653,6 +1660,21 @@ static int parse_argv(int argc, char *argv[]) { static int verify_arguments(void) { int r; + SET_FLAG(arg_mount_settings, MOUNT_PRIVILEGED, arg_privileged); + + if (!arg_privileged) { + /* machined is not accessible to unpriv clients */ + if (arg_register) { + log_notice("Automatically implying --register=no, since machined is not accessible to unprivileged clients."); + arg_register = false; + } + + 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; + } + } + 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 * in the container supports, because we are not using it. Let's immediately pick the right @@ -2692,6 +2714,9 @@ static int reset_audit_loginuid(void) { if ((arg_clone_ns_flags & CLONE_NEWPID) == 0) return 0; + if (!arg_privileged) + return 0; + r = read_one_line_file("/proc/self/loginuid", &p); if (r == -ENOENT) return 0; @@ -2721,6 +2746,11 @@ 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."); + return 0; + } + (void) mkdir_p("/run/systemd/nspawn/", 0755); (void) mkdir_p("/run/systemd/nspawn/propagate", 0600); p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); @@ -2749,6 +2779,11 @@ 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."); + return 0; + } + r = mount_follow_verbose(LOG_ERR, NULL, NSPAWN_MOUNT_TUNNEL, NULL, MS_SLAVE, NULL); if (r < 0) return r; @@ -3270,20 +3305,32 @@ static int inner_child( return r; if (!arg_network_namespace_path && arg_private_network) { - r = unshare(CLONE_NEWNET); + _cleanup_close_ int netns_fd = -EBADF; + + if (arg_privileged) { + if (unshare(CLONE_NEWNET) < 0) + return log_error_errno(errno, "Failed to unshare network namespace: %m"); + } + + netns_fd = namespace_open_by_type(NAMESPACE_NET); + if (netns_fd < 0) + return log_error_errno(netns_fd, "Failed to open newly allocate network namespace: %m"); + + r = send_one_fd(fd_inner_socket, netns_fd, 0); if (r < 0) - return log_error_errno(errno, "Failed to unshare network namespace: %m"); + return log_error_errno(r, "Failed to send network namespace to supervisor: %m"); /* Tell the parent that it can setup network interfaces. */ (void) barrier_place(barrier); /* #3 */ } - r = mount_sysfs(NULL, arg_mount_settings); - if (r < 0) - return r; + if (arg_privileged) { + r = mount_sysfs(NULL, arg_mount_settings); + if (r < 0) + return r; + } - /* Wait until we are cgroup-ified, so that we - * can mount the right cgroup path writable */ + /* Wait until we are cgroup-ified, so that we can mount the right cgroup path writable */ if (!barrier_place_and_sync(barrier)) /* #4 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Parent died too early"); @@ -3584,11 +3631,11 @@ static int inner_child( return log_error_errno(errno, "execv(%s) failed: %m", exec_target); } -static int setup_notify_child(void) { +static int setup_notify_child(const void *directory) { _cleanup_close_ int fd = -EBADF; - static const union sockaddr_union sa = { + _cleanup_free_ char *j = NULL; + union sockaddr_union sa = { .un.sun_family = AF_UNIX, - .un.sun_path = NSPAWN_NOTIFY_SOCKET_PATH, }; int r; @@ -3596,7 +3643,17 @@ static int setup_notify_child(void) { if (fd < 0) return log_error_errno(errno, "Failed to allocate notification socket: %m"); - (void) mkdir_parents(NSPAWN_NOTIFY_SOCKET_PATH, 0755); + if (directory) { + j = path_join(directory, NSPAWN_NOTIFY_SOCKET_PATH); + if (!j) + return log_oom(); + } + + r = sockaddr_un_set_path(&sa.un, j ?: NSPAWN_NOTIFY_SOCKET_PATH); + if (r < 0) + return log_error_errno(r, "Failed to set AF_UNIX path to %s: %m", j ?: NSPAWN_NOTIFY_SOCKET_PATH); + + (void) mkdir_parents(sa.un.sun_path, 0755); (void) sockaddr_un_unlink(&sa.un); WITH_UMASK(0577) { /* only set "w" bit, which is all that's necessary for connecting from the container */ @@ -3605,7 +3662,7 @@ static int setup_notify_child(void) { return log_error_errno(errno, "bind(" NSPAWN_NOTIFY_SOCKET_PATH ") failed: %m"); } - r = userns_lchown(NSPAWN_NOTIFY_SOCKET_PATH, 0, 0); + r = userns_lchown(sa.un.sun_path, 0, 0); if (r < 0) return log_error_errno(r, "Failed to chown " NSPAWN_NOTIFY_SOCKET_PATH ": %m"); @@ -3621,6 +3678,11 @@ static int setup_unix_export_dir_outside(char **ret) { assert(ret); + if (!arg_privileged) { + log_debug("Not digging socket tunnel, because running unprivileged."); + return 0; + } + _cleanup_free_ char *p = NULL; p = path_join("/run/systemd/nspawn/unix-export", arg_machine); if (!p) @@ -3672,6 +3734,10 @@ static int setup_unix_export_host_inside(const char *directory, const char *unix int r; assert(directory); + + if (!arg_privileged) + return 0; + assert(unix_export_path); r = make_run_host(directory); @@ -3714,10 +3780,16 @@ static int setup_unix_export_host_inside(const char *directory, const char *unix static DissectImageFlags determine_dissect_image_flags(void) { return + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | DISSECT_IMAGE_USR_NO_ROOT | DISSECT_IMAGE_DISCARD_ON_LOOP | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES | (arg_read_only ? DISSECT_IMAGE_READ_ONLY : DISSECT_IMAGE_FSCK|DISSECT_IMAGE_GROWFS) | - DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | + (arg_console_mode == CONSOLE_INTERACTIVE ? DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH : 0); } static int outer_child( @@ -4093,47 +4165,59 @@ static int outer_child( return r; } - /* 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 /dev) done after this - * will inherit the shared propagation mode. - * - * IMPORTANT: Do not overmount the root directory anymore from now on to enable moving the root - * directory mount to root later on. - * https://github.com/systemd/systemd/issues/3847#issuecomment-562735251 - */ - r = mount_switch_root(directory, MS_SHARED); - if (r < 0) - return log_error_errno(r, "Failed to move root directory: %m"); + /* We have different codepaths here for privileged and non-privileged mode. In privileged mode we'll + * now switch into the target directory, and then do the final setup from there. If a user namespace + * is then allocated for the container, the root mount and everything else will be out of reach for + * it. For unprivileged containers we cannot do that however, since we couldn't mount a sysfs and + * procfs then anymore, since that only works if there's an unobstructed instance currently + * visible. Hence there we do it the other way round: we first allocate a new set set of namespaces + * (and fork for it) for which we then mount sysfs/procfs, and only then switch root. */ - /* We finished setting up the rootfs which is a shared mount. The mount tunnel needs to be a - * dependent mount otherwise we can't MS_MOVE mounts that were propagated from the host into - * the container. */ - r = mount_tunnel_open(); - if (r < 0) - return r; + if (arg_privileged) { + /* 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 + * /dev/) done after this will inherit the shared propagation mode. + * + * IMPORTANT: Do not overmount the root directory anymore from now on to enable moving the root + * directory mount to root later on. + * https://github.com/systemd/systemd/issues/3847#issuecomment-562735251 + */ + r = mount_switch_root(directory, MS_SHARED); + if (r < 0) + return log_error_errno(r, "Failed to move root directory: %m"); - if (arg_userns_mode != USER_NAMESPACE_NO) { - /* In order to mount procfs and sysfs in an unprivileged container the kernel - * requires that a fully visible instance is already present in the target mount - * namespace. Mount one here so the inner child can mount its own instances. Later - * we umount the temporary instances created here before we actually exec the - * payload. Since the rootfs is shared the umount will propagate into the container. - * Note, the inner child wouldn't be able to unmount the instances on its own since - * it doesn't own the originating mount namespace. IOW, the outer child needs to do - * this. */ - r = pin_fully_visible_fs(); + /* We finished setting up the rootfs which is a shared mount. The mount tunnel needs to be a + * dependent mount otherwise we can't MS_MOVE mounts that were propagated from the host into + * the container. */ + r = mount_tunnel_open(); if (r < 0) return r; - } - fd = setup_notify_child(); + if (arg_userns_mode != USER_NAMESPACE_NO) { + /* In order to mount procfs and sysfs in an unprivileged container the kernel + * requires that a fully visible instance is already present in the target mount + * namespace. Mount one here so the inner child can mount its own instances. Later + * we umount the temporary instances created here before we actually exec the + * payload. Since the rootfs is shared the umount will propagate into the container. + * Note, the inner child wouldn't be able to unmount the instances on its own since + * it doesn't own the originating mount namespace. IOW, the outer child needs to do + * this. */ + r = pin_fully_visible_fs(); + if (r < 0) + return r; + } + + fd = setup_notify_child(NULL); + } else + fd = setup_notify_child(directory); if (fd < 0) return fd; pid = raw_clone(SIGCHLD|CLONE_NEWNS| arg_clone_ns_flags | - (arg_userns_mode != USER_NAMESPACE_NO ? CLONE_NEWUSER : 0)); + (arg_userns_mode != USER_NAMESPACE_NO ? CLONE_NEWUSER : 0) | + ((arg_private_network && !arg_privileged) ? CLONE_NEWNET : 0)); if (pid < 0) return log_error_errno(errno, "Failed to fork inner child: %m"); if (pid == 0) { @@ -4152,6 +4236,26 @@ 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. */ + _cleanup_free_ char *j = path_join(directory, "/proc"); + if (!j) + return log_oom(); + + r = mount_follow_verbose(LOG_ERR, "proc", j, "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL); + if (r < 0) + return r; + + r = mount_sysfs(directory, arg_mount_settings); + if (r < 0) + return r; + + r = mount_switch_root(directory, MS_SHARED); + if (r < 0) + return log_error_errno(r, "Failed to move root directory: %m"); + } + r = inner_child(barrier, fd_inner_socket, fds, os_release_pairs); if (r < 0) _exit(EXIT_FAILURE); @@ -4433,6 +4537,9 @@ static int nspawn_dispatch_notify_fd(sd_event_source *source, int fd, uint32_t r static int setup_notify_parent(sd_event *event, int fd, pid_t *inner_child_pid, sd_event_source **notify_event_source) { int r; + if (fd < 0) + return 0; + r = sd_event_add_io(event, notify_event_source, fd, EPOLLIN, nspawn_dispatch_notify_fd, inner_child_pid); if (r < 0) return log_error_errno(r, "Failed to allocate notify event source: %m"); @@ -4812,26 +4919,28 @@ static int load_settings(void) { return 0; /* We first look in the admin's directories in /etc and /run */ - FOREACH_STRING(i, "/etc/systemd/nspawn", "/run/systemd/nspawn") { - _cleanup_free_ char *j = NULL; + if (arg_privileged) { + FOREACH_STRING(i, "/etc/systemd/nspawn", "/run/systemd/nspawn") { + _cleanup_free_ char *j = NULL; - j = path_join(i, arg_settings_filename); - if (!j) - return log_oom(); + j = path_join(i, arg_settings_filename); + if (!j) + return log_oom(); - f = fopen(j, "re"); - if (f) { - p = TAKE_PTR(j); + f = fopen(j, "re"); + if (f) { + p = TAKE_PTR(j); - /* By default, we trust configuration from /etc and /run */ - if (arg_settings_trusted < 0) - arg_settings_trusted = true; + /* By default, we trust configuration from /etc and /run */ + if (arg_settings_trusted < 0) + arg_settings_trusted = true; - break; + break; + } + + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open %s: %m", j); } - - if (errno != ENOENT) - return log_error_errno(errno, "Failed to open %s: %m", j); } if (!f) { @@ -4891,10 +5000,14 @@ static int load_oci_bundle(void) { static int run_container( DissectedImage *dissected_image, + int userns_fd, FDSet *fds, - char veth_name[IFNAMSIZ], bool *veth_created, + char veth_name[IFNAMSIZ], + bool *veth_created, struct ExposeArgs *expose_args, - int *master, pid_t *pid, int *ret) { + int *master, + pid_t *pid, + int *ret) { static const struct sigaction sa = { .sa_handler = nop_signal_handler, @@ -4979,11 +5092,44 @@ static int run_container( "Path %s doesn't refer to a network namespace, refusing.", arg_network_namespace_path); } - *pid = raw_clone(SIGCHLD|CLONE_NEWNS); - if (*pid < 0) - return log_error_errno(errno, "clone() failed%s: %m", - errno == EINVAL ? - ", do you have namespace support enabled in your kernel? (You need UTS, IPC, PID and NET namespacing built in)" : ""); + if (arg_privileged) { + assert(userns_fd < 0); + + /* If we have no user namespace then we'll clone and create a new mount namepsace right-away. */ + + *pid = raw_clone(SIGCHLD|CLONE_NEWNS); + if (*pid < 0) + return log_error_errno(errno, "clone() failed%s: %m", + errno == EINVAL ? + ", 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 */ + + *pid = raw_clone(SIGCHLD); + if (*pid < 0) + return log_error_errno(errno, "clone() failed: %m"); + + if (*pid == 0) { + if (setns(userns_fd, CLONE_NEWUSER) < 0) { + log_error_errno(errno, "Failed to join allocate user namespace: %m"); + _exit(EXIT_FAILURE); + } + + r = reset_uid_gid(); + if (r < 0) { + log_error_errno(r, "Failed to reset UID/GID to root: %m"); + _exit(EXIT_FAILURE); + } + + if (unshare(CLONE_NEWNS) < 0) { + log_error_errno(errno, "Failed to unshare file system namespace: %m"); + _exit(EXIT_FAILURE); + } + } + } if (*pid == 0) { /* The outer child only has a file system namespace. */ @@ -5120,19 +5266,13 @@ static int run_container( /* Wait until the child has unshared its network namespace. */ if (!barrier_place_and_sync(&barrier)) /* #3 */ return log_error_errno(SYNTHETIC_ERRNO(ESRCH), "Child died too early"); - } - if (child_netns_fd < 0) { - /* Make sure we have an open file descriptor to the child's network - * namespace so it stays alive even if the child exits. */ - r = namespace_open(*pid, - /* ret_pidns_fd = */ NULL, - /* ret_mntns_fd = */ NULL, - &child_netns_fd, - /* ret_userns_fd = */ NULL, - /* ret_root_fd = */ NULL); - if (r < 0) - return log_error_errno(r, "Failed to open child network namespace: %m"); + /* Make sure we have an open file descriptor to the child's network namespace so it + * stays alive even if the child exits. */ + assert(child_netns_fd < 0); + child_netns_fd = receive_one_fd(fd_inner_socket_pair[0], 0); + if (child_netns_fd < 0) + return log_error_errno(r, "Failed to receive child network namespace: %m"); } r = move_network_interfaces(child_netns_fd, arg_network_interfaces); @@ -5140,12 +5280,29 @@ static int run_container( return r; if (arg_network_veth) { - r = setup_veth(arg_machine, *pid, veth_name, - arg_network_bridge || arg_network_zone, &arg_network_provided_mac); - if (r < 0) - return r; - else if (r > 0) - ifi = r; + if (arg_privileged) { + r = setup_veth(arg_machine, *pid, veth_name, + arg_network_bridge || arg_network_zone, &arg_network_provided_mac); + if (r < 0) + return r; + else if (r > 0) + ifi = r; + } else { + _cleanup_free_ char *host_ifname = NULL; + + r = nsresource_add_netif(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to add network interface to container: %m"); + + ifi = if_nametoindex(host_ifname); + if (ifi == 0) + return log_error_errno(errno, "Failed to resolve interface '%s': %m", host_ifname); + + if (strlen(host_ifname) >= IFNAMSIZ) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Host interface name too long?"); + + strcpy(veth_name, host_ifname); + } if (arg_network_bridge) { /* Add the interface to a bridge */ @@ -5184,9 +5341,12 @@ static int run_container( } if (arg_register || !arg_keep_unit) { - r = sd_bus_default_system(&bus); + if (arg_privileged) + r = sd_bus_default_system(&bus); + else + r = sd_bus_default_user(&bus); if (r < 0) - return log_error_errno(r, "Failed to open system bus: %m"); + return log_error_errno(r, "Failed to open bus: %m"); r = sd_bus_set_close_on_exit(bus, false); if (r < 0) @@ -5247,7 +5407,13 @@ static int run_container( } else if (arg_slice || arg_property) log_notice("Machine and scope registration turned off, --slice= and --property= settings will have no effect."); - r = create_subcgroup(*pid, arg_keep_unit, arg_unified_cgroup_hierarchy); + r = create_subcgroup( + *pid, + arg_keep_unit, + arg_unified_cgroup_hierarchy, + arg_uid_shift, + userns_fd, + arg_privileged); if (r < 0) return r; @@ -5255,14 +5421,8 @@ static int run_container( if (r < 0) return r; - r = chown_cgroup(*pid, arg_unified_cgroup_hierarchy, arg_uid_shift); - if (r < 0) - return r; - - /* Notify the child that the parent is ready with all - * its setup (including cgroup-ification), and that - * the child can now hand over control to the code to - * run inside the container. */ + /* Notify the child that the parent is ready with all its setup (including cgroup-ification), and + * that the child can now hand over control to the code to run inside the container. */ (void) barrier_place(&barrier); /* #4 */ /* Block SIGCHLD here, before notifying child. @@ -5428,7 +5588,7 @@ static int run_container( fd_kmsg_fifo = safe_close(fd_kmsg_fifo); - if (arg_private_network) { + if (arg_private_network && arg_privileged) { r = move_back_network_interfaces(child_netns_fd, arg_network_interfaces); if (r < 0) return r; @@ -5569,6 +5729,10 @@ static int cant_be_in_netns(void) { if (r == -ENOENT || ERRNO_IS_NEG_DISCONNECT(r)) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Sorry, but --image= requires access to the host's /run/ hierarchy, since we need access to udev."); + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + log_debug_errno(r, "Can't connect to udev control socket, assuming we are in same netns."); + return 0; + } if (r < 0) return log_error_errno(r, "Failed to connect socket to udev control socket: %m"); @@ -5587,7 +5751,7 @@ static int cant_be_in_netns(void) { 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; + _cleanup_close_ int master = -EBADF, userns_fd = -EBADF; _cleanup_fdset_free_ FDSet *fds = NULL; int r, n_fd_passed, ret = EXIT_SUCCESS; char veth_name[IFNAMSIZ] = ""; @@ -5602,17 +5766,12 @@ static int run(int argc, char *argv[]) { log_parse_environment(); log_open(); + arg_privileged = getuid() == 0; + r = parse_argv(argc, argv); if (r <= 0) goto finish; - if (geteuid() != 0) { - r = log_warning_errno(SYNTHETIC_ERRNO(EPERM), - argc >= 2 ? "Need to be root." : - "Need to be root (and some arguments are usually required).\nHint: try --help"); - goto finish; - } - r = cant_be_in_netns(); if (r < 0) goto finish; @@ -5643,7 +5802,7 @@ static int run(int argc, char *argv[]) { if (!arg_private_network && arg_userns_mode != USER_NAMESPACE_NO && arg_uid_shift > 0) arg_caps_retain &= ~(UINT64_C(1) << CAP_NET_BIND_SERVICE); - r = cg_unified(); + r = cg_unified(); /* initialize cache early */ if (r < 0) { log_error_errno(r, "Failed to determine whether the unified cgroups hierarchy is used: %m"); goto finish; @@ -5664,6 +5823,16 @@ static int run(int argc, char *argv[]) { /* Reapply environment settings. */ (void) detect_unified_cgroup_hierarchy_from_environment(); + if (!arg_privileged) { + 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."); + } + /* Ignore SIGPIPE here, because we use splice() on the ptyfwd stuff and that will generate SIGPIPE if * the result is closed. Note that the container payload child will reset signal mask+handler anyway, * so just turning this off here means we only turn it off in nspawn itself, not any children. */ @@ -5683,9 +5852,21 @@ static int run(int argc, char *argv[]) { * the child. Functions like copy_devnodes() change the umask temporarily. */ umask(0022); + if (arg_console_mode < 0) + arg_console_mode = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) ? + CONSOLE_INTERACTIVE : CONSOLE_READ_ONLY; + + 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_privileged) { + r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Invoking container from plain directory tree is currently not supported if called without privileges."); + goto finish; + } + /* 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 @@ -5722,7 +5903,11 @@ static int run(int argc, char *argv[]) { /* We take an exclusive lock on this image, since it's our private, ephemeral copy * only owned by us and no one else. */ - r = image_path_lock(np, LOCK_EX|LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + np, + LOCK_EX|LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r < 0) { log_error_errno(r, "Failed to lock %s: %m", np); goto finish; @@ -5754,7 +5939,11 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; - r = image_path_lock(arg_directory, (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + arg_directory, + (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r == -EBUSY) { log_error_errno(r, "Directory tree %s is currently busy.", arg_directory); goto finish; @@ -5846,15 +6035,12 @@ static int run(int argc, char *argv[]) { } else { DissectImageFlags dissect_image_flags = - DISSECT_IMAGE_GENERIC_ROOT | - DISSECT_IMAGE_REQUIRE_ROOT | - DISSECT_IMAGE_RELAX_VAR_CHECK | - DISSECT_IMAGE_USR_NO_ROOT | - DISSECT_IMAGE_ADD_PARTITION_DEVICES | - DISSECT_IMAGE_PIN_PARTITION_DEVICES; + determine_dissect_image_flags(); + assert(arg_image); assert(!arg_template); + r = chase_and_update(&arg_image, 0); if (r < 0) goto finish; @@ -5869,7 +6055,11 @@ static int run(int argc, char *argv[]) { } /* Always take an exclusive lock on our own ephemeral copy. */ - r = image_path_lock(np, LOCK_EX|LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + np, + LOCK_EX|LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r < 0) { log_error_errno(r, "Failed to create image lock: %m"); goto finish; @@ -5894,7 +6084,11 @@ static int run(int argc, char *argv[]) { free_and_replace(arg_image, np); remove_image = true; } else { - r = image_path_lock(arg_image, (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, &tree_global_lock, &tree_local_lock); + r = image_path_lock( + arg_image, + (arg_read_only ? LOCK_SH : LOCK_EX) | LOCK_NB, + arg_privileged ? &tree_global_lock : NULL, + &tree_local_lock); if (r == -EBUSY) { log_error_errno(r, "Disk image %s is currently busy.", arg_image); goto finish; @@ -5929,57 +6123,81 @@ static int run(int argc, char *argv[]) { goto finish; } - r = loop_device_make_by_path( - arg_image, - arg_read_only ? O_RDONLY : O_RDWR, - /* sector_size= */ UINT32_MAX, - FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, - LOCK_SH, - &loop); - if (r < 0) { - log_error_errno(r, "Failed to set up loopback block device: %m"); - goto finish; + if (arg_privileged) { + r = loop_device_make_by_path( + arg_image, + arg_read_only ? O_RDONLY : O_RDWR, + /* sector_size= */ UINT32_MAX, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE) ? 0 : LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop); + if (r < 0) { + log_error_errno(r, "Failed to set up loopback block device: %m"); + goto finish; + } + + r = dissect_loop_device_and_warn( + loop, + &arg_verity_settings, + /* mount_options=*/ NULL, + arg_image_policy ?: &image_policy_container, + dissect_image_flags, + &dissected_image); + if (r == -ENOPKG) { + /* dissected_image_and_warn() already printed a brief error message. Extend on that with more details */ + log_notice("Note that the disk image needs to\n" + " a) either contain only a single MBR partition of type 0x83 that is marked bootable\n" + " b) or contain a single GPT partition of type 0FC63DAF-8483-4772-8E79-3D69D8477DE4\n" + " c) or follow https://uapi-group.org/specifications/specs/discoverable_partitions_specification\n" + " d) or contain a file system without a partition table\n" + "in order to be bootable with systemd-nspawn."); + goto finish; + } + if (r < 0) + goto finish; + + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop->fd, + &arg_verity_settings); + if (r < 0) + goto finish; + + if (dissected_image->has_verity && !arg_verity_settings.root_hash && !dissected_image->has_verity_sig) + log_notice("Note: image %s contains verity information, but no root hash specified and no embedded " + "root hash signature found! Proceeding without integrity checking.", arg_image); + + r = dissected_image_decrypt_interactively( + dissected_image, + NULL, + &arg_verity_settings, + dissect_image_flags); + 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, + arg_image_policy, + dissect_image_flags, + &dissected_image); + if (r < 0) + goto finish; } - r = dissect_loop_device_and_warn( - loop, - &arg_verity_settings, - /* mount_options=*/ NULL, - arg_image_policy ?: &image_policy_container, - dissect_image_flags, - &dissected_image); - if (r == -ENOPKG) { - /* dissected_image_and_warn() already printed a brief error message. Extend on that with more details */ - log_notice("Note that the disk image needs to\n" - " a) either contain only a single MBR partition of type 0x83 that is marked bootable\n" - " b) or contain a single GPT partition of type 0FC63DAF-8483-4772-8E79-3D69D8477DE4\n" - " c) or follow https://uapi-group.org/specifications/specs/discoverable_partitions_specification\n" - " d) or contain a file system without a partition table\n" - "in order to be bootable with systemd-nspawn."); - goto finish; - } - if (r < 0) - goto finish; - - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop->fd, - &arg_verity_settings); - if (r < 0) - goto finish; - - if (dissected_image->has_verity && !arg_verity_settings.root_hash && !dissected_image->has_verity_sig) - log_notice("Note: image %s contains verity information, but no root hash specified and no embedded " - "root hash signature found! Proceeding without integrity checking.", arg_image); - - r = dissected_image_decrypt_interactively( - dissected_image, - NULL, - &arg_verity_settings, - 0); - if (r < 0) - goto finish; - /* Now that we mounted the image, let's try to remove it again, if it is ephemeral */ if (remove_image && unlink(arg_image) >= 0) remove_image = false; @@ -5992,13 +6210,6 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; - if (arg_console_mode < 0) - arg_console_mode = isatty(STDIN_FILENO) && isatty(STDOUT_FILENO) ? - CONSOLE_INTERACTIVE : CONSOLE_READ_ONLY; - - 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_quiet) { const char *t = arg_image ?: arg_directory; _cleanup_free_ char *u = NULL; @@ -6029,11 +6240,13 @@ static int run(int argc, char *argv[]) { expose_args.fw_ctx = fw_ctx; } for (;;) { - r = run_container(dissected_image, - fds, - veth_name, &veth_created, - &expose_args, &master, - &pid, &ret); + r = run_container( + dissected_image, + userns_fd, + fds, + veth_name, &veth_created, + &expose_args, &master, + &pid, &ret); if (r <= 0) break; } @@ -6075,7 +6288,7 @@ finish: log_debug_errno(errno, "Can't remove temporary root directory '%s', ignoring: %m", tmprootdir); } - if (arg_machine) { + if (arg_machine && arg_privileged) { const char *p; p = strjoina("/run/systemd/nspawn/propagate/", arg_machine); @@ -6089,9 +6302,11 @@ 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 (veth_created) - (void) remove_veth_links(veth_name, arg_network_veth_extra); - (void) remove_bridge(arg_network_zone); + if (arg_privileged) { + if (veth_created) + (void) remove_veth_links(veth_name, arg_network_veth_extra); + (void) remove_bridge(arg_network_zone); + } custom_mount_free_all(arg_custom_mounts, arg_n_custom_mounts); expose_port_free_all(arg_expose_ports); diff --git a/src/nsresourced/bpf/userns_restrict/meson.build b/src/nsresourced/bpf/userns_restrict/meson.build new file mode 100644 index 0000000000..d773c75859 --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/meson.build @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +if conf.get('HAVE_VMLINUX_H') != 1 + subdir_done() +endif + +userns_restrict_bpf_o_unstripped = custom_target( + 'userns-restrict.bpf.unstripped.o', + input : 'userns-restrict.bpf.c', + output : 'userns-restrict.bpf.unstripped.o', + command : bpf_o_unstripped_cmd, + depends : vmlinux_h_dependency) + +userns_restrict_bpf_o = custom_target( + 'userns-restrict.bpf.o', + input : userns_restrict_bpf_o_unstripped, + output : 'userns-restrict.bpf.o', + command : bpf_o_cmd) + +userns_restrict_skel_h = custom_target( + 'userns-restrict.skel.h', + input : userns_restrict_bpf_o, + output : 'userns-restrict.skel.h', + command : skel_h_cmd, + capture : true) diff --git a/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h b/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h new file mode 100644 index 0000000000..271caf4662 --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/userns-restrict-skel.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +#include "bpf-dlopen.h" + +/* libbpf is used via dlopen(), so rename symbols */ +#define bpf_object__attach_skeleton sym_bpf_object__attach_skeleton +#define bpf_object__destroy_skeleton sym_bpf_object__destroy_skeleton +#define bpf_object__load_skeleton sym_bpf_object__load_skeleton +#define bpf_object__open_skeleton sym_bpf_object__open_skeleton + +#include "bpf/userns_restrict/userns-restrict.skel.h" diff --git a/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c b/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c new file mode 100644 index 0000000000..956f91759a --- /dev/null +++ b/src/nsresourced/bpf/userns_restrict/userns-restrict.bpf.c @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* If offsetof() is implemented via __builtin_offset() then it doesn't work on current compilers, since the + * built-ins do not understand CO-RE. Let's undefine any such macros here, to force bpf_helpers.h to define + * its own definitions for this. (In new versions it will do so automatically, but at least in libbpf 1.1.0 + * it does not.) */ +#undef offsetof +#undef container_of + +#include "vmlinux.h" + +#include +#include +#include +#include + +void *bpf_rdonly_cast(void *, __u32) __ksym; + +/* BPF module that implements an allowlist of mounts (identified by mount ID) for user namespaces (identified + * by their inode number in nsfs) that restricts creation of inodes (which would inherit the callers UID/GID) + * or changing of ownership (similar). + * + * This hooks into the varius path-based LSM entrypoints that control inode creation as well as chmod(), and + * then looks up the calling process' user namespace in a global map of namespaces, which points us to + * another map that is simply a list of allowed mnt_ids. */ + +// FIXME: ACL adjustments are currently not blocked. There's no path-based LSM hook available in the kernel +// for setting xattrs or ACLs, hence we cannot easily block them, even though we want that. We can get away +// with ignoring this for now, as ACLs never define ownership, but purely access: i.e. ACLs never allow +// taking possession of an object, but only control access to it. Thus, things like suid access modes should +// not be reachable through it. It still sucks though that a user can persistently add an ACL entry to a file +// with their transient UIDs/GIDs. + +/* kernel currently enforces a maximum usernamespace nesting depth of 32, see create_user_ns() in the kernel sources */ +#define USER_NAMESPACE_DEPTH_MAX 32U + +struct mnt_id_map { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ + __type(key, int); + __type(value, int); +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS); + __uint(max_entries, 1); /* placeholder, configured otherwise by nsresourced */ + __type(key, unsigned); /* userns inode */ + __array(values, struct mnt_id_map); +} userns_mnt_id_hash SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 4096); +} userns_ringbuf SEC(".maps"); + +static inline struct mount *real_mount(struct vfsmount *mnt) { + return container_of(mnt, struct mount, mnt); +} + +static int validate_inode_on_mount(struct inode *inode, struct vfsmount *v) { + struct user_namespace *mount_userns, *task_userns, *p; + unsigned task_userns_inode; + struct task_struct *task; + void *mnt_id_map; + struct mount *m; + int mnt_id; + + /* Get user namespace from vfsmount */ + m = bpf_rdonly_cast(real_mount(v), bpf_core_type_id_kernel(struct mount)); + mount_userns = m->mnt_ns->user_ns; + + /* Get user namespace from task */ + task = (struct task_struct*) bpf_get_current_task_btf(); + task_userns = task->cred->user_ns; + + /* Is the file on a mount that belongs to our own user namespace or a child of it? If so, say + * yes immediately. */ + p = mount_userns; + for (unsigned i = 0; i < USER_NAMESPACE_DEPTH_MAX; i++) { + if (p == task_userns) + return 0; /* our task's user namespace (or a child thereof) owns this superblock: allow! */ + + p = p->parent; + if (!p) + break; + } + + /* Hmm, something is fishy if there's more than 32 levels of namespaces involved. Let's better be + * safe than sorry, and refuse. */ + if (p) + return -EPERM; + + /* This is a mount foreign to our task's user namespace, let's consult our allow list */ + task_userns_inode = task_userns->ns.inum; + + mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &task_userns_inode); + if (!mnt_id_map) /* No rules installed for this userns? Then say yes, too! */ + return 0; + + mnt_id = m->mnt_id; + + /* Otherwise, say yes if the mount ID is allowlisted */ + if (bpf_map_lookup_elem(mnt_id_map, &mnt_id)) + return 0; + + return -EPERM; +} + +static int validate_path(const struct path *path, int ret) { + struct inode *inode; + struct vfsmount *v; + + if (ret != 0) /* propagate earlier error */ + return ret; + + inode = path->dentry->d_inode; + v = path->mnt; + + return validate_inode_on_mount(inode, v); +} + +SEC("lsm/path_chown") +int BPF_PROG(userns_restrict_path_chown, struct path *path, void* uid, void *gid, int ret) { + return validate_path(path, ret); +} + +SEC("lsm/path_mkdir") +int BPF_PROG(userns_restrict_path_mkdir, struct path *dir, struct dentry *dentry, umode_t mode, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_mknod") +int BPF_PROG(userns_restrict_path_mknod, const struct path *dir, struct dentry *dentry, umode_t mode, unsigned int dev, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_symlink") +int BPF_PROG(userns_restrict_path_symlink, const struct path *dir, struct dentry *dentry, const char *old_name, int ret) { + return validate_path(dir, ret); +} + +SEC("lsm/path_link") +int BPF_PROG(userns_restrict_path_link, struct dentry *old_dentry, const struct path *new_dir, struct dentry *new_dentry, int ret) { + return validate_path(new_dir, ret); +} + +SEC("kprobe/free_user_ns") +void BPF_KPROBE(userns_restrict_free_user_ns, struct work_struct *work) { + struct user_namespace *userns; + unsigned inode; + void *mnt_id_map; + + /* Inform userspace that a user namespace just went away. I wish there was a nicer way to hook into + * user namespaces being deleted than using kprobes, but couldn't find any. */ + + userns = bpf_rdonly_cast(container_of(work, struct user_namespace, work), + bpf_core_type_id_kernel(struct user_namespace)); + + inode = userns->ns.inum; + + mnt_id_map = bpf_map_lookup_elem(&userns_mnt_id_hash, &inode); + if (!mnt_id_map) /* No rules installed for this userns? Then send no notification. */ + return; + + bpf_ringbuf_output(&userns_ringbuf, &inode, sizeof(inode), 0); +} + +static const char _license[] SEC("license") = "GPL"; diff --git a/src/nsresourced/meson.build b/src/nsresourced/meson.build new file mode 100644 index 0000000000..cb131f0b9d --- /dev/null +++ b/src/nsresourced/meson.build @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +subdir('bpf/userns_restrict') + +systemd_nsresourcework_sources = files( + 'nsresourcework.c', + 'userns-restrict.c', + 'userns-registry.c', +) + +systemd_nsresourced_sources = files( + 'nsresourced-manager.c', + 'nsresourced.c', + 'userns-restrict.c', + 'userns-registry.c', +) + +userns_restrict_include = include_directories('.') + +if conf.get('HAVE_VMLINUX_H') == 1 + systemd_nsresourcework_sources += userns_restrict_skel_h + systemd_nsresourced_sources += userns_restrict_skel_h + + executables += [ + test_template + { + 'sources' : files('test-userns-restrict.c', 'userns-restrict.c') + userns_restrict_skel_h, + 'conditions' : ['ENABLE_NSRESOURCED', 'HAVE_VMLINUX_H'], + 'include_directories' : [ includes, userns_restrict_include ], + }, + ] +endif + +executables += [ + libexec_template + { + 'name' : 'systemd-nsresourcework', + 'conditions' : ['ENABLE_NSRESOURCED'], + 'sources' : systemd_nsresourcework_sources, + 'dependencies' : threads, + 'include_directories' : [ includes, userns_restrict_include ], + }, + libexec_template + { + 'name' : 'systemd-nsresourced', + 'conditions' : ['ENABLE_NSRESOURCED'], + 'sources' : systemd_nsresourced_sources, + 'dependencies' : threads, + 'include_directories' : [ includes, userns_restrict_include ], + }, +] diff --git a/src/nsresourced/nsresourced-manager.c b/src/nsresourced/nsresourced-manager.c new file mode 100644 index 0000000000..d87da586f7 --- /dev/null +++ b/src/nsresourced/nsresourced-manager.c @@ -0,0 +1,647 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "sd-daemon.h" + +#include "bpf-dlopen.h" +#include "build-path.h" +#include "common-signal.h" +#include "env-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mkdir.h" +#include "nsresourced-manager.h" +#include "parse-util.h" +#include "process-util.h" +#include "recurse-dir.h" +#include "set.h" +#include "signal-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "stdio-util.h" +#include "strv.h" +#include "umask-util.h" +#include "unaligned.h" +#include "user-util.h" +#include "userns-registry.h" +#include "userns-restrict.h" + +#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC) + +static int start_workers(Manager *m, bool explicit_request); + +static int on_worker_exit(sd_event_source *s, const siginfo_t *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + assert_se(!set_remove(m->workers_dynamic, s) != !set_remove(m->workers_fixed, s)); + sd_event_source_disable_unref(s); + + if (si->si_code == CLD_EXITED) { + if (si->si_status == EXIT_SUCCESS) + log_debug("Worker " PID_FMT " exited successfully.", si->si_pid); + else + log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", si->si_pid, si->si_status); + } else if (si->si_code == CLD_KILLED) + log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else if (si->si_code == CLD_DUMPED) + log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", si->si_pid, signal_to_string(si->si_status)); + else + log_warning("Got unexpected exit code via SIGCHLD, ignoring."); + + (void) start_workers(m, /* explicit_request= */ false); /* Fill up workers again if we fell below the low watermark */ + return 0; +} + +static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + (void) start_workers(m, /* explicit_request=*/ true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */ + return 0; +} + +static int on_deferred_start_worker(sd_event_source *s, uint64_t usec, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + + m->deferred_start_worker_event_source = sd_event_source_unref(m->deferred_start_worker_event_source); + + (void) start_workers(m, /* explicit_request=*/ false); + return 0; +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + event_source_hash_ops, + sd_event_source, + (void (*)(const sd_event_source*, struct siphash*)) trivial_hash_func, + (int (*)(const sd_event_source*, const sd_event_source*)) trivial_compare_func, + sd_event_source_disable_unref); + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .listen_fd = -EBADF, + .worker_ratelimit = { + .interval = 2 * USEC_PER_SEC, + .burst = 250, + }, + .registry_fd = -EBADF, + }; + + r = sd_event_new(&m->event); + if (r < 0) + return r; + + r = sd_event_set_signal_exit(m->event, true); + if (r < 0) + return r; + + r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, NULL); + if (r < 0) + return r; + + r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL); + if (r < 0) + log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m"); + + r = sd_event_set_watchdog(m->event, true); + if (r < 0) + log_debug_errno(r, "Failed to enable watchdog handling, ignoring: %m"); + + r = sd_event_add_signal(m->event, NULL, SIGUSR2|SD_EVENT_SIGNAL_PROCMASK, on_sigusr2, m); + if (r < 0) + return r; + + *ret = TAKE_PTR(m); + return 0; +} + +Manager* manager_free(Manager *m) { + if (!m) + return NULL; + + set_free(m->workers_fixed); + set_free(m->workers_dynamic); + + m->deferred_start_worker_event_source = sd_event_source_unref(m->deferred_start_worker_event_source); + + safe_close(m->listen_fd); + +#if HAVE_VMLINUX_H + sd_event_source_disable_unref(m->userns_restrict_bpf_ring_buffer_event_source); + if (m->userns_restrict_bpf_ring_buffer) + sym_ring_buffer__free(m->userns_restrict_bpf_ring_buffer); + userns_restrict_bpf_free(m->userns_restrict_bpf); +#endif + + safe_close(m->registry_fd); + + sd_event_unref(m->event); + + return mfree(m); +} + +static size_t manager_current_workers(Manager *m) { + assert(m); + + return set_size(m->workers_fixed) + set_size(m->workers_dynamic); +} + +static int start_one_worker(Manager *m) { + _cleanup_(sd_event_source_disable_unrefp) sd_event_source *source = NULL; + bool fixed; + pid_t pid; + int r; + + assert(m); + + fixed = set_size(m->workers_fixed) < NSRESOURCE_WORKERS_MIN; + + r = safe_fork_full( + "(sd-worker)", + /* stdio_fds= */ NULL, + &m->listen_fd, 1, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_LOG|FORK_CLOSE_ALL_FDS, + &pid); + if (r < 0) + return log_error_errno(r, "Failed to fork new worker child: %m"); + if (r == 0) { + char pids[DECIMAL_STR_MAX(pid_t)]; + /* Child */ + + if (m->listen_fd == 3) { + r = fd_cloexec(3, false); + if (r < 0) { + log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m"); + _exit(EXIT_FAILURE); + } + } else { + if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */ + log_error_errno(errno, "Failed to move listen fd to 3: %m"); + _exit(EXIT_FAILURE); + } + + safe_close(m->listen_fd); + } + + xsprintf(pids, PID_FMT, pid); + if (setenv("LISTEN_PID", pids, 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_PID: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("LISTEN_FDS", "1", 1) < 0) { + log_error_errno(errno, "Failed to set $LISTEN_FDS: %m"); + _exit(EXIT_FAILURE); + } + + if (setenv("NSRESOURCE_FIXED_WORKER", one_zero(fixed), 1) < 0) { + log_error_errno(errno, "Failed to set $NSRESOURCE_FIXED_WORKER: %m"); + _exit(EXIT_FAILURE); + } + +#if HAVE_VMLINUX_H + bool supported = m->userns_restrict_bpf; +#else + bool supported = false; +#endif + + /* Tell the workers whether to enable the userns API */ + if (setenv("NSRESOURCE_API", one_zero(supported), 1) < 0) { + log_error_errno(errno, "Failed to set $NSRESOURCE_API: %m"); + _exit(EXIT_FAILURE); + } + + r = setenv_systemd_log_level(); + if (r < 0) { + log_error_errno(r, "Failed to set $SYSTEMD_LOG_LEVEL: %m"); + _exit(EXIT_FAILURE); + } + + r = invoke_callout_binary(SYSTEMD_NSRESOURCEWORK_PATH, STRV_MAKE("systemd-nsresourcework", "xxxxxxxxxxxxxxxx")); /* With some extra space rename_process() can make use of */ + log_error_errno(r, "Failed start worker process: %m"); + _exit(EXIT_FAILURE); + } + + r = sd_event_add_child(m->event, &source, pid, WEXITED, on_worker_exit, m); + if (r < 0) + return log_error_errno(r, "Failed to watch child " PID_FMT ": %m", pid); + + r = set_ensure_put( + fixed ? &m->workers_fixed : &m->workers_dynamic, + &event_source_hash_ops, + source); + if (r < 0) + return log_error_errno(r, "Failed to add child process to set: %m"); + + TAKE_PTR(source); + + return 0; +} + +static int start_workers(Manager *m, bool explicit_request) { + int r; + + assert(m); + + for (;;) { + size_t n; + + n = manager_current_workers(m); + if (n >= NSRESOURCE_WORKERS_MIN && (!explicit_request || n >= NSRESOURCE_WORKERS_MAX)) + break; + + if (!ratelimit_below(&m->worker_ratelimit)) { + + /* If we keep starting workers too often but none sticks, let's fail the whole + * daemon, something is wrong */ + if (n == 0) { + sd_event_exit(m->event, EXIT_FAILURE); + return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, but worker count is zero, something is wrong."); + } + + /* Otherwise, let's stop spawning more for a while. */ + log_warning("Worker threads requested too frequently, not starting new ones for a while."); + + if (!m->deferred_start_worker_event_source) { + r = sd_event_add_time( + m->event, + &m->deferred_start_worker_event_source, + CLOCK_MONOTONIC, + ratelimit_end(&m->worker_ratelimit), + /* accuracy_usec= */ 0, + on_deferred_start_worker, + m); + if (r < 0) + return log_error_errno(r, "Failed to allocate deferred start worker event source: %m"); + } + + break; + } + + r = start_one_worker(m); + if (r < 0) + return r; + + explicit_request = false; + } + + return 0; +} + +static void manager_release_userns_bpf(Manager *m, uint64_t inode) { +#if HAVE_VMLINUX_H + int r; + + assert(m); + + if (inode == 0) + return; + + assert(m->userns_restrict_bpf); + + r = userns_restrict_reset_by_inode(m->userns_restrict_bpf, inode); + if (r < 0) + return (void) log_warning_errno(r, "Failed to remove namespace inode from BPF map, ignoring: %m"); +#endif +} + +static void manager_release_userns_fds(Manager *m, uint64_t inode) { + int r; + + assert(m); + assert(inode != 0); + + r = sd_notifyf(/* unset_environment= */ false, + "FDSTOREREMOVE=1\n" + "FDNAME=userns-%" PRIu64 "\n", inode); + if (r < 0) + log_warning_errno(r, "Failed to send fd store removal message, ignoring: %m"); +} + +static void manager_release_userns_by_inode(Manager *m, uint64_t inode) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int lock_fd = -EBADF; + int r; + + assert(m); + assert(inode != 0); + + lock_fd = userns_registry_lock(m->registry_fd); + if (lock_fd < 0) + return (void) log_error_errno(lock_fd, "Failed to lock registry: %m"); + + r = userns_registry_load_by_userns_inode(m->registry_fd, inode, &userns_info); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to find userns for inode %" PRIu64 ", ignoring: %m", inode); + + if (userns_info && uid_is_valid(userns_info->start)) + log_debug("Removing user namespace mapping %" PRIu64 " for UID " UID_FMT ".", inode, userns_info->start); + else + log_debug("Removing user namespace mapping %" PRIu64 ".", inode); + + /* Remove the BPF rules */ + manager_release_userns_bpf(m, inode); + + /* Remove the resources from the fdstore */ + manager_release_userns_fds(m, inode); + + /* And finally remove the resources file from disk */ + if (userns_info) { + /* Remove the cgroups of this userns */ + r = userns_info_remove_cgroups(userns_info); + if (r < 0) + log_warning_errno(r, "Failed to remove cgroups of user namespace: %m"); + + r = userns_registry_remove(m->registry_fd, userns_info); + if (r < 0) + log_warning_errno(r, "Failed to remove user namespace '%s', ignoring.", userns_info->name); + } +} + +static int manager_scan_registry(Manager *m, Set **registry_inodes) { + _cleanup_free_ DirectoryEntries *de = NULL; + int r; + + assert(m); + assert(registry_inodes); + assert(m->registry_fd >= 0); + + r = readdir_all(m->registry_fd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate registry."); + + for (size_t i = 0; i < de->n_entries; i++) { + struct dirent *dentry = de->entries[i]; + _cleanup_free_ char *u = NULL; + const char *e, *p; + uint64_t inode; + + p = startswith(dentry->d_name, "i"); + if (!p) + continue; + + e = endswith(p, ".userns"); + if (!e) + continue; + + u = strndup(p, e - p); + if (!u) + return log_oom(); + + r = safe_atou64(u, &inode); + if (r < 0) { + log_warning_errno(r, "Failed to parse userns inode number from '%s', skipping: %m", dentry->d_name); + continue; + } + + if (inode > UINT32_MAX) { /* namespace inode numbers are 23bit only right now */ + log_warning("userns inode number outside of 32bit range, skipping."); + continue; + } + + if (set_ensure_put(registry_inodes, NULL, UINT32_TO_PTR(inode)) < 0) + return log_oom(); + + log_debug("Found user namespace %" PRIu64 " in registry directory", inode); + } + + return 0; +} + +static int manager_make_listen_socket(Manager *m) { + static const union sockaddr_union sockaddr = { + .un.sun_family = AF_UNIX, + .un.sun_path = "/run/systemd/io.systemd.NamespaceResource", + }; + int r; + + assert(m); + + if (m->listen_fd >= 0) + return 0; + + m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + if (m->listen_fd < 0) + return log_error_errno(errno, "Failed to bind on socket: %m"); + + (void) sockaddr_un_unlink(&sockaddr.un); + + WITH_UMASK(0000) + if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0) + return log_error_errno(errno, "Failed to bind socket: %m"); + + r = mkdir_p("/run/systemd/userdb", 0755); + if (r < 0) + return log_error_errno(r, "Failed to create /run/systemd/userdb: %m"); + + r = symlink_idempotent("../io.systemd.NamespaceResource", "/run/systemd/userdb/io.systemd.NamespaceResource", /* make_relative= */ false); + if (r < 0) + return log_error_errno(r, "Failed to symlink userdb socket: %m"); + + if (listen(m->listen_fd, SOMAXCONN) < 0) + return log_error_errno(errno, "Failed to listen on socket: %m"); + + return 1; +} + +static int manager_scan_listen_fds(Manager *m, Set **fdstore_inodes) { + _cleanup_strv_free_ char **names = NULL; + int n, r; + + assert(m); + assert(fdstore_inodes); + + n = sd_listen_fds_with_names(/* unset_environment= */ true, &names); + if (n < 0) + return log_error_errno(n, "Failed to determine number of passed file descriptors: %m"); + + for (int i = 0; i < n; i++) { + _cleanup_close_ int fd = SD_LISTEN_FDS_START + i; /* Take possession */ + const char *e; + + /* If this is a BPF allowlist related fd, just close it, but remember which start UIDs this covers */ + e = startswith(names[i], "userns-"); + if (e) { + uint64_t inode; + + r = safe_atou64(e, &inode); + if (r < 0) { + log_warning_errno(r, "Failed to parse UID from fd name '%s', ignoring: %m", e); + continue; + } + + if (inode > UINT32_MAX) { + log_warning("Inode number outside of 32bit range, ignoring"); + continue; + } + + if (set_ensure_put(fdstore_inodes, NULL, UINT32_TO_PTR(inode)) < 0) + return log_oom(); + + continue; + } + + /* We don't check the name for the stream socket, for compatibility with older versions */ + r = sd_is_socket(fd, AF_UNIX, SOCK_STREAM, 1); + if (r < 0) + return log_error_errno(r, "Failed to detect if passed file descriptor is a socket: %m"); + if (r > 0) { + if (m->listen_fd >= 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Passed more than one AF_UNIX/SOCK_STREAM socket, refusing."); + + m->listen_fd = TAKE_FD(fd); + continue; + } + + log_warning("Closing passed file descriptor %i (%s) we don't recognize.", fd, names[i]); + } + + return 0; +} + +#if HAVE_VMLINUX_H +static int ringbuf_event(void *userdata, void *data, size_t size) { + Manager *m = ASSERT_PTR(userdata); + size_t n; + + if ((size % sizeof(unsigned int)) != 0) /* Not multiples of "unsigned int"? */ + return -EIO; + + n = size / sizeof(unsigned int); + for (size_t i = 0; i < n; i++) { + const void *d; + uint64_t inode; + + d = (const uint8_t*) data + i * sizeof(unsigned int); + inode = unaligned_read_ne32(d); + + log_debug("Got BPF ring buffer notification that user namespace %" PRIu64 " is now dead.", inode); + manager_release_userns_by_inode(m, inode); + } + + return 0; +} + +static int on_ringbuf_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + int r; + + r = sym_ring_buffer__poll(m->userns_restrict_bpf_ring_buffer, 0); + if (r < 0) + return log_error_errno(r, "Got failure reading from BPF ring buffer: %m"); + + return 0; +} + +static int manager_setup_bpf(Manager *m) { + int rb_fd = -EBADF, poll_fd = -EBADF, r; + + assert(m); + assert(!m->userns_restrict_bpf); + assert(!m->userns_restrict_bpf_ring_buffer); + assert(!m->userns_restrict_bpf_ring_buffer_event_source); + + r = userns_restrict_install(/* pin= */ true, &m->userns_restrict_bpf); + if (r < 0) { + log_notice_errno(r, "Proceeding with user namespace interfaces disabled."); + return 0; + } + + rb_fd = sym_bpf_map__fd(m->userns_restrict_bpf->maps.userns_ringbuf); + if (rb_fd < 0) + return log_error_errno(rb_fd, "Failed to get fd of ring buffer: %m"); + + m->userns_restrict_bpf_ring_buffer = sym_ring_buffer__new(rb_fd, ringbuf_event, m, NULL); + if (!m->userns_restrict_bpf_ring_buffer) + return log_error_errno(errno, "Failed to allocate BPF ring buffer object: %m"); + + poll_fd = sym_ring_buffer__epoll_fd(m->userns_restrict_bpf_ring_buffer); + if (poll_fd < 0) + return log_error_errno(poll_fd, "Failed to get poll fd of ring buffer: %m"); + + r = sd_event_add_io( + m->event, + &m->userns_restrict_bpf_ring_buffer_event_source, + poll_fd, + EPOLLIN, + on_ringbuf_io, + m); + if (r < 0) + return log_error_errno(r, "Failed to allocate event source for BPF ring buffer: %m"); + + return 0; +} +#else +static int manager_setup_bpf(Manager *m) { + log_notice("Not setting up BPF subsystem, as functionality has been disabled at compile time."); + return 0; +} +#endif + +int manager_startup(Manager *m) { + _cleanup_(set_freep) Set *fdstore_inodes = NULL, *registry_inodes = NULL; + void *p; + int r; + + assert(m); + assert(m->registry_fd < 0); + assert(m->listen_fd < 0); + + m->registry_fd = userns_registry_open_fd(); + if (m->registry_fd < 0) + return log_error_errno(m->registry_fd, "Failed to open registry directory: %m"); + + r = manager_setup_bpf(m); + if (r < 0) + return r; + + r = manager_scan_listen_fds(m, &fdstore_inodes); + if (r < 0) + return r; + + r = manager_scan_registry(m, ®istry_inodes); + if (r < 0) + return r; + + /* If there are resources tied to UIDs not found in the registry, then release them */ + SET_FOREACH(p, fdstore_inodes) { + uint64_t inode; + + if (set_contains(registry_inodes, p)) + continue; + + inode = PTR_TO_UINT32(p); + + log_debug("Found stale fd store entry for user namespace %" PRIu64 ", removing.", inode); + manager_release_userns_by_inode(m, inode); + } + + r = manager_make_listen_socket(m); + if (r < 0) + return r; + + /* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be + * GC'ed on idle */ + if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, TIMEVAL_STORE(LISTEN_TIMEOUT_USEC), sizeof(struct timeval)) < 0) + return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m"); + + r = start_workers(m, /* explicit_request= */ false); + if (r < 0) + return r; + + return 0; +} diff --git a/src/nsresourced/nsresourced-manager.h b/src/nsresourced/nsresourced-manager.h new file mode 100644 index 0000000000..5ecf378a60 --- /dev/null +++ b/src/nsresourced/nsresourced-manager.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" +#include "sd-event.h" + +typedef struct Manager Manager; + +#include "hashmap.h" +#include "ratelimit.h" + +#define NSRESOURCE_WORKERS_MIN 5 +#define NSRESOURCE_WORKERS_MAX 4096 + +struct Manager { + sd_event *event; + + Set *workers_fixed; /* Workers 0…NSRESOURCE_WORKERS_MIN */ + Set *workers_dynamic; /* Workers NSRESOURCES_WORKERS_MIN+1…NSRESOURCES_WORKERS_MAX */ + + int listen_fd; + + RateLimit worker_ratelimit; + + sd_event_source *deferred_start_worker_event_source; + +#if HAVE_VMLINUX_H + struct userns_restrict_bpf *userns_restrict_bpf; + struct ring_buffer *userns_restrict_bpf_ring_buffer; + sd_event_source *userns_restrict_bpf_ring_buffer_event_source; +#endif + + int registry_fd; +}; + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +int manager_startup(Manager *m); diff --git a/src/nsresourced/nsresourced.c b/src/nsresourced/nsresourced.c new file mode 100644 index 0000000000..7056897243 --- /dev/null +++ b/src/nsresourced/nsresourced.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "daemon-util.h" +#include "nsresourced-manager.h" +#include "log.h" +#include "main-func.h" +#include "signal-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + log_setup(); + + umask(0022); + + if (argc != 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments."); + + if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NamespaceResource", 1) < 0) + return log_error_errno(errno, "Failed to set $SYSTEMD_BYPASS_USERDB: %m"); + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_startup(m); + if (r < 0) + return log_error_errno(r, "Failed to start up daemon: %m"); + + _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/nsresourcework.c b/src/nsresourced/nsresourcework.c new file mode 100644 index 0000000000..9e68249ea8 --- /dev/null +++ b/src/nsresourced/nsresourcework.c @@ -0,0 +1,1782 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include +#include +#include +#include + +#include "sd-daemon.h" +#include "sd-netlink.h" + +#include "env-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "group-record.h" +#include "io-util.h" +#include "lock-util.h" +#include "main-func.h" +#include "missing_magic.h" +#include "missing_mount.h" +#include "missing_syscall.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "namespace-util.h" +#include "netlink-util.h" +#include "process-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "stat-util.h" +#include "strv.h" +#include "time-util.h" +#include "uid-classification.h" +#include "uid-range.h" +#include "user-record-nss.h" +#include "user-record.h" +#include "user-util.h" +#include "userdb.h" +#include "userns-registry.h" +#include "userns-restrict.h" +#include "varlink-io.systemd.NamespaceResource.h" +#include "varlink-io.systemd.UserDatabase.h" +#include "varlink.h" + +#define ITERATIONS_MAX 64U +#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE) +#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC) +#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC) +#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC) +#define USERNS_PER_UID 256 + +typedef struct LookupParameters { + const char *user_name; + const char *group_name; + union { + uid_t uid; + gid_t gid; + }; + const char *service; +} LookupParameters; + +static int build_user_json(UserNamespaceInfo *userns_info, uid_t offset, JsonVariant **ret) { + _cleanup_free_ char *name = NULL, *realname = NULL; + UserDisposition disposition; + int r; + + assert(userns_info); + assert(offset < userns_info->size); + + if (asprintf(&name, "ns-%s-" UID_FMT, userns_info->name, offset) < 0) + return -ENOMEM; + + if (userns_info->size > 1) { + disposition = USER_CONTAINER; + r = asprintf(&realname, "User " UID_FMT " of Allocated Namespace %s", offset, userns_info->name); + } else { + disposition = USER_DYNAMIC; + r = asprintf(&realname, "Allocated Namespace %s", userns_info->name); + } + if (r < 0) + return -ENOMEM; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(userns_info->start + offset)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(GID_NOBODY)), + JSON_BUILD_PAIR("realName", JSON_BUILD_STRING(realname)), + JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_CONST_STRING("/")), + JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)), + JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)), + JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NamespaceResource")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING(user_disposition_to_string(disposition))))); +} + +static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 }, + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .uid = UID_INVALID, + }; + uid_t offset; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.user_name) { + _cleanup_free_ char *n = NULL; + const char *e, *f; + + e = startswith(p.user_name, "ns-"); + if (!e) + goto not_found; + + f = strrchr(e, '-'); + if (!f) + goto not_found; + + if (parse_uid(f+1, &offset) < 0) + goto not_found; + + n = strndup(e, f - e); + if (!n) + return log_oom(); + + r = userns_registry_load_by_name( + /* registry_fd= */ -EBADF, + n, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + + if (uid_is_valid(p.uid) && p.uid != userns_info->start + offset) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + } else if (uid_is_valid(p.uid)) { + uid_t start, uidmask; + + if (uid_is_container(p.uid)) + uidmask = (uid_t) UINT32_C(0xFFFF0000); + else if (uid_is_dynamic(p.uid)) + uidmask = (uid_t) UINT32_C(0xFFFFFFFF); + else + goto not_found; + + start = p.uid & uidmask; + offset = p.uid - start; + + r = userns_registry_load_by_start_uid( + /* registry_fd= */ -EBADF, + start, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + } else + return varlink_error(link, "io.systemd.UserDatabase.EnumerationNotSupported", NULL); + + r = build_user_json(userns_info, offset, &v); + if (r < 0) + return r; + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)))); + +not_found: + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int build_group_json(UserNamespaceInfo *userns_info, gid_t offset, JsonVariant **ret) { + _cleanup_free_ char *name = NULL, *description = NULL; + UserDisposition disposition; + int r; + + assert(userns_info); + assert(offset < userns_info->size); + + if (asprintf(&name, "ns-%s-" GID_FMT, userns_info->name, offset) < 0) + return -ENOMEM; + + if (userns_info->size > 1) { + disposition = USER_CONTAINER; + r = asprintf(&description, "Group " GID_FMT " of Allocated Namespace %s", offset, userns_info->name); + } else { + disposition = USER_DYNAMIC; + r = asprintf(&description, "Allocated Namespace %s", userns_info->name); + } + if (r < 0) + return -ENOMEM; + + return json_build(ret, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(userns_info->start + offset)), + JSON_BUILD_PAIR("description", JSON_BUILD_STRING(description)), + JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NamespaceResource")), + JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING(user_disposition_to_string(disposition))))); +} + +static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + LookupParameters p = { + .gid = GID_INVALID, + }; + gid_t offset; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + if (p.group_name) { + _cleanup_free_ char *n = NULL; + const char *e, *f; + + e = startswith(p.group_name, "ns-"); + if (!e) + goto not_found; + + f = strrchr(e, '-'); + if (!f) + goto not_found; + + if (parse_gid(f+1, &offset) < 0) + goto not_found; + + n = strndup(e, f - e); + if (!n) + return log_oom(); + + r = userns_registry_load_by_name( + /* registry_fd= */ -EBADF, + n, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + + if (gid_is_valid(p.gid) && p.uid != userns_info->start + offset) + return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL); + + } else if (gid_is_valid(p.gid)) { + gid_t start, gidmask; + + if (gid_is_container(p.gid)) + gidmask = (gid_t) UINT32_C(0xFFFF0000); + else if (gid_is_dynamic(p.gid)) + gidmask = (gid_t) UINT32_C(0xFFFFFFFF); + else + goto not_found; + + start = p.gid & gidmask; + offset = p.gid - start; + + r = userns_registry_load_by_start_uid( + /* registry_fd= */ -EBADF, + (uid_t) start, + &userns_info); + if (r == -ENOENT) + goto not_found; + if (r < 0) + return r; + + if (offset >= userns_info->size) /* Outside of range? */ + goto not_found; + } else + return varlink_error(link, "io.systemd.UserDatabase.EnumerationNotSupported", NULL); + + r = build_group_json(userns_info, offset, &v); + if (r < 0) + return r; + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)))); + +not_found: + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 }, + { "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 }, + { "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 }, + {} + }; + + LookupParameters p = {}; + int r; + + assert(parameters); + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + if (!streq_ptr(p.service, "io.systemd.NamespaceResource")) + return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL); + + /* We don't support auxiliary groups for namespace allocations */ + return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL); +} + +static int uid_is_available( + int registry_dir_fd, + uid_t candidate) { + + int r; + + assert(registry_dir_fd >= 0); + + log_debug("Checking if UID " UID_FMT " is available.", candidate); + + r = userns_registry_uid_exists(registry_dir_fd, candidate); + if (r < 0) + return r; + if (r > 0) + return false; + + r = userdb_by_uid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + r = groupdb_by_gid(candidate, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + log_debug("UID " UID_FMT " is available.", candidate); + + return true; +} + +static int name_is_available( + int registry_dir_fd, + const char *name) { + + _cleanup_free_ char *user_name = NULL; + int r; + + assert(registry_dir_fd >= 0); + assert(name); + + r = userns_registry_name_exists(registry_dir_fd, name); + if (r < 0) + return r; + if (r > 0) + return false; + + user_name = strjoin("ns-", name, "-0"); + if (!user_name) + return -ENOMEM; + + r = userdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + r = groupdb_by_name(user_name, USERDB_AVOID_MULTIPLEXER, NULL); + if (r >= 0) + return false; + if (r != -ESRCH) + return r; + + log_debug("Namespace name '%s' is available.", name); + + return true; +} + +static int allocate_now( + int registry_dir_fd, + UserNamespaceInfo *info, + int *ret_lock_fd) { + + static const uint8_t hash_key[16] = { + 0xd4, 0xd7, 0x33, 0xa7, 0x4d, 0xd3, 0x42, 0xcd, + 0xaa, 0xe9, 0x45, 0xd0, 0xfb, 0xec, 0x79, 0xee, + }; + + _cleanup_(uid_range_freep) UIDRange *valid_range = NULL; + uid_t candidate, uidmin, uidmax, uidmask; + unsigned n_tries = 100; + int r; + + /* Returns the following error codes: + * + * EBUSY → all UID candidates we checked are already taken + * EEXIST → the name for the userns already exists + * EDEADLK → the userns is already registered in the registry + */ + + assert(registry_dir_fd >= 0); + assert(info); + + switch (info->size) { + + case 0x10000U: + uidmin = CONTAINER_UID_BASE_MIN; + uidmax = CONTAINER_UID_BASE_MAX; + uidmask = (uid_t) UINT32_C(0xFFFF0000); + break; + + case 1U: + uidmin = DYNAMIC_UID_MIN; + uidmax = DYNAMIC_UID_MAX; + uidmask = (uid_t) UINT32_C(0xFFFFFFFF); + break; + + default: + assert_not_reached(); + } + + r = uid_range_load_userns(&valid_range, /* path= */ NULL, UID_RANGE_USERNS_INSIDE); + if (r < 0) + return r; + + /* Check early whether we have any chance at all given our own uid range */ + if (!uid_range_overlaps(valid_range, uidmin, uidmax)) + return log_debug_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Relevant UID range not delegated, can't allocate."); + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + /* Enforce limit on user namespaces per UID */ + r = userns_registry_per_uid(registry_dir_fd, info->owner); + if (r < 0) + return log_debug_errno(r, "Failed to determine number of currently registered user namespaces per UID " UID_FMT ": %m", info->owner); + if (r >= USERNS_PER_UID) + return log_debug_errno(SYNTHETIC_ERRNO(EUSERS), "User already registered %i user namespaces, refusing.", r); + + r = userns_registry_inode_exists(registry_dir_fd, info->userns_inode); + if (r < 0) + return r; + if (r > 0) + return -EDEADLK; + + r = name_is_available(registry_dir_fd, info->name); + if (r < 0) + return r; + if (r == 0) + return -EEXIST; + + for (candidate = siphash24_string(info->name, hash_key) & UINT32_MAX;; /* Start from a hash of the input name */ + candidate = random_u32()) { /* Use random values afterwards */ + + if (--n_tries <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Try limit hit, no UIDs available."); + + candidate = (candidate % (uidmax - uidmin)) + uidmin; + candidate &= uidmask; + + if (!uid_range_covers(valid_range, candidate, info->size)) + continue; + + /* We only check the base UID for each range (!) */ + r = uid_is_available(registry_dir_fd, candidate); + if (r < 0) + return log_debug_errno(r, "Can't determine if UID range " UID_FMT " is available: %m", candidate); + if (r > 0) { + info->start = candidate; + + log_debug("Allocating UID range " UID_FMT "…" UID_FMT, candidate, candidate + info->size - 1); + + if (ret_lock_fd) + *ret_lock_fd = TAKE_FD(lock_fd); + + return 0; + } + + log_debug("UID range " UID_FMT " already taken.", candidate); + } +} + +static int write_userns(int usernsfd, const UserNamespaceInfo *userns_info) { + _cleanup_(sigkill_waitp) pid_t pid = 0; + _cleanup_close_ int efd = -EBADF; + uint64_t u; + int r; + + assert(usernsfd >= 0); + assert(userns_info); + assert(uid_is_valid(userns_info->target)); + assert(uid_is_valid(userns_info->start)); + assert(userns_info->size > 0); + assert(userns_info->size <= UINT32_MAX - userns_info->start); + + efd = eventfd(0, EFD_CLOEXEC); + if (efd < 0) + return log_error_errno(errno, "Failed to allocate eventfd(): %m"); + + r = safe_fork("(sd-userns)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid); + if (r < 0) + return r; + if (r == 0) { + /* child */ + + if (setns(usernsfd, CLONE_NEWUSER) < 0) { + log_error_errno(errno, "Failed to join user namespace: %m"); + goto child_fail; + } + + if (eventfd_write(efd, 1) < 0) { + log_error_errno(errno, "Failed to ping event fd: %m"); + goto child_fail; + } + + freeze(); + + child_fail: + _exit(EXIT_FAILURE); + } + + /* Wait until child joined the user namespace */ + if (eventfd_read(efd, &u) < 0) + return log_error_errno(errno, "Failed to wait for event fd: %m"); + + /* Now write mapping */ + + _cleanup_free_ char *pmap = NULL; + + if (asprintf(&pmap, "/proc/" PID_FMT "/uid_map", pid) < 0) + return log_oom(); + + r = write_string_filef(pmap, 0, UID_FMT " " UID_FMT " " UID_FMT "\n", userns_info->target, userns_info->start, userns_info->size); + if (r < 0) + return log_error_errno(r, "Failed to write 'uid_map' file of user namespace: %m"); + + pmap = mfree(pmap); + if (asprintf(&pmap, "/proc/" PID_FMT "/gid_map", pid) < 0) + return log_oom(); + + r = write_string_filef(pmap, 0, GID_FMT " " GID_FMT " " GID_FMT "\n", (gid_t) userns_info->target, (gid_t) userns_info->start, (gid_t) userns_info->size); + if (r < 0) + return log_error_errno(r, "Failed to write 'gid_map' file of user namespace: %m"); + + /* We are done! */ + + log_debug("Successfully configured user namespace."); + return 0; +} + +static int test_userns_api_support(Varlink *link) { + int r; + + assert(link); + + /* We only expose the userns API if our manager daemon told us this OK to do. It will set this + * boolean only if it managed to set up BPF correctly for itself (i.e. watches for userns going away + * via BPF APIs). This should make very sure we don't accidentally allow any of the userns stuff to + * go through without the BPF LSM in effect. */ + + r = getenv_bool("NSRESOURCE_API"); + if (r < 0) + return log_error_errno(r, "Failed to parse $NSRESOURCE_API: %m"); + if (r == 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceInterfaceNotSupported", NULL); + + return 0; +} + +static int validate_name(Varlink *link, const char *name, char **ret) { + _cleanup_free_ char *un = NULL; + int r; + + assert(link); + assert(name); + assert(ret); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (peer_uid == 0) { + if (!userns_name_is_valid(name)) + return varlink_error_invalid_parameter_name(link, "name"); + + un = strdup(name); + if (!un) + return -ENOMEM; + } else { + /* The the client is not root then prefix the name with the UID of the peer, so that they + * live in separate namespaces and cannot steal each other's names. */ + + if (asprintf(&un, UID_FMT "-%s", peer_uid, name) < 0) + return -ENOMEM; + + if (!userns_name_is_valid(un)) + return varlink_error_invalid_parameter_name(link, "name"); + } + + *ret = TAKE_PTR(un); + return 0; +} + +static int validate_target_and_size(Varlink *link, unsigned target, unsigned size) { + assert(link); + + if (!IN_SET(size, 1U, 0x10000)) + return varlink_error_invalid_parameter_name(link, "size"); + + if (!uid_is_valid(target) || target > UINT32_MAX - size) + return varlink_error_invalid_parameter_name(link, "target"); + + return 0; +} + +static int validate_userns(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + r = fd_verify_safe_flags(userns_fd); + if (r < 0) + return log_debug_errno(r, "User namespace file descriptor has unsafe flags set: %m"); + + /* Validate this is actually a valid user namespace fd */ + r = fd_is_ns(userns_fd, CLONE_NEWUSER); + if (r < 0) + return log_debug_errno(r, "Failed to check if user namespace fd is actually a user namespace: %m"); + if (r == 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* And refuse the thing if it is our own */ + r = is_our_namespace(userns_fd, NAMESPACE_USER); + if (r < 0) + return log_debug_errno(r, "Failed to check if user namespace fd refers to our own user namespace: %m"); + if (r > 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to acquire peer UID: %m"); + + if (peer_uid != 0) { + /* Refuse if the userns is not actually owned by our client. */ + uid_t owner_uid; + if (ioctl(userns_fd, NS_GET_OWNER_UID, &owner_uid) < 0) + return log_debug_errno(errno, "Failed to get owner UID of user namespace: %m"); + + if (owner_uid != peer_uid) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + } + + return 0; +} + +static int validate_userns_is_empty(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + _cleanup_(uid_range_freep) UIDRange *range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); + + if (!uid_range_is_empty(range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + range = uid_range_free(range); + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + + if (!uid_range_is_empty(range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + return 0; +} + +typedef struct AllocateParameters { + const char *name; + unsigned size; + unsigned target; + unsigned userns_fd_idx; +} AllocateParameters; + +static int vl_method_allocate_user_range(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AllocateParameters, name), JSON_MANDATORY }, + { "size", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, size), JSON_MANDATORY }, + { "target", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, target), 0 }, + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AllocateParameters, userns_fd_idx), JSON_MANDATORY }, + {} + }; + + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + _cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF, lock_fd = -EBADF; + _cleanup_free_ char *userns_name = NULL; + uid_t peer_uid; + struct stat userns_st; + AllocateParameters p = { + .size = UINT_MAX, + .userns_fd_idx = UINT_MAX, + }; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + r = validate_name(link, p.name, &userns_name); + if (r != 0) + return r; + + r = validate_target_and_size(link, p.target, p.size); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m"); + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + r = validate_userns_is_empty(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + userns_info->name = TAKE_PTR(userns_name); + if (!userns_info->name) + return -ENOMEM; + + userns_info->owner = peer_uid; + userns_info->userns_inode = userns_st.st_ino; + userns_info->size = p.size; + userns_info->target = p.target; + + r = allocate_now(registry_dir_fd, userns_info, &lock_fd); + if (r == -EHOSTDOWN) /* The needed UID range is not delegated to us */ + return varlink_error(link, "io.systemd.NamespaceResource.DynamicRangeUnavailable", NULL); + if (r == -EBUSY) /* All used up */ + return varlink_error(link, "io.systemd.NamespaceResource.NoDynamicRange", NULL); + if (r == -EDEADLK) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceExists", NULL); + if (r == -EEXIST) + return varlink_error(link, "io.systemd.NamespaceResource.NameExists", NULL); + if (r < 0) + return r; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return r; + + /* Register the userns in the BPF map with an empty allowlist */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + goto fail; + + r = write_userns(userns_fd, userns_info); + if (r < 0) + goto fail; + + lock_fd = safe_close(lock_fd); + + /* Send user namespace and process fd to our manager process, which will watch the process and user namespace */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &userns_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_info->userns_inode); + if (r < 0) + goto fail; + + /* Note, we'll not return UID values from the host, since the child might not run in the same + * user namespace as us. If they want to know the ranges they should read them off the userns fd, so + * that they are translated into their PoV */ + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); + +fail: + /* Note: we don't have to clean-up the BPF maps in the error path: the bpf map type used will + * automatically do that once the userns inode goes away */ + userns_registry_remove(registry_dir_fd, userns_info); + return r; +} + +static int validate_userns_is_safe(Varlink *link, int userns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + + /* Read the outside UID range and verify it isn't empty */ + _cleanup_(uid_range_freep) UIDRange *outside_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &outside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns UID range: %m"); + if (uid_range_is_empty(outside_range)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Read the outside GID range and check it is the same as the UID range */ + _cleanup_(uid_range_freep) UIDRange *outside_range_gid = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &outside_range_gid); + if (r < 0) + return log_debug_errno(r, "Failed to read userns GID range: %m"); + if (!uid_range_equal(outside_range, outside_range_gid)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* Read the inside UID range, and verify it matches the size of the outside UID range */ + _cleanup_(uid_range_freep) UIDRange *inside_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_INSIDE, &inside_range); + if (r < 0) + return log_debug_errno(r, "Failed to read userns contents: %m"); + if (uid_range_size(outside_range) != uid_range_size(inside_range)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Uh, inside and outside UID range sizes don't match."); + + /* Read the inside GID range, and verify it matches the inside UID range */ + _cleanup_(uid_range_freep) UIDRange *inside_range_gid = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_INSIDE, &inside_range_gid); + if (r < 0) + return log_debug_errno(r, "Failed to read userns contents: %m"); + if (!uid_range_equal(inside_range, inside_range_gid)) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + uid_t peer_gid; + r = varlink_get_peer_gid(link, &peer_gid); + if (r < 0) + return r; + + /* Insist that the first UID/GID in the range matches the client's UID/GID */ + if (outside_range->entries[0].start != peer_uid || + outside_range_gid->entries[0].start != peer_gid) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + /* If there are more than one UID in the range, then also insist that the first UID maps to root inside the userns */ + if (uid_range_size(outside_range) > 1 && inside_range->entries[0].start != 0) + return varlink_error_invalid_parameter_name(link, "userNamespaceFileDescriptor"); + + return 0; +} + +typedef struct RegisterParameters { + const char *name; + unsigned userns_fd_idx; +} RegisterParameters; + +static int vl_method_register_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch dispatch_table[] = { + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(RegisterParameters, name), JSON_MANDATORY }, + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(RegisterParameters, userns_fd_idx), JSON_MANDATORY }, + {} + }; + + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + _cleanup_close_ int userns_fd = -EBADF, registry_dir_fd = -EBADF; + _cleanup_free_ char *userns_name = NULL; + uid_t peer_uid; + struct stat userns_st; + RegisterParameters p = { + .userns_fd_idx = UINT_MAX, + }; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + r = validate_name(link, p.name, &userns_name); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + r = validate_userns_is_safe(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + r = userns_registry_inode_exists(registry_dir_fd, userns_st.st_ino); + if (r < 0) + return r; + if (r > 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceExists", NULL); + + r = name_is_available(registry_dir_fd, userns_name); + if (r < 0) + return r; + if (r == 0) + return varlink_error(link, "io.systemd.NamespaceResource.NameExists", NULL); + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + userns_info->name = TAKE_PTR(userns_name); + if (!userns_info->name) + return -ENOMEM; + + userns_info->owner = peer_uid; + userns_info->userns_inode = userns_st.st_ino; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return log_debug_errno(r, "Failed to update userns registry: %m"); + + /* Register the userns in the BPF map with an empty allowlist */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + goto fail; + + /* Send user namespace and process fd to our manager process, which will watch the process and user namespace */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &userns_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_info->userns_inode); + if (r < 0) + goto fail; + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); + +fail: + userns_registry_remove(registry_dir_fd, userns_info); + return r; +} + +typedef struct AddMountParameters { + unsigned userns_fd_idx; + unsigned mount_fd_idx; +} AddMountParameters; + +static int vl_method_add_mount_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddMountParameters, userns_fd_idx), JSON_MANDATORY }, + { "mountFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddMountParameters, mount_fd_idx), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, mount_fd = -EBADF, registry_dir_fd = -EBADF; + struct userns_restrict_bpf **bpf = ASSERT_PTR(userdata); + AddMountParameters p = { + .userns_fd_idx = UINT_MAX, + .mount_fd_idx = UINT_MAX, + }; + int r, mnt_id = 0; + struct stat userns_st; + uid_t peer_uid; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + /* Allowlisting arbitrary mounts is a privileged operation */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + if (peer_uid != 0) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return -errno; + + mount_fd = varlink_take_fd(link, p.mount_fd_idx); + if (mount_fd < 0) + return mount_fd; + + r = fd_verify_safe_flags_full(mount_fd, O_PATH|O_DIRECTORY); + if (r < 0) + return log_debug_errno(r, "Mount file descriptor has unsafe flags set: %m"); + + r = fd_verify_directory(mount_fd); + if (r < 0) + return r; + + r = path_get_mnt_id_at(mount_fd, NULL, &mnt_id); + if (r < 0) + return r; + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + if (!*bpf) { + r = userns_restrict_install(/* pin= */ true, bpf); + if (r < 0) + return r; + } + + /* Pin the mount fd */ + r = sd_pid_notifyf_with_fds( + /* pid= */ 0, + /* unset_environment= */ false, + &mount_fd, 1, + "FDSTORE=1\n" + "FDNAME=userns-" INO_FMT "\n", userns_st.st_ino); + if (r < 0) + return r; + + /* Add this mount to the user namespace's BPF map allowlist entry. */ + r = userns_restrict_put_by_fd( + *bpf, + userns_fd, + /* replace= */ false, + &mount_fd, + 1); + if (r < 0) + return r; + + if (userns_info->size > 0) + log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s' @ UID " UID_FMT ")", + mnt_id, userns_st.st_ino, userns_info->name, userns_info->start); + else + log_debug("Granting access to mount %i to user namespace " INO_FMT " ('%s')", + mnt_id, userns_st.st_ino, userns_info->name); + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); +} + +static int validate_cgroup(Varlink *link, int fd, uint64_t *ret_cgroup_id) { + int r; + + assert(link); + assert(fd >= 0); + assert(ret_cgroup_id); + + r = fd_verify_safe_flags_full(fd, O_DIRECTORY); + if (r < 0) + return log_debug_errno(r, "Control group file descriptor has unsafe flags set: %m"); + + r = fd_verify_directory(fd); + if (r < 0) + return log_debug_errno(r, "Verification that cgroup fd refers to directory failed: %m"); + + r = fd_is_fs_type(fd, CGROUP2_SUPER_MAGIC); + if (r < 0) + return log_debug_errno(r, "Failed to check if cgroup fd actually refers to cgroupfs: %m"); + if (r == 0) + return varlink_error_invalid_parameter_name(link, "controlGroupFileDescriptor"); + + r = cg_fd_get_cgroupid(fd, ret_cgroup_id); + if (r < 0) + return log_debug_errno(r, "Failed to read cgroup ID from cgroupfs: %m"); + + return 0; +} + +typedef struct AddCGroupParameters { + unsigned userns_fd_idx; + unsigned cgroup_fd_idx; +} AddCGroupParameters; + +static int vl_method_add_cgroup_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddCGroupParameters, userns_fd_idx), JSON_MANDATORY }, + { "controlGroupFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddCGroupParameters, cgroup_fd_idx), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, cgroup_fd = -EBADF, registry_dir_fd = -EBADF; + AddCGroupParameters p = { + .userns_fd_idx = UINT_MAX, + .cgroup_fd_idx = UINT_MAX, + }; + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + struct stat userns_st, cgroup_st; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to take user namespace fd from Varlink connection: %m"); + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace fd: %m"); + + cgroup_fd = varlink_take_fd(link, p.cgroup_fd_idx); + if (cgroup_fd < 0) + return log_debug_errno(cgroup_fd, "Failed to take cgroup fd from Varlink connection: %m"); + + uint64_t cgroup_id; + r = validate_cgroup(link, cgroup_fd, &cgroup_id); + if (r != 0) + return r; + + if (fstat(cgroup_fd, &cgroup_st) < 0) + return log_debug_errno(errno, "Failed to fstat() cgroup fd: %m"); + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return lock_fd; + + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + /* The user namespace must have a user assigned */ + if (userns_info->size == 0) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceWithoutUserRange", NULL); + if (userns_info_has_cgroup(userns_info, cgroup_id)) + return varlink_error(link, "io.systemd.NamespaceResource.ControlGroupAlreadyAdded", NULL); + if (userns_info->n_cgroups > USER_NAMESPACE_CGROUPS_DELEGATE_MAX) + return varlink_error(link, "io.systemd.NamespaceResource.TooManyControlGroups", NULL); + + /* Registering a cgroup for this client is only allowed for the root or the owner of a userns */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return log_debug_errno(r, "Failed to get connection peer: %m"); + if (peer_uid != 0) { + if (peer_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + /* The cgroup must be owned by the owner of the userns */ + if (cgroup_st.st_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + } + + r = userns_info_add_cgroup(userns_info, cgroup_id); + if (r < 0) + return r; + + r = userns_registry_store(registry_dir_fd, userns_info); + if (r < 0) + return r; + + if (fchown(cgroup_fd, userns_info->start, userns_info->start) < 0) + return log_debug_errno(errno, "Failed to change ownership of cgroup: %m"); + + if (fchmod(cgroup_fd, 0755) < 0) + return log_debug_errno(errno, "Failed to change access mode of cgroup: %m"); + + FOREACH_STRING(attr, "cgroup.procs", "cgroup.subtree_control", "cgroup.threads") { + (void) fchmodat(cgroup_fd, attr, 0644, AT_SYMLINK_NOFOLLOW); + (void) fchownat(cgroup_fd, attr, userns_info->start, userns_info->start, AT_SYMLINK_NOFOLLOW); + } + + log_debug("Granting ownership to cgroup %" PRIu64 " to userns " INO_FMT " ('%s' @ UID " UID_FMT ")", + cgroup_id, userns_st.st_ino, userns_info->name, userns_info->start); + + return varlink_replyb(link, JSON_BUILD_EMPTY_OBJECT); +} + +static uint64_t hash_ifname_id(UserNamespaceInfo *userns_info, const char *ifname) { + struct siphash state; + + assert(userns_info); + + siphash24_init(&state, (const uint8_t[]) { 0xc4, 0x6c, 0x96, 0xe8, 0xad, 0x37, 0x4d, 0x5f, 0xa1, 0xae, 0xfe, 0x70, 0x40, 0xed, 0x41, 0x5f }); + siphash24_compress_string(userns_info->name, &state); + siphash24_compress_byte(0, &state); /* separator */ + siphash24_compress_string(strempty(ifname), &state); + + return siphash24_finalize(&state); +} + +static void hash_ether_addr(UserNamespaceInfo *userns_info, const char *ifname, uint64_t n, struct ether_addr *ret) { + struct siphash state; + uint64_t h; + + assert(userns_info); + assert(ret); + + siphash24_init(&state, (const uint8_t[]) { 0x36, 0xaa, 0xd1, 0x69, 0xc7, 0xe5, 0x4c, 0xaa, 0x1e, 0xb2, 0x9e, 0xb3, 0x3a, 0x6b, 0xd4, 0x71 }); + siphash24_compress_string(userns_info->name, &state); + siphash24_compress_byte(0, &state); /* separator */ + siphash24_compress_string(strempty(ifname), &state); + siphash24_compress_byte(0, &state); /* separator */ + n = htole64(n); /* add the 'index' to the mix in an endianess-independent fashion */ + siphash24_compress(&n, sizeof(n), &state); + + h = htole64(siphash24_finalize(&state)); + + assert(sizeof(h) >= sizeof_field(struct ether_addr, ether_addr_octet)); + + memcpy(ret->ether_addr_octet, &h, sizeof_field(struct ether_addr, ether_addr_octet)); + ether_addr_mark_random(ret); +} + +static int create_veth( + int netns_fd, + const char *ifname_host, + const char *altifname_host, + struct ether_addr *mac_host, + const char *ifname_namespace, + struct ether_addr *mac_namespace) { + + int r; + + assert(netns_fd >= 0); + assert(ifname_host); + assert(mac_host); + assert(ifname_namespace); + assert(mac_namespace); + + log_debug("Creating veth link on host %s (%s) with address %s to container as %s with address %s", + ifname_host, strna(altifname_host), ETHER_ADDR_TO_STR(mac_host), + ifname_namespace, ETHER_ADDR_TO_STR(mac_namespace)); + + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + r = sd_netlink_open(&rtnl); + if (r < 0) + return r; + + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *m = NULL; + r = sd_rtnl_message_new_link(rtnl, &m, RTM_NEWLINK, 0); + if (r < 0) + return log_error_errno(r, "Failed to allocate netlink message: %m"); + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, ifname_host); + if (r < 0) + return log_error_errno(r, "Failed to add netlink interface name: %m"); + + r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, mac_host); + if (r < 0) + return log_error_errno(r, "Failed to add netlink MAC address: %m"); + + r = sd_netlink_message_open_container(m, IFLA_LINKINFO); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, "veth"); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_open_container(m, VETH_INFO_PEER); + if (r < 0) + return log_error_errno(r, "Failed to open netlink container: %m"); + + r = sd_netlink_message_append_string(m, IFLA_IFNAME, ifname_namespace); + if (r < 0) + return log_error_errno(r, "Failed to add netlink interface name: %m"); + + r = sd_netlink_message_append_ether_addr(m, IFLA_ADDRESS, mac_namespace); + if (r < 0) + return log_error_errno(r, "Failed to add netlink MAC address: %m"); + + r = sd_netlink_message_append_u32(m, IFLA_NET_NS_FD, netns_fd); + if (r < 0) + return log_error_errno(r, "Failed to add netlink namespace field: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_message_close_container(m); + if (r < 0) + return log_error_errno(r, "Failed to close netlink container: %m"); + + r = sd_netlink_call(rtnl, m, 0, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add new veth interfaces (%s:%s): %m", ifname_host, ifname_namespace); + + r = rtnl_set_link_alternative_names_by_ifname(&rtnl, ifname_host, STRV_MAKE(altifname_host)); + if (r < 0) + log_warning_errno(r, "Failed to set alternative interface name to '%s', ignoring: %m", altifname_host); + + return 0; +} + +static int validate_netns(Varlink *link, int userns_fd, int netns_fd) { + int r; + + assert(link); + assert(userns_fd >= 0); + assert(netns_fd >= 0); + + r = fd_verify_safe_flags(netns_fd); + if (r < 0) + return log_debug_errno(r, "Network namespace file descriptor has unsafe flags set: %m"); + + /* Validate this is actually a valid network namespace fd */ + r = fd_is_ns(netns_fd, CLONE_NEWNET); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + /* And refuse the thing if it is our own */ + r = is_our_namespace(netns_fd, NAMESPACE_NET); + if (r < 0) + return r; + if (r > 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + /* Check if the netns actually belongs to the userns */ + _cleanup_close_ int owner_userns_fd = -EBADF; + owner_userns_fd = ioctl(netns_fd, NS_GET_USERNS); + if (owner_userns_fd < 0) + return -errno; + + r = inode_same_at(owner_userns_fd, /* path_a= */ NULL, userns_fd, /* path_b= */ NULL, AT_EMPTY_PATH); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + + uid_t peer_uid; + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + + if (peer_uid != 0) { + /* Refuse if the netns is not actually owned by our client. */ + + uid_t owner_uid; + if (ioctl(owner_userns_fd, NS_GET_OWNER_UID, &owner_uid) < 0) + return -errno; + + if (owner_uid != peer_uid) + return varlink_error_invalid_parameter_name(link, "networkNamespaceFileDescriptor"); + } + + return 0; +} + +typedef struct AddNetworkParameters { + unsigned userns_fd_idx; + unsigned netns_fd_idx; + const char *ifname; + const char *mode; +} AddNetworkParameters; + +static int vl_method_add_netif_to_user_namespace(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch parameter_dispatch_table[] = { + { "userNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddNetworkParameters, userns_fd_idx), JSON_MANDATORY }, + { "networkNamespaceFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(AddNetworkParameters, netns_fd_idx), JSON_MANDATORY }, + { "namespaceInterfaceName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AddNetworkParameters, ifname), 0 }, + { "mode", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(AddNetworkParameters, mode), JSON_MANDATORY }, + {} + }; + + _cleanup_close_ int userns_fd = -EBADF, netns_fd = -EBADF, registry_dir_fd = -EBADF; + AddNetworkParameters p = { + .userns_fd_idx = UINT_MAX, + }; + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + struct stat userns_st; + uid_t peer_uid; + int r; + + assert(link); + assert(parameters); + + r = test_userns_api_support(link); + if (r != 0) + return r; + + r = varlink_dispatch(link, parameters, parameter_dispatch_table, &p); + if (r != 0) + return r; + + userns_fd = varlink_take_fd(link, p.userns_fd_idx); + if (userns_fd < 0) + return userns_fd; + + r = validate_userns(link, userns_fd); + if (r != 0) + return r; + + if (fstat(userns_fd, &userns_st) < 0) + return -errno; + + netns_fd = varlink_take_fd(link, p.netns_fd_idx); + if (netns_fd < 0) + return netns_fd; + + r = validate_netns(link, userns_fd, netns_fd); + if (r != 0) + return r; + + if (!streq_ptr(p.mode, "veth")) + return varlink_error_invalid_parameter_name(link, "mode"); + + if (p.ifname && !ifname_valid(p.ifname)) + return varlink_error_invalid_parameter_name(link, "interfaceName"); + + registry_dir_fd = userns_registry_open_fd(); + if (registry_dir_fd < 0) + return registry_dir_fd; + + _cleanup_close_ int lock_fd = -EBADF; + lock_fd = userns_registry_lock(registry_dir_fd); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + r = userns_registry_load_by_userns_inode( + registry_dir_fd, + userns_st.st_ino, + &userns_info); + if (r == -ENOENT) + return varlink_error(link, "io.systemd.NamespaceResource.UserNamespaceNotRegistered", NULL); + if (r < 0) + return r; + + /* Registering a network interface for this client is only allowed for the root or the owner of a userns */ + r = varlink_get_peer_uid(link, &peer_uid); + if (r < 0) + return r; + if (peer_uid != 0 && peer_uid != userns_info->owner) + return varlink_error(link, VARLINK_ERROR_PERMISSION_DENIED, NULL); + + _cleanup_free_ char *ifname_host = NULL, *altifname_host = NULL; + const char *ifname_namespace = p.ifname ?: "host0"; + + /* The short ifname is just too short to generate readable and unique names where unprivileged users + * can't take each others names. Hence just hash it. The alternative name however contains more useful + * information. */ + if (asprintf(&ifname_host, "ns-%08" PRIx64, hash_ifname_id(userns_info, p.ifname)) < 0) + return -ENOMEM; + strshorten(ifname_host, IFNAMSIZ-1); + + if (p.ifname) + r = asprintf(&altifname_host, "ns-" UID_FMT "-%s-%s", userns_info->owner, userns_info->name, p.ifname); + else + r = asprintf(&altifname_host, "ns-" UID_FMT "-%s", userns_info->owner, userns_info->name); + if (r < 0) + return -ENOMEM; + + struct ether_addr ether_addr_host, ether_addr_namespace; + + hash_ether_addr(userns_info, p.ifname, 0, ðer_addr_host); + hash_ether_addr(userns_info, p.ifname, 1, ðer_addr_namespace); + + r = create_veth(netns_fd, + ifname_host, altifname_host, ðer_addr_host, + ifname_namespace, ðer_addr_namespace); + if (r < 0) + return r; + + log_debug("Adding veth tunnel %s from host to userns " INO_FMT " ('%s' @ UID " UID_FMT ", interface %s).", + ifname_host, userns_st.st_ino, userns_info->name, userns_info->start, ifname_namespace); + + return varlink_replyb(link, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hostInterfaceName", JSON_BUILD_STRING(ifname_host)), + JSON_BUILD_PAIR("namespaceInterfaceName", JSON_BUILD_STRING(ifname_namespace)))); +} + +static int process_connection(VarlinkServer *server, int _fd) { + _cleanup_close_ int fd = TAKE_FD(_fd); /* always take possession */ + _cleanup_(varlink_close_unrefp) Varlink *vl = NULL; + int r; + + r = varlink_server_add_connection(server, fd, &vl); + if (r < 0) + return log_error_errno(r, "Failed to add connection: %m"); + + TAKE_FD(fd); + vl = varlink_ref(vl); + + r = varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for read: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing for write: %m"); + + for (;;) { + r = varlink_process(vl); + if (r == -ENOTCONN) { + log_debug("Connection terminated."); + break; + } + if (r < 0) + return log_error_errno(r, "Failed to process connection: %m"); + if (r > 0) + continue; + + r = varlink_wait(vl, CONNECTION_IDLE_USEC); + if (r < 0) + return log_error_errno(r, "Failed to wait for connection events: %m"); + if (r == 0) + break; + } + + return 0; +} + +static int run(int argc, char *argv[]) { + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *bpf = NULL; + usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY; + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + _cleanup_(pidref_done) PidRef parent = PIDREF_NULL; + unsigned n_iterations = 0; + int m, listen_fd, r; + + log_setup(); + + m = sd_listen_fds(false); + if (m < 0) + return log_error_errno(m, "Failed to determine number of listening fds: %m"); + if (m == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received."); + if (m > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time."); + + listen_fd = SD_LISTEN_FDS_START; + + r = fd_nonblock(listen_fd, false); + if (r < 0) + return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m"); + + r = varlink_server_new(&server, VARLINK_SERVER_INHERIT_USERDATA); + if (r < 0) + return log_error_errno(r, "Failed to allocate server: %m"); + + r = varlink_server_add_interface_many( + server, + &vl_interface_io_systemd_NamespaceResource, + &vl_interface_io_systemd_UserDatabase); + if (r < 0) + return log_error_errno(r, "Failed to add UserDatabase and NamespaceResource interface to varlink server: %m"); + + r = varlink_server_bind_method_many( + server, + "io.systemd.NamespaceResource.AllocateUserRange", vl_method_allocate_user_range, + "io.systemd.NamespaceResource.RegisterUserNamespace", vl_method_register_user_namespace, + "io.systemd.NamespaceResource.AddMountToUserNamespace", vl_method_add_mount_to_user_namespace, + "io.systemd.NamespaceResource.AddControlGroupToUserNamespace", vl_method_add_cgroup_to_user_namespace, + "io.systemd.NamespaceResource.AddNetworkToUserNamespace", vl_method_add_netif_to_user_namespace, + "io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record, + "io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record, + "io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships); + if (r < 0) + return log_error_errno(r, "Failed to bind methods: %m"); + + varlink_server_set_userdata(server, &bpf); + + r = getenv_bool("NSRESOURCE_FIXED_WORKER"); + if (r < 0) + return log_error_errno(r, "Failed to parse NSRESOURCE_FIXED_WORKER: %m"); + listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC; + + r = pidref_set_parent(&parent); + if (r < 0) + return log_error_errno(r, "Failed to acquire pidfd of parent process: %m"); + + start_time = now(CLOCK_MONOTONIC); + + for (;;) { + _cleanup_close_ int fd = -EBADF; + usec_t n; + + /* Exit the worker in regular intervals, to flush out all memory use */ + if (n_iterations++ > ITERATIONS_MAX) { + log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations); + break; + } + + n = now(CLOCK_MONOTONIC); + if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) { + log_debug("Exiting worker, ran for %s, that's enough.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, start_time), 0)); + break; + } + + if (last_busy_usec == USEC_INFINITY) + last_busy_usec = n; + else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) { + log_debug("Exiting worker, been idle for %s.", + FORMAT_TIMESPAN(usec_sub_unsigned(n, last_busy_usec), 0)); + break; + } + + (void) rename_process("systemd-nsresourcework: waiting..."); + fd = RET_NERRNO(accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC)); + (void) rename_process("systemd-nsresourcework: processing..."); + + if (fd == -EAGAIN) + continue; /* The listening socket has SO_RECVTIMEO set, hence a timeout is expected + * after a while, let's check if it's time to exit though. */ + if (fd == -EINTR) + continue; /* Might be that somebody attached via strace, let's just continue in that + * case */ + if (fd < 0) + return log_error_errno(fd, "Failed to accept() from listening socket: %m"); + + if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) { + /* We only slept a very short time? If so, let's see if there are more sockets + * pending, and if so, let's ask our parent for more workers */ + + r = fd_wait_for_event(listen_fd, POLLIN, 0); + if (r < 0) + return log_error_errno(r, "Failed to test for POLLIN on listening socket: %m"); + + if (FLAGS_SET(r, POLLIN)) { + r = pidref_kill(&parent, SIGUSR2); + if (r == -ESRCH) + return log_error_errno(r, "Parent already died?"); + if (r < 0) + return log_error_errno(r, "Failed to send SIGUSR2 signal to parent. %m"); + } + } + + (void) process_connection(server, TAKE_FD(fd)); + last_busy_usec = USEC_INFINITY; + } + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/test-userns-restrict.c b/src/nsresourced/test-userns-restrict.c new file mode 100644 index 0000000000..7ef1d7b423 --- /dev/null +++ b/src/nsresourced/test-userns-restrict.c @@ -0,0 +1,182 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "main-func.h" +#include "missing_mount.h" +#include "missing_syscall.h" +#include "namespace-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "tmpfile-util.h" +#include "userns-restrict.h" + +static int make_tmpfs_fsmount(void) { + _cleanup_close_ int fsfd = -EBADF, mntfd = -EBADF; + + fsfd = fsopen("tmpfs", FSOPEN_CLOEXEC); + assert_se(fsfd >= 0); + assert_se(fsconfig(fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) >= 0); + + mntfd = fsmount(fsfd, FSMOUNT_CLOEXEC, 0); + assert_se(mntfd >= 0); + + return TAKE_FD(mntfd); +} + +static void test_works_reg(int parent_fd, const char *fname) { + _cleanup_close_ int fd = -EBADF; + + fd = openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666); + assert_se(fd >= 0); +} + +static void test_fails_reg(int parent_fd, const char *fname) { + errno = 0; + assert_se(openat(parent_fd, fname, O_RDWR|O_CREAT|O_CLOEXEC, 0666) < 0); + assert_se(errno == EPERM); +} + +static void test_works_dir(int parent_fd, const char *fname) { + assert_se(mkdirat(parent_fd, fname, 0666) >= 0); +} + +static void test_fails_dir(int parent_fd, const char *fname) { + errno = 0; + assert_se(mkdirat(parent_fd, fname, 0666) < 0); + assert_se(errno == EPERM); +} + +static int run(int argc, char *argv[]) { + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL; + _cleanup_close_ int userns_fd = -EBADF, host_fd1 = -EBADF, host_tmpfs = -EBADF, afd = -EBADF, bfd = -EBADF; + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_(sigkill_waitp) pid_t pid = 0; + int r; + + log_set_max_level(LOG_DEBUG); + log_open(); + + r = userns_restrict_install(/* pin= */ false, &obj); + if (ERRNO_IS_NOT_SUPPORTED(r)) { + log_notice("Skipping test, LSM-BPF logic not supported."); + return EXIT_TEST_SKIP; + } + if (ERRNO_IS_PRIVILEGE(r)) { + log_notice("Skipping test, lacking privileges."); + return EXIT_TEST_SKIP; + } + if (r < 0) + return r; + + assert_se(mkdtemp_malloc(NULL, &t) >= 0); + + host_fd1 = open(t, O_DIRECTORY|O_CLOEXEC); + assert_se(host_fd1 >= 0); + + host_tmpfs = make_tmpfs_fsmount(); + assert_se(host_tmpfs >= 0); + + userns_fd = userns_acquire("0 0 1", "0 0 1"); + if (userns_fd < 0) + return log_error_errno(userns_fd, "Failed to make user namespace: %m"); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ true, + /* mount_fds= */ NULL, + /* n_mount_fds= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to restrict user namespace: %m"); + + afd = eventfd(0, EFD_CLOEXEC); + bfd = eventfd(0, EFD_CLOEXEC); + + assert_se(afd >= 0 && bfd >= 0); + + r = safe_fork("(test)", FORK_DEATHSIG_SIGKILL, &pid); + assert_se(r >= 0); + if (r == 0) { + _cleanup_close_ int private_tmpfs = -EBADF; + + assert_se(setns(userns_fd, CLONE_NEWUSER) >= 0); + assert_se(unshare(CLONE_NEWNS) >= 0); + + /* Allocate tmpfs locally */ + private_tmpfs = make_tmpfs_fsmount(); + + /* These two host mounts should be inaccessible */ + test_fails_reg(host_fd1, "test"); + test_fails_reg(host_tmpfs, "xxx"); + test_fails_dir(host_fd1, "test2"); + test_fails_dir(host_tmpfs, "xxx2"); + + /* But this mount created locally should be fine */ + test_works_reg(private_tmpfs, "yyy"); + test_works_dir(private_tmpfs, "yyy2"); + + /* Let's sync with the parent, so that it allowlists more stuff for us */ + assert_se(eventfd_write(afd, 1) >= 0); + uint64_t x; + assert_se(eventfd_read(bfd, &x) >= 0); + + /* And now we should also have access to the host tmpfs */ + test_works_reg(host_tmpfs, "zzz"); + test_works_reg(private_tmpfs, "aaa"); + test_works_dir(host_tmpfs, "zzz2"); + test_works_dir(private_tmpfs, "aaa2"); + + /* But this one should still fail */ + test_fails_reg(host_fd1, "bbb"); + test_fails_dir(host_fd1, "bbb2"); + + /* Sync again, to get more stuff allowlisted */ + assert_se(eventfd_write(afd, 1) >= 0); + assert_se(eventfd_read(bfd, &x) >= 0); + + /* Everything should now be allowed */ + test_works_reg(host_tmpfs, "ccc"); + test_works_reg(host_fd1, "ddd"); + test_works_reg(private_tmpfs, "eee"); + test_works_dir(host_tmpfs, "ccc2"); + test_works_reg(host_fd1, "ddd2"); + test_works_dir(private_tmpfs, "eee2"); + + _exit(EXIT_SUCCESS); + } + + uint64_t x; + assert_se(eventfd_read(afd, &x) >= 0); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ false, + &host_tmpfs, + 1); + if (r < 0) + return log_error_errno(r, "Failed to loosen user namespace: %m"); + + assert_se(eventfd_write(bfd, 1) >= 0); + + assert_se(eventfd_read(afd, &x) >= 0); + + r = userns_restrict_put_by_fd( + obj, + userns_fd, + /* replace= */ false, + &host_fd1, + 1); + if (r < 0) + return log_error_errno(r, "Failed to loosen user namespace: %m"); + + assert_se(eventfd_write(bfd, 1) >= 0); + + assert_se(wait_for_terminate_and_check("(test)", pid, WAIT_LOG) >= 0); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nsresourced/userns-registry.c b/src/nsresourced/userns-registry.c new file mode 100644 index 0000000000..2cc1b1f9ef --- /dev/null +++ b/src/nsresourced/userns-registry.c @@ -0,0 +1,646 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "chase.h" +#include "fd-util.h" +#include "fileio.h" +#include "format-util.h" +#include "fs-util.h" +#include "json.h" +#include "missing_magic.h" +#include "path-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "user-util.h" +#include "userns-registry.h" + +int userns_registry_open_fd(void) { + int fd; + + fd = chase_and_open( + "/run/systemd/nsresource/registry", + /* root= */ NULL, + CHASE_MKDIR_0755, + O_CLOEXEC|O_DIRECTORY|O_CREAT, + /* ret_path= */ NULL); + if (fd < 0) + return log_debug_errno(fd, "Failed to open registry dir: %m"); + + return fd; +} + +int userns_registry_lock(int dir_fd) { + _cleanup_close_ int registry_fd = -EBADF, lock_fd = -EBADF; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + lock_fd = xopenat_lock_full(dir_fd, "lock", O_CREAT|O_RDWR|O_CLOEXEC, /* xopen_flags= */ 0, 0600, LOCK_BSD, LOCK_EX); + if (lock_fd < 0) + return log_debug_errno(lock_fd, "Failed to open nsresource registry lock file: %m"); + + return TAKE_FD(lock_fd); +} + +UserNamespaceInfo* userns_info_new(void) { + UserNamespaceInfo *info = new(UserNamespaceInfo, 1); + if (!info) + return NULL; + + *info = (UserNamespaceInfo) { + .owner = UID_INVALID, + .start = UID_INVALID, + .target = UID_INVALID, + }; + + return info; +} + +UserNamespaceInfo *userns_info_free(UserNamespaceInfo *userns) { + if (!userns) + return NULL; + + free(userns->cgroups); + free(userns->name); + + return mfree(userns); +} + +static int dispatch_cgroups_array(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + UserNamespaceInfo *info = ASSERT_PTR(userdata); + _cleanup_free_ uint64_t *cgroups = NULL; + size_t n_cgroups = 0; + + if (json_variant_is_null(variant)) { + info->cgroups = mfree(info->cgroups); + info->n_cgroups = 0; + return 0; + } + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + cgroups = new(uint64_t, json_variant_elements(variant)); + if (!cgroups) + return json_log_oom(variant, flags); + + JsonVariant *e; + JSON_VARIANT_ARRAY_FOREACH(e, variant) { + bool found = false; + + if (!json_variant_is_unsigned(e)) + return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a number."); + + FOREACH_ARRAY(cg, cgroups, n_cgroups) + if (*cg == json_variant_unsigned(e)) { + found = true; + break; + } + if (found) /* suppress duplicate */ + continue; + + cgroups[n_cgroups++] = json_variant_unsigned(e); + } + + assert(n_cgroups <= json_variant_elements(variant)); + + free_and_replace(info->cgroups, cgroups); + info->n_cgroups = n_cgroups; + + return 0; +} + +static int userns_registry_load(int dir_fd, const char *fn, UserNamespaceInfo **ret) { + + static const JsonDispatch dispatch_table[] = { + { "owner", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, owner), JSON_MANDATORY }, + { "name", JSON_VARIANT_STRING, json_dispatch_string, offsetof(UserNamespaceInfo, name), JSON_MANDATORY }, + { "userns", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(UserNamespaceInfo, userns_inode), JSON_MANDATORY }, + { "start", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, start), 0 }, + { "size", JSON_VARIANT_UNSIGNED, json_dispatch_uint32, offsetof(UserNamespaceInfo, size), 0 }, + { "target", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(UserNamespaceInfo, target), 0 }, + { "cgroups", JSON_VARIANT_ARRAY, dispatch_cgroups_array, 0, 0 }, + {} + }; + + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + _cleanup_close_ int registry_fd = -EBADF; + int r; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + r = json_parse_file_at(NULL, dir_fd, fn, 0, &v, NULL, NULL); + if (r < 0) + return r; + + userns_info = userns_info_new(); + if (!userns_info) + return -ENOMEM; + + r = json_dispatch(v, dispatch_table, 0, userns_info); + if (r < 0) + return r; + + if (userns_info->userns_inode == 0) + return -EBADMSG; + if (userns_info->start == 0) + return -EBADMSG; + if (userns_info->size == 0) { + if (uid_is_valid(userns_info->start) || uid_is_valid(userns_info->target)) + return -EBADMSG; + } else { + if (!uid_is_valid(userns_info->start) || !uid_is_valid(userns_info->target)) + return -EBADMSG; + + if (userns_info->size > UINT32_MAX - userns_info->start || + userns_info->size > UINT32_MAX - userns_info->target) + return -EBADMSG; + } + + if (ret) + *ret = TAKE_PTR(userns_info); + return 0; +} + +int userns_registry_uid_exists(int dir_fd, uid_t start) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!uid_is_valid(start)) + return -ENOENT; + + if (start == 0) + return true; + + if (asprintf(&fn, "u" UID_FMT ".userns", start) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_name_exists(int dir_fd, const char *name) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (!userns_name_is_valid(name)) + return -EINVAL; + + fn = strjoin("n", name, ".userns"); + if (!fn) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_inode_exists(int dir_fd, uint64_t inode) { + _cleanup_free_ char *fn = NULL; + + assert(dir_fd >= 0); + + if (inode <= 0) + return -EINVAL; + + if (asprintf(&fn, "i%" PRIu64 ".userns", inode) < 0) + return -ENOMEM; + + if (faccessat(dir_fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) + return errno == ENOENT ? false : -errno; + + return true; +} + +int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + if (!uid_is_valid(start)) + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + if (asprintf(&fn, "u" UID_FMT ".userns", start) < 0) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (userns_info->start != start) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_load_by_userns_inode(int dir_fd, uint64_t inode, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + if (inode == 0) + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + if (asprintf(&fn, "i%" PRIu64 ".userns", inode) < 0) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (userns_info->userns_inode != inode) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_load_by_name(int dir_fd, const char *name, UserNamespaceInfo **ret) { + _cleanup_(userns_info_freep) UserNamespaceInfo *userns_info = NULL; + _cleanup_close_ int registry_fd = -EBADF; + _cleanup_free_ char *fn = NULL; + int r; + + assert(name); + + if (!userns_name_is_valid(name)) /* Invalid names never exist */ + return -ENOENT; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + fn = strjoin("n", name, ".userns"); + if (!fn) + return -ENOMEM; + + r = userns_registry_load(dir_fd, fn, &userns_info); + if (r < 0) + return r; + + if (!streq_ptr(userns_info->name, name)) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(userns_info); + + return 0; +} + +int userns_registry_store(int dir_fd, UserNamespaceInfo *info) { + _cleanup_close_ int registry_fd = -EBADF; + int r; + + assert(info); + + if (!uid_is_valid(info->owner) || + !info->name || + info->userns_inode == 0) + return -EINVAL; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_(json_variant_unrefp) JsonVariant *cgroup_array = NULL; + FOREACH_ARRAY(cg, info->cgroups, info->n_cgroups) { + r = json_variant_append_arrayb( + &cgroup_array, + JSON_BUILD_UNSIGNED(*cg)); + if (r < 0) + return r; + } + + _cleanup_(json_variant_unrefp) JsonVariant *def = NULL; + r = json_build(&def, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("owner", JSON_BUILD_UNSIGNED(info->owner)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(info->name)), + JSON_BUILD_PAIR("userns", JSON_BUILD_UNSIGNED(info->userns_inode)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "start", JSON_BUILD_UNSIGNED(info->start)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "size", JSON_BUILD_UNSIGNED(info->size)), + JSON_BUILD_PAIR_CONDITION(uid_is_valid(info->start), "target", JSON_BUILD_UNSIGNED(info->target)), + JSON_BUILD_PAIR_CONDITION(cgroup_array, "cgroups", JSON_BUILD_VARIANT(cgroup_array)))); + if (r < 0) + return r; + + _cleanup_free_ char *def_buf = NULL; + r = json_variant_format(def, 0, &def_buf); + if (r < 0) + return log_debug_errno(r, "Failed to format userns JSON object: %m"); + + _cleanup_free_ char *reg_fn = NULL, *link1_fn = NULL, *link2_fn = NULL, *owner_fn = NULL, *uid_fn = NULL; + if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0) + return log_oom_debug(); + + r = write_string_file_at(dir_fd, reg_fn, def_buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + return log_debug_errno(r, "Failed to write userns data to '%s' in registry: %m", reg_fn); + + link1_fn = strjoin("n", info->name, ".userns"); + if (!link1_fn) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, link1_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", link1_fn); + goto fail; + } + + if (uid_is_valid(info->start)) { + if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, link2_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", link2_fn); + goto fail; + } + } + + if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0) { + r = log_oom_debug(); + goto fail; + } + + if (mkdirat(dir_fd, uid_fn, 0755) < 0 && errno != EEXIST) { + r = log_debug_errno(errno, "Failed to create per-UID subdir '%s' of registry: %m", uid_fn); + goto fail; + } + + if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0) { + r = log_oom_debug(); + goto fail; + } + + r = linkat_replace(dir_fd, reg_fn, dir_fd, owner_fn); + if (r < 0) { + log_debug_errno(r, "Failed to link userns data to '%s' in registry: %m", owner_fn); + goto fail; + } + + return 0; + +fail: + if (reg_fn) + (void) unlinkat(dir_fd, reg_fn, /* flags= */ 0); + if (link1_fn) + (void) unlinkat(dir_fd, link1_fn, /* flags= */ 0); + if (link2_fn) + (void) unlinkat(dir_fd, link2_fn, /* flags= */ 0); + if (owner_fn) + (void) unlinkat(dir_fd, owner_fn, /* flags= */ 0); + if (uid_fn) + (void) unlinkat(dir_fd, uid_fn, AT_REMOVEDIR); + + return r; +} + +int userns_registry_remove(int dir_fd, UserNamespaceInfo *info) { + _cleanup_close_ int registry_fd = -EBADF; + int ret = 0, r; + + assert(info); + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_free_ char *reg_fn = NULL; + if (asprintf(®_fn, "i%" PRIu64 ".userns", info->userns_inode) < 0) + return log_oom_debug(); + + ret = RET_NERRNO(unlinkat(dir_fd, reg_fn, 0)); + + _cleanup_free_ char *link1_fn = NULL; + link1_fn = strjoin("n", info->name, ".userns"); + if (!link1_fn) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link1_fn, 0))); + + if (uid_is_valid(info->start)) { + _cleanup_free_ char *link2_fn = NULL; + + if (asprintf(&link2_fn, "u" UID_FMT ".userns", info->start) < 0) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, link2_fn, 0))); + } + + _cleanup_free_ char *uid_fn = NULL; + if (asprintf(&uid_fn, "o" UID_FMT ".owns", info->owner) < 0) + return log_oom_debug(); + + _cleanup_free_ char *owner_fn = NULL; + if (asprintf(&owner_fn, "%s/i%" PRIu64 ".userns", uid_fn, info->userns_inode) < 0) + return log_oom_debug(); + + RET_GATHER(ret, RET_NERRNO(unlinkat(dir_fd, owner_fn, 0))); + + r = RET_NERRNO(unlinkat(dir_fd, uid_fn, AT_REMOVEDIR)); + if (r != -ENOTEMPTY) + RET_GATHER(ret, r); + + return ret; +} + +bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) { + assert(userns); + + FOREACH_ARRAY(i, userns->cgroups, userns->n_cgroups) + if (*i == cgroup_id) + return true; + + return false; +} + +int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id) { + + if (userns_info_has_cgroup(userns, cgroup_id)) + return 0; + + if (!GREEDY_REALLOC(userns->cgroups, userns->n_cgroups+1)) + return -ENOMEM; + + userns->cgroups[userns->n_cgroups++] = cgroup_id; + return 1; +} + +static int userns_destroy_cgroup(uint64_t cgroup_id) { + _cleanup_close_ int cgroup_fd = -EBADF, parent_fd = -EBADF; + int r; + + cgroup_fd = cg_cgroupid_open(/* cgroupfsfd= */ -EBADF, cgroup_id); + if (cgroup_fd == -ESTALE) { + log_debug_errno(cgroup_fd, "Control group %" PRIu64 " already gone, ignoring: %m", cgroup_id); + return 0; + } + if (cgroup_fd < 0) + return log_debug_errno(errno, "Failed to open cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + _cleanup_free_ char *path = NULL; + r = fd_get_path(cgroup_fd, &path); + if (r < 0) + return log_debug_errno(r, "Failed to get path of cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + const char *e = path_startswith(path, "/sys/fs/cgroup/"); + if (!e) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Got cgroup path that doesn't start with /sys/fs/cgroup/, refusing: %s", path); + if (isempty(e)) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Got root cgroup path, which can't be right, refusing."); + + log_debug("Path of cgroup %" PRIu64 " is: %s", cgroup_id, path); + + _cleanup_free_ char *fname = NULL; + r = path_extract_filename(path, &fname); + if (r < 0) + return log_debug_errno(r, "Failed to extract name of cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + parent_fd = openat(cgroup_fd, "..", O_CLOEXEC|O_DIRECTORY); + if (parent_fd < 0) + return log_debug_errno(errno, "Failed to open parent cgroup of %" PRIu64 ", ignoring: %m", cgroup_id); + + /* Safety check, never leave cgroupfs */ + r = fd_is_fs_type(parent_fd, CGROUP2_SUPER_MAGIC); + if (r < 0) + return log_debug_errno(r, "Failed to determine if parent directory of cgroup %" PRIu64 " is still a cgroup, ignoring: %m", cgroup_id); + if (!r) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Parent directory of cgroup %" PRIu64 " is not a cgroup, refusing.", cgroup_id); + + cgroup_fd = safe_close(cgroup_fd); + + r = rm_rf_child(parent_fd, fname, REMOVE_ONLY_DIRECTORIES|REMOVE_PHYSICAL|REMOVE_CHMOD); + if (r < 0) + log_debug_errno(r, "Failed to remove delegated cgroup %" PRIu64 ", ignoring: %m", cgroup_id); + + return 0; +} + +int userns_info_remove_cgroups(UserNamespaceInfo *userns) { + int ret = 0; + + assert(userns); + + FOREACH_ARRAY(c, userns->cgroups, userns->n_cgroups) + RET_GATHER(ret, userns_destroy_cgroup(*c)); + + userns->cgroups = mfree(userns->cgroups); + userns->n_cgroups = 0; + + return ret; +} + +bool userns_name_is_valid(const char *name) { + + /* Checks if the specified string is suitable as user namespace name. */ + + if (strlen(name) > NAME_MAX) /* before we use alloca(), let's check for size */ + return false; + + const char *f = strjoina("n", name, ".userns"); /* Make sure we can name our lookup symlink with this name */ + if (!filename_is_valid(f)) + return false; + + const char *u = strjoina("ns-", name, "-65535"); /* Make sure we can turn this into valid user names */ + if (!valid_user_group_name(u, 0)) + return false; + + return true; +} + +int userns_registry_per_uid(int dir_fd, uid_t owner) { + _cleanup_close_ int registry_fd = -EBADF; + int n = 0, r; + + if (dir_fd < 0) { + registry_fd = userns_registry_open_fd(); + if (registry_fd < 0) + return registry_fd; + + dir_fd = registry_fd; + } + + _cleanup_free_ char *uid_fn = NULL; + if (asprintf(&uid_fn, "o" UID_FMT ".owns", owner) < 0) + return log_oom_debug(); + + _cleanup_free_ DirectoryEntries *de = NULL; + + r = readdir_all_at(dir_fd, uid_fn, RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &de); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_debug_errno(r, "Failed to enumerate contents of '%s' sub-directory: %m", uid_fn); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + struct dirent *e = *i; + + if (e->d_type != DT_REG) + continue; + + if (!startswith(e->d_name, "i") || !endswith(e->d_name, ".userns")) + continue; + + n++; + + if (n == INT_MAX) /* overflow safety check, just in case */ + break; + } + + return n; +} diff --git a/src/nsresourced/userns-registry.h b/src/nsresourced/userns-registry.h new file mode 100644 index 0000000000..9e66a6feec --- /dev/null +++ b/src/nsresourced/userns-registry.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#define USER_NAMESPACE_CGROUPS_DELEGATE_MAX 16 + +typedef struct UserNamespaceInfo { + uid_t owner; + char *name; + uint64_t userns_inode; + uid_t start; + uint32_t size; + uid_t target; + uint64_t *cgroups; + size_t n_cgroups; +} UserNamespaceInfo; + +UserNamespaceInfo* userns_info_new(void); +UserNamespaceInfo* userns_info_free(UserNamespaceInfo *userns); + +DEFINE_TRIVIAL_CLEANUP_FUNC(UserNamespaceInfo*, userns_info_free); + +bool userns_info_has_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id); +int userns_info_add_cgroup(UserNamespaceInfo *userns, uint64_t cgroup_id); +int userns_info_remove_cgroups(UserNamespaceInfo *userns); + +bool userns_name_is_valid(const char *name); + +int userns_registry_open_fd(void); +int userns_registry_lock(int dir_fd); + +int userns_registry_load_by_start_uid(int dir_fd, uid_t start, UserNamespaceInfo **ret); +int userns_registry_load_by_userns_inode(int dir_fd, uint64_t userns, UserNamespaceInfo **ret); +int userns_registry_load_by_name(int dir_fd, const char *name, UserNamespaceInfo **ret); + +int userns_registry_store(int dir_fd, UserNamespaceInfo *info); +int userns_registry_remove(int dir_fd, UserNamespaceInfo *info); + +int userns_registry_inode_exists(int dir_fd, uint64_t inode); +int userns_registry_name_exists(int dir_fd, const char *name); +int userns_registry_uid_exists(int dir_fd, uid_t start); + +int userns_registry_per_uid(int dir_fd, uid_t owner); diff --git a/src/nsresourced/userns-restrict.c b/src/nsresourced/userns-restrict.c new file mode 100644 index 0000000000..4e917fd758 --- /dev/null +++ b/src/nsresourced/userns-restrict.c @@ -0,0 +1,346 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "userns-restrict.h" + +#if HAVE_VMLINUX_H + +#include + +#include "bpf-dlopen.h" +#include "bpf-link.h" +#include "fd-util.h" +#include "fs-util.h" +#include "lsm-util.h" +#include "missing_mount.h" +#include "mkdir.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "namespace-util.h" +#include "path-util.h" + +#define USERNS_MAX (16U*1024U) +#define MOUNTS_MAX 4096U + +#define PROGRAM_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/programs" +#define MAP_LINK_PREFIX "/sys/fs/bpf/systemd/userns-restrict/maps" + +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) { + (void) userns_restrict_bpf__destroy(obj); /* this call is fine with NULL */ + return NULL; +} + +static int make_inner_hash_map(void) { + int fd; + + fd = compat_bpf_map_create( + BPF_MAP_TYPE_HASH, + NULL, + sizeof(int), + sizeof(uint32_t), + MOUNTS_MAX, + NULL); + if (fd < 0) + return log_debug_errno(errno, "Failed allocate inner BPF map: %m"); + + return fd; +} + +int userns_restrict_install( + bool pin, + struct userns_restrict_bpf **ret) { + + _cleanup_(userns_restrict_bpf_freep) struct userns_restrict_bpf *obj = NULL; + _cleanup_close_ int dummy_mnt_id_hash_fd = -EBADF; + int r; + + r = lsm_supported("bpf"); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "bpf-lsm not supported, can't lock down user namespace."); + + r = dlopen_bpf(); + if (r < 0) + return r; + + /* bpf_object__next_map() is not available in libbpf pre-0.7.0, and we want to use it. */ + if (!sym_bpf_object__next_map) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "libbpf too old for locking down user namespace."); + + obj = userns_restrict_bpf__open(); + if (!obj) + return log_error_errno(errno, "Failed to open userns_restrict BPF object: %m"); + + if (pin) { + struct bpf_map *map; + + /* libbpf will only create one level of dirs. Let's create the rest */ + (void) mkdir_p(MAP_LINK_PREFIX, 0755); + (void) mkdir_p(PROGRAM_LINK_PREFIX, 0755); + + map = sym_bpf_object__next_map(obj->obj, NULL); + while (map) { + _cleanup_free_ char *fn = NULL; + + fn = path_join(MAP_LINK_PREFIX, sym_bpf_map__name(map)); + if (!fn) + return log_oom(); + + r = sym_bpf_map__set_pin_path(map, fn); + if (r < 0) + return log_error_errno(r, "Failed to set pin path to '%s': %m", fn); + + map = sym_bpf_object__next_map(obj->obj, map); + } + } + + r = sym_bpf_map__set_max_entries(obj->maps.userns_mnt_id_hash, USERNS_MAX); + if (r < 0) + return log_error_errno(r, "Failed to size userns/mnt_id hash table: %m"); + + r = sym_bpf_map__set_max_entries(obj->maps.userns_ringbuf, USERNS_MAX * sizeof(unsigned int)); + if (r < 0) + return log_error_errno(r, "Failed to size userns ring buffer: %m"); + + /* Dummy map to satisfy the verifier */ + dummy_mnt_id_hash_fd = make_inner_hash_map(); + if (dummy_mnt_id_hash_fd < 0) + return dummy_mnt_id_hash_fd; + + r = sym_bpf_map__set_inner_map_fd(obj->maps.userns_mnt_id_hash, dummy_mnt_id_hash_fd); + if (r < 0) + return log_error_errno(r, "Failed to set inner BPF map: %m"); + + r = userns_restrict_bpf__load(obj); + if (r < 0) + return log_error_errno(r, "Failed to load BPF object: %m"); + + for (int i = 0; i < obj->skeleton->prog_cnt; i++) { + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + struct bpf_prog_skeleton *ps = obj->skeleton->progs + i; + _cleanup_free_ char *fn = NULL; + bool linked = false; + const char *e; + + e = startswith(ps->name, "userns_restrict_"); + assert(e); + + if (pin) { + fn = path_join(PROGRAM_LINK_PREFIX, e); + if (!fn) + return log_oom(); + + link = sym_bpf_link__open(fn); + r = sym_libbpf_get_error(link); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Unable to open pinned program link: %m"); + link = NULL; + } else { + linked = true; + log_info("userns-restrict BPF-LSM program %s already attached.", ps->name); + } + } + + if (!link) { + link = sym_bpf_program__attach(*ps->prog); + r = sym_libbpf_get_error(link); + if (r < 0) + return log_error_errno(r, "Failed to attach LSM BPF program: %m"); + + log_info("userns-restrict BPF-LSM program %s now attached.", ps->name); + } + + if (pin && !linked) { + assert(fn); + + r = sym_bpf_link__pin(link, fn); + if (r < 0) + return log_error_errno(r, "Failed to pin LSM attachment: %m"); + } + + *ps->link = TAKE_PTR(link); + } + + if (pin) { + r = sym_bpf_object__pin_maps(obj->obj, NULL); + if (r < 0) + return log_error_errno(r, "Failed to pin BPF maps: %m"); + } + + if (ret) + *ret = TAKE_PTR(obj); + + return 0; +} + +int userns_restrict_put_by_inode( + struct userns_restrict_bpf *obj, + uint64_t userns_inode, + bool replace, + const int mount_fds[], + size_t n_mount_fds) { + + _cleanup_close_ int inner_map_fd = -EBADF; + _cleanup_free_ int *mnt_ids = NULL; + uint64_t ino = userns_inode; + int r, outer_map_fd; + + assert(obj); + assert(userns_inode != 0); + assert(n_mount_fds == 0 || mount_fds); + + /* The BPF map type BPF_MAP_TYPE_HASH_OF_MAPS only supports 32bit keys, and user namespace inode + * numbers are 32bit too, even though ino_t is 64bit these days. Should we ever run into a 64bit + * inode let's refuse early, we can't support this with the current BPF code for now. */ + if (userns_inode > UINT32_MAX) + return -EINVAL; + + mnt_ids = new(int, n_mount_fds); + if (!mnt_ids) + return -ENOMEM; + + for (size_t i = 0; i < n_mount_fds; i++) { + r = path_get_mnt_id_at(mount_fds[i], "", mnt_ids + i); + if (r < 0) + return log_debug_errno(r, "Failed to get mount ID: %m"); + } + + outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash); + if (outer_map_fd < 0) + return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m"); + + if (replace) { + /* Add if missing, replace if already exists */ + inner_map_fd = make_inner_hash_map(); + if (inner_map_fd < 0) + return inner_map_fd; + + r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_ANY); + if (r < 0) + return log_debug_errno(errno, "Failed to replace map in inode hash: %m"); + } else { + /* Let's add an entry for this userns inode if missing. If it exists just extend the existing map. We + * might race against each other, hence we try a couple of times */ + for (size_t n_try = 10;; n_try--) { + uint32_t innermap_id; + + if (n_try == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Stillcan't create inode entry in BPF map after 10 tries."); + + r = sym_bpf_map_lookup_elem(outer_map_fd, &ino, &innermap_id); + if (r >= 0) { + inner_map_fd = sym_bpf_map_get_fd_by_id(innermap_id); + if (inner_map_fd < 0) + return log_debug_errno(inner_map_fd, "Failed to get file descriptor for inner map: %m"); + + break; + } + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to look up inode hash entry: %m"); + + /* No entry for this user namespace yet. Let's create one */ + inner_map_fd = make_inner_hash_map(); + if (inner_map_fd < 0) + return inner_map_fd; + + r = sym_bpf_map_update_elem(outer_map_fd, &ino, &inner_map_fd, BPF_NOEXIST); + if (r >= 0) + break; + if (errno != EEXIST) + return log_debug_errno(errno, "Failed to add mount ID list to inode hash: %m"); + } + } + + FOREACH_ARRAY(mntid, mnt_ids, n_mount_fds) { + uint32_t dummy_value = 1; + + r = sym_bpf_map_update_elem(inner_map_fd, mntid, &dummy_value, BPF_ANY); + if (r < 0) + return log_debug_errno(errno, "Failed to add mount ID to map: %m"); + + log_debug("Allowing mount %i on userns inode %" PRIu64, *mntid, ino); + } + + return 0; +} + +int userns_restrict_put_by_fd( + struct userns_restrict_bpf *obj, + int userns_fd, + bool replace, + const int mount_fds[], + size_t n_mount_fds) { + + struct stat st; + int r; + + assert(obj); + assert(userns_fd >= 0); + assert(n_mount_fds == 0 || mount_fds); + + r = fd_is_ns(userns_fd, CLONE_NEWUSER); + if (r < 0) + return log_debug_errno(r, "Failed to determine if file descriptor is user namespace: %m"); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADF), "User namespace fd is not actually a user namespace fd."); + + if (fstat(userns_fd, &st) < 0) + return log_debug_errno(errno, "Failed to fstat() user namespace: %m"); + + return userns_restrict_put_by_inode( + obj, + st.st_ino, + replace, + mount_fds, + n_mount_fds); +} + +int userns_restrict_reset_by_inode( + struct userns_restrict_bpf *obj, + uint64_t ino) { + + int r, outer_map_fd; + unsigned u; + + assert(obj); + assert(ino != 0); + + if (ino > UINT32_MAX) /* inodes larger than 32bit are definitely not included in our map, exit early */ + return 0; + + outer_map_fd = sym_bpf_map__fd(obj->maps.userns_mnt_id_hash); + if (outer_map_fd < 0) + return log_debug_errno(outer_map_fd, "Failed to get outer BPF map fd: %m"); + + u = (uint32_t) ino; + + r = sym_bpf_map_delete_elem(outer_map_fd, &u); + if (r < 0) + return log_debug_errno(outer_map_fd, "Failed to remove entry for inode %" PRIu64 " from outer map: %m", ino); + + return 0; +} + +#else +int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj) { + return NULL; +} + +int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} + +int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode) { + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "User Namespace Restriction BPF support disabled."); +} +#endif diff --git a/src/nsresourced/userns-restrict.h b/src/nsresourced/userns-restrict.h new file mode 100644 index 0000000000..37aed7b350 --- /dev/null +++ b/src/nsresourced/userns-restrict.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "macro.h" + +#if HAVE_VMLINUX_H +#include "bpf/userns_restrict/userns-restrict-skel.h" +#else +struct userns_restrict_bpf; +#endif + +int userns_restrict_install(bool pin, struct userns_restrict_bpf **ret); +struct userns_restrict_bpf *userns_restrict_bpf_free(struct userns_restrict_bpf *obj); + +int userns_restrict_put_by_fd(struct userns_restrict_bpf *obj, int userns_fd, bool replace, const int mount_fds[], size_t n_mount_fds); +int userns_restrict_put_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode, bool replace, const int mount_fds[], size_t n_mount_fds); + +int userns_restrict_reset_by_inode(struct userns_restrict_bpf *obj, uint64_t userns_inode); + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct userns_restrict_bpf*, userns_restrict_bpf_free); diff --git a/src/shared/bpf-compat.h b/src/shared/bpf-compat.h index 9ccb7d8205..5a7945cb9b 100644 --- a/src/shared/bpf-compat.h +++ b/src/shared/bpf-compat.h @@ -26,6 +26,7 @@ struct bpf_map_create_opts; * When removing this file move these back to bpf-dlopen.h */ extern int (*sym_bpf_map_create)(enum bpf_map_type, const char *, __u32, __u32, __u32, const struct bpf_map_create_opts *); extern int (*sym_libbpf_probe_bpf_prog_type)(enum bpf_prog_type, const void *); +extern struct bpf_map* (*sym_bpf_object__next_map)(const struct bpf_object *obj, const struct bpf_map *map); /* compat symbols removed in libbpf 1.0 */ extern int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index 6d59001b2c..de3e40516e 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -18,28 +18,41 @@ #define MODERN_LIBBPF 0 #endif -DLSYM_FUNCTION(bpf_program__attach_cgroup); -DLSYM_FUNCTION(bpf_program__attach_lsm); -DLSYM_FUNCTION(bpf_link__fd); DLSYM_FUNCTION(bpf_link__destroy); +DLSYM_FUNCTION(bpf_link__fd); +DLSYM_FUNCTION(bpf_link__open); +DLSYM_FUNCTION(bpf_link__pin); DLSYM_FUNCTION(bpf_map__fd); DLSYM_FUNCTION(bpf_map__name); -DLSYM_FUNCTION(bpf_map__set_max_entries); -DLSYM_FUNCTION(bpf_map_update_elem); -DLSYM_FUNCTION(bpf_map_delete_elem); DLSYM_FUNCTION(bpf_map__set_inner_map_fd); -DLSYM_FUNCTION(bpf_object__open_skeleton); -DLSYM_FUNCTION(bpf_object__load_skeleton); +DLSYM_FUNCTION(bpf_map__set_max_entries); +DLSYM_FUNCTION(bpf_map__set_pin_path); +DLSYM_FUNCTION(bpf_map_delete_elem); +DLSYM_FUNCTION(bpf_map_get_fd_by_id); +DLSYM_FUNCTION(bpf_map_lookup_elem); +DLSYM_FUNCTION(bpf_map_update_elem); DLSYM_FUNCTION(bpf_object__attach_skeleton); -DLSYM_FUNCTION(bpf_object__detach_skeleton); DLSYM_FUNCTION(bpf_object__destroy_skeleton); +DLSYM_FUNCTION(bpf_object__detach_skeleton); +DLSYM_FUNCTION(bpf_object__load_skeleton); +DLSYM_FUNCTION(bpf_object__name); +DLSYM_FUNCTION(bpf_object__open_skeleton); +DLSYM_FUNCTION(bpf_object__pin_maps); +DLSYM_FUNCTION(bpf_program__attach); +DLSYM_FUNCTION(bpf_program__attach_cgroup); +DLSYM_FUNCTION(bpf_program__attach_lsm); DLSYM_FUNCTION(bpf_program__name); -DLSYM_FUNCTION(libbpf_set_print); DLSYM_FUNCTION(libbpf_get_error); +DLSYM_FUNCTION(libbpf_set_print); +DLSYM_FUNCTION(ring_buffer__epoll_fd); +DLSYM_FUNCTION(ring_buffer__free); +DLSYM_FUNCTION(ring_buffer__new); +DLSYM_FUNCTION(ring_buffer__poll); /* new symbols available from libbpf 0.7.0 */ int (*sym_bpf_map_create)(enum bpf_map_type, const char *, __u32, __u32, __u32, const struct bpf_map_create_opts *); int (*sym_libbpf_probe_bpf_prog_type)(enum bpf_prog_type, const void *); +struct bpf_map* (*sym_bpf_object__next_map)(const struct bpf_object *obj, const struct bpf_map *map); /* compat symbols removed in libbpf 1.0 */ int (*sym_bpf_create_map)(enum bpf_map_type, int key_size, int value_size, int max_entries, __u32 map_flags); @@ -90,6 +103,8 @@ int dlopen_bpf(void) { DLSYM_ARG(bpf_probe_prog_type) #endif ); + + /* NB: we don't try to load bpf_object__next_map() on old versions */ } else { log_debug("Loaded 'libbpf.so.1' via dlopen()"); @@ -98,11 +113,13 @@ int dlopen_bpf(void) { dl, LOG_DEBUG, #if MODERN_LIBBPF DLSYM_ARG(bpf_map_create), - DLSYM_ARG(libbpf_probe_bpf_prog_type) + DLSYM_ARG(libbpf_probe_bpf_prog_type), + DLSYM_ARG(bpf_object__next_map) #else /* These symbols did not exist in old libbpf, hence we cannot type check them */ DLSYM_ARG_FORCE(bpf_map_create), - DLSYM_ARG_FORCE(libbpf_probe_bpf_prog_type) + DLSYM_ARG_FORCE(libbpf_probe_bpf_prog_type), + DLSYM_ARG_FORCE(bpf_object__next_map) #endif ); } @@ -113,28 +130,41 @@ int dlopen_bpf(void) { dl, LOG_DEBUG, DLSYM_ARG(bpf_link__destroy), DLSYM_ARG(bpf_link__fd), + DLSYM_ARG(bpf_link__open), + DLSYM_ARG(bpf_link__pin), DLSYM_ARG(bpf_map__fd), DLSYM_ARG(bpf_map__name), - DLSYM_ARG(bpf_map__set_max_entries), - DLSYM_ARG(bpf_map_update_elem), - DLSYM_ARG(bpf_map_delete_elem), DLSYM_ARG(bpf_map__set_inner_map_fd), - DLSYM_ARG(bpf_object__open_skeleton), - DLSYM_ARG(bpf_object__load_skeleton), + DLSYM_ARG(bpf_map__set_max_entries), + DLSYM_ARG(bpf_map__set_pin_path), + DLSYM_ARG(bpf_map_delete_elem), + DLSYM_ARG(bpf_map_get_fd_by_id), + DLSYM_ARG(bpf_map_lookup_elem), + DLSYM_ARG(bpf_map_update_elem), DLSYM_ARG(bpf_object__attach_skeleton), - DLSYM_ARG(bpf_object__detach_skeleton), DLSYM_ARG(bpf_object__destroy_skeleton), + DLSYM_ARG(bpf_object__detach_skeleton), + DLSYM_ARG(bpf_object__load_skeleton), + DLSYM_ARG(bpf_object__name), + DLSYM_ARG(bpf_object__open_skeleton), + DLSYM_ARG(bpf_object__pin_maps), #if MODERN_LIBBPF + DLSYM_ARG(bpf_program__attach), DLSYM_ARG(bpf_program__attach_cgroup), DLSYM_ARG(bpf_program__attach_lsm), #else /* libbpf added a "const" to function parameters where it should not have, ignore this type incompatibility */ + DLSYM_ARG_FORCE(bpf_program__attach), DLSYM_ARG_FORCE(bpf_program__attach_cgroup), DLSYM_ARG_FORCE(bpf_program__attach_lsm), #endif DLSYM_ARG(bpf_program__name), + DLSYM_ARG(libbpf_get_error), DLSYM_ARG(libbpf_set_print), - DLSYM_ARG(libbpf_get_error)); + DLSYM_ARG(ring_buffer__epoll_fd), + DLSYM_ARG(ring_buffer__free), + DLSYM_ARG(ring_buffer__new), + DLSYM_ARG(ring_buffer__poll)); if (r < 0) return r; diff --git a/src/shared/bpf-dlopen.h b/src/shared/bpf-dlopen.h index 859405e3c6..6deca829f1 100644 --- a/src/shared/bpf-dlopen.h +++ b/src/shared/bpf-dlopen.h @@ -9,26 +9,38 @@ #include "bpf-compat.h" #include "dlfcn-util.h" -DLSYM_PROTOTYPE(bpf_program__attach_cgroup); -DLSYM_PROTOTYPE(bpf_program__attach_lsm); -DLSYM_PROTOTYPE(bpf_link__fd); DLSYM_PROTOTYPE(bpf_link__destroy); +DLSYM_PROTOTYPE(bpf_link__fd); +DLSYM_PROTOTYPE(bpf_link__open); +DLSYM_PROTOTYPE(bpf_link__pin); DLSYM_PROTOTYPE(bpf_map__fd); DLSYM_PROTOTYPE(bpf_map__name); -DLSYM_PROTOTYPE(bpf_map__set_max_entries); -DLSYM_PROTOTYPE(bpf_map_update_elem); -DLSYM_PROTOTYPE(bpf_map_delete_elem); DLSYM_PROTOTYPE(bpf_map__set_inner_map_fd); +DLSYM_PROTOTYPE(bpf_map__set_max_entries); +DLSYM_PROTOTYPE(bpf_map__set_pin_path); +DLSYM_PROTOTYPE(bpf_map_delete_elem); +DLSYM_PROTOTYPE(bpf_map_get_fd_by_id); +DLSYM_PROTOTYPE(bpf_map_lookup_elem); +DLSYM_PROTOTYPE(bpf_map_update_elem); /* The *_skeleton APIs are autogenerated by bpftool, the targets can be found * in ./build/src/core/bpf/socket_bind/socket-bind.skel.h */ -DLSYM_PROTOTYPE(bpf_object__open_skeleton); -DLSYM_PROTOTYPE(bpf_object__load_skeleton); DLSYM_PROTOTYPE(bpf_object__attach_skeleton); -DLSYM_PROTOTYPE(bpf_object__detach_skeleton); DLSYM_PROTOTYPE(bpf_object__destroy_skeleton); +DLSYM_PROTOTYPE(bpf_object__detach_skeleton); +DLSYM_PROTOTYPE(bpf_object__load_skeleton); +DLSYM_PROTOTYPE(bpf_object__name); +DLSYM_PROTOTYPE(bpf_object__open_skeleton); +DLSYM_PROTOTYPE(bpf_object__pin_maps); +DLSYM_PROTOTYPE(bpf_program__attach); +DLSYM_PROTOTYPE(bpf_program__attach_cgroup); +DLSYM_PROTOTYPE(bpf_program__attach_lsm); DLSYM_PROTOTYPE(bpf_program__name); -DLSYM_PROTOTYPE(libbpf_set_print); DLSYM_PROTOTYPE(libbpf_get_error); +DLSYM_PROTOTYPE(libbpf_set_print); +DLSYM_PROTOTYPE(ring_buffer__epoll_fd); +DLSYM_PROTOTYPE(ring_buffer__free); +DLSYM_PROTOTYPE(ring_buffer__new); +DLSYM_PROTOTYPE(ring_buffer__poll); #endif diff --git a/src/shared/cgroup-setup.c b/src/shared/cgroup-setup.c index d9cca6ced4..f82af4cf43 100644 --- a/src/shared/cgroup-setup.c +++ b/src/shared/cgroup-setup.c @@ -384,6 +384,20 @@ int cg_attach(const char *controller, const char *path, pid_t pid) { return 0; } +int cg_fd_attach(int fd, pid_t pid) { + char c[DECIMAL_STR_MAX(pid_t) + 2]; + + assert(fd >= 0); + assert(pid >= 0); + + if (pid == 0) + pid = getpid_cached(); + + xsprintf(c, PID_FMT "\n", pid); + + return write_string_file_at(fd, "cgroup.procs", c, WRITE_STRING_FILE_DISABLE_BUFFER); +} + int cg_attach_fallback(const char *controller, const char *path, pid_t pid) { int r; diff --git a/src/shared/cgroup-setup.h b/src/shared/cgroup-setup.h index c35884ee4d..283ab6757c 100644 --- a/src/shared/cgroup-setup.h +++ b/src/shared/cgroup-setup.h @@ -21,6 +21,7 @@ int cg_trim(const char *controller, const char *path, bool delete_root); int cg_create(const char *controller, const char *path); int cg_attach(const char *controller, const char *path, pid_t pid); +int cg_fd_attach(int fd, pid_t pid); int cg_attach_fallback(const char *controller, const char *path, pid_t pid); int cg_create_and_attach(const char *controller, const char *path, pid_t pid); diff --git a/src/shared/cgroup-show.c b/src/shared/cgroup-show.c index c2ee1c5aef..c7af10687d 100644 --- a/src/shared/cgroup-show.c +++ b/src/shared/cgroup-show.c @@ -150,18 +150,9 @@ static int show_cgroup_name( delegate = r > 0; if (FLAGS_SET(flags, OUTPUT_CGROUP_ID)) { - cg_file_handle fh = CG_FILE_HANDLE_INIT; - int mnt_id = -1; - - if (name_to_handle_at( - fd, - "", - &fh.file_handle, - &mnt_id, - AT_EMPTY_PATH) < 0) + r = cg_fd_get_cgroupid(fd, &cgroupid); + if (r < 0) log_debug_errno(errno, "Failed to determine cgroup ID of %s, ignoring: %m", path); - else - cgroupid = CG_FILE_HANDLE_CGROUPID(fh); } r = path_extract_filename(path, &b); diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 00d32a9c87..b7a97f2b63 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -48,7 +48,7 @@ #include "vpick.h" #include "xattr-util.h" -static const char* const image_search_path[_IMAGE_CLASS_MAX] = { +const char* const image_search_path[_IMAGE_CLASS_MAX] = { [IMAGE_MACHINE] = "/etc/machines\0" /* only place symlinks here */ "/run/machines\0" /* and here too */ "/var/lib/machines\0" /* the main place for images */ @@ -1295,7 +1295,12 @@ static void make_lock_dir(void) { (void) mkdir("/run/systemd/nspawn/locks", 0700); } -int image_path_lock(const char *path, int operation, LockFile *global, LockFile *local) { +int image_path_lock( + const char *path, + int operation, + LockFile *ret_global, + LockFile *ret_local) { + _cleanup_free_ char *p = NULL; LockFile t = LOCK_FILE_INIT; struct stat st; @@ -1303,8 +1308,7 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile int r; assert(path); - assert(global); - assert(local); + assert(ret_local); /* Locks an image path. This actually creates two locks: one "local" one, next to the image path * itself, which might be shared via NFS. And another "global" one, in /run, that uses the @@ -1326,7 +1330,9 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile } if (getenv_bool("SYSTEMD_NSPAWN_LOCK") == 0) { - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } @@ -1342,19 +1348,23 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (exclusive) return -EBUSY; - *local = *global = (LockFile) LOCK_FILE_INIT; + *ret_local = LOCK_FILE_INIT; + if (ret_global) + *ret_global = LOCK_FILE_INIT; return 0; } - if (stat(path, &st) >= 0) { - if (S_ISBLK(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); - else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) - r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); - else - return -ENOTTY; - if (r < 0) - return -ENOMEM; + if (ret_global) { + if (stat(path, &st) >= 0) { + if (S_ISBLK(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/block-%u:%u", major(st.st_rdev), minor(st.st_rdev)); + else if (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)) + r = asprintf(&p, "/run/systemd/nspawn/locks/inode-%lu:%lu", (unsigned long) st.st_dev, (unsigned long) st.st_ino); + else + return -ENOTTY; + if (r < 0) + return -ENOMEM; + } } /* For block devices we don't need the "local" lock, as the major/minor lock above should be @@ -1372,15 +1382,15 @@ int image_path_lock(const char *path, int operation, LockFile *global, LockFile if (p) { make_lock_dir(); - r = make_lock_file(p, operation, global); + r = make_lock_file(p, operation, ret_global); if (r < 0) { release_lock_file(&t); return r; } - } else - *global = (LockFile) LOCK_FILE_INIT; + } else if (ret_global) + *ret_global = LOCK_FILE_INIT; - *local = t; + *ret_local = t; return 0; } @@ -1512,7 +1522,10 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { if (r < 0) return r; - r = dissected_image_acquire_metadata(m, flags); + r = dissected_image_acquire_metadata( + m, + /* userns_fd= */ -EBADF, + flags); if (r < 0) return r; diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 350c3a22cc..6491cec886 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -122,3 +122,5 @@ int image_to_json(const struct Image *i, JsonVariant **ret); const char *image_root_to_string(ImageClass c) _const_; extern const struct hash_ops image_hash_ops; + +extern const char* const image_search_path[_IMAGE_CLASS_MAX]; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index a0832c9828..c5f35c7cb7 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -74,6 +74,7 @@ #include "tmpfile-util.h" #include "udev-util.h" #include "user-util.h" +#include "varlink.h" #include "xattr-util.h" /* how many times to wait for the device nodes to appear */ @@ -1643,6 +1644,20 @@ int dissect_image_file_and_warn( verity); } +void dissected_image_close(DissectedImage *m) { + if (!m) + return; + + /* Closes all fds we keep open assocated with this, but nothing else */ + + FOREACH_ARRAY(p, m->partitions, _PARTITION_DESIGNATOR_MAX) { + p->mount_node_fd = safe_close(p->mount_node_fd); + p->fsmount_fd = safe_close(p->fsmount_fd); + } + + m->loop = loop_device_unref(m->loop); +} + DissectedImage* dissected_image_unref(DissectedImage *m) { if (!m) return NULL; @@ -2937,6 +2952,7 @@ int dissected_image_decrypt( * > 0 → Decrypted successfully * -ENOKEY → There's something to decrypt but no key was supplied * -EKEYREJECTED → Passed key was not correct + * -EBUSY → Generic Verity error (kernel is not very explanatory) */ if (verity && verity->root_hash && verity->root_hash_size < sizeof(sd_id128_t)) @@ -3386,7 +3402,10 @@ int dissected_image_load_verity_sig_partition( return 1; } -int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_flags) { +int dissected_image_acquire_metadata( + DissectedImage *m, + int userns_fd, + DissectImageFlags extra_flags) { enum { META_HOSTNAME, @@ -3445,13 +3464,22 @@ int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_ goto finish; } - r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, &child); + r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM, &child); if (r < 0) goto finish; if (r == 0) { - /* Child in a new mount namespace */ + /* Child */ error_pipe[0] = safe_close(error_pipe[0]); + if (userns_fd < 0) + r = detach_mount_namespace_harder(0, 0); + else + r = detach_mount_namespace_userns(userns_fd); + if (r < 0) { + log_debug_errno(r, "Failed to detach mount namespace: %m"); + goto inner_fail; + } + r = dissected_image_mount( m, t, @@ -4129,3 +4157,210 @@ int get_common_dissect_directory(char **ret) { return 0; } + +#if HAVE_BLKID + +static JSON_DISPATCH_ENUM_DEFINE(dispatch_architecture, Architecture, architecture_from_string); +static JSON_DISPATCH_ENUM_DEFINE(dispatch_partition_designator, PartitionDesignator, partition_designator_from_string); + +typedef struct PartitionFields { + PartitionDesignator designator; + bool rw; + bool growfs; + unsigned partno; + Architecture architecture; + sd_id128_t uuid; + char *fstype; + char *label; + uint64_t size; + uint64_t offset; + unsigned fsmount_fd_idx; +} PartitionFields; + +static void partition_fields_done(PartitionFields *f) { + assert(f); + + f->fstype = mfree(f->fstype); + f->label = mfree(f->label); +} + +typedef struct ReplyParameters { + JsonVariant *partitions; + char *image_policy; + uint64_t image_size; + uint32_t sector_size; + sd_id128_t image_uuid; +} ReplyParameters; + +static void reply_parameters_done(ReplyParameters *p) { + assert(p); + + p->image_policy = mfree(p->image_policy); + p->partitions = json_variant_unref(p->partitions); +} + +#endif + +int mountfsd_mount_image( + const char *path, + int userns_fd, + const ImagePolicy *image_policy, + DissectImageFlags flags, + DissectedImage **ret) { + +#if HAVE_BLKID + _cleanup_(reply_parameters_done) ReplyParameters p = {}; + + static const JsonDispatch dispatch_table[] = { + { "partitions", JSON_VARIANT_ARRAY, json_dispatch_variant, offsetof(struct ReplyParameters, partitions), JSON_MANDATORY }, + { "imagePolicy", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct ReplyParameters, image_policy), 0 }, + { "imageSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct ReplyParameters, image_size), JSON_MANDATORY }, + { "sectorSize", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint32, offsetof(struct ReplyParameters, sector_size), JSON_MANDATORY }, + { "imageUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(struct ReplyParameters, image_uuid), 0 }, + {} + }; + + _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; + _cleanup_close_ int image_fd = -EBADF; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_free_ char *ps = NULL; + unsigned max_fd = UINT_MAX; + const char *error_id; + int r; + + assert(path); + assert(ret); + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = 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 = 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"); + + image_fd = open(path, O_RDONLY|O_CLOEXEC); + if (image_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", path); + + r = varlink_push_dup_fd(vl, image_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + + if (userns_fd >= 0) { + r = varlink_push_dup_fd(vl, userns_fd); + if (r < 0) + return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + } + + if (image_policy) { + r = image_policy_to_string(image_policy, /* simplify= */ false, &ps); + if (r < 0) + return log_error_errno(r, "Failed format image policy to string: %m"); + } + + JsonVariant *reply = NULL; + r = varlink_callb( + vl, + "io.systemd.MountFileSystem.MountImage", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("imageFileDescriptor", JSON_BUILD_UNSIGNED(0)), + JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(1)), + JSON_BUILD_PAIR("readOnly", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY))), + JSON_BUILD_PAIR("growFileSystems", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_GROWFS))), + JSON_BUILD_PAIR_CONDITION(ps, "imagePolicy", JSON_BUILD_STRING(ps)), + JSON_BUILD_PAIR("allowInteractiveAuthentication", JSON_BUILD_BOOLEAN(FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))))); + if (r < 0) + return log_error_errno(r, "Failed to call MountImage() varlink call: %m"); + if (!isempty(error_id)) + return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to call MountImage() varlink call: %s", error_id); + + r = json_dispatch(reply, dispatch_table, JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + + log_debug("Effective image policy: %s", p.image_policy); + + JsonVariant *i; + JSON_VARIANT_ARRAY_FOREACH(i, p.partitions) { + _cleanup_close_ int fsmount_fd = -EBADF; + + _cleanup_(partition_fields_done) PartitionFields pp = { + .designator = _PARTITION_DESIGNATOR_INVALID, + .architecture = _ARCHITECTURE_INVALID, + .size = UINT64_MAX, + .offset = UINT64_MAX, + .fsmount_fd_idx = UINT_MAX, + }; + + static const JsonDispatch partition_dispatch_table[] = { + { "designator", JSON_VARIANT_STRING, dispatch_partition_designator, offsetof(struct PartitionFields, designator), JSON_MANDATORY }, + { "writable", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct PartitionFields, rw), JSON_MANDATORY }, + { "growFileSystem", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(struct PartitionFields, growfs), JSON_MANDATORY }, + { "partitionNumber", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(struct PartitionFields, partno), 0 }, + { "architecture", JSON_VARIANT_STRING, dispatch_architecture, offsetof(struct PartitionFields, architecture), 0 }, + { "partitionUuid", JSON_VARIANT_STRING, json_dispatch_id128, offsetof(struct PartitionFields, uuid), 0 }, + { "fileSystemType", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct PartitionFields, fstype), JSON_MANDATORY }, + { "partitionLabel", JSON_VARIANT_STRING, json_dispatch_string, offsetof(struct PartitionFields, label), 0 }, + { "size", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct PartitionFields, size), JSON_MANDATORY }, + { "offset", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct PartitionFields, offset), JSON_MANDATORY }, + { "mountFileDescriptor", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint, offsetof(struct PartitionFields, fsmount_fd_idx), JSON_MANDATORY }, + {} + }; + + r = json_dispatch(i, partition_dispatch_table, JSON_ALLOW_EXTENSIONS, &pp); + if (r < 0) + return log_error_errno(r, "Failed to parse partition data: %m"); + + if (pp.fsmount_fd_idx != UINT_MAX) { + if (max_fd == UINT_MAX || pp.fsmount_fd_idx > max_fd) + max_fd = pp.fsmount_fd_idx; + + fsmount_fd = varlink_take_fd(vl, pp.fsmount_fd_idx); + if (fsmount_fd < 0) + return fsmount_fd; + } + + assert(pp.designator >= 0); + + if (!di) { + r = dissected_image_new(path, &di); + if (r < 0) + return log_error_errno(r, "Failed to allocated new dissected image structure: %m"); + } + + if (di->partitions[pp.designator].found) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate partition data for '%s'.", partition_designator_to_string(pp.designator)); + + di->partitions[pp.designator] = (DissectedPartition) { + .found = true, + .rw = pp.rw, + .growfs = pp.growfs, + .partno = pp.partno, + .architecture = pp.architecture, + .uuid = pp.uuid, + .fstype = TAKE_PTR(pp.fstype), + .label = TAKE_PTR(pp.label), + .mount_node_fd = -EBADF, + .size = pp.size, + .offset = pp.offset, + .fsmount_fd = TAKE_FD(fsmount_fd), + }; + } + + di->image_size = p.image_size; + di->sector_size = p.sector_size; + di->image_uuid = p.image_uuid; + + *ret = TAKE_PTR(di); + return 0; +#else + return -EOPNOTSUPP; +#endif +} diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 559d5b861d..e31fd54f84 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -88,6 +88,7 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_ALLOW_EMPTY = 1 << 24, /* Allow that no usable partitions is present */ 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 */ } DissectImageFlags; struct DissectedImage { @@ -163,6 +164,7 @@ int dissect_image_file_and_warn(const char *path, const VeritySettings *verity, int dissect_loop_device(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); int dissect_loop_device_and_warn(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +void dissected_image_close(DissectedImage *m); DissectedImage* dissected_image_unref(DissectedImage *m); DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref); @@ -171,7 +173,7 @@ int dissected_image_decrypt_interactively(DissectedImage *m, const char *passphr int dissected_image_mount(DissectedImage *m, const char *dest, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags); int dissected_image_mount_and_warn(DissectedImage *m, const char *where, uid_t uid_shift, uid_t uid_range, int userns_fd, DissectImageFlags flags); -int dissected_image_acquire_metadata(DissectedImage *m, DissectImageFlags extra_flags); +int dissected_image_acquire_metadata(DissectedImage *m, int userns_fd, DissectImageFlags extra_flags); Architecture dissected_image_architecture(DissectedImage *m); @@ -198,6 +200,14 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DecryptedImage*, decrypted_image_unref); int dissected_image_relinquish(DissectedImage *m); int verity_settings_load(VeritySettings *verity, const char *image, const char *root_hash_path, const char *root_hash_sig_path); + +static inline bool verity_settings_set(const VeritySettings *settings) { + return settings && + (settings->root_hash_size > 0 || + (settings->root_hash_sig_size > 0 || + settings->data_path)); +} + void verity_settings_done(VeritySettings *verity); static inline bool verity_settings_data_covers(const VeritySettings *verity, PartitionDesignator d) { @@ -232,3 +242,5 @@ 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); diff --git a/src/shared/image-policy.c b/src/shared/image-policy.c index 47ca62c731..e7bd84e144 100644 --- a/src/shared/image-policy.c +++ b/src/shared/image-policy.c @@ -50,6 +50,20 @@ PartitionPolicyFlags partition_policy_flags_extend(PartitionPolicyFlags flags) { return flags; } +PartitionPolicyFlags partition_policy_flags_reduce(PartitionPolicyFlags flags) { + /* The reverse of partition_policy_flags_extend(): if some parts of the flags field allow all + * possible options, let's remove it from the flags to make them shorter */ + + if (FLAGS_SET(flags, _PARTITION_POLICY_USE_MASK)) + flags &= ~_PARTITION_POLICY_USE_MASK; + if (FLAGS_SET(flags, _PARTITION_POLICY_READ_ONLY_MASK)) + flags &= ~_PARTITION_POLICY_READ_ONLY_MASK; + if (FLAGS_SET(flags, _PARTITION_POLICY_GROWFS_MASK)) + flags &= ~_PARTITION_POLICY_GROWFS_MASK; + + return flags; +} + static PartitionPolicyFlags partition_policy_normalized_flags(const PartitionPolicy *policy) { PartitionPolicyFlags flags = ASSERT_PTR(policy)->flags; @@ -676,6 +690,90 @@ int parse_image_policy_argument(const char *s, ImagePolicy **policy) { return free_and_replace_full(*policy, np, image_policy_free); } +static bool partition_policy_flags_has_unspecified(PartitionPolicyFlags flags) { + + if ((flags & _PARTITION_POLICY_USE_MASK) == 0) + return true; + if ((flags & _PARTITION_POLICY_READ_ONLY_MASK) == 0) + return true; + if ((flags & _PARTITION_POLICY_GROWFS_MASK) == 0) + return true; + + return false; +} + +int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret) { + _cleanup_(image_policy_freep) ImagePolicy *p = NULL; + + /* Calculates the intersection of the specified policies, i.e. only what is permitted in both. This + * might fail with -ENAVAIL if the intersection is an "impossible policy". For example, if a root + * partition my neither be used, nor be absent, nor be unused then this is considered + * "impossible". */ + + p = image_policy_new(_PARTITION_DESIGNATOR_MAX); + if (!p) + return -ENOMEM; + + p->default_flags = + partition_policy_flags_extend(image_policy_default(a)) & + partition_policy_flags_extend(image_policy_default(b)); + + if (partition_policy_flags_has_unspecified(p->default_flags)) /* Intersection empty? */ + return -ENAVAIL; + + p->default_flags = partition_policy_flags_reduce(p->default_flags); + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags x, y, z, df; + + /* If this designator has no entry in either policy we don't need to include it in the intersection either. */ + if (!image_policy_bsearch(a, d) && !image_policy_bsearch(b, d)) + continue; + + /* Expand this policy flags field to the "long" form, i.e. for each part of the flags that + * are left unspcified add in all possible options */ + x = image_policy_get_exhaustively(a, d); + if (x < 0) + return x; + + y = image_policy_get_exhaustively(b, d); + if (y < 0) + return y; + + /* Mask it */ + z = x & y; + + /* Check if the intersection is empty for this partition. If so, generate a clear error */ + if (partition_policy_flags_has_unspecified(z)) + return -ENAVAIL; + + df = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(p), + .designator = d, + }); + if (df < 0) + return df; + if (df == z) /* Same as default? then let's skip this */ + continue; + + /* image_policy_get_exhaustively() may have extended the flags mask to include all + * read-only/growfs flags if not set. Let's remove them again, if they are both set to + * minimize the policy again. */ + z = partition_policy_flags_reduce(z); + + p->policies[p->n_policies++] = (struct PartitionPolicy) { + .designator = d, + .flags = z, + }; + } + + if (ret) + *ret = TAKE_PTR(p); + + return 0; +} + const ImagePolicy image_policy_allow = { /* Allow policy */ .n_policies = 0, diff --git a/src/shared/image-policy.h b/src/shared/image-policy.h index c2a0a5ac40..a1a6afa345 100644 --- a/src/shared/image-policy.h +++ b/src/shared/image-policy.h @@ -80,6 +80,7 @@ static inline size_t image_policy_n_entries(const ImagePolicy *policy) { } PartitionPolicyFlags partition_policy_flags_extend(PartitionPolicyFlags flags); +PartitionPolicyFlags partition_policy_flags_reduce(PartitionPolicyFlags flags); PartitionPolicyFlags partition_policy_flags_from_string(const char *s); int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret); @@ -95,6 +96,8 @@ bool image_policy_equiv_deny(const ImagePolicy *policy); bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b); /* checks if defined the same way, i.e. has literally the same ruleset */ int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b); /* checks if the outcome is the same, i.e. for all partitions results in the same decisions. */ +int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret); + static inline ImagePolicy* image_policy_free(ImagePolicy *p) { return mfree(p); } diff --git a/src/shared/meson.build b/src/shared/meson.build index 71db6d8694..17313aefed 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -126,6 +126,7 @@ shared_sources = files( 'netif-sriov.c', 'netif-util.c', 'nsflags.c', + 'nsresource.c', 'numa-util.c', 'open-file.c', 'openssl-util.c', @@ -180,6 +181,8 @@ shared_sources = files( 'varlink-io.systemd.Hostname.c', 'varlink-io.systemd.Journal.c', 'varlink-io.systemd.ManagedOOM.c', + 'varlink-io.systemd.MountFileSystem.c', + 'varlink-io.systemd.NamespaceResource.c', 'varlink-io.systemd.Network.c', 'varlink-io.systemd.PCRExtend.c', 'varlink-io.systemd.PCRLock.c', diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c new file mode 100644 index 0000000000..4419f2fdde --- /dev/null +++ b/src/shared/nsresource.c @@ -0,0 +1,329 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "fd-util.h" +#include "format-util.h" +#include "missing_sched.h" +#include "namespace-util.h" +#include "nsresource.h" +#include "process-util.h" +#include "varlink.h" + +static int make_pid_name(char **ret) { + char comm[TASK_COMM_LEN]; + + assert(ret); + + if (prctl(PR_GET_NAME, comm) < 0) + return -errno; + + /* So the namespace name should be 16 chars at max (because we want that it is usable in usernames, + * which have a limit of 31 chars effectively, and the nsresourced service wants to prefix/suffix + * some bits). But it also should be unique if we are called multiple times in a row. Hence we take + * the "comm" name (which is 15 chars), and suffix it with the UID, possibly overriding the end. */ + assert_cc(TASK_COMM_LEN == 15 + 1); + + char spid[DECIMAL_STR_MAX(pid_t)]; + xsprintf(spid, PID_FMT, getpid_cached()); + + assert(strlen(spid) <= 16); + strshorten(comm, 16 - strlen(spid)); + + _cleanup_free_ char *s = strjoin(comm, spid); + if (!s) + return -ENOMEM; + + *ret = TAKE_PTR(s); + return 0; +} + +int nsresource_allocate_userns(const char *name, uint64_t size) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int userns_fd = -EBADF; + _cleanup_free_ char *_name = NULL; + const char *error_id; + int r, userns_fd_idx; + + /* Allocate a new dynamic user namespace via the userdb registry logic */ + + if (!name) { + r = make_pid_name(&_name); + if (r < 0) + return r; + + name = _name; + } + + if (size <= 0 || size > UINT64_C(0x100000000)) /* Note: the server actually only allows allocating 1 or 64K right now */ + return -EINVAL; + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd = userns_acquire_empty(); + if (userns_fd < 0) + return log_debug_errno(userns_fd, "Failed to acquire empty user namespace: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AllocateUserRange", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("size", JSON_BUILD_UNSIGNED(size)), + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AllocateUserRange() varlink call: %m"); + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to allocate user namespace with %" PRIu64 " users: %s", size, error_id); + + return TAKE_FD(userns_fd); +} + +int nsresource_register_userns(const char *name, int userns_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int _userns_fd = -EBADF; + _cleanup_free_ char *_name = NULL; + const char *error_id; + int r, userns_fd_idx; + + /* Register the specified user namespace with userbd. */ + + if (!name) { + r = make_pid_name(&_name); + if (r < 0) + return r; + + name = _name; + } + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.RegisterUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(name)), + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call RegisterUserNamespace() varlink call: %m"); + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to register user namespace: %s", error_id); + + return 0; +} + +int nsresource_add_mount(int userns_fd, int mount_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r, userns_fd_idx, mount_fd_idx; + const char *error_id; + + assert(mount_fd >= 0); + + if (userns_fd < 0) { + int _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_error_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = 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"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_error_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + mount_fd_idx = varlink_push_dup_fd(vl, mount_fd); + if (mount_fd_idx < 0) + return log_error_errno(mount_fd_idx, "Failed to push mount fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddMountToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("mountFileDescriptor", JSON_BUILD_UNSIGNED(mount_fd_idx)))); + if (r < 0) + return log_error_errno(r, "Failed to call AddMountToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding mount to registration."); + return 0; + } + if (error_id) + return log_error_errno(varlink_error_to_errno(error_id, reply), "Failed to mount image: %s", error_id); + + return 1; +} + +int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + _cleanup_close_ int _userns_fd = -EBADF; + int r, userns_fd_idx, cgroup_fd_idx; + const char *error_id; + + assert(cgroup_fd >= 0); + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + cgroup_fd_idx = varlink_push_dup_fd(vl, cgroup_fd); + if (cgroup_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push cgroup fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddControlGroupToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("controlGroupFileDescriptor", JSON_BUILD_UNSIGNED(cgroup_fd_idx)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AddControlGroupToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding cgroup to registration."); + return 0; + } + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to add cgroup to user namespace: %s", error_id); + + return 1; +} + +int nsresource_add_netif( + int userns_fd, + int netns_fd, + const char *namespace_ifname, + char **ret_host_ifname, + char **ret_namespace_ifname) { + + _cleanup_close_ int _userns_fd = -EBADF, _netns_fd = -EBADF; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r, userns_fd_idx, netns_fd_idx; + const char *error_id; + + if (userns_fd < 0) { + _userns_fd = namespace_open_by_type(NAMESPACE_USER); + if (_userns_fd < 0) + return -errno; + + userns_fd = _userns_fd; + } + + if (netns_fd < 0) { + _netns_fd = namespace_open_by_type(NAMESPACE_NET); + if (_netns_fd < 0) + return -errno; + + netns_fd = _netns_fd; + } + + r = varlink_connect_address(&vl, "/run/systemd/io.systemd.NamespaceResource"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to namespace resource manager: %m"); + + r = varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + userns_fd_idx = varlink_push_dup_fd(vl, userns_fd); + if (userns_fd_idx < 0) + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); + + netns_fd_idx = varlink_push_dup_fd(vl, netns_fd); + if (netns_fd_idx < 0) + return log_debug_errno(netns_fd_idx, "Failed to push netns fd into varlink connection: %m"); + + JsonVariant *reply = NULL; + r = varlink_callb(vl, + "io.systemd.NamespaceResource.AddNetworkToUserNamespace", + &reply, + &error_id, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("userNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(userns_fd_idx)), + JSON_BUILD_PAIR("networkNamespaceFileDescriptor", JSON_BUILD_UNSIGNED(netns_fd_idx)), + JSON_BUILD_PAIR("mode", JSON_BUILD_CONST_STRING("veth")), + JSON_BUILD_PAIR_CONDITION(namespace_ifname, "namespaceInterfaceName", JSON_BUILD_STRING(namespace_ifname)))); + if (r < 0) + return log_debug_errno(r, "Failed to call AddNetworkToUserNamespace() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.NamespaceResource.UserNamespaceNotRegistered")) { + log_notice("User namespace has not been allocated via namespace resource registry, not adding network to registration."); + return 0; + } + if (error_id) + return log_debug_errno(varlink_error_to_errno(error_id, reply), "Failed to add network to user namespace: %s", error_id); + + _cleanup_free_ char *host_interface_name = NULL, *namespace_interface_name = NULL; + r = json_dispatch( + reply, + (const JsonDispatch[]) { + { "hostInterfaceName", JSON_VARIANT_STRING, json_dispatch_string, PTR_TO_SIZE(&host_interface_name) }, + { "namespaceInterfaceName", JSON_VARIANT_STRING, json_dispatch_string, PTR_TO_SIZE(&namespace_interface_name) }, + }, + JSON_ALLOW_EXTENSIONS, + /* userdata= */ NULL); + + if (ret_host_ifname) + *ret_host_ifname = TAKE_PTR(host_interface_name); + if (ret_namespace_ifname) + *ret_namespace_ifname = TAKE_PTR(namespace_interface_name); + + return 1; +} diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h new file mode 100644 index 0000000000..6b807b327b --- /dev/null +++ b/src/shared/nsresource.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +int nsresource_allocate_userns(const char *name, uint64_t size); +int nsresource_register_userns(const char *name, int userns_fd); +int nsresource_add_mount(int userns_fd, int mount_fd); +int nsresource_add_cgroup(int userns_fd, int cgroup_fd); +int nsresource_add_netif(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); diff --git a/src/shared/varlink-io.systemd.MountFileSystem.c b/src/shared/varlink-io.systemd.MountFileSystem.c new file mode 100644 index 0000000000..4a33578a10 --- /dev/null +++ b/src/shared/varlink-io.systemd.MountFileSystem.c @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.MountFileSystem.h" + +static VARLINK_DEFINE_ENUM_TYPE( + PartitionDesignator, + VARLINK_DEFINE_ENUM_VALUE(root), + VARLINK_DEFINE_ENUM_VALUE(usr), + VARLINK_DEFINE_ENUM_VALUE(home), + VARLINK_DEFINE_ENUM_VALUE(srv), + VARLINK_DEFINE_ENUM_VALUE(esp), + VARLINK_DEFINE_ENUM_VALUE(xbootldr), + VARLINK_DEFINE_ENUM_VALUE(swap), + VARLINK_DEFINE_ENUM_VALUE(root_verity), + VARLINK_DEFINE_ENUM_VALUE(usr_verity), + VARLINK_DEFINE_ENUM_VALUE(root_verity_sig), + VARLINK_DEFINE_ENUM_VALUE(usr_verity_sig), + VARLINK_DEFINE_ENUM_VALUE(tmp), + VARLINK_DEFINE_ENUM_VALUE(var)); + +static VARLINK_DEFINE_STRUCT_TYPE( + PartitionInfo, + VARLINK_DEFINE_FIELD(designator, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(writable, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(growFileSystem, VARLINK_BOOL, 0), + VARLINK_DEFINE_FIELD(partitionNumber, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(architecture, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(partitionUuid, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(fileSystemType, VARLINK_STRING, 0), + VARLINK_DEFINE_FIELD(partitionLabel, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_FIELD(size, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(offset, VARLINK_INT, 0), + VARLINK_DEFINE_FIELD(mountFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + MountImage, + VARLINK_DEFINE_INPUT(imageFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(readOnly, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(growFileSystems, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(password, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(imagePolicy, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(allowInteractiveAuthentication, VARLINK_BOOL, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT_BY_TYPE(partitions, PartitionInfo, VARLINK_ARRAY), + VARLINK_DEFINE_OUTPUT(imagePolicy, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(imageSize, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(sectorSize, VARLINK_INT, 0), + VARLINK_DEFINE_OUTPUT(imageName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_OUTPUT(imageUuid, VARLINK_STRING, VARLINK_NULLABLE)); + +static VARLINK_DEFINE_ERROR(IncompatibleImage); +static VARLINK_DEFINE_ERROR(MultipleRootPartitionsFound); +static VARLINK_DEFINE_ERROR(RootPartitionNotFound); +static VARLINK_DEFINE_ERROR(DeniedByImagePolicy); +static VARLINK_DEFINE_ERROR(KeyNotFound); +static VARLINK_DEFINE_ERROR(VerityFailure); + +VARLINK_DEFINE_INTERFACE( + io_systemd_MountFileSystem, + "io.systemd.MountFileSystem", + &vl_type_PartitionDesignator, + &vl_type_PartitionInfo, + &vl_method_MountImage, + &vl_error_IncompatibleImage, + &vl_error_MultipleRootPartitionsFound, + &vl_error_RootPartitionNotFound, + &vl_error_DeniedByImagePolicy, + &vl_error_KeyNotFound, + &vl_error_VerityFailure); diff --git a/src/shared/varlink-io.systemd.MountFileSystem.h b/src/shared/varlink-io.systemd.MountFileSystem.h new file mode 100644 index 0000000000..dc75957df4 --- /dev/null +++ b/src/shared/varlink-io.systemd.MountFileSystem.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_MountFileSystem; diff --git a/src/shared/varlink-io.systemd.NamespaceResource.c b/src/shared/varlink-io.systemd.NamespaceResource.c new file mode 100644 index 0000000000..e98c6c6663 --- /dev/null +++ b/src/shared/varlink-io.systemd.NamespaceResource.c @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "varlink-io.systemd.NamespaceResource.h" + +static VARLINK_DEFINE_METHOD( + AllocateUserRange, + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(size, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(target, VARLINK_INT, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + RegisterUserNamespace, + VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0), + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddMountToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(mountFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddControlGroupToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(controlGroupFileDescriptor, VARLINK_INT, 0)); + +static VARLINK_DEFINE_METHOD( + AddNetworkToUserNamespace, + VARLINK_DEFINE_INPUT(userNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(networkNamespaceFileDescriptor, VARLINK_INT, 0), + VARLINK_DEFINE_INPUT(namespaceInterfaceName, VARLINK_STRING, VARLINK_NULLABLE), + VARLINK_DEFINE_INPUT(mode, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(hostInterfaceName, VARLINK_STRING, 0), + VARLINK_DEFINE_OUTPUT(namespaceInterfaceName, VARLINK_STRING, 0)); + +static VARLINK_DEFINE_ERROR(UserNamespaceInterfaceNotSupported); +static VARLINK_DEFINE_ERROR(NameExists); +static VARLINK_DEFINE_ERROR(UserNamespaceExists); +static VARLINK_DEFINE_ERROR(DynamicRangeUnavailable); +static VARLINK_DEFINE_ERROR(NoDynamicRange); +static VARLINK_DEFINE_ERROR(UserNamespaceNotRegistered); +static VARLINK_DEFINE_ERROR(UserNamespaceWithoutUserRange); +static VARLINK_DEFINE_ERROR(TooManyControlGroups); +static VARLINK_DEFINE_ERROR(ControlGroupAlreadyAdded); + +VARLINK_DEFINE_INTERFACE( + io_systemd_NamespaceResource, + "io.systemd.NamespaceResource", + &vl_method_AllocateUserRange, + &vl_method_RegisterUserNamespace, + &vl_method_AddMountToUserNamespace, + &vl_method_AddControlGroupToUserNamespace, + &vl_method_AddNetworkToUserNamespace, + &vl_error_UserNamespaceInterfaceNotSupported, + &vl_error_NameExists, + &vl_error_UserNamespaceExists, + &vl_error_DynamicRangeUnavailable, + &vl_error_NoDynamicRange, + &vl_error_UserNamespaceNotRegistered, + &vl_error_UserNamespaceWithoutUserRange, + &vl_error_TooManyControlGroups, + &vl_error_ControlGroupAlreadyAdded); diff --git a/src/shared/varlink-io.systemd.NamespaceResource.h b/src/shared/varlink-io.systemd.NamespaceResource.h new file mode 100644 index 0000000000..443cb97e48 --- /dev/null +++ b/src/shared/varlink-io.systemd.NamespaceResource.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "varlink-idl.h" + +extern const VarlinkInterface vl_interface_io_systemd_NamespaceResource; diff --git a/src/shared/varlink.c b/src/shared/varlink.c index d4072ec398..713aff2895 100644 --- a/src/shared/varlink.c +++ b/src/shared/varlink.c @@ -2807,12 +2807,29 @@ int varlink_get_peer_uid(Varlink *v, uid_t *ret) { return varlink_log_errno(v, r, "Failed to acquire credentials: %m"); if (!uid_is_valid(v->ucred.uid)) - return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer uid is invalid."); + return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer UID is invalid."); *ret = v->ucred.uid; return 0; } +int varlink_get_peer_gid(Varlink *v, gid_t *ret) { + int r; + + assert_return(v, -EINVAL); + assert_return(ret, -EINVAL); + + r = varlink_acquire_ucred(v); + if (r < 0) + return varlink_log_errno(v, r, "Failed to acquire credentials: %m"); + + if (!gid_is_valid(v->ucred.gid)) + return varlink_log_errno(v, SYNTHETIC_ERRNO(ENODATA), "Peer GID is invalid."); + + *ret = v->ucred.gid; + return 0; +} + int varlink_get_peer_pid(Varlink *v, pid_t *ret) { int r; @@ -3076,7 +3093,7 @@ int varlink_push_fd(Varlink *v, int fd) { return i; } -int varlink_dup_fd(Varlink *v, int fd) { +int varlink_push_dup_fd(Varlink *v, int fd) { _cleanup_close_ int dp = -1; int r; @@ -3124,6 +3141,16 @@ int varlink_peek_fd(Varlink *v, size_t i) { return v->input_fds[i]; } +int varlink_peek_dup_fd(Varlink *v, size_t i) { + int fd; + + fd = varlink_peek_fd(v, i); + if (fd < 0) + return fd; + + return RET_NERRNO(fcntl(fd, F_DUPFD_CLOEXEC, 3)); +} + int varlink_take_fd(Varlink *v, size_t i) { assert_return(v, -EINVAL); diff --git a/src/shared/varlink.h b/src/shared/varlink.h index db7227b215..f6dda63549 100644 --- a/src/shared/varlink.h +++ b/src/shared/varlink.h @@ -156,11 +156,12 @@ int varlink_dispatch(Varlink *v, JsonVariant *parameters, const JsonDispatch tab /* Write outgoing fds into the socket (to be associated with the next enqueued message) */ int varlink_push_fd(Varlink *v, int fd); -int varlink_dup_fd(Varlink *v, int fd); +int varlink_push_dup_fd(Varlink *v, int fd); int varlink_reset_fds(Varlink *v); /* Read incoming fds from the socket (associated with the currently handled message) */ int varlink_peek_fd(Varlink *v, size_t i); +int varlink_peek_dup_fd(Varlink *v, size_t i); int varlink_take_fd(Varlink *v, size_t i); int varlink_set_allow_fd_passing_input(Varlink *v, bool b); @@ -173,6 +174,7 @@ void* varlink_set_userdata(Varlink *v, void *userdata); void* varlink_get_userdata(Varlink *v); int varlink_get_peer_uid(Varlink *v, uid_t *ret); +int varlink_get_peer_gid(Varlink *v, gid_t *ret); int varlink_get_peer_pid(Varlink *v, pid_t *ret); int varlink_get_peer_pidref(Varlink *v, PidRef *ret); diff --git a/src/test/test-cgroup.c b/src/test/test-cgroup.c index 9b174d708b..06cf886729 100644 --- a/src/test/test-cgroup.c +++ b/src/test/test-cgroup.c @@ -5,6 +5,7 @@ #include "cgroup-setup.h" #include "cgroup-util.h" #include "errno-util.h" +#include "fd-util.h" #include "path-util.h" #include "process-util.h" #include "string-util.h" @@ -129,4 +130,47 @@ TEST(cg_create) { assert_se(cg_rmdir(SYSTEMD_CGROUP_CONTROLLER, test_a) == 0); } +TEST(id) { + _cleanup_free_ char *p = NULL, *p2 = NULL; + _cleanup_close_ int fd = -EBADF, fd2 = -EBADF; + uint64_t id, id2; + int r; + + r = cg_all_unified(); + if (r == 0) { + log_tests_skipped("skipping cgroupid test, not running in unified mode"); + return; + } + if (IN_SET(r, -ENOMEDIUM, -ENOENT)) { + log_tests_skipped("cgroupfs is not mounted"); + return; + } + assert_se(r > 0); + + fd = cg_path_open(SYSTEMD_CGROUP_CONTROLLER, "/"); + assert_se(fd >= 0); + + assert_se(fd_get_path(fd, &p) >= 0); + assert_se(path_equal(p, "/sys/fs/cgroup")); + + assert_se(cg_fd_get_cgroupid(fd, &id) >= 0); + + fd2 = cg_cgroupid_open(fd, id); + + if (ERRNO_IS_NEG_PRIVILEGE(fd2)) + log_notice("Skipping open-by-cgroup-id test because lacking privs."); + else { + assert_se(fd2 >= 0); + + assert_se(fd_get_path(fd2, &p2) >= 0); + assert_se(path_equal(p2, "/sys/fs/cgroup")); + + assert_se(cg_fd_get_cgroupid(fd2, &id2) >= 0); + + assert_se(id == id2); + + assert_se(inode_same_at(fd, NULL, fd2, NULL, AT_EMPTY_PATH) > 0); + } +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/test/test-image-policy.c b/src/test/test-image-policy.c index 1572093c9d..12fc10acf0 100644 --- a/src/test/test-image-policy.c +++ b/src/test/test-image-policy.c @@ -130,4 +130,36 @@ TEST(extend) { assert_se(partition_policy_flags_extend(PARTITION_POLICY_GROWFS_ON) == (PARTITION_POLICY_GROWFS_ON|_PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK)); } +static void test_policy_intersect_one(const char *a, const char *b, const char *c) { + _cleanup_(image_policy_freep) ImagePolicy *x = NULL, *y = NULL, *z = NULL, *t = NULL; + + assert_se(image_policy_from_string(a, &x) >= 0); + assert_se(image_policy_from_string(b, &y) >= 0); + assert_se(image_policy_from_string(c, &z) >= 0); + + assert_se(image_policy_intersect(x, y, &t) >= 0); + + _cleanup_free_ char *s1 = NULL, *s2 = NULL, *s3 = NULL, *s4 = NULL; + assert_se(image_policy_to_string(x, false, &s1) >= 0); + assert_se(image_policy_to_string(y, false, &s2) >= 0); + assert_se(image_policy_to_string(z, false, &s3) >= 0); + assert_se(image_policy_to_string(t, false, &s4) >= 0); + + log_info("%s ^ %s → %s vs. %s", s1, s2, s3, s4); + + assert_se(image_policy_equivalent(z, t) > 0); +} + +TEST(image_policy_intersect) { + test_policy_intersect_one("", "", ""); + test_policy_intersect_one("-", "-", "-"); + test_policy_intersect_one("*", "*", "*"); + test_policy_intersect_one("~", "~", "~"); + test_policy_intersect_one("root=verity+signed", "root=signed+verity", "root=verity+signed"); + test_policy_intersect_one("root=verity+signed", "root=signed", "root=signed"); + test_policy_intersect_one("root=verity+signed", "root=verity", "root=verity"); + test_policy_intersect_one("root=open", "root=verity", "root=verity"); + test_policy_intersect_one("root=open", "=verity+ignore", "root=verity+ignore:=ignore"); +} + DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c index e8799e7810..c31fb30e7e 100644 --- a/src/test/test-uid-range.c +++ b/src/test/test-uid-range.c @@ -23,9 +23,15 @@ TEST(uid_range) { assert_se(!uid_range_covers(p, UINT32_MAX, 1)); assert_se(!uid_range_covers(p, UINT32_MAX - 10, 11)); + assert_se(uid_range_entries(p) == 0); + assert_se(uid_range_size(p) == 0); + assert_se(uid_range_is_empty(p)); + assert_se(uid_range_add_str(&p, "500-999") >= 0); assert_se(p); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 500); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 500); assert_se(p->entries[0].nr == 500); @@ -56,19 +62,23 @@ TEST(uid_range) { assert_se(uid_range_next_lower(p, &search) == -EBUSY); assert_se(uid_range_add_str(&p, "1000") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); assert_se(p->entries[0].start == 500); assert_se(p->entries[0].nr == 501); assert_se(uid_range_add_str(&p, "30-40") >= 0); - assert_se(p->n_entries == 2); + assert_se(uid_range_entries(p) == 2); + assert_se(uid_range_size(p) == 500 + 1 + 11); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 30); assert_se(p->entries[0].nr == 11); assert_se(p->entries[1].start == 500); assert_se(p->entries[1].nr == 501); assert_se(uid_range_add_str(&p, "60-70") >= 0); - assert_se(p->n_entries == 3); + assert_se(uid_range_entries(p) == 3); + assert_se(uid_range_size(p) == 500 + 1 + 11 + 11); + assert_se(!uid_range_is_empty(p)); assert_se(p->entries[0].start == 30); assert_se(p->entries[0].nr == 11); assert_se(p->entries[1].start == 60); @@ -77,21 +87,34 @@ TEST(uid_range) { assert_se(p->entries[2].nr == 501); assert_se(uid_range_add_str(&p, "20-2000") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 1981); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1981); assert_se(uid_range_add_str(&p, "2002") >= 0); - assert_se(p->n_entries == 2); + assert_se(uid_range_entries(p) == 2); + assert_se(uid_range_size(p) == 1982); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1981); assert_se(p->entries[1].start == 2002); assert_se(p->entries[1].nr == 1); + _cleanup_(uid_range_freep) UIDRange *q = NULL; + assert_se(!uid_range_equal(p, q)); + assert_se(uid_range_add_str(&q, "20-2000") >= 0); + assert_se(!uid_range_equal(p, q)); + assert_se(uid_range_add_str(&q, "2002") >= 0); + assert_se(uid_range_equal(p, q)); + assert_se(uid_range_add_str(&p, "2001") >= 0); - assert_se(p->n_entries == 1); + assert_se(uid_range_entries(p) == 1); + assert_se(uid_range_size(p) == 1983); assert_se(p->entries[0].start == 20); assert_se(p->entries[0].nr == 1983); + + assert_se(uid_range_add_str(&q, "2001") >= 0); + assert_se(uid_range_equal(p, q)); } TEST(load_userns) { @@ -100,7 +123,7 @@ TEST(load_userns) { _cleanup_fclose_ FILE *f = NULL; int r; - r = uid_range_load_userns(&p, NULL); + r = uid_range_load_userns(&p, NULL, UID_RANGE_USERNS_INSIDE); if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) return; @@ -123,7 +146,7 @@ TEST(load_userns) { p = uid_range_free(p); - assert_se(uid_range_load_userns(&p, fn) >= 0); + assert_se(uid_range_load_userns(&p, fn, UID_RANGE_USERNS_INSIDE) >= 0); assert_se(uid_range_contains(p, 0)); assert_se(uid_range_contains(p, 19)); diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c index 3ca36a07a4..3323810368 100644 --- a/src/test/test-varlink-idl.c +++ b/src/test/test-varlink-idl.c @@ -12,11 +12,13 @@ #include "varlink-io.systemd.Credentials.h" #include "varlink-io.systemd.Journal.h" #include "varlink-io.systemd.ManagedOOM.h" +#include "varlink-io.systemd.MountFileSystem.h" +#include "varlink-io.systemd.NamespaceResource.h" #include "varlink-io.systemd.Network.h" #include "varlink-io.systemd.PCRExtend.h" #include "varlink-io.systemd.PCRLock.h" -#include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.Resolve.h" +#include "varlink-io.systemd.Resolve.Monitor.h" #include "varlink-io.systemd.UserDatabase.h" #include "varlink-io.systemd.oom.h" #include "varlink-io.systemd.service.h" @@ -129,6 +131,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_UserDatabase); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_NamespaceResource); + print_separator(); test_parse_format_one(&vl_interface_io_systemd_Journal); print_separator(); test_parse_format_one(&vl_interface_io_systemd_Resolve); @@ -137,6 +141,8 @@ TEST(parse_format) { print_separator(); test_parse_format_one(&vl_interface_io_systemd_ManagedOOM); print_separator(); + test_parse_format_one(&vl_interface_io_systemd_MountFileSystem); + print_separator(); test_parse_format_one(&vl_interface_io_systemd_Network); print_separator(); test_parse_format_one(&vl_interface_io_systemd_oom); diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index ecbdcb0f41..c16e44d6b6 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -428,7 +428,7 @@ static int display_user(int argc, char *argv[], void *userdata) { _cleanup_(uid_range_freep) UIDRange *uid_range = NULL; int boundary_lines, uid_map_lines; - r = uid_range_load_userns(&uid_range, "/proc/self/uid_map"); + r = uid_range_load_userns(&uid_range, "/proc/self/uid_map", UID_RANGE_USERNS_INSIDE); if (r < 0) log_debug_errno(r, "Failed to load /proc/self/uid_map, ignoring: %m"); @@ -731,7 +731,7 @@ static int display_group(int argc, char *argv[], void *userdata) { _cleanup_(uid_range_freep) UIDRange *gid_range = NULL; int boundary_lines, gid_map_lines; - r = uid_range_load_userns(&gid_range, "/proc/self/gid_map"); + r = uid_range_load_userns(&gid_range, "/proc/self/gid_map", UID_RANGE_USERNS_INSIDE); if (r < 0) log_debug_errno(r, "Failed to load /proc/self/gid_map, ignoring: %m"); diff --git a/test/TEST-50-DISSECT/test.sh b/test/TEST-50-DISSECT/test.sh index 613bb086ce..b93697b73c 100755 --- a/test/TEST-50-DISSECT/test.sh +++ b/test/TEST-50-DISSECT/test.sh @@ -12,6 +12,15 @@ TEST_INSTALL_VERITY_MINIMAL=1 # shellcheck source=test/test-functions . "${TEST_BASE_DIR:?}/test-functions" +# On Ubuntu the BPF LSM is not enabled by default, so we need to do it via the +# kernel command line on boot +if [ "$LOOKS_LIKE_UBUNTU" = "yes" ]; then + KERNEL_OPTIONS=( + "lsm=lockdown,capability,landlock,yama,apparmor,bpf" + ) + KERNEL_APPEND+=" ${KERNEL_OPTIONS[*]}" +fi + test_require_bin mksquashfs veritysetup sfdisk test_append_files() { diff --git a/test/test-functions b/test/test-functions index a09092a5b0..c62cf3d521 100644 --- a/test/test-functions +++ b/test/test-functions @@ -39,6 +39,8 @@ os_release=$(test -e /etc/os-release && echo /etc/os-release || echo /usr/lib/os # shellcheck source=/dev/null source "$os_release" [[ "$ID" == "debian" || " $ID_LIKE " == *" debian "* ]] && LOOKS_LIKE_DEBIAN=yes || LOOKS_LIKE_DEBIAN=no +# shellcheck disable=SC2034 +[[ "$ID" == "ubuntu" ]] && LOOKS_LIKE_UBUNTU=yes || LOOKS_LIKE_UBUNTU=no [[ "$ID" == "arch" || " $ID_LIKE " == *" arch "* ]] && LOOKS_LIKE_ARCH=yes || LOOKS_LIKE_ARCH=no [[ "$ID" == "fedora" ]] && LOOKS_LIKE_FEDORA=yes || LOOKS_LIKE_FEDORA=no [[ " $ID_LIKE " == *" suse "* ]] && LOOKS_LIKE_SUSE=yes || LOOKS_LIKE_SUSE=no @@ -733,6 +735,7 @@ install_verity_minimal() { BASICTOOLS=( bash cat + echo grep mount sleep diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index cd2d6aa7f7..9c2e6f354c 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -6,6 +6,9 @@ set -eux set -o pipefail +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + export SYSTEMD_LOG_LEVEL=debug cleanup_image_dir() { @@ -798,6 +801,62 @@ if command -v mksquashfs >/dev/null 2>&1; then (! test -f /usr/waldo) fi +if test -f /usr/lib/systemd/system/systemd-mountfsd.socket -a -f /usr/lib/systemd/system/systemd-nsresourced.socket && \ + command -v command -v mksquashfs >/dev/null 2>&1 && \ + grep -q bpf /sys/kernel/security/lsm && \ + test "$(find /usr/lib* -name libbpf.so.1 2>/dev/null)" != "" ; then + + cleanunprivfiles() { + umount -R /tmp/unpriv/mount + rmdir /tmp/unpriv + rm -f /tmp/test-50-unpriv-privkey.key /tmp/test-50-unpriv-cert.crt /run/verity.d/test-50-unpriv-cert.crt + rm -f /var/tmp/unpriv.raw /tmp/unpriv.raw.mtree /tmp/unpriv2.raw.mtree + rm -f /tmp/unpriv.out /tmp/unpriv.out2 /tmp/unpriv.out3 + } + + trap cleanunprivfiles EXIT + + systemctl start systemd-mountfsd.socket systemd-nsresourced.socket + + openssl req -config "$OPENSSL_CONFIG" -subj="/CN=waldo" -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout /tmp/test-50-unpriv-privkey.key -out /tmp/test-50-unpriv-cert.crt + + systemd-dissect --mkdir --mount "${image}.raw" /tmp/unpriv/mount + SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs systemd-repart -P -s /tmp/unpriv/mount --certificate=/tmp/test-50-unpriv-cert.crt --private-key=/tmp/test-50-unpriv-privkey.key /var/tmp/unpriv.raw + systemd-dissect --rmdir --umount /tmp/unpriv/mount + + systemd-dissect --image-policy='root=unprotected:=absent+unused' /var/tmp/unpriv.raw + systemd-dissect --image-policy='root=unprotected:=absent+unused' --mtree /var/tmp/unpriv.raw | tee /tmp/unpriv.raw.mtree + + # Run unpriv, should fail due to lack of privs + (! runas testuser systemd-dissect /var/tmp/unpriv.raw ) + (! runas testuser systemd-dissect --mtree /var/tmp/unpriv.raw ) + + # Install key in keychain + cp /tmp/test-50-unpriv-cert.crt /run/verity.d + + # Now run unpriv again, should be OK now. + runas testuser systemd-dissect /var/tmp/unpriv.raw + runas testuser systemd-dissect --mtree /var/tmp/unpriv.raw | tee /tmp/unpriv2.raw.mtree + + # Check that unpriv and priv run yielded same results + cmp /tmp/unpriv.raw.mtree /tmp/unpriv2.raw.mtree + + # Make sure nspawn works unpriv, too (for now do not nest) + if ! systemd-detect-virt -c ; then + systemd-nspawn --pipe -i /var/tmp/unpriv.raw --read-only echo thisisatest > /tmp/unpriv.out + echo thisisatest | cmp /tmp/unpriv.out - + + # The unpriv user has no rights to lock the image or write to it. Let's + # turn off both for this test, so that we don't have to copy the image + # around. + systemd-run -M testuser@ --user --pipe -p Environment=SYSTEMD_NSPAWN_LOCK=0 -p Delegate=1 -p DelegateSubgroup=supervisor -p Environment=SYSTEMD_LOG_LEVEL=debug --wait systemd-nspawn --keep-unit -i /var/tmp/unpriv.raw --read-only --pipe echo thisisatest > /tmp/unpriv.out2 + echo thisisatest | cmp /tmp/unpriv.out2 - + fi + + systemd-run -M testuser@ --user --pipe -p RootImage=/var/tmp/unpriv.raw -p PrivateUsers=1 --wait echo thisisatest > /tmp/unpriv.out3 + echo thisisatest | cmp /tmp/unpriv.out3 - +fi + # Sneak in a couple of expected-to-fail invocations to cover # https://github.com/systemd/systemd/issues/29610 (! systemd-run -P -p MountImages="/this/should/definitely/not/exist.img:/run/img2\:3:nosuid" false) diff --git a/units/meson.build b/units/meson.build index 16a5564086..2f972901d4 100644 --- a/units/meson.build +++ b/units/meson.build @@ -732,6 +732,22 @@ units = [ 'file' : 'systemd-userdbd.socket', 'conditions' : ['ENABLE_USERDB'], }, + { + 'file' : 'systemd-mountfsd.service.in', + 'conditions' : ['ENABLE_MOUNTFSD'], + }, + { + 'file' : 'systemd-mountfsd.socket', + 'conditions' : ['ENABLE_MOUNTFSD'], + }, + { + 'file' : 'systemd-nsresourced.service.in', + 'conditions' : ['ENABLE_NSRESOURCED'], + }, + { + 'file' : 'systemd-nsresourced.socket', + 'conditions' : ['ENABLE_NSRESOURCED'], + }, { 'file' : 'systemd-vconsole-setup.service.in', 'conditions' : ['ENABLE_VCONSOLE'], diff --git a/units/systemd-mountfsd.service.in b/units/systemd-mountfsd.service.in new file mode 100644 index 0000000000..20a9b425ab --- /dev/null +++ b/units/systemd-mountfsd.service.in @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=DDI File System Mounter +Documentation=man:systemd-mountfsd.service(8) +Requires=systemd-mountfsd.socket +After=systemd-mountfsd.socket +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +DefaultDependencies=no + +[Service] +#CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID +ExecStart={{LIBEXECDIR}}/systemd-mountfsd +IPAddressDeny=any +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 +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service @mount +Type=notify +NotifyAccess=all +FileDescriptorStoreMax=4096 +{{SERVICE_WATCHDOG}} + +[Install] +Also=systemd-mountfsd.socket diff --git a/units/systemd-mountfsd.socket b/units/systemd-mountfsd.socket new file mode 100644 index 0000000000..cd88003af9 --- /dev/null +++ b/units/systemd-mountfsd.socket @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=DDI File System Mounter Socket +Documentation=man:systemd-mountfsd.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sockets.target shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.MountFileSystem +SocketMode=0666 + +[Install] +WantedBy=sockets.target diff --git a/units/systemd-nsresourced.service.in b/units/systemd-nsresourced.service.in new file mode 100644 index 0000000000..3c927059e8 --- /dev/null +++ b/units/systemd-nsresourced.service.in @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Namespace Resource Manager +Documentation=man:systemd-nsresourced.service(8) +Requires=systemd-nsresourced.socket +After=systemd-nsresourced.socket +Conflicts=shutdown.target +Before=sysinit.target shutdown.target +DefaultDependencies=no + +[Service] +CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_SYS_RESOURCE CAP_BPF CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_CHOWN CAP_FOWNER +ExecStart={{LIBEXECDIR}}/systemd-nsresourced +IPAddressDeny=any +LimitNOFILE={{HIGH_RLIMIT_NOFILE}} +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +ProtectProc=invisible +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectSystem=strict +RestrictAddressFamilies=AF_UNIX AF_NETLINK +RestrictRealtime=yes +RestrictSUIDSGID=yes +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service bpf perf_event_open open_by_handle_at +Type=notify +NotifyAccess=all +FileDescriptorStoreMax=4096 +{{SERVICE_WATCHDOG}} + +[Install] +Also=systemd-nsresourced.socket diff --git a/units/systemd-nsresourced.socket b/units/systemd-nsresourced.socket new file mode 100644 index 0000000000..2e3c8e9baa --- /dev/null +++ b/units/systemd-nsresourced.socket @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Namespace Resource Manager Socket +Documentation=man:systemd-nsresourced.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +Before=sockets.target shutdown.target + +[Socket] +ListenStream=/run/systemd/io.systemd.NamespaceResource +Symlinks=/run/systemd/userdb/io.systemd.NamespaceResource +SocketMode=0666 + +[Install] +WantedBy=sockets.target