Merge pull request #26826 from poettering/mntfsd

unprivileged DDI mounts + dynamic userns range allocation via IPC
This commit is contained in:
Luca Boccassi
2024-04-07 19:43:34 +01:00
committed by GitHub
81 changed files with 7892 additions and 430 deletions
+12
View File
@@ -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
+18
View File
@@ -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.
+5
View File
@@ -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',
+70
View File
@@ -0,0 +1,70 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-mountfsd.service" conditional='ENABLE_MOUNTFSD'>
<refentryinfo>
<title>systemd-mountfsd.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-mountfsd.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-mountfsd.service</refname>
<refname>systemd-mountfsd</refname>
<refpurpose>Disk Image File System Mount Service</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-mountfsd.service</filename></para>
<para><filename>/usr/lib/systemd/systemd-mountfsd</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-mountfsd</command> 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.</para>
<para>The disk images provided must contain a raw file system image or must follow the <ulink
url="https://uapi-group.org/specifications/specs/discoverable_partitions_specification/">Discoverable
Partitions Specification</ulink>. Before mounting any file systems authenticity of the disk image is
established in one or a combination of the following ways:</para>
<orderedlist>
<listitem><para>If the disk image is located in a regular file in one of the directories
<filename>/var/lib/machines/</filename>, <filename>/var/lib/portables/</filename>,
<filename>/var/lib/extensions/</filename>, <filename>/var/lib/confexts/</filename> or their
counterparts in the <filename>/etc/</filename>, <filename>/run/</filename>,
<filename>/usr/lib/</filename> it is assumed to be trusted.</para></listitem>
<listitem><para>If the disk image contains a Verity enabled disk image, along with a signature
partition with a key in the kernel keyring or in <filename>/etc/verity.d/</filename> (and related
directories) the disk image is considered trusted.</para></listitem>
</orderedlist>
<para>This service provides one <ulink url="https://varlink.org/">Varlink</ulink> service:
<constant>io.systemd.MountFileSystem</constant> which accepts a file descriptor to a regular file or
block device, and returns a number of file descriptors referring to an <function>fsmount()</function>
file descriptor the client may then attach to a path of their choice.</para>
<para>The returned mounts are automatically allowlisted in the per-user-namespace allowlist maintained by
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
<para>The file systems are automatically fsck'ed before mounting.</para>
</refsect1>
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>
+24
View File
@@ -118,6 +118,28 @@
keeps track of running containers, and provides programming interfaces to interact with them.</para>
</refsect1>
<refsect1>
<title>Unprivileged Operation</title>
<para><command>systemd-nspawn</command> 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:</para>
<itemizedlist>
<listitem><para>Only disk image based containers are supported (i.e. <option>--image=</option>).
Directory based ones (i.e. <option>--directory=</option>) are not supported.</para></listitem>
<listitem><para>Machine registration via <option>--machine=</option> is not supported.</para></listitem>
<listitem><para>Only <option>--private-network</option> and <option>--network-veth</option> networking modes are supported.</para></listitem>
</itemizedlist>
<para>When running in unprivileged mode, some needed functionality is provided via
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and
<citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></para>
</refsect1>
<refsect1>
<title>Options</title>
@@ -1910,6 +1932,8 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
<member><citerefentry><refentrytitle>systemd.slice</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>machinectl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>importctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-nsresourced.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
<member><citerefentry project='url'><refentrytitle url='https://btrfs.readthedocs.io/en/latest/btrfs.html'>btrfs</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
+81
View File
@@ -0,0 +1,81 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
<refentry id="systemd-nsresourced.service" conditional='ENABLE_NSRESOURCED'>
<refentryinfo>
<title>systemd-nsresourced.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-nsresourced.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-nsresourced.service</refname>
<refname>systemd-nsresourced</refname>
<refpurpose>User Namespace Resource Delegation Service</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-nsresourced.service</filename></para>
<para><filename>/usr/lib/systemd/systemd-nsresourced</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-nsresourced</command> is a system service that permits transient delegation of a a
UID/GID range to a user namespace (see <citerefentry
project='man-pages'><refentrytitle>user_namespaces</refentrytitle><manvolnum>7</manvolnum></citerefentry>)
allocated by a client, via a Varlink IPC API.</para>
<para>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.</para>
<para>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
<command>systemd-nsresourced</command>.</para>
<para><command>systemd-nsresourced</command> automatically ensures that any registered UID ranges show up
in the system's NSS database via the <ulink url="https://systemd.io/USER_GROUP_API">User/Group Record
Lookup API via Varlink</ulink>.</para>
<para>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.</para>
<para>The service provides API calls to allowlist mounts (referenced via their mount file descriptors as
per Linux <function>fsmount()</function> 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
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>, fully
unprivileged <varname>RootImage=</varname> (see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>) or
fully unprivileged disk image tools such as
<citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<para>This service provides one <ulink url="https://varlink.org/">Varlink</ulink> service:
<constant>io.systemd.NamespaceResource</constant> allows registering user namespaces, and assign mounts,
cgroups and network interfaces to it.</para>
</refsect1>
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-mountfsd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-dissect</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>user_namespaces</refentrytitle><manvolnum>7</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>
+81
View File
@@ -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')
+13 -2
View File
@@ -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')
+31
View File
@@ -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
+1
View File
@@ -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',
+2
View File
@@ -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
+48 -1
View File
@@ -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;
+10 -1
View File
@@ -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)
+1 -1
View File
@@ -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);
+5
View File
@@ -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
+137
View File
@@ -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);
}
+9
View File
@@ -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);
+110 -3
View File
@@ -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;
}
+43 -1
View File
@@ -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);
+1 -1
View File
@@ -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)
+55 -35
View File
@@ -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)
+135 -63
View File
@@ -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);
+1 -1
View File
@@ -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");
}
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<!--
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.
-->
<policyconfig>
<vendor>The systemd Project</vendor>
<vendor_url>https://systemd.io</vendor_url>
<!-- Allow mounting DDIs into the host user namespace -->
<action id="io.systemd.mount-file-system.mount-image">
<!-- This action is generally checked first: we'll first try to mount the image with
signature checks on. If that fails, we'll retry with the untrusted action below. -->
<description gettext-domain="systemd">Allow mounting of file system image</description>
<message gettext-domain="systemd">Authentication is required for an application to mount a file system image.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
<action id="io.systemd.mount-file-system.mount-untrusted-image">
<!-- If the image cannot be mounted via the regular action because it is not signed by a
recognized key, we'll try this action. -->
<description gettext-domain="systemd">Allow mounting of untrusted file system image</description>
<message gettext-domain="systemd">Authentication is required for an application to mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.imply">io.systemd.mount-file-system.mount-image</annotate>
</action>
<!-- Allow mounting DDIs into a private user namespace -->
<action id="io.systemd.mount-file-system.mount-image-privately">
<description gettext-domain="systemd">Allow private mounting of trusted file system image</description>
<message gettext-domain="systemd">Authentication is required for an application to privately mount a file system image or an image whose cryptographic signature is recognized.</message>
<defaults>
<allow_any>yes</allow_any>
<allow_inactive>yes</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
<action id="io.systemd.mount-file-system.mount-untrusted-image-privately">
<description gettext-domain="systemd">Allow private mounting of untrusted file system image</description>
<message gettext-domain="systemd">Authentication is required for an application to privately mount a cryptographically unsigned file system image or an image whose cryptographic signature is not recognized.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.imply">io.systemd.mount-file-system.mount-image-privately</annotate>
</action>
</policyconfig>
+28
View File
@@ -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)
+277
View File
@@ -0,0 +1,277 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/wait.h>
#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);
}
+30
View File
@@ -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);
+43
View File
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/stat.h>
#include <sys/types.h>
#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);
+703
View File
@@ -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);
+52 -35
View File
@@ -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();
+1 -2
View File
@@ -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);
+46 -24
View File
@@ -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;
+1
View File
@@ -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 {
+396 -181
View File
@@ -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);
@@ -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)
@@ -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"
@@ -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 <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <errno.h>
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";
+48
View File
@@ -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 ],
},
]
+647
View File
@@ -0,0 +1,647 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/mount.h>
#include <sys/wait.h>
#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, &registry_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;
}
+40
View File
@@ -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);
+46
View File
@@ -0,0 +1,46 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/stat.h>
#include <sys/types.h>
#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);
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/eventfd.h>
#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);
+646
View File
@@ -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(&reg_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(&reg_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;
}
+42
View File
@@ -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);
+346
View File
@@ -0,0 +1,346 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "userns-restrict.h"
#if HAVE_VMLINUX_H
#include <sched.h>
#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
+22
View File
@@ -0,0 +1,22 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <stdbool.h>
#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);
+1
View File
@@ -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);
+49 -19
View File
@@ -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;
+22 -10
View File
@@ -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
+14
View File
@@ -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;
+1
View File
@@ -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);
+2 -11
View File
@@ -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);
+33 -20
View File
@@ -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;
+2
View File
@@ -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];
+238 -3
View File
@@ -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
}
+13 -1
View File
@@ -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);
+98
View File
@@ -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,
+3
View File
@@ -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);
}
+3
View File
@@ -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',
+329
View File
@@ -0,0 +1,329 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/prctl.h>
#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;
}
+10
View File
@@ -0,0 +1,10 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <inttypes.h>
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);
@@ -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);
@@ -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;
@@ -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);
@@ -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;
+29 -2
View File
@@ -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);
+3 -1
View File
@@ -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);
+44
View File
@@ -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);
+32
View File
@@ -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);
+32 -9
View File
@@ -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));
+7 -1
View File
@@ -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);
+2 -2
View File
@@ -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");
+9
View File
@@ -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() {
+3
View File
@@ -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
+59
View File
@@ -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)
+16
View File
@@ -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'],
+46
View File
@@ -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
+22
View File
@@ -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
+47
View File
@@ -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
+23
View File
@@ -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