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