diff --git a/README b/README index d85295b10e..0b2d53de1c 100644 --- a/README +++ b/README @@ -77,7 +77,8 @@ REQUIREMENTS: ≥ 6.10 for fcntl(F_DUPFD_QUERY), unprivileged linkat(AT_EMPTY_PATH), and block device 'partscan' sysfs attribute ≥ 6.12 for AT_HANDLE_MNT_ID_UNIQUE - ≥ 6.13 for PIDFD_GET_INFO and {set,remove}xattrat() + ≥ 6.13 for PIDFD_GET_INFO and {set,remove}xattrat() and + FSCONFIG_SET_FD support for overlayfs layers ≥ 6.16 for coredump pattern '%F' (pidfd) specifier and SO_PASSRIGHTS ✅ systemd utilizes several new kernel APIs, but will fall back gracefully diff --git a/TODO b/TODO index cf346e0509..d2065bc75b 100644 --- a/TODO +++ b/TODO @@ -124,6 +124,15 @@ Features: * networkd: maintain a file in /run/ that can be symlinked into /run/issue.d/ that always shows the current primary IP address +* oci: add support for blake hashes for layers + +* oci: add support for "importctl import-oci" which implements the "OCI layout" + spec (i.e. acquiring via local fs access), as opposed to the current + "importctl pull-oci" which focusses on the "OCI image spec", i.e. downloads + from the web (i.e. acquiring via URLs). + +* oci: support "data" in any OCI decriptor, not just manifest config. + * report: - should the list of metrics use JSON-SEQ? or maybe be wrapped in a json array (the latter might be necessary, once we sign the combination) @@ -204,6 +213,10 @@ Features: * homed/pam_systemd: allow authentication by ssh-agent, so that run0/polkit can be allowed if caller comes with the right ssh-agent keys. +* machined: gc for OCI layers that are not referenced anymore by any .mstack/ links. + +* pull-oci: progress notification + * networkd/machined: implement reverse name lookups in the resolved hook * networkd's resolved hook: optionally map all lease IP addresses handed out to @@ -220,19 +233,8 @@ Features: the file systems to disk later (using btrfs device replacement), if needed as part of an installer logic. -* add a concept of overlay directory stacks to image discovery, i.e. have a dir - with a name suffix of ".ovl" or so that contains DDIs and plain dirs (and - possible .v dirs) that are glued together on use via overlayfs. one special - subdir should be used as writable top layer. - * journald: log pidfid as another field, i.e. _PIDFDID= -* systemd-nspawn: something like --volatile= but that isn't volatile, but - stores the data in some separate dir on disk. Usecase: keep always up-to-date - DDIs of some OS in your home dir, but combine its /usr/ with a locally - maintained root fs in a regular dir to maintain local state. (idea: call it - --mutable= and take dir or DDI and merge in) - * measure all log-in attempts into a new nvpcr * measure all DDI activations into a new nvpcr @@ -1128,12 +1130,6 @@ Features: automatically support reverting back to older OS version images if newer ones fail to boot. -* implement new "systemd-fsrebind" tool that works like gpt-auto-generator but - looks at a root dir and then applies vpick on various dirs/images to pick a - root tree, a /usr/ tree, a /home/, a /srv/, a /var/ tree and so on. Dirs - could also be btrfs subvols (combine with btrfs auto-snapshort approach for - creating versions like these automatically). - * remove tomoyo support, it's obsolete and unmaintained apparently * In .socket units, add ConnectStream=, ConnectDatagram=, diff --git a/man/importctl.xml b/man/importctl.xml index 9e97b2a241..3c14b83f08 100644 --- a/man/importctl.xml +++ b/man/importctl.xml @@ -39,9 +39,9 @@ importctl may be used to download, import, and export disk images via systemd-importd.service8. - importctl operates both on block-level disk images (such as DDIs) as well as - file-system-level images (tarballs). It supports disk images in one of the four following - classes: + importctl operates both on block-level disk images (such as DDIs), + file-system-level images (tarballs), as well as OCI images. It supports disk images in one of the four + following classes: VM images or full OS container images, that may be run via @@ -171,6 +171,24 @@ + + pull-oci REF [NAME] + + Downloads the specified OCI container image, and makes it available under the + specified local name in the image directory for the selected . The first + argument must be an OCI container reference, such as library/nginx If the local + name is omitted, it is automatically derived from the last component of the URL, with its suffix + removed. + + When downloading images of this type no image verification is done beyond the usual + authentication of the HTTPS certificates. + + Note that pressing Control-c during execution of this command will not abort the download. Use + cancel-transfer, described below. + + + + import-tar FILE [NAME] import-raw FILE [NAME] diff --git a/man/org.freedesktop.import1.xml b/man/org.freedesktop.import1.xml index cece4cad0b..95ecbd79e6 100644 --- a/man/org.freedesktop.import1.xml +++ b/man/org.freedesktop.import1.xml @@ -136,6 +136,12 @@ node /org/freedesktop/import1 { in t flags, out u transfer_id, out o transfer_path); + PullOci(in s ref, + in s local_name, + in s class, + in t flags, + out u transfer_id, + out o transfer_path); ListTransfers(out a(usssdo) transfers); ListTransfersEx(in s class, in t flags, @@ -191,6 +197,8 @@ node /org/freedesktop/import1 { + + @@ -290,6 +298,19 @@ node /org/freedesktop/import1 { export calls above, these calls return a pair of transfer identifier and object path for the ongoing download. + PullOci() is similar to PullTarEx() or + PullRawEx() and may be used to download and import an OCI container image from an + OCI registry. It takes an OCI container reference as argument. The second argument is a local name for + the image (which will be generated as + systemd.mstack7 + directory referencing the OCI layers). It should be suitable as a hostname, similarly to the matching + argument of the PullTar()/PullTarEx() and + PullRaw()/PullRawEx() methods above. The last argument is a + 64bit flags parameter, where bit 0 controls the force flag, bit 1 is a + read_only flag that controls whether the created image shall be marked + read-only. Like the pull calls above, this call return a pair of transfer identifier and object path + for the ongoing download. + ImportFileSystem()/ImportFileSystemEx() are similar to ImportTar()/ImportTarEx() but import a directory tree. The first argument must refer to a directory file descriptor for the source hierarchy to import. @@ -464,6 +485,7 @@ node /org/freedesktop/import1/transfer/_1 { ExportRawEx(), PullTarEx(), PullRawEx(), ListTransfersEx(), ListImages() were added in version 256. + PullOci() was added in version 260. Transfer Objects diff --git a/man/org.freedesktop.systemd1.xml b/man/org.freedesktop.systemd1.xml index fbed12c4be..f0a7ccc6d6 100644 --- a/man/org.freedesktop.systemd1.xml +++ b/man/org.freedesktop.systemd1.xml @@ -3137,6 +3137,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -4496,6 +4498,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { + + @@ -4938,6 +4942,7 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice { MountImages ExtensionImages ExtensionDirectories + RootMStack see systemd.exec(5) for their meaning. MemoryAvailable takes into account unit's and parents' MemoryMax @@ -5392,6 +5397,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -6741,6 +6748,8 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket { + + @@ -7461,6 +7470,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -8642,6 +8653,8 @@ node /org/freedesktop/systemd1/unit/home_2emount { + + @@ -9495,6 +9508,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly b RootEphemeral = ...; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") + readonly s RootMStack = '...'; + @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly as ExtensionDirectories = ['...', ...]; @org.freedesktop.DBus.Property.EmitsChangedSignal("const") readonly a(sba(ss)) ExtensionImages = [...]; @@ -10640,6 +10655,8 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap { + + @@ -12520,9 +12537,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ ManagedOOMKills, ExecReloadPost, and ExecReloadPostEx were added in version 259. - BindNetworkInterface, - MemoryTHP, and - RefreshOnReload were added in version 260. + BindNetworkInterface, MemoryTHP, + RefreshOnReload, and RootMStack were added in version 260. Socket Unit Objects @@ -12591,8 +12607,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface MemoryTHP, and + RootMStack were added in version 260. Mount Unit Objects @@ -12656,8 +12672,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface MemoryTHP, and + RootMStack were added in version 260. Swap Unit Objects @@ -12719,8 +12735,8 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \ UserNamespacePath, OOMKills, and ManagedOOMKills were added in 259. - BindNetworkInterface, and - MemoryTHP were added in version 260. + BindNetworkInterface, MemoryTHP, and + RootMStack were added in version 260. Slice Unit Objects diff --git a/man/rules/meson.build b/man/rules/meson.build index 9eae3e8249..94ac1c919b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1074,6 +1074,7 @@ manpages = [ ['systemd-modules-load.service', '8', ['systemd-modules-load'], 'HAVE_KMOD'], ['systemd-mount', '1', ['systemd-umount'], ''], ['systemd-mountfsd.service', '8', ['systemd-mountfsd'], 'ENABLE_MOUNTFSD'], + ['systemd-mstack', '1', ['mount.mstack'], 'HAVE_BLKID'], ['systemd-mute-console', '1', ['systemd-mute-console.socket', 'systemd-mute-console@.service'], @@ -1249,6 +1250,7 @@ manpages = [ ['systemd.kill', '5', [], ''], ['systemd.link', '5', [], ''], ['systemd.mount', '5', [], ''], + ['systemd.mstack', '7', [], ''], ['systemd.net-naming-scheme', '7', [], ''], ['systemd.netdev', '5', [], 'ENABLE_NETWORKD'], ['systemd.network', '5', [], 'ENABLE_NETWORKD'], diff --git a/man/systemd-mstack.xml b/man/systemd-mstack.xml new file mode 100644 index 0000000000..41ae1fd831 --- /dev/null +++ b/man/systemd-mstack.xml @@ -0,0 +1,218 @@ + + + + + + + + systemd-mstack + systemd + + + + systemd-mstack + 1 + + + + systemd-mstack + mount.mstack + Mstack Discoverable Disk Images (DDIs) + + + + + systemd-mstack OPTIONS IMAGE + + + systemd-mstack OPTIONS --mount IMAGE PATH + + + systemd-mstack OPTIONS --umount PATH + + + + + Description + + systemd-mstack is a tool for introspecting and interacting with + .mstack/ mount stack directories, as described in + systemd.mstack7. It + supports three different operations: + + + Show general mount stack information, including all described + overlayfs layers and bind mounts. + + Mount a mount stack to a local directory. + + Unmount a mount stack from a local directory. + + + The systemd-mstack command may be invoked as mount.mstack in + which case it implements the mount8 "external + helper" interface. This ensures mount stack directories compatible with systemd-mstack + can be mounted directly by mount and fstab5. For + details see below. + + + + + + Commands + + If neither of the command switches listed below are passed the specified mount stack is opened and + general information about it is shown, including a list of all defined layers. + + + + + + + Mount the specified mount stack to the specified directory. + + To unmount a mount stack directory mounted like this use the operation. + + Note that this functionality is also available in mount8 via a + command such as mount -t mstack mystack.mstack targetdir/, as well as in fstab5. For + details, see below. + + + + + + + + This is a shortcut for . + + + + + + + + + Unmount a mount stack from the specified directory. This command expects one argument: + a directory where mount stack was mounted. + + All mounted mounts will be recursively unmounted + + + + + + + + This is a shortcut for . + + + + + + + + + + + + Options + + The following options are understood: + + + + + + + Operate in read-only mode. By default, will establish + writable mount points. If this option is specified they are established in read-only mode + instead. + + + + + + + + If combined with the directory to mount the mount stack to + is created if it is missing. Note that the directory is not automatically removed when the mount + stack is unmounted again. + + + + + + + + If combined with the specified directory where the mount stack + is mounted is removed after unmounting it. + + + + + + + + + + + + + + Exit status + + On success, 0 is returned, a non-zero failure code otherwise. + + + + Invocation as <command>/sbin/mount.mstack</command> + + + + The systemd-mstack executable may be symlinked to + /sbin/mount.mstack. If invoked through that it implements mount8's + "external helper" interface for the (pseudo) file system type mstack. This means + conformant mount stack directories may be mounted directly via + + # mount -t mstack mystack.mstack targetdir/ + + in a fashion mostly equivalent to: + + # systemd-mstack --mount mystack.mstack targetdir/ + + Note that since a single mount stack may contain multiple mount points it should later be unmounted with + umount -R targetdir/, for recursive operation. + + This functionality is particularly useful to mount mount stacks automatically at boot via simple + /etc/fstab entries. For example: + + /path/to/mystack.nspawn /images/mystack/ mstack defaults 0 0 + + When invoked this way the mount options ro, rw map to the + corresponding options listed above (i.e. ). + + + + See Also + + systemd1 + systemd.mstack7 + systemd-nspawn1 + systemd.exec5 + mount8 + umount8 + + + + diff --git a/man/systemd-nspawn.xml b/man/systemd-nspawn.xml index fc2c0e145b..da598d090c 100644 --- a/man/systemd-nspawn.xml +++ b/man/systemd-nspawn.xml @@ -370,6 +370,18 @@ + + + + Mount stack to mount the root directory for the container from. Takes a path to a + directory implementing + systemd.mstack7. This + may be used to run a container of an overlayfs assembled from a number of layers, + possibly writable and augmented with additional bind mounts. + + + + @@ -2038,6 +2050,7 @@ After=sys-subsystem-net-devices-ens1.device importctl1 systemd-mountfsd.service8 systemd-nsresourced.service8 + systemd.mstack7 btrfs8 diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 0ba3e011f2..825621693e 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -352,6 +352,26 @@ + + RootMStack= + + Takes a path to a + systemd.mstack7 + directory encapsulating a mount stack consisting of layers and bind mounts. Similar to + RootDirectory= and RootImage= this runs the service off a + distinct root file system, in this case set up via overlayfs. + + Since .mstack/ directories may reference disk images (DDIs) similar device + policy extensions and dependencies are in effect when RootMStack= is used as are + if RootImage= is used. + + + + + + + + MountAPIVFS= @@ -450,16 +470,16 @@ BindPaths= BindReadOnlyPaths= - Configures unit-specific bind mounts. A bind mount makes a particular file or directory - available at an additional place in the unit's view of the file system. Any bind mounts created with this - option are specific to the unit, and are not visible in the host's mount table. This option expects a - whitespace separated list of bind mount definitions. Each definition consists of a colon-separated triple of - source path, destination path and option string, where the latter two are optional. If only a source path is - specified the source and destination is taken to be the same. The option string may be either - rbind or norbind for configuring a recursive or non-recursive bind - mount. If the destination path is omitted, the option string must be omitted too. - Each bind mount definition may be prefixed with -, in which case it will be ignored - when its source path does not exist. + Configures unit-specific bind mounts. A bind mount makes a particular file or + directory available at an additional place in the unit's view of the file system. Any bind mounts + created with this option are specific to the unit, and are not visible in the host's mount + table. This option expects a whitespace separated list of bind mount definitions. Each definition + consists of a colon-separated triple of source path, destination path and option string, where the + latter two are optional. If only a source path is specified the source and destination is taken to be + the same. The option string may be either rbind or norbind for + configuring a recursive or non-recursive bind mount. If the destination path is omitted, the option + string must be omitted too. Each bind mount definition may be prefixed with -, in + which case it will be ignored when its source path does not exist or is not accessible. BindPaths= creates regular writable bind mounts (unless the source file system mount is already marked read-only), while BindReadOnlyPaths= creates read-only bind mounts. These @@ -2202,16 +2222,17 @@ BindReadOnlyPaths=/var/lib/systemd PrivateUsers= - Takes a boolean argument or one of self, identity, - or full. Defaults to false. If enabled, sets up a new user namespace for the - executed processes and configures a user and group mapping. If set to a true value or - self, a minimal user and group mapping is configured that maps the - root user and group as well as the unit's own user and group to themselves and - everything else to the nobody user and group. This is useful to securely detach - the user and group databases used by the unit from the rest of the system, and thus to create an - effective sandbox environment. All files, directories, processes, IPC objects and other resources - owned by users/groups not equaling root or the unit's own will stay visible from - within the unit but appear owned by the nobody user and group. + Takes a boolean argument or one of self, + identity, full or managed. Defaults to + false. If enabled, sets up a new user namespace for the executed processes and configures a user and + group mapping. If set to a true value or self, a minimal user and group mapping is + configured that maps the root user and group as well as the unit's own user and + group to themselves and everything else to the nobody user and group. This is + useful to securely detach the user and group databases used by the unit from the rest of the system, + and thus to create an effective sandbox environment. All files, directories, processes, IPC objects + and other resources owned by users/groups not equaling root or the unit's own will + stay visible from within the unit but appear owned by the nobody user and + group. If the parameter is identity, user namespacing is set up with an identity mapping for the first 65536 UIDs/GIDs. Any UIDs/GIDs above 65536 will be mapped to the @@ -2224,14 +2245,21 @@ BindReadOnlyPaths=/var/lib/systemd to call setgroups() system calls (by setting /proc/pid/setgroups to allow). Similar to identity, this does not provide UID/GID isolation, but it does provide - process capability isolation. + process capability isolation. If this mode is enabled, all unit processes are run without privileges + in the host user namespace (regardless of whether the unit's own user/group is + root or not). Specifically this means that the process will have zero process + capabilities on the host's user namespace, but full capabilities within the service's user + namespace. Settings such as CapabilityBoundingSet= will affect only the latter, + and there's no way to acquire additional capabilities in the host's user namespace. - If this mode is enabled, all unit processes are run without privileges in the host user - namespace (regardless of whether the unit's own user/group is root or not). Specifically - this means that the process will have zero process capabilities on the host's user namespace, but - full capabilities within the service's user namespace. Settings such as - CapabilityBoundingSet= will affect only the latter, and there's no way to acquire - additional capabilities in the host's user namespace. + If the paramater is managed a transient, dynamically allocated range of + 65536 UIDs/GIDs is allocated for the unit, and a UID/GID mapping is assigned to the unit's process + so the UID/GID 0 from inside the unit maps to the first UID/GID of the allocated mapping. Note that + in this mode the UID/GID the service process will run as is different depending if looking from the + host side (where it will be a high, dynamically assigned UID) or from inside the unit (where it will + be 0). Also note that this mode will enable file system UID mapping for the file systems this service + accesses, mapping the "foreign" UID range on disk to the selected dynamic UID range at + runtime. When this setting is set up by a per-user instance of the service manager, the mapping of the root user and group to itself is omitted (unless the user manager is root). diff --git a/man/systemd.mstack.xml b/man/systemd.mstack.xml new file mode 100644 index 0000000000..778d2cc9dc --- /dev/null +++ b/man/systemd.mstack.xml @@ -0,0 +1,126 @@ + + + + + + + + systemd.mstack + systemd + + + + systemd.mstack + 7 + + + + systemd.mstack + Mount stacks in self descriptive directories + + + + Description + + Directories with the .mstack/ suffix may encode 'mount stacks' for assembling OS + mount hierarchies based on bind and overlay mounts, for use in + systemd-nspawn1's + switch or the service manager's setting for + services. .mstack/ directroies may contain various files and subdirectories, where + each will effect one layer of an overlayfs mount, or a bind mount. The name of the + file or subdirectory indicates how it shall used in the mount hierarchy. Specifically, the following + names are defined: + + + A layer@id/ directory will be turned into + a layer of an overlayfs mount. The id identifier is used to define the order of the + layers: a version sort is executed, with the first entry being the bottom layer in the + overlayfs stack, and the last entry becoming the highest layer (precisely: + highest "lowerdir") in the overlayfs stack. + + Similar, a layer@id.raw regular file + will be mounted as a DDI, and the resulting mount will be turned into an overlayfs layer, following the + same sorting rules. + + An rw directory will be turned into a writable layer at the very top + of the overlayfs stack. A subdirectory data of it will become + the "upperdir", a subdirectory work will become the "workdir". Note that these two + subdirectories do not need to be created explicitly, they are created automatically on first use should + they be missing. + + A bind@location/ directory will be bind + mounted to the mount point indicated by the location identifier, in read-write + fashion. The loction is encoded via the same escaping logic used for naming .mount + units, i.e. slashes become dashes. + + Similar, a + bind@location.raw file will be mounted as a DDI, and the + resulting mount bind mounted to the specified location. + + A robind@location/ is treated very + similar to bind@location/, but the resulting bind mount + is read-only. + + Similar, robind@location.raw creates a + read-only bind mount from a DDI. + + If a root/ subdirectory it is used as root of the resulting mount + hierarchy, and only the usr/ subtree of the overlayfs mount will be bound to + usr/ in the hierarchy. + + + Note that each of the entry types above may be a symbolic link pointing to a directory or image + file, instead a directory or image file itself. + + On each listed file or subdirectory type the + systemd.v7 + functionality may be used, for automatic selection of versioned resources. + + Use the + systemd-mstack1 tool + to process or mount .mstack/ directories from the command line. + + + + Examples + + The following .mstack/ consists of two read-only overlayfs layers as DDI, plus one + writable directory one on top. The read-only layers are symlinked: + + + foobar.mstack/layer@0.raw → ../base.raw + foobar.mstack/layer@1.raw → ../app.raw + foobar.mstack/rw/ + + + The following .mstack/ consists of a read-only DDI mounted to /usr/ + and writable root: + + + waldo.mstack/layer@0.raw → ../vendor.raw + waldo.mstack/root/ + + + The following .mstack/ consists of a read-only DDI mounted as root, but a + writable /var/ mounted on top: + + + quux.mstack/layer@0.raw → ../myapp1.raw + quux.mstack/bind:var → ../myapp1-var/ + + + + + See Also + + systemd1 + systemd-mstack1 + systemd-nspawn1 + systemd.exec5 + systemd.v7 + systemd-vpick1 + + + + diff --git a/meson.build b/meson.build index bf419b7f60..61a2c253ee 100644 --- a/meson.build +++ b/meson.build @@ -2379,6 +2379,7 @@ subdir('src/measure') subdir('src/modules-load') subdir('src/mount') subdir('src/mountfsd') +subdir('src/mstack') subdir('src/mute-console') subdir('src/network') subdir('src/notify') diff --git a/meson_options.txt b/meson_options.txt index 4d13d2866d..c1af7ce237 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -565,3 +565,6 @@ option('vmlinux-h-path', type : 'string', value : '', option('default-mountfsd-trusted-directories', type : 'boolean', value: false, description : 'controls whether mountfsd should apply a relaxed policy on DDIs in system DDI directories') + +option('default-oci-registry', type : 'string', value : 'docker.io', + description : 'Default OCI registry name') diff --git a/src/basic/architecture.h b/src/basic/architecture.h index 467e5acbbf..ee91ea0d34 100644 --- a/src/basic/architecture.h +++ b/src/basic/architecture.h @@ -9,7 +9,7 @@ * processor features, models, generations or even ABIs. Hence we * focus on general family, and distinguish word width and endianness. */ -typedef enum { +typedef enum Architecture { ARCHITECTURE_ALPHA, ARCHITECTURE_ARC, ARCHITECTURE_ARC_BE, diff --git a/src/basic/basic-forward.h b/src/basic/basic-forward.h index adfe85eca0..ef616dc7a9 100644 --- a/src/basic/basic-forward.h +++ b/src/basic/basic-forward.h @@ -83,6 +83,7 @@ struct iovec_wrapper; union in_addr_union; union sockaddr_union; +typedef enum Architecture Architecture; typedef enum CGroupFlags CGroupFlags; typedef enum CGroupMask CGroupMask; typedef enum ChaseFlags ChaseFlags; diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c index 10c4f75efd..624a3fcbed 100644 --- a/src/basic/mountpoint-util.c +++ b/src/basic/mountpoint-util.c @@ -621,7 +621,7 @@ bool mount_new_api_supported(void) { if (cache >= 0) return cache; - /* This is the newer API among the ones we use, so use it as boundary */ + /* This is the newest API among the ones we use, so use it as boundary */ r = RET_NERRNO(mount_setattr(-EBADF, NULL, 0, NULL, 0)); if (r == 0 || ERRNO_IS_NOT_SUPPORTED(r)) /* This should return an error if it is working properly */ return (cache = false); diff --git a/src/basic/uid-range.c b/src/basic/uid-range.c index 81e736ef5a..c498b5afe1 100644 --- a/src/basic/uid-range.c +++ b/src/basic/uid-range.c @@ -379,3 +379,18 @@ int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret) { } } } + +uid_t uid_range_base(const UIDRange *range) { + + /* Returns the lowest UID in the range (notw that elements are sorted, hence we just need to look at + * the first one that is populated. */ + + if (uid_range_is_empty(range)) + return UID_INVALID; + + FOREACH_ARRAY(e, range->entries, range->n_entries) + if (e->nr > 0) + return e->start; + + return UID_INVALID; +} diff --git a/src/basic/uid-range.h b/src/basic/uid-range.h index b74ab9d332..c28b02fa7d 100644 --- a/src/basic/uid-range.h +++ b/src/basic/uid-range.h @@ -54,3 +54,5 @@ int uid_range_load_userns_by_fd(int userns_fd, UIDRangeUsernsMode mode, UIDRange bool uid_range_overlaps(const UIDRange *range, uid_t start, uid_t nr); int uid_map_search_root(pid_t pid, UIDRangeUsernsMode mode, uid_t *ret); + +uid_t uid_range_base(const UIDRange *range); diff --git a/src/core/dbus-execute.c b/src/core/dbus-execute.c index 6fb9060415..2bd7b1c07e 100644 --- a/src/core/dbus-execute.c +++ b/src/core/dbus-execute.c @@ -1258,6 +1258,7 @@ const sd_bus_vtable bus_exec_vtable[] = { SD_BUS_PROPERTY("RootHashSignaturePath", "s", NULL, offsetof(ExecContext, root_hash_sig_path), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RootVerity", "s", NULL, offsetof(ExecContext, root_verity), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("RootEphemeral", "b", bus_property_get_bool, offsetof(ExecContext, root_ephemeral), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("RootMStack", "s", NULL, offsetof(ExecContext, root_mstack), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ExtensionDirectories", "as", NULL, offsetof(ExecContext, extension_directories), SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("ExtensionImages", "a(sba(ss))", property_get_extension_images, 0, SD_BUS_VTABLE_PROPERTY_CONST), SD_BUS_PROPERTY("MountImages", "a(ssba(ss))", property_get_mount_images, 0, SD_BUS_VTABLE_PROPERTY_CONST), @@ -1882,6 +1883,9 @@ int bus_exec_context_set_transient_property( if (streq(name, "RootImage")) return bus_set_transient_path(u, name, &c->root_image, message, flags, reterr_error); + if (streq(name, "RootMStack")) + return bus_set_transient_path(u, name, &c->root_mstack, message, flags, reterr_error); + if (streq(name, "RootImageOptions")) { _cleanup_(mount_options_free_allp) MountOptions *options = NULL; _cleanup_free_ char *format_str = NULL; diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index a0b0a87a65..315ef41031 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -14,6 +14,7 @@ #include #include "sd-messages.h" +#include "sd-varlink.h" #include "apparmor-util.h" /* IWYU pragma: keep */ #include "argv-util.h" @@ -55,8 +56,10 @@ #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "nsflags.h" +#include "nsresource.h" #include "open-file.h" #include "osc-context.h" #include "pam-util.h" @@ -81,6 +84,7 @@ #include "strv.h" #include "strxcpyx.h" #include "terminal-util.h" +#include "uid-range.h" #include "user-util.h" #include "utmp-wtmp.h" #include "vpick.h" @@ -2395,11 +2399,14 @@ static int setup_private_users_child(int unshare_ready_fd, const char *uid_map, } static int setup_private_users( + sd_varlink *nsresource_link, PrivateUsers private_users, - uid_t ouid, - gid_t ogid, - uid_t uid, - gid_t gid, + uid_t saved_uid, /* service manager uid */ + gid_t saved_gid, /* service manager gid */ + uid_t *uid, /* unit uid (seen from inside) [input+output] */ + gid_t *gid, /* unit gid (ditto) [input+output] */ + uid_t *outside_uid, /* uid seen from the outside (which is the same as *uid, except of userns is used) */ + gid_t *outside_gid, /* gid seen from the outside (similar) */ bool allow_setgroups) { _cleanup_free_ char *uid_map = NULL, *gid_map = NULL; @@ -2410,6 +2417,11 @@ static int setup_private_users( ssize_t n; int r; + assert(uid); + assert(gid); + assert(outside_uid); + assert(outside_gid); + /* Set up a user namespace and map the original UID/GID (IDs from before any user or group changes, i.e. * the IDs from the user or system manager(s)) to itself, the selected UID/GID to itself, and everything else to * nobody. In order to be able to write this mapping we need CAP_SETUID in the original user namespace, which @@ -2425,6 +2437,45 @@ static int setup_private_users( case PRIVATE_USERS_NO: return 0; /* Early exit */ + case PRIVATE_USERS_MANAGED: { + if (uid_is_valid(*uid) || uid_is_valid(*gid)) + return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "When allocating dynamic user namespace range, target UID/GID must be root, refusing."); + + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + nsresource_link, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); + if (userns_fd < 0) + return userns_fd; + + _cleanup_(uid_range_freep) UIDRange *uid_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, UID_RANGE_USERNS_OUTSIDE, &uid_range); + if (r < 0) + return log_debug_errno(r, "Failed to read outside userns UID range: %m"); + + _cleanup_(uid_range_freep) UIDRange *gid_range = NULL; + r = uid_range_load_userns_by_fd(userns_fd, GID_RANGE_USERNS_OUTSIDE, &gid_range); + if (r < 0) + return log_debug_errno(r, "Failed to read outside userns GID range: %m"); + + if (setns(userns_fd, CLONE_NEWUSER) < 0) + return log_debug_errno(errno, "Failed to join freshly allocated user namespace: %m"); + + /* In "managed" mode the originating UID is not mapped hence we need to explicitly become root in the new userns now. */ + r = reset_uid_gid(); + if (r < 0) + return log_debug_errno(r, "Failed to reset UID/GID to root: %m"); + + /* Now report are new UIDs/GIDS, both inside and outside */ + *uid = 0; + *gid = 0; + + *outside_uid = uid_range_base(uid_range); + *outside_gid = uid_range_base(gid_range); + + return 1; /* Early exit */ + } + case PRIVATE_USERS_IDENTITY: uid_map = strdup("0 0 65536\n"); if (!uid_map) @@ -2445,15 +2496,15 @@ static int setup_private_users( case PRIVATE_USERS_SELF: /* Can only set up multiple mappings with CAP_SETUID. */ - if (uid_is_valid(uid) && uid != ouid && have_effective_cap(CAP_SETUID) > 0) + if (uid_is_valid(*uid) && *uid != saved_uid && have_effective_cap(CAP_SETUID) > 0) r = asprintf(&uid_map, UID_FMT " " UID_FMT " 1\n" /* Map $OUID → $OUID */ UID_FMT " " UID_FMT " 1\n", /* Map $UID → $UID */ - ouid, ouid, uid, uid); + saved_uid, saved_uid, *uid, *uid); else r = asprintf(&uid_map, UID_FMT " " UID_FMT " 1\n", /* Map $OUID → $OUID */ - ouid, ouid); + saved_uid, saved_uid); if (r < 0) return -ENOMEM; @@ -2479,15 +2530,15 @@ static int setup_private_users( case PRIVATE_USERS_SELF: /* Can only set up multiple mappings with CAP_SETGID. */ - if (gid_is_valid(gid) && gid != ogid && have_effective_cap(CAP_SETGID) > 0) + if (gid_is_valid(*gid) && *gid != saved_gid && have_effective_cap(CAP_SETGID) > 0) r = asprintf(&gid_map, GID_FMT " " GID_FMT " 1\n" /* Map $OGID → $OGID */ GID_FMT " " GID_FMT " 1\n", /* Map $GID → $GID */ - ogid, ogid, gid, gid); + saved_gid, saved_gid, *gid, *gid); else r = asprintf(&gid_map, GID_FMT " " GID_FMT " 1\n", /* Map $OGID -> $OGID */ - ogid, ogid); + saved_gid, saved_gid); if (r < 0) return -ENOMEM; break; @@ -3487,8 +3538,7 @@ static int compile_symlinks( static bool insist_on_sandboxing( const ExecContext *context, - const char *root_dir, - const char *root_image, + const PinnedResource *rootfs, const BindMount *bind_mounts, size_t n_bind_mounts) { @@ -3502,7 +3552,7 @@ static bool insist_on_sandboxing( if (context->n_temporary_filesystems > 0) return true; - if (root_dir || root_image || context->root_directory_as_fd) + if (pinned_resource_is_set(rootfs)) return true; if (context->n_mount_images > 0) @@ -3529,8 +3579,7 @@ static bool insist_on_sandboxing( static int setup_ephemeral( const ExecContext *context, ExecRuntime *runtime, - char **root_image, /* both input and output! modified if ephemeral logic enabled */ - char **root_directory, /* ditto */ + PinnedResource *rootfs, /* both input and output! modified if ephemeral logic enabled */ char **reterr_path) { _cleanup_close_ int fd = -EBADF; @@ -3538,12 +3587,10 @@ static int setup_ephemeral( int r; assert(context); - assert(!context->root_directory_as_fd); assert(runtime); - assert(root_image); - assert(root_directory); + assert(rootfs); - if (!*root_image && !*root_directory) + if (!rootfs->image && !rootfs->directory) return 0; if (!runtime->ephemeral_copy) @@ -3569,32 +3616,32 @@ static int setup_ephemeral( if (fd != -EAGAIN) return log_debug_errno(fd, "Failed to receive file descriptor queued on ephemeral storage socket: %m"); - if (*root_image) { - log_debug("Making ephemeral copy of %s to %s", *root_image, new_root); + if (rootfs->image) { + log_debug("Making ephemeral copy of %s to %s", rootfs->image, new_root); - fd = copy_file(*root_image, new_root, O_EXCL, 0600, + fd = copy_file(rootfs->image, new_root, O_EXCL, 0600, COPY_LOCK_BSD|COPY_REFLINK|COPY_CRTIME|COPY_NOCOW_AFTER); if (fd < 0) { - *reterr_path = strdup(*root_image); + *reterr_path = strdup(rootfs->image); return log_debug_errno(fd, "Failed to copy image %s to %s: %m", - *root_image, new_root); + rootfs->image, new_root); } } else { - assert(*root_directory); + assert(rootfs->directory); - log_debug("Making ephemeral snapshot of %s to %s", *root_directory, new_root); + log_debug("Making ephemeral snapshot of %s to %s", rootfs->directory, new_root); fd = btrfs_subvol_snapshot_at( - AT_FDCWD, *root_directory, + AT_FDCWD, rootfs->directory, AT_FDCWD, new_root, BTRFS_SNAPSHOT_FALLBACK_COPY | BTRFS_SNAPSHOT_FALLBACK_DIRECTORY | BTRFS_SNAPSHOT_RECURSIVE | BTRFS_SNAPSHOT_LOCK_BSD); if (fd < 0) { - *reterr_path = strdup(*root_directory); + *reterr_path = strdup(rootfs->directory); return log_debug_errno(fd, "Failed to snapshot directory %s to %s: %m", - *root_directory, new_root); + rootfs->directory, new_root); } } @@ -3602,11 +3649,14 @@ static int setup_ephemeral( if (r < 0) return log_debug_errno(r, "Failed to queue file descriptor on ephemeral storage socket: %m"); - if (*root_image) - free_and_replace(*root_image, new_root); - else { - assert(*root_directory); - free_and_replace(*root_directory, new_root); + if (rootfs->image) { + free_and_replace(rootfs->image, new_root); + close_and_replace(rootfs->image_fd, fd); + } else { + assert(rootfs->directory); + + free_and_replace(rootfs->directory, new_root); + close_and_replace(rootfs->directory_fd, fd); } return 1; @@ -3660,20 +3710,35 @@ static int verity_settings_prepare( return 0; } -static int pick_versions( +static int pin_rootfs( const ExecContext *context, const ExecParameters *params, - char **ret_root_image, - char **ret_root_directory, + PinnedResource *ret, char **reterr_path) { int r; assert(context); - assert(!context->root_directory_as_fd); assert(params); - assert(ret_root_image); - assert(ret_root_directory); + assert(ret); + + if (!FLAGS_SET(params->flags, EXEC_APPLY_CHROOT)) { + *ret = PINNED_RESOURCE_NULL; + return 0; + } + + if (context->root_directory_as_fd) { + _cleanup_close_ int fd = fcntl(params->root_directory_fd, F_DUPFD_CLOEXEC, 3); + if (fd < 0) + return log_debug_errno(errno, "Failed to duplicate root directory fd: %m"); + + *ret = (PinnedResource) { + .directory_fd = TAKE_FD(fd), + .image_fd = -EBADF, + }; + + return 1; + } if (context->root_image) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; @@ -3695,8 +3760,24 @@ static int pick_versions( return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_image); } - *ret_root_image = TAKE_PTR(result.path); - *ret_root_directory = NULL; + /* path_pick() returns us an O_PATH fd, let's turn this into a fully opened file, because + * mountfsd will want this later, and it wants a fully opened fd, so that security checks + * have been passed */ + _cleanup_close_ int reopened_fd = -EBADF; + reopened_fd = fd_reopen(result.fd, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_RDWR); + if (ERRNO_IS_NEG_FS_WRITE_REFUSED(reopened_fd)) + reopened_fd = fd_reopen(result.fd, O_CLOEXEC|O_NONBLOCK|O_NOCTTY|O_RDONLY); + if (reopened_fd < 0) { + *reterr_path = strdup(context->root_image); + return log_debug_errno(reopened_fd, "Failed to open image '%s': %m", context->root_image); + } + + *ret = (PinnedResource) { + .image = TAKE_PTR(result.path), + .image_fd = TAKE_FD(reopened_fd), + .directory_fd = -EBADF, + }; + return r; } @@ -3720,12 +3801,53 @@ static int pick_versions( return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_directory); } - *ret_root_image = NULL; - *ret_root_directory = TAKE_PTR(result.path); + *ret = (PinnedResource) { + .directory = TAKE_PTR(result.path), + .directory_fd = TAKE_FD(result.fd), + .image_fd = -EBADF, + }; + return r; } - *ret_root_image = *ret_root_directory = NULL; + if (context->root_mstack) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + + r = path_pick(/* toplevel_path= */ NULL, + /* toplevel_fd= */ AT_FDCWD, + context->root_mstack, + pick_filter_image_mstack, + /* n_filters= */ 1, + PICK_ARCHITECTURE|PICK_TRIES|PICK_RESOLVE, + &result); + if (r < 0) { + *reterr_path = strdup(context->root_mstack); + return r; + } + + if (!result.path) { + *reterr_path = strdup(context->root_mstack); + return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "No matching entry in .v/ directory %s found.", context->root_mstack); + } + + _cleanup_(mstack_freep) MStack *mstack = NULL; + r = mstack_load(result.path, result.fd, &mstack); + if (r < 0) { + *reterr_path = TAKE_PTR(result.path); + return r; + } + + *ret = (PinnedResource) { + .mstack = TAKE_PTR(result.path), + .mstack_loaded = TAKE_PTR(mstack), + .image_fd = -EBADF, + .directory_fd = -EBADF, + }; + + return r; + } + + *ret = PINNED_RESOURCE_NULL; return 0; } @@ -3733,7 +3855,8 @@ static int apply_mount_namespace( ExecCommandFlags command_flags, const ExecContext *context, const ExecParameters *params, - ExecRuntime *runtime, + const ExecRuntime *runtime, + const PinnedResource *rootfs, const char *memory_pressure_path, bool needs_sandboxing, uid_t exec_directory_uid, @@ -3741,13 +3864,14 @@ static int apply_mount_namespace( PidRef *bpffs_pidref, int bpffs_socket_fd, int bpffs_errno_pipe, + sd_varlink *mountfsd_link, char **reterr_path) { _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; _cleanup_strv_free_ char **empty_directories = NULL, **symlinks = NULL, **read_write_paths_cleanup = NULL; _cleanup_free_ char *creds_path = NULL, *incoming_dir = NULL, *propagate_dir = NULL, - *private_namespace_dir = NULL, *host_os_release_stage = NULL, *root_image = NULL, *root_dir = NULL; + *private_namespace_dir = NULL, *host_os_release_stage = NULL; const char *tmp_dir = NULL, *var_tmp_dir = NULL; char **read_write_paths; bool setup_os_release_symlink; @@ -3761,26 +3885,6 @@ static int apply_mount_namespace( CLEANUP_ARRAY(bind_mounts, n_bind_mounts, bind_mount_free_many); - if (params->flags & EXEC_APPLY_CHROOT && !context->root_directory_as_fd) { - r = pick_versions( - context, - params, - &root_image, - &root_dir, - reterr_path); - if (r < 0) - return r; - - r = setup_ephemeral( - context, - runtime, - &root_image, - &root_dir, - reterr_path); - if (r < 0) - return r; - } - r = compile_bind_mounts(context, params, exec_directory_uid, exec_directory_gid, &bind_mounts, &n_bind_mounts, &empty_directories); if (r < 0) return r; @@ -3819,7 +3923,7 @@ static int apply_mount_namespace( } /* Symlinks (exec dirs, os-release) are set up after other mounts, before they are made read-only. */ - setup_os_release_symlink = needs_sandboxing && exec_context_get_effective_mount_apivfs(context) && (root_dir || root_image); + setup_os_release_symlink = needs_sandboxing && exec_context_get_effective_mount_apivfs(context) && pinned_resource_is_set(rootfs); r = compile_symlinks(context, params, setup_os_release_symlink, &symlinks); if (r < 0) return r; @@ -3866,10 +3970,10 @@ static int apply_mount_namespace( return -ENOMEM; } - if (root_image) { + if (rootfs->image) { r = verity_settings_prepare( &verity, - root_image, + rootfs->image, &context->root_hash, context->root_hash_path, &context->root_hash_sig, context->root_hash_sig_path, context->root_verity); @@ -3880,9 +3984,7 @@ static int apply_mount_namespace( NamespaceParameters parameters = { .runtime_scope = params->runtime_scope, - .root_directory = root_dir, - .root_image = root_image, - .root_directory_fd = params->flags & EXEC_APPLY_CHROOT ? params->root_directory_fd : -EBADF, + .rootfs = rootfs, .root_image_options = context->root_image_options, .root_image_policy = context->root_image_policy ?: &image_policy_service, @@ -3930,7 +4032,7 @@ static int apply_mount_namespace( /* If DynamicUser=no and RootDirectory= is set then lets pass a relaxed sandbox info, * otherwise enforce it, don't ignore protected paths and fail if we are enable to apply the * sandbox inside the mount namespace. */ - .ignore_protect_paths = !needs_sandboxing && !context->dynamic_user && root_dir, + .ignore_protect_paths = !needs_sandboxing && !context->dynamic_user && pinned_resource_is_set(rootfs), .protect_control_groups = needs_sandboxing ? exec_get_protect_control_groups(context) : PROTECT_CONTROL_GROUPS_NO, .protect_kernel_tunables = needs_sandboxing && context->protect_kernel_tunables, @@ -3956,10 +4058,13 @@ static int apply_mount_namespace( .protect_proc = needs_sandboxing ? context->protect_proc : PROTECT_PROC_DEFAULT, .proc_subset = needs_sandboxing ? context->proc_subset : PROC_SUBSET_ALL, .private_bpf = needs_sandboxing ? context->private_bpf : PRIVATE_BPF_NO, + .private_users = needs_sandboxing ? context->private_users : PRIVATE_USERS_NO, .bpffs_pidref = bpffs_pidref, .bpffs_socket_fd = bpffs_socket_fd, .bpffs_errno_pipe = bpffs_errno_pipe, + + .mountfsd_link = mountfsd_link, }; r = setup_namespace(¶meters, reterr_path); @@ -3970,17 +4075,18 @@ static int apply_mount_namespace( if (r == -ENOANO) { if (insist_on_sandboxing( context, - root_dir, root_image, + rootfs, bind_mounts, n_bind_mounts)) return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Failed to set up namespace, and refusing to continue since " "the selected namespacing options alter mount environment non-trivially.\n" - "Bind mounts: %zu, temporary filesystems: %zu, root directory: %s, root image: %s, dynamic user: %s", + "Bind mounts: %zu, temporary filesystems: %zu, root directory: %s, root image: %s, root mstack: %s, dynamic user: %s", n_bind_mounts, context->n_temporary_filesystems, - yes_no(root_dir), - yes_no(root_image), + yes_no(rootfs->directory_fd >= 0), + yes_no(rootfs->image_fd >= 0), + yes_no(!!rootfs->mstack_loaded), yes_no(context->dynamic_user)); log_debug("Failed to set up namespace, assuming containerized execution and ignoring."); @@ -4530,10 +4636,8 @@ static bool exec_needs_cap_sys_admin(const ExecContext *context, const ExecParam context->bind_log_sockets > 0 || context->n_bind_mounts > 0 || context->n_temporary_filesystems > 0 || - context->root_directory || - context->root_directory_as_fd || + exec_context_with_rootfs_strict(context) || !strv_isempty(context->extension_directories) || - context->root_image || context->n_mount_images > 0 || context->n_extension_images > 0 || context->protect_system != PROTECT_SYSTEM_NO || @@ -4605,7 +4709,8 @@ static bool exec_namespace_is_delegated( static int setup_delegated_namespaces( const ExecContext *context, ExecParameters *params, - ExecRuntime *runtime, + const ExecRuntime *runtime, + const PinnedResource *rootfs, bool delegate, const char *memory_pressure_path, uid_t uid, @@ -4616,6 +4721,7 @@ static int setup_delegated_namespaces( PidRef *bpffs_pidref, int bpffs_socket_fd, int bpffs_errno_pipe, + sd_varlink *mountfsd_link, int *reterr_exit_status) { int r; @@ -4630,6 +4736,7 @@ static int setup_delegated_namespaces( assert(context); assert(params); assert(runtime); + assert(rootfs); assert(reterr_exit_status); if (exec_needs_network_namespace(context) && @@ -4730,18 +4837,21 @@ static int setup_delegated_namespaces( exec_namespace_is_delegated(context, params, have_cap_sys_admin, CLONE_NEWNS) == delegate) { _cleanup_free_ char *error_path = NULL; - r = apply_mount_namespace(command->flags, - context, - params, - runtime, - memory_pressure_path, - needs_sandboxing, - uid, - gid, - bpffs_pidref, - bpffs_socket_fd, - bpffs_errno_pipe, - &error_path); + r = apply_mount_namespace( + command->flags, + context, + params, + runtime, + rootfs, + memory_pressure_path, + needs_sandboxing, + uid, + gid, + bpffs_pidref, + bpffs_socket_fd, + bpffs_errno_pipe, + mountfsd_link, + &error_path); if (r < 0) { *reterr_exit_status = EXIT_NAMESPACE; return log_error_errno(r, "Failed to set up mount namespacing%s%s: %m", @@ -5093,10 +5203,10 @@ int exec_invoke( #if HAVE_SECCOMP uint64_t saved_bset = 0; #endif - uid_t saved_uid = getuid(); - gid_t saved_gid = getgid(); - uid_t uid = UID_INVALID; - gid_t gid = GID_INVALID; + /* saved_uid → where we started from; uid → where we ended up in; outside_uid → where we ended up in, + * but seen from the outside (covers userns mappings) */ + uid_t uid = UID_INVALID, saved_uid = getuid(), outside_uid = UID_INVALID; + gid_t gid = GID_INVALID, saved_gid = getgid(), outside_gid = GID_INVALID; int secure_bits; _cleanup_free_ gid_t *gids = NULL, *gids_after_pam = NULL; int ngids = 0, ngids_after_pam = 0; @@ -5266,7 +5376,9 @@ int exec_invoke( return log_error_errno(errno, "Failed to update environment: %m"); } - if (context->dynamic_user && runtime->dynamic_creds) { + if (exec_context_get_effective_private_users(context, params) == PRIVATE_USERS_MANAGED) + log_debug("Running with a managed user namespace, not initializing UIDs/GIDs."); + else if (context->dynamic_user && runtime->dynamic_creds) { _cleanup_strv_free_ char **suggested_paths = NULL; /* On top of that, make sure we bypass our own NSS module nss-systemd comprehensively for any NSS @@ -5348,20 +5460,22 @@ int exec_invoke( } } - /* Initialize user supplementary groups and get SupplementaryGroups= ones */ - ngids = get_supplementary_groups(context, username, gid, &gids); - if (ngids < 0) { - *exit_status = EXIT_GROUP; - return log_error_errno(ngids, "Failed to determine supplementary groups: %m"); - } + if (exec_context_get_effective_private_users(context, params) != PRIVATE_USERS_MANAGED) { + /* Initialize user supplementary groups and get SupplementaryGroups= ones */ + ngids = get_supplementary_groups(context, username, gid, &gids); + if (ngids < 0) { + *exit_status = EXIT_GROUP; + return log_error_errno(ngids, "Failed to determine supplementary groups: %m"); + } - r = send_user_lookup(params->unit_id, params->user_lookup_fd, uid, gid); - if (r < 0) { - *exit_status = EXIT_USER; - return log_error_errno(r, "Failed to send user credentials to PID1: %m"); - } + r = send_user_lookup(params->unit_id, params->user_lookup_fd, uid, gid); + if (r < 0) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to send user credentials to PID1: %m"); + } - params->user_lookup_fd = safe_close(params->user_lookup_fd); + params->user_lookup_fd = safe_close(params->user_lookup_fd); + } r = acquire_home(context, &pwent_home, &home_buffer); if (r < 0) { @@ -5697,6 +5811,24 @@ int exec_invoke( } } + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL, *nsresource_link = NULL; + if (needs_sandboxing && + exec_context_get_effective_private_users(context, params) == PRIVATE_USERS_MANAGED) { + + /* In managed mode we need to allocate a userns via nsresource, and then assign mounts to + * it. We must do so with our original privileges (since after creating the userns, we might + * simply not have the necessary privs for the IPC calls anymore), hence do this here, ahead + * of time. */ + + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + r = nsresource_connect(&nsresource_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to nsresourced: %m"); + } + needs_mount_namespace = exec_needs_mount_namespace(context, params, runtime); for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) { @@ -5867,6 +5999,20 @@ int exec_invoke( } } + _cleanup_(pinned_resource_done) PinnedResource rootfs = PINNED_RESOURCE_NULL; + _cleanup_free_ char *error_path = NULL; + r = pin_rootfs(context, params, &rootfs, &error_path); + if (r < 0) { + *exit_status = EXIT_NAMESPACE; + return log_error_errno(r, "Failed to open service's root fs%s%s: %m", error_path ? ": " : "", strempty(error_path)); + } + + r = setup_ephemeral(context, runtime, &rootfs, &error_path); + if (r < 0) { + *exit_status = EXIT_NAMESPACE; + return log_error_errno(r, "Failed to make ephemeral copy of service's root fs%s%s: %m", error_path ? ": " : "", strempty(error_path)); + } + /* Load a bunch of libraries we'll possibly need later, before we turn off dlopen() */ (void) dlopen_bpf(); (void) dlopen_cryptsetup(); @@ -5891,16 +6037,26 @@ int exec_invoke( /* The kernel requires /proc/pid/setgroups be set to "deny" prior to writing /proc/pid/gid_map in * unprivileged user namespaces. */ - r = setup_private_users(pu, saved_uid, saved_gid, uid, gid, /* allow_setgroups= */ false); + r = setup_private_users( + nsresource_link, + pu, + saved_uid, + saved_gid, + &uid, + &gid, + &outside_uid, + &outside_gid, + /* allow_setgroups= */ false); /* If it was requested explicitly and we can't set it up, fail early. Otherwise, continue and let * the actual requested operations fail (or silently continue). */ - if (r < 0 && context->private_users != PRIVATE_USERS_NO) { - *exit_status = EXIT_USER; - return log_error_errno(r, "Failed to set up user namespacing for unprivileged user: %m"); - } - if (r < 0) - log_info_errno(r, "Failed to set up user namespacing for unprivileged user, ignoring: %m"); - else { + if (r < 0) { + if (context->private_users != PRIVATE_USERS_NO) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to set up user namespacing for unprivileged user: %m"); + } + + log_notice_errno(r, "Failed to set up user namespacing for unprivileged user, ignoring: %m"); + } else { assert(r > 0); userns_set_up = true; log_debug("Set up unprivileged user namespace"); @@ -5912,6 +6068,7 @@ int exec_invoke( context, params, runtime, + &rootfs, /* delegate= */ false, memory_pressure_path, uid, @@ -5922,6 +6079,7 @@ int exec_invoke( &bpffs_pidref, bpffs_socket_fd, bpffs_errno_pipe, + mountfsd_link, exit_status); if (r < 0) return r; @@ -5971,8 +6129,16 @@ int exec_invoke( } else if (needs_sandboxing && !userns_set_up) { PrivateUsers pu = exec_context_get_effective_private_users(context, params); - r = setup_private_users(pu, saved_uid, saved_gid, uid, gid, - /* allow_setgroups= */ pu == PRIVATE_USERS_FULL); + r = setup_private_users( + nsresource_link, + pu, + saved_uid, + saved_gid, + &uid, + &gid, + &outside_uid, + &outside_gid, + /* allow_setgroups= */ pu == PRIVATE_USERS_FULL); if (r < 0) { *exit_status = EXIT_USER; return log_error_errno(r, "Failed to set up user namespacing: %m"); @@ -5981,11 +6147,25 @@ int exec_invoke( log_debug("Set up privileged user namespace"); } + if (params->user_lookup_fd >= 0) { + /* If we haven't sent the UIDs/GIDs we settled on upstream yet, let's do so now, as we + * finally know our final UID/GID range */ + + r = send_user_lookup(params->unit_id, params->user_lookup_fd, outside_uid, outside_gid); + if (r < 0) { + *exit_status = EXIT_USER; + return log_error_errno(r, "Failed to send user credentials to PID1: %m"); + } + + params->user_lookup_fd = safe_close(params->user_lookup_fd); + } + /* Call setup_delegated_namespaces() the second time to unshare all delegated namespaces. */ r = setup_delegated_namespaces( context, params, runtime, + &rootfs, /* delegate= */ true, memory_pressure_path, uid, @@ -5996,10 +6176,19 @@ int exec_invoke( &bpffs_pidref, bpffs_socket_fd, bpffs_errno_pipe, + mountfsd_link, exit_status); if (r < 0) return r; + /* We are done now with the nsresourced/mountfsd shenanigans, let's close the connections */ + nsresource_link = sd_varlink_unref(nsresource_link); + mountfsd_link = sd_varlink_unref(mountfsd_link); + + /* We don't need the pinned rootfs anymore at this point. Close the fds now, so that they are + * definitely gone before we do our fd rearrangements below. */ + pinned_resource_done(&rootfs); + /* Kill unnecessary process, for the case that e.g. when the bpffs mount point is hidden. */ pidref_done_sigkill_wait(&bpffs_pidref); @@ -6433,7 +6622,6 @@ int exec_invoke( } } #endif - } if (!strv_isempty(context->unset_environment)) { diff --git a/src/core/execute-serialize.c b/src/core/execute-serialize.c index 3ffedf58db..560df95287 100644 --- a/src/core/execute-serialize.c +++ b/src/core/execute-serialize.c @@ -1633,6 +1633,10 @@ static int exec_context_serialize(const ExecContext *c, FILE *f) { if (r < 0) return r; + r = serialize_item_escaped(f, "exec-context-root-mstack", c->root_mstack); + if (r < 0) + return r; + r = serialize_item_format(f, "exec-context-umask", "%04o", c->umask); if (r < 0) return r; @@ -2568,6 +2572,14 @@ static int exec_context_deserialize(ExecContext *c, FILE *f) { if (r < 0) return r; c->root_ephemeral = r; + } else if ((val = startswith(l, "exec-context-root-mstack="))) { + ssize_t k; + char *p; + + k = cunescape(val, 0, &p); + if (k < 0) + return k; + free_and_replace(c->root_mstack, p); } else if ((val = startswith(l, "exec-context-umask="))) { r = parse_mode(val, &c->umask); if (r < 0) diff --git a/src/core/execute.c b/src/core/execute.c index 6d2291cadd..6a7fc65f20 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -263,7 +263,8 @@ bool exec_needs_mount_namespace( assert(context); - if (context->root_image) + if (context->root_image || + context->root_mstack) return true; if (context->root_directory_as_fd) @@ -356,7 +357,7 @@ const char* exec_get_private_notify_socket_path(const ExecContext *context, cons if (!needs_sandboxing) return NULL; - if (!context->root_directory && !context->root_image && !context->root_directory_as_fd) + if (!exec_context_with_rootfs(context)) return NULL; if (!exec_context_get_effective_mount_apivfs(context)) @@ -684,6 +685,7 @@ void exec_context_done(ExecContext *c) { iovec_done(&c->root_hash_sig); c->root_hash_sig_path = mfree(c->root_hash_sig_path); c->root_verity = mfree(c->root_verity); + c->root_mstack = mfree(c->root_mstack); c->tty_path = mfree(c->tty_path); c->syslog_identifier = mfree(c->syslog_identifier); c->user = mfree(c->user); @@ -1203,6 +1205,9 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) { if (c->root_verity) fprintf(f, "%sRootVerity: %s\n", prefix, c->root_verity); + if (c->root_mstack) + fprintf(f, "%sRootMStack: %s\n", prefix, c->root_mstack); + STRV_FOREACH(e, c->environment) fprintf(f, "%sEnvironment: %s\n", prefix, *e); @@ -2058,9 +2063,19 @@ bool exec_context_restrict_filesystems_set(const ExecContext *c) { bool exec_context_with_rootfs(const ExecContext *c) { assert(c); - /* Checks if RootDirectory=, RootImage= or RootDirectoryFileDescriptor= are used */ + /* Checks if RootDirectory=, RootImage=, RootMStack= or RootDirectoryFileDescriptor= are used */ - return !empty_or_root(c->root_directory) || c->root_image || c->root_directory_as_fd; + return !empty_or_root(c->root_directory) || c->root_image || c->root_directory_as_fd || c->root_mstack; +} + +bool exec_context_with_rootfs_strict(const ExecContext *c) { + assert(c); + + /* just like exec_context_with_rootfs(), but doesn't suppress a root directory of "/", i.e. returns + * true in more cases: when a root directory is explicitly configured, even if it's our usual + * root. */ + + return c->root_directory || c->root_image || c->root_directory_as_fd || c->root_mstack; } int exec_context_has_vpicked_extensions(const ExecContext *context) { diff --git a/src/core/execute.h b/src/core/execute.h index 6624c3a71c..c5f3988371 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -199,9 +199,12 @@ typedef struct ExecContext { char **unset_environment; struct rlimit *rlimit[_RLIMIT_MAX]; - char *working_directory, *root_directory, *root_image, *root_verity, *root_hash_path, *root_hash_sig_path; + char *working_directory; + char *root_directory; + char *root_image, *root_verity, *root_hash_path, *root_hash_sig_path; struct iovec root_hash, root_hash_sig; MountOptions *root_image_options; + char *root_mstack; bool root_ephemeral; bool working_directory_missing_ok:1; bool working_directory_home:1; @@ -577,6 +580,7 @@ char** exec_context_get_restrict_filesystems(const ExecContext *c); bool exec_context_restrict_namespaces_set(const ExecContext *c); bool exec_context_restrict_filesystems_set(const ExecContext *c); bool exec_context_with_rootfs(const ExecContext *c); +bool exec_context_with_rootfs_strict(const ExecContext *c); int exec_context_has_vpicked_extensions(const ExecContext *context); diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 5605445c34..60e616a03f 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -11,6 +11,7 @@ {{type}}.RootHashSignature, config_parse_exec_root_hash_sig, 0, offsetof({{type}}, exec_context) {{type}}.RootVerity, config_parse_unit_path_printf, true, offsetof({{type}}, exec_context.root_verity) {{type}}.RootEphemeral, config_parse_bool, 0, offsetof({{type}}, exec_context.root_ephemeral) +{{type}}.RootMStack, config_parse_unit_path_printf, true, offsetof({{type}}, exec_context.root_mstack) {{type}}.ExtensionDirectories, config_parse_namespace_path_strv, 0, offsetof({{type}}, exec_context.extension_directories) {{type}}.ExtensionImages, config_parse_extension_images, 0, offsetof({{type}}, exec_context) {{type}}.ExtensionImagePolicy, config_parse_image_policy, 0, offsetof({{type}}, exec_context.extension_image_policy) diff --git a/src/core/namespace.c b/src/core/namespace.c index 0ed686f9ec..d363ac0973 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -34,6 +34,7 @@ #include "mkdir-label.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace.h" #include "namespace-util.h" #include "nsflags.h" @@ -1360,7 +1361,7 @@ static int mount_private_dev(const MountEntry *m, const NamespaceParameters *p) /* We assume /run/systemd/journal/ is available if not changing root, which isn't entirely accurate * but shouldn't matter, as either way the user would get ENOENT when accessing /dev/log */ - if ((!p->root_image && !p->root_directory && p->root_directory_fd < 0) || p->bind_log_sockets) { + if (!pinned_resource_is_set(p->rootfs) || p->bind_log_sockets) { const char *devlog = strjoina(temporary_mount, "/dev/log"); if (symlink("/run/systemd/journal/dev-log", devlog) < 0) log_debug_errno(errno, @@ -1454,16 +1455,45 @@ static int mount_private_apivfs( assert(entry_path); assert(bind_source); + bool noprivs = false; + + /* First, check if we have enough privileges to mount a new instance. */ + _cleanup_close_ int mount_fd = make_fsmount( + LOG_DEBUG, + /* what= */ fstype, + fstype, + MS_NOSUID|MS_NOEXEC|MS_NODEV, + opts, + /* userns_fd= */ -EBADF); + if (ERRNO_IS_NEG_PRIVILEGE(mount_fd)) + noprivs = true; + else if (ERRNO_IS_NEG_NOT_SUPPORTED(mount_fd)) { + /* Fallback for kernels lacking mount_setattr() */ + + // FIXME: This compatibility code path shall be removed once kernel 5.12 + // becomes the new minimal baseline + + r = create_temporary_mount_point(scope, &temporary_mount); + if (r < 0) + return r; + + r = mount_nofollow_verbose( + LOG_DEBUG, + fstype, + temporary_mount, + fstype, + MS_NOSUID|MS_NOEXEC|MS_NODEV, + opts); + if (ERRNO_IS_NEG_PRIVILEGE(r)) + noprivs = true; + else if (r < 0) + return r; + } else if (mount_fd < 0) + return log_debug_errno(mount_fd, "Failed to make file system mount: %m"); + (void) mkdir_p_label(entry_path, 0755); - /* First, check if we have enough privileges to mount a new instance. Note, a new sysfs instance - * cannot be mounted on an already existing mount. Let's use a temporary place. */ - r = create_temporary_mount_point(scope, &temporary_mount); - if (r < 0) - return r; - - r = mount_nofollow_verbose(LOG_DEBUG, fstype, temporary_mount, fstype, MS_NOSUID|MS_NOEXEC|MS_NODEV, opts); - if (ERRNO_IS_NEG_PRIVILEGE(r)) { + if (noprivs) { /* When we do not have enough privileges to mount a new instance, fall back to use an * existing mount. */ @@ -1482,8 +1512,6 @@ static int mount_private_apivfs( return 1; } - if (r < 0) - return r; /* OK. We have a new mount instance. Let's clear an existing mount and its submounts. */ r = umount_recursive(entry_path, /* flags= */ 0); @@ -1491,9 +1519,16 @@ static int mount_private_apivfs( log_debug_errno(r, "Failed to unmount directories below '%s', ignoring: %m", entry_path); /* Then, move the new mount instance. */ - r = mount_nofollow_verbose(LOG_DEBUG, temporary_mount, entry_path, /* fstype= */ NULL, MS_MOVE, /* options= */ NULL); - if (r < 0) - return r; + if (mount_fd >= 0) { + r = RET_NERRNO(move_mount(mount_fd, "", -EBADF, entry_path, MOVE_MOUNT_F_EMPTY_PATH)); + if (r < 0) + return log_debug_errno(r, "Failed to attach '%s' to '%s': %m", fstype, entry_path); + } else if (temporary_mount) { + r = mount_nofollow_verbose(LOG_DEBUG, temporary_mount, entry_path, /* fstype= */ NULL, MS_MOVE, /* options= */ NULL); + if (r < 0) + return r; + } else + assert_not_reached(); /* We mounted a new instance now. Let's bind mount the children over now. This matters for nspawn * where a bunch of files are overmounted, in particular the boot id. */ @@ -1974,12 +2009,20 @@ static int apply_one_mount( } r = chase(mount_entry_source(m), NULL, CHASE_TRAIL_SLASH|CHASE_TRIGGER_AUTOFS, &chased, NULL); - if (r == -ENOENT && m->ignore) { - log_debug_errno(r, "Path %s does not exist, ignoring.", mount_entry_source(m)); - return 0; - } - if (r < 0) + if (r < 0) { + if (m->ignore) { + if (r == -ENOENT) { + log_debug_errno(r, "Path '%s' does not exist, ignoring.", mount_entry_source(m)); + return 0; + } + if (ERRNO_IS_NEG_PRIVILEGE(r)) { + log_debug_errno(r, "Path '%s' is not accessible, ignoring: %m", mount_entry_source(m)); + return 0; + } + } + return log_debug_errno(r, "Failed to follow symlinks on %s: %m", mount_entry_source(m)); + } log_debug("Followed source symlinks %s %s %s.", mount_entry_source(m), glyph(GLYPH_ARROW_RIGHT), chased); @@ -2497,6 +2540,17 @@ static bool home_read_only( return false; } +static bool namespace_read_only(const NamespaceParameters *p) { + assert(p); + + return root_read_only(p->read_only_paths, + p->protect_system) && + home_read_only(p->read_only_paths, p->inaccessible_paths, p->empty_directories, + p->bind_mounts, p->n_bind_mounts, p->temporary_filesystems, p->n_temporary_filesystems, + p->protect_home) && + strv_isempty(p->read_write_paths); +} + int setup_namespace(const NamespaceParameters *p, char **reterr_path) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; @@ -2518,6 +2572,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { DISSECT_IMAGE_PIN_PARTITION_DEVICES | DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | DISSECT_IMAGE_VERITY_SHARE; + MStackFlags mstack_flags = 0; int r; assert(p); @@ -2529,87 +2584,136 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { bool setup_propagate = !isempty(p->propagate_dir) && !isempty(p->incoming_dir); unsigned long mount_propagation_flag = p->mount_propagation_flag != 0 ? p->mount_propagation_flag : MS_SHARED; - if (p->root_image) { - /* Make the whole image read-only if we can determine that we only access it in a read-only fashion. */ - if (root_read_only(p->read_only_paths, - p->protect_system) && - home_read_only(p->read_only_paths, p->inaccessible_paths, p->empty_directories, - p->bind_mounts, p->n_bind_mounts, p->temporary_filesystems, p->n_temporary_filesystems, - p->protect_home) && - strv_isempty(p->read_write_paths)) - dissect_image_flags |= DISSECT_IMAGE_READ_ONLY; + /* Make the whole image read-only if we can determine that we only access it in a read-only fashion. */ + bool ro = namespace_read_only(p); + if (ro) { + dissect_image_flags |= DISSECT_IMAGE_READ_ONLY; + mstack_flags |= MSTACK_RDONLY; + } - SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); + _cleanup_close_ int _root_mount_fd = -EBADF; + int root_mount_fd = -EBADF; + if (pinned_resource_is_set(p->rootfs)) { + if (p->rootfs->directory_fd >= 0) { + + /* In "managed" mode we need to map from foreign UID/GID space, hence go via mountfsd */ + if (p->private_users == PRIVATE_USERS_MANAGED) { + 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_directory_fd( + p->mountfsd_link, + p->rootfs->directory_fd, + userns_fd, + dissect_image_flags, + &_root_mount_fd); + if (r < 0) + return r; + + root_mount_fd = _root_mount_fd; + } + + /* Try to to clone the directory mount if we have privs to, so that we can apply the + * MS_SLAVE propagation settings right-away. */ + if (root_mount_fd < 0) { + _root_mount_fd = open_tree_attr_with_fallback( + p->rootfs->directory_fd, + "", + OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH|AT_RECURSIVE, + &(struct mount_attr) { + /* We just remounted / as slave, but that didn't affect the detached + * mount that we just mounted, so remount that one as slave recursive + * as well now. */ + .propagation = MS_SLAVE, + }); + if (_root_mount_fd < 0 && !ERRNO_IS_NEG_PRIVILEGE(_root_mount_fd) && _root_mount_fd != -EINVAL) + return log_debug_errno(_root_mount_fd, "Failed to clone specified directory: %m"); + + root_mount_fd = _root_mount_fd; + } + /* If we have only a root fd (and we couldn't make it ours), and we have no path, + * then try to go on with the literal fd */ + if (root_mount_fd < 0 && !p->rootfs->directory) + root_mount_fd = p->rootfs->directory_fd; + } + + if (p->rootfs->image_fd >= 0) { + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_NO_PARTITION_TABLE, p->verity && p->verity->data_path); - /* First check if we have a verity device already open and with a fstype pinned by policy. If it - * cannot be found, then fallback to the slow path (full dissect). */ - r = dissected_image_new_from_existing_verity( - p->root_image, - p->verity, - p->root_image_options, - p->root_image_policy, - /* image_filter= */ NULL, - p->runtime_scope, - dissect_image_flags, - &dissected_image); - if (r < 0 && !ERRNO_IS_NEG_DEVICE_ABSENT(r) && r != -ENOPKG) - return r; - if (r >= 0) - log_debug("Reusing pre-existing verity-protected root image %s", p->root_image); - else { if (p->runtime_scope == RUNTIME_SCOPE_SYSTEM) { /* In system mode we mount directly */ - 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 = dissect_loop_device( - loop_device, + /* First check if we have a verity device already open and with a fstype pinned by policy. If it + * cannot be found, then fallback to the slow path (full dissect). */ + r = dissected_image_new_from_existing_verity( + p->rootfs->image, p->verity, p->root_image_options, p->root_image_policy, /* image_filter= */ NULL, + p->runtime_scope, dissect_image_flags, &dissected_image); - if (r < 0) - return log_debug_errno(r, "Failed to dissect image: %m"); - - r = dissected_image_load_verity_sig_partition( - dissected_image, - loop_device->fd, - p->verity); - if (r < 0) + if (r < 0 && !ERRNO_IS_NEG_DEVICE_ABSENT(r) && r != -ENOPKG) return r; + if (r >= 0) + log_debug("Reusing pre-existing verity-protected root image %s", p->rootfs->image); + else { + r = loop_device_make( + p->rootfs->image_fd, + FLAGS_SET(dissect_image_flags, DISSECT_IMAGE_DEVICE_READ_ONLY) ? O_RDONLY : -1 /* < 0 means take access mode from fd */, + /* offset= */ 0, + /* size= */ UINT64_MAX, + /* 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_guess_verity_roothash( - dissected_image, - p->verity); - if (r < 0) - return r; + r = dissect_loop_device( + loop_device, + p->verity, + p->root_image_options, + p->root_image_policy, + /* image_filter= */ NULL, + dissect_image_flags, + &dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to dissect image: %m"); - r = dissected_image_decrypt( - dissected_image, - /* root= */ NULL, - /* passphrase= */ NULL, - p->verity, - p->root_image_policy, - 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_guess_verity_roothash( + dissected_image, + p->verity); + if (r < 0) + return r; + + r = dissected_image_decrypt( + dissected_image, + /* root= */ NULL, + /* passphrase= */ NULL, + p->verity, + p->root_image_policy, + 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, + r = mountfsd_mount_image_fd( + p->mountfsd_link, + p->rootfs->image_fd, userns_fd, p->root_image_options, p->root_image_policy, @@ -2620,10 +2724,28 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; } } + + if (p->rootfs->mstack_loaded) { + if (p->runtime_scope != RUNTIME_SCOPE_SYSTEM) { + 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 = mstack_open_images( + p->rootfs->mstack_loaded, + p->mountfsd_link, + userns_fd, + p->root_image_policy, + /* image_filter= */ NULL, + mstack_flags); + if (r < 0) + return r; + } } - if (p->root_directory) - root = p->root_directory; + if (p->rootfs && p->rootfs->directory) + root = p->rootfs->directory; else { /* /run/systemd should have been created by PID 1 early on already, but in some cases, like * when running tests (test-execute), it might not have been created yet so let's make sure @@ -2964,21 +3086,36 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { /* Remount / as SLAVE so that nothing now mounted in the namespace * shows up in the parent */ - if (mount(NULL, "/", NULL, MS_SLAVE|MS_REC, NULL) < 0) - return log_debug_errno(errno, "Failed to remount '/' as SLAVE: %m"); + r = mount_nofollow_verbose(LOG_DEBUG, /* what= */ NULL, "/", /* fstype= */ NULL, MS_SLAVE|MS_REC, /* options= */ NULL); + if (r < 0) + return r; - if (p->root_directory_fd >= 0) { + if (root_mount_fd >= 0) { + /* If we have root_mount_fd we have a ready-to-use detached mount. Attach it. */ - if (move_mount(p->root_directory_fd, "", AT_FDCWD, root, MOVE_MOUNT_F_EMPTY_PATH) < 0) + if (move_mount(root_mount_fd, "", AT_FDCWD, root, MOVE_MOUNT_F_EMPTY_PATH) < 0) return log_debug_errno(errno, "Failed to move detached mount to '%s': %m", root); - /* We just remounted / as slave, but that didn't affect the detached mount that we just - * mounted, so remount that one as slave recursive as well now. */ + r = mount_nofollow_verbose(LOG_DEBUG, /* what= */ NULL, root, /* fstype= */ NULL, MS_SLAVE|MS_REC, /* options= */ NULL); + if (r < 0) + return r; - if (mount(NULL, root, NULL, MS_SLAVE|MS_REC, NULL) < 0) - return log_debug_errno(errno, "Failed to remount '%s' as SLAVE: %m", root); + } else if (p->rootfs && p->rootfs->directory) { + + /* If we do not have root_mount_fd, but a directory was specified, then we can use it directly. */ + + /* A root directory is specified. Turn its directory into bind mount, if it isn't one yet. */ + r = path_is_mount_point_full(root, /* root = */ NULL, AT_SYMLINK_FOLLOW); + if (r < 0) + return log_debug_errno(r, "Failed to detect that %s is a mount point or not: %m", root); + if (r == 0) { + r = mount_nofollow_verbose(LOG_DEBUG, root, root, /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); + if (r < 0) + return r; + } + + } else if (dissected_image) { - } else if (p->root_image) { /* A root image is specified, mount it to the right place */ r = dissected_image_mount( dissected_image, @@ -3002,17 +3139,15 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { if (r < 0) return log_debug_errno(r, "Failed to relinquish dissected image: %m"); - } else if (p->root_directory) { + } else if (p->rootfs && p->rootfs->mstack_loaded) { - /* A root directory is specified. Turn its directory into bind mount, if it isn't one yet. */ - r = path_is_mount_point_full(root, /* root= */ NULL, AT_SYMLINK_FOLLOW); + r = mstack_make_mounts(p->rootfs->mstack_loaded, root, mstack_flags); if (r < 0) - return log_debug_errno(r, "Failed to detect that %s is a mount point or not: %m", root); - if (r == 0) { - r = mount_nofollow_verbose(LOG_DEBUG, root, root, NULL, MS_BIND|MS_REC, NULL); - if (r < 0) - return r; - } + return r; + + r = mstack_bind_mounts(p->rootfs->mstack_loaded, root, /* where_fd= */ -EBADF, mstack_flags, /* ret_root_fd= */ NULL); + if (r < 0) + return r; } else { /* Let's mount the main root directory to the root directory to use */ @@ -3022,7 +3157,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { } /* Try to set up the new root directory before mounting anything else there. */ - if (p->root_image || p->root_directory || p->root_directory_fd >= 0) + if (pinned_resource_is_set(p->rootfs)) (void) base_filesystem_create(root, UID_INVALID, GID_INVALID); /* Now make the magic happen */ @@ -3031,8 +3166,8 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { return r; /* MS_MOVE does not work on MS_SHARED so the remount MS_SHARED will be done later */ - r = mount_switch_root(root, /* mount_propagation_flag= */ 0); - if (r == -EINVAL && p->root_directory) { + r = mount_switch_root(root, /* mount_propagation_flag = */ 0); + if (r == -EINVAL && p->rootfs && p->rootfs->directory) { /* If we are using root_directory and we don't have privileges (ie: user manager in a user * namespace) and the root_directory is already a mount point in the parent namespace, * MS_MOVE will fail as we don't have permission to change it (with EINVAL rather than @@ -3041,6 +3176,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { r = mount_nofollow_verbose(LOG_DEBUG, root, root, NULL, MS_BIND|MS_REC, NULL); if (r < 0) return r; + r = mount_switch_root(root, /* mount_propagation_flag= */ 0); } if (r < 0) @@ -4094,6 +4230,7 @@ static const char* const private_users_table[_PRIVATE_USERS_MAX] = { [PRIVATE_USERS_SELF] = "self", [PRIVATE_USERS_IDENTITY] = "identity", [PRIVATE_USERS_FULL] = "full", + [PRIVATE_USERS_MANAGED] = "managed", }; DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(private_users, PrivateUsers, PRIVATE_USERS_SELF); @@ -4104,3 +4241,26 @@ static const char* const private_pids_table[_PRIVATE_PIDS_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(private_pids, PrivatePIDs, PRIVATE_PIDS_YES); + +void pinned_resource_done(PinnedResource *p) { + assert(p); + + p->directory_fd = safe_close(p->directory_fd); + p->directory = mfree(p->directory); + p->image_fd = safe_close(p->image_fd); + p->image = mfree(p->image); + p->mstack_loaded = mstack_free(p->mstack_loaded); + p->mstack = mfree(p->mstack); +} + +bool pinned_resource_is_set(const PinnedResource *p) { + if (!p) + return false; + + return p->directory_fd >= 0 || + p->directory || + p->image_fd >= 0 || + p->image || + p->mstack_loaded || + p->mstack; +} diff --git a/src/core/namespace.h b/src/core/namespace.h index b96e7b4372..318836651c 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -70,6 +70,7 @@ typedef enum PrivateUsers { PRIVATE_USERS_SELF, PRIVATE_USERS_IDENTITY, PRIVATE_USERS_FULL, + PRIVATE_USERS_MANAGED, _PRIVATE_USERS_MAX, _PRIVATE_USERS_INVALID = -EINVAL, } PrivateUsers; @@ -90,6 +91,24 @@ typedef enum PrivatePIDs { _PRIVATE_PIDS_INVALID = -EINVAL, } PrivatePIDs; +typedef struct PinnedResource { + /* Pins a disk image, directory or mstack by file descriptors. The paths are stored too, but they are + * intended to be decoration only, to enhance log messages and should not be load-bearing + * otherwise. */ + int directory_fd; + char *directory; + int image_fd; + char *image; + MStack *mstack_loaded; + char *mstack; +} PinnedResource; + +#define PINNED_RESOURCE_NULL \ + (PinnedResource) { \ + .directory_fd = -EBADF, \ + .image_fd = -EBADF, \ + } + typedef struct BindMount { char *source; char *destination; @@ -127,9 +146,7 @@ typedef struct MountImage { typedef struct NamespaceParameters { RuntimeScope runtime_scope; - int root_directory_fd; - const char *root_directory; - const char *root_image; + const PinnedResource *rootfs; const MountOptions *root_image_options; const ImagePolicy *root_image_policy; @@ -199,10 +216,13 @@ typedef struct NamespaceParameters { PrivateTmp private_tmp; PrivateTmp private_var_tmp; PrivatePIDs private_pids; + PrivateUsers private_users; PidRef *bpffs_pidref; int bpffs_socket_fd; int bpffs_errno_pipe; + + sd_varlink *mountfsd_link; } NamespaceParameters; int setup_namespace(const NamespaceParameters *p, char **reterr_path); @@ -300,3 +320,6 @@ int refresh_extensions_in_namespace( const PidRef *target, const char *hierarchy_env, const NamespaceParameters *p); + +void pinned_resource_done(PinnedResource *p); +bool pinned_resource_is_set(const PinnedResource *p); diff --git a/src/core/service.c b/src/core/service.c index aa133a57db..b9efd9bdb9 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -3019,7 +3019,6 @@ static void service_enter_refresh_extensions(Service *s) { .n_extension_images = s->exec_context.n_extension_images, .extension_directories = s->exec_context.extension_directories, .extension_image_policy = s->exec_context.extension_image_policy, - .root_directory_fd = -EBADF, }; /* Only reload confext, and not sysext as they also typically contain the executable(s) used @@ -5887,8 +5886,9 @@ int service_determine_exec_selinux_label(Service *s, char **ret) { if (s->exec_context.root_image || s->exec_context.n_extension_images > 0 || - !strv_isempty(s->exec_context.extension_directories)) /* We cannot chase paths through images */ - return log_unit_debug_errno(UNIT(s), SYNTHETIC_ERRNO(ENODATA), "Service with RootImage=, ExtensionImages= or ExtensionDirectories= set, cannot determine socket SELinux label before activation, ignoring."); + !strv_isempty(s->exec_context.extension_directories) || + s->exec_context.root_mstack) /* We cannot chase paths through images */ + return log_unit_debug_errno(UNIT(s), SYNTHETIC_ERRNO(ENODATA), "Service with RootImage=, ExtensionImages=, ExtensionDirectories= or RootMStack= set cannot determine socket SELinux label before activation, ignoring."); ExecCommand *c = s->exec_command[SERVICE_EXEC_START]; if (!c) diff --git a/src/core/unit.c b/src/core/unit.c index 0f6ea87756..78c67c9833 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -1266,6 +1266,12 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { return r; } + if (c->root_mstack) { + r = unit_add_mounts_for(u, c->root_mstack, UNIT_DEPENDENCY_FILE, UNIT_MOUNT_WANTS); + if (r < 0) + return r; + } + for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) { if (!u->manager->prefix[dt]) continue; @@ -1322,9 +1328,9 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { return r; } - if (c->root_image) { + if (c->root_image || c->root_mstack) { /* We need to wait for /dev/loopX to appear when doing RootImage=, hence let's add an - * implicit dependency on udev */ + * implicit dependency on udev. (And for RootMStack= we might need it) */ r = unit_add_dependency_by_name(u, UNIT_AFTER, SPECIAL_UDEVD_SERVICE, true, UNIT_DEPENDENCY_FILE); if (r < 0) @@ -4322,6 +4328,9 @@ static int unit_verify_contexts(const Unit *u) { if (ec->pam_name && kc && !IN_SET(kc->kill_mode, KILL_CONTROL_GROUP, KILL_MIXED)) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "Unit has PAM enabled. Kill mode must be set to 'control-group' or 'mixed'. Refusing."); + if ((ec->user || ec->dynamic_user || ec->group || ec->pam_name) && ec->private_users == PRIVATE_USERS_MANAGED) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=/Group=/PAMName=, refusing."); + return 0; } @@ -4461,9 +4470,10 @@ int unit_patch_contexts(Unit *u) { /* Only add these if needed, as they imply that everything else is blocked. */ if (cgroup_context_has_device_policy(cc)) { - if (ec->root_image || ec->mount_images) { + if (ec->root_image || ec->mount_images || ec->root_mstack) { - /* When RootImage= or MountImages= is specified, the following devices are touched. */ + /* When RootImage= or MountImages= is specified, the following devices are + * touched. For RootMStack= there's the possibility the are touched. */ FOREACH_STRING(p, "/dev/loop-control", "/dev/mapper/control") { r = cgroup_context_add_device_allow(cc, p, CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE); if (r < 0) diff --git a/src/core/varlink-execute.c b/src/core/varlink-execute.c index 35ea1b1330..e6efd59895 100644 --- a/src/core/varlink-execute.c +++ b/src/core/varlink-execute.c @@ -794,6 +794,7 @@ int unit_exec_context_build_json(sd_json_variant **ret, const char *name, void * JSON_BUILD_PAIR_CALLBACK_NON_NULL("WorkingDirectory", working_directory_build_json, c), JSON_BUILD_PAIR_STRING_NON_EMPTY("RootDirectory", c->root_directory), JSON_BUILD_PAIR_STRING_NON_EMPTY("RootImage", c->root_image), + JSON_BUILD_PAIR_STRING_NON_EMPTY("RootMStack", c->root_mstack), JSON_BUILD_PAIR_CALLBACK_NON_NULL("RootImageOptions", root_image_options_build_json, c->root_image_options), SD_JSON_BUILD_PAIR_BOOLEAN("RootEphemeral", c->root_ephemeral), JSON_BUILD_PAIR_BASE64_NON_EMPTY("RootHash", c->root_hash.iov_base, c->root_hash.iov_len), diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index a699c007bf..33ddb53d0a 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2247,12 +2247,16 @@ static int run(int argc, char *argv[]) { /* Don't run things in private userns, if the mount shall be attached to the host * or if we're copying from/to the host. */ if (!IN_SET(arg_action, ACTION_MOUNT, ACTION_WITH, ACTION_COPY_FROM, ACTION_COPY_TO)) { - userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users by default */ + userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); /* 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( + /* vl= */ NULL, arg_image, userns_fd, /* options= */ NULL, @@ -2261,7 +2265,7 @@ static int run(int argc, char *argv[]) { arg_flags, &m); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount image via mountfsd: %m"); } } diff --git a/src/import/export-tar.c b/src/import/export-tar.c index e945abe8c3..22f731de57 100644 --- a/src/import/export-tar.c +++ b/src/import/export-tar.c @@ -363,9 +363,14 @@ int tar_export_start( return log_error_errno(r, "Failed to open '%s': %m", p); _cleanup_close_ int mapped_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, e->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mapped_fd); + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + directory_fd, + e->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &mapped_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); /* Drop O_PATH */ e->tree_fd = fd_reopen(mapped_fd, O_DIRECTORY|O_CLOEXEC); diff --git a/src/import/import-common.c b/src/import/import-common.c index 0c448355fd..0a5144f94e 100644 --- a/src/import/import-common.c +++ b/src/import/import-common.c @@ -375,7 +375,10 @@ int import_make_foreign_userns(int *userns_fd) { if (*userns_fd >= 0) return 0; - *userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); /* allocate 64K users */ + *userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); /* allocate 64K users */ if (*userns_fd < 0) return log_error_errno(*userns_fd, "Failed to allocate transient user namespace: %m"); diff --git a/src/import/import-common.h b/src/import/import-common.h index b568906a8a..6b10f8c29d 100644 --- a/src/import/import-common.h +++ b/src/import/import-common.h @@ -23,13 +23,14 @@ typedef enum ImportFlags { IMPORT_PULL_ROOTHASH_SIGNATURE = 1 << 11, /* only for raw: download .roothash.p7s file for verity */ IMPORT_PULL_VERITY = 1 << 12, /* only for raw: download .verity file for verity */ - /* The supported flags for the tar and the raw importing */ + /* The supported flags for the tar and raw importing */ IMPORT_FLAGS_MASK_TAR = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_DIRECT|IMPORT_SYNC|IMPORT_FOREIGN_UID, IMPORT_FLAGS_MASK_RAW = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_CONVERT_QCOW2|IMPORT_DIRECT|IMPORT_SYNC, - /* The supported flags for the tar and the raw pulling */ + /* The supported flags for the tar, raw, oci pulling */ IMPORT_PULL_FLAGS_MASK_TAR = IMPORT_FLAGS_MASK_TAR|IMPORT_PULL_KEEP_DOWNLOAD|IMPORT_PULL_SETTINGS, IMPORT_PULL_FLAGS_MASK_RAW = IMPORT_FLAGS_MASK_RAW|IMPORT_PULL_KEEP_DOWNLOAD|IMPORT_PULL_SETTINGS|IMPORT_PULL_ROOTHASH|IMPORT_PULL_ROOTHASH_SIGNATURE|IMPORT_PULL_VERITY, + IMPORT_PULL_FLAGS_MASK_OCI = IMPORT_FORCE|IMPORT_READ_ONLY|IMPORT_BTRFS_SUBVOL|IMPORT_BTRFS_QUOTA|IMPORT_SYNC|IMPORT_FOREIGN_UID|IMPORT_PULL_SETTINGS, _IMPORT_FLAGS_INVALID = -EINVAL, } ImportFlags; diff --git a/src/import/import-tar.c b/src/import/import-tar.c index ed96e27222..5e74de896e 100644 --- a/src/import/import-tar.c +++ b/src/import/import-tar.c @@ -4,6 +4,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "btrfs-util.h" @@ -256,14 +257,29 @@ static int tar_import_fork_tar(TarImport *i) { if (r < 0) return r; - _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(d, MODE_INVALID, /* flags= */ 0, &directory_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); if (r < 0) - return r; + return log_error_errno(r, "Failed to connect to mountfsd: %m"); - r = mountfsd_mount_directory_fd(directory_fd, i->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &i->tree_fd); + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory( + mountfsd_link, + d, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); + + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + i->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &i->tree_fd); + if (r < 0) + return log_error_errno(r, "Failed mount directory via mountfsd: %m"); } else { if (i->flags & IMPORT_BTRFS_SUBVOL) r = btrfs_subvol_make_fallback(AT_FDCWD, d, 0755); diff --git a/src/import/importctl.c b/src/import/importctl.c index eddddb86d1..c2ddd08f27 100644 --- a/src/import/importctl.c +++ b/src/import/importctl.c @@ -19,6 +19,7 @@ #include "import-util.h" #include "log.h" #include "main-func.h" +#include "oci-util.h" #include "os-util.h" #include "pager.h" #include "parse-argument.h" @@ -769,6 +770,61 @@ static int pull_raw(int argc, char *argv[], void *userdata) { return transfer_image_common(bus, m); } +static int pull_oci(int argc, char *argv[], void *userdata) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_free_ char *l = NULL; + const char *local, *remote; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + r = settle_image_class(); + if (r < 0) + return r; + + remote = argv[1]; + _cleanup_free_ char *image = NULL; + r = oci_ref_parse(remote, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL); + if (r == -EINVAL) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Ref '%s' is not valid.", remote); + if (r < 0) + return log_error_errno(r, "Failed to determine if ref '%s' is valid.", remote); + + if (argc >= 3) + local = argv[2]; + else { + r = path_extract_filename(image, &l); + if (r < 0) + return log_error_errno(r, "Failed to get final component of reference: %m"); + + local = l; + } + + local = empty_or_dash_to_null(local); + + if (local) { + if (!image_name_is_valid(local)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Local name %s is not a suitable image name.", + local); + } + + r = bus_message_new_method_call(bus, &m, bus_import_mgr, "PullOci"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append( + m, + "ssst", + remote, + local, + image_class_to_string(arg_image_class), + (uint64_t) arg_import_flags & (IMPORT_FORCE|IMPORT_READ_ONLY)); + if (r < 0) + return bus_log_create_error(r); + + return transfer_image_common(bus, m); +} + static int list_transfers(int argc, char *argv[], void *userdata) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; @@ -1007,6 +1063,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%3$sCommands:%4$s\n" " pull-tar URL [NAME] Download a TAR container image\n" " pull-raw URL [NAME] Download a RAW container or VM image\n" + " pull-oci REF [NAME] Download an OCI container image\n" " import-tar FILE [NAME] Import a local TAR container image\n" " import-raw FILE [NAME] Import a local RAW container or VM image\n" " import-fs DIRECTORY [NAME] Import a local directory container image\n" @@ -1245,6 +1302,7 @@ static int importctl_main(int argc, char *argv[], sd_bus *bus) { { "export-tar", 2, 3, 0, export_tar }, { "export-raw", 2, 3, 0, export_raw }, { "pull-tar", 2, 3, 0, pull_tar }, + { "pull-oci", 2, 3, 0, pull_oci }, { "pull-raw", 2, 3, 0, pull_raw }, { "list-transfers", VERB_ANY, 1, VERB_DEFAULT, list_transfers }, { "cancel-transfer", 2, VERB_ANY, 0, cancel_transfer }, diff --git a/src/import/importd.c b/src/import/importd.c index 280aa88d46..d3363d446c 100644 --- a/src/import/importd.c +++ b/src/import/importd.c @@ -29,6 +29,7 @@ #include "json-util.h" #include "main-func.h" #include "notify-recv.h" +#include "oci-util.h" #include "os-util.h" #include "parse-util.h" #include "path-lookup.h" @@ -58,6 +59,7 @@ typedef enum TransferType { TRANSFER_EXPORT_RAW, TRANSFER_PULL_TAR, TRANSFER_PULL_RAW, + TRANSFER_PULL_OCI, _TRANSFER_TYPE_MAX, _TRANSFER_TYPE_INVALID = -EINVAL, } TransferType; @@ -127,6 +129,7 @@ static const char* const transfer_type_table[_TRANSFER_TYPE_MAX] = { [TRANSFER_EXPORT_RAW] = "export-raw", [TRANSFER_PULL_TAR] = "pull-tar", [TRANSFER_PULL_RAW] = "pull-raw", + [TRANSFER_PULL_OCI] = "pull-oci", }; DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(transfer_type, TransferType); @@ -497,6 +500,7 @@ static int transfer_start(Transfer *t) { case TRANSFER_PULL_TAR: case TRANSFER_PULL_RAW: + case TRANSFER_PULL_OCI: cmd[k++] = SYSTEMD_PULL_PATH; break; @@ -518,6 +522,10 @@ static int transfer_start(Transfer *t) { cmd[k++] = "raw"; break; + case TRANSFER_PULL_OCI: + cmd[k++] = "oci"; + break; + case TRANSFER_IMPORT_FS: cmd[k++] = "run"; break; @@ -1053,7 +1061,7 @@ static int method_export_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_ return 1; } -static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_error *error) { +static int method_pull_tar_or_raw_or_oci(sd_bus_message *msg, void *userdata, sd_bus_error *error) { _cleanup_(transfer_unrefp) Transfer *t = NULL; ImageClass class = _IMAGE_CLASS_INVALID; const char *remote, *local, *verify; @@ -1078,7 +1086,24 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return 1; /* Will call us back */ } - if (endswith(sd_bus_message_get_member(msg), "Ex")) { + if (streq(sd_bus_message_get_member(msg), "PullOci")) { + const char *sclass; + + r = sd_bus_message_read(msg, "ssst", &remote, &local, &sclass, &flags); + if (r < 0) + return r; + + class = image_class_from_string(sclass); + if (class < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Image class '%s' not known", sclass); + + if (flags & ~(IMPORT_FORCE|IMPORT_READ_ONLY)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Flags 0x%" PRIx64 " invalid", flags); + + verify = NULL; + } else if (endswith(sd_bus_message_get_member(msg), "Ex")) { const char *sclass; r = sd_bus_message_read(msg, "sssst", &remote, &local, &sclass, &verify, &flags); @@ -1106,9 +1131,21 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er SET_FLAG(flags, IMPORT_FORCE, force); } - if (!http_url_is_valid(remote) && !file_url_is_valid(remote)) + type = startswith(sd_bus_message_get_member(msg), "PullTar") ? TRANSFER_PULL_TAR : + startswith(sd_bus_message_get_member(msg), "PullRaw") ? TRANSFER_PULL_RAW : + streq(sd_bus_message_get_member(msg), "PullOci") ? TRANSFER_PULL_OCI : _TRANSFER_TYPE_INVALID; + assert(type >= 0); + + if (type == TRANSFER_PULL_OCI) { + r = oci_ref_valid(remote); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Reference '%s' is invalid", remote); + } else if (!http_url_is_valid(remote) && !file_url_is_valid(remote)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "URL %s is invalid", remote); + "URL '%s' is invalid", remote); if (isempty(local)) local = NULL; @@ -1116,13 +1153,16 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Local image name %s is invalid", local); - if (isempty(verify)) + if (type == TRANSFER_PULL_OCI) + v = _IMPORT_VERIFY_INVALID; + else if (isempty(verify)) v = IMPORT_VERIFY_SIGNATURE; - else + else { v = import_verify_from_string(verify); - if (v < 0) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, - "Unknown verification mode %s", verify); + if (v < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Unknown verification mode %s", verify); + } if (class == IMAGE_MACHINE) { r = image_setup_pool(m->runtime_scope, class, m->use_btrfs_subvol, m->use_btrfs_quota); @@ -1130,9 +1170,6 @@ static int method_pull_tar_or_raw(sd_bus_message *msg, void *userdata, sd_bus_er return sd_bus_error_set_errnof(error, r, "Failed to set up machine pool: %m"); } - type = startswith(sd_bus_message_get_member(msg), "PullTar") ? - TRANSFER_PULL_TAR : TRANSFER_PULL_RAW; - if (manager_find(m, type, remote)) return sd_bus_error_setf(error, BUS_ERROR_TRANSFER_IN_PROGRESS, "Transfer for %s already in progress.", remote); @@ -1613,52 +1650,53 @@ static const sd_bus_vtable manager_vtable[] = { SD_BUS_PARAM(transfer_path), method_export_tar_or_raw, SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullTar", - "sssb", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(force), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullTarEx", - "sssst", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(class) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(flags), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullRaw", - "sssb", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(force), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), - SD_BUS_METHOD_WITH_NAMES("PullRawEx", - "sssst", - SD_BUS_PARAM(url) - SD_BUS_PARAM(local_name) - SD_BUS_PARAM(class) - SD_BUS_PARAM(verify_mode) - SD_BUS_PARAM(flags), - "uo", - SD_BUS_PARAM(transfer_id) - SD_BUS_PARAM(transfer_path), - method_pull_tar_or_raw, - SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullTar", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", verify_mode, + "b", force), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullTarEx", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", class, + "s", verify_mode, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullRaw", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", verify_mode, + "b", force), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullRawEx", + SD_BUS_ARGS("s", url, + "s", local_name, + "s", class, + "s", verify_mode, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("PullOci", + SD_BUS_ARGS("s", ref, + "s", local_name, + "s", class, + "t", flags), + SD_BUS_RESULT("u", transfer_id, + "o", transfer_path), + method_pull_tar_or_raw_or_oci, + SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD_WITH_NAMES("ListTransfers", NULL,, "a(usssdo)", @@ -1864,7 +1902,13 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl if (r != 0) return r; - if (!http_url_is_valid(p.remote) && !file_url_is_valid(p.remote)) + if (p.type == IMPORT_OCI) { + r = oci_ref_valid(p.remote); + if (r < 0) + return r; + if (r == 0) + return sd_varlink_error_invalid_parameter_name(link, "remote"); + } else if (!http_url_is_valid(p.remote) && !file_url_is_valid(p.remote)) return sd_varlink_error_invalid_parameter_name(link, "remote"); if (p.local && !image_name_is_valid(p.local)) @@ -1874,8 +1918,8 @@ static int vl_method_pull(sd_varlink *link, sd_json_variant *parameters, sd_varl TransferType tt = p.type == IMPORT_TAR ? TRANSFER_PULL_TAR : - p.type == IMPORT_RAW ? TRANSFER_PULL_RAW : _TRANSFER_TYPE_INVALID; - + p.type == IMPORT_RAW ? TRANSFER_PULL_RAW : + p.type == IMPORT_OCI ? TRANSFER_PULL_OCI : _TRANSFER_TYPE_INVALID; assert(tt >= 0); if (manager_find(m, tt, p.remote)) diff --git a/src/import/meson.build b/src/import/meson.build index 46333fb15f..c0589ddd4e 100644 --- a/src/import/meson.build +++ b/src/import/meson.build @@ -18,8 +18,10 @@ executables += [ 'dbus' : true, 'sources' : files( 'importd.c', + 'oci-util.c', ), 'extract' : files( + 'oci-util.c', 'import-common.c', 'import-compress.c', 'qcow2-util.c', @@ -30,12 +32,13 @@ executables += [ 'name' : 'systemd-pull', 'public' : true, 'sources' : files( + 'curl-util.c', 'pull.c', + 'pull-common.c', + 'pull-job.c', + 'pull-oci.c', 'pull-raw.c', 'pull-tar.c', - 'pull-job.c', - 'pull-common.c', - 'curl-util.c', ), 'objects' : ['systemd-importd'], 'dependencies' : common_deps + [ @@ -77,6 +80,8 @@ executables += [ 'name' : 'importctl', 'public' : true, 'sources' : files('importctl.c'), + 'objects': ['systemd-importd'], + 'dependencies' : common_deps, }, generator_template + { 'name' : 'systemd-import-generator', @@ -92,7 +97,11 @@ executables += [ 'dependencies' : common_deps, 'type' : 'manual', }, - + test_template + { + 'sources' : files('test-oci-util.c'), + 'objects': ['systemd-importd'], + 'dependencies' : common_deps, + }, ] install_data('org.freedesktop.import1.conf', @@ -108,3 +117,28 @@ install_data('org.freedesktop.import1.policy', install_data('import-pubring.pgp', install_dir : libexecdir) # TODO: shouldn't this be in pkgdatadir? + +oci_registry_files = [ + 'registry.docker.io.oci-registry', + 'registry.fedora.oci-registry', +] +oci_registry_symlinks = [ + [ 'default.oci-registry', 'registry.' + get_option('default-oci-registry') + '.oci-registry' ], + [ 'image.alpine.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.archlinux.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.debian.oci-registry', 'registry.docker.io.oci-registry' ], + [ 'image.fedora-minimal.oci-registry', 'registry.fedora.oci-registry' ], + [ 'image.fedora.oci-registry', 'registry.fedora.oci-registry' ], + [ 'image.ubuntu.oci-registry', 'registry.docker.io.oci-registry' ], +] + +foreach file : oci_registry_files + install_data('oci-registry' / file, + install_dir : libexecdir / 'oci-registry') +endforeach + +foreach tuple: oci_registry_symlinks + install_symlink(tuple[0], + pointing_to : tuple[1], + install_dir : libexecdir / 'oci-registry') +endforeach diff --git a/src/import/oci-registry/registry.docker.io.oci-registry b/src/import/oci-registry/registry.docker.io.oci-registry new file mode 100644 index 0000000000..a36b54eb90 --- /dev/null +++ b/src/import/oci-registry/registry.docker.io.oci-registry @@ -0,0 +1,4 @@ +{ + "shortImagePrefix" : "library/", + "overrideRegistry" : "registry-1.docker.io" +} diff --git a/src/import/oci-registry/registry.fedora.oci-registry b/src/import/oci-registry/registry.fedora.oci-registry new file mode 100644 index 0000000000..e416bf2853 --- /dev/null +++ b/src/import/oci-registry/registry.fedora.oci-registry @@ -0,0 +1,3 @@ +{ + "defaultRegistry" : "registry.fedoraproject.org" +} diff --git a/src/import/oci-util.c b/src/import/oci-util.c new file mode 100644 index 0000000000..37a617b28c --- /dev/null +++ b/src/import/oci-util.c @@ -0,0 +1,401 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-json.h" + +#include "alloc-util.h" +#include "architecture.h" +#include "constants.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "log.h" +#include "oci-util.h" +#include "parse-util.h" +#include "string-table.h" +#include "string-util.h" + +bool oci_image_is_valid(const char *n) { + bool slash = true; + + /* The OCI spec suggests validating this regex: + * + * [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)* + * + * We implement a generalization of this, i.e. do not insist on the single ".", "_", "__", "-", "+" + * separator, but allow any number of them. And we refuse leading dots, since if used in the fs this + * would make the files hidden, and we probably don't want that. + */ + + for (const char *p = n; *p; p++) { + if (*p == '/') { + if (slash) + return false; + + slash = true; + continue; + } + + if (!strchr(slash ? LOWERCASE_LETTERS DIGITS "_-+" : + "." LOWERCASE_LETTERS DIGITS "_-+", *p)) + return false; + + slash = false; + } + + return !slash; +} + +int oci_registry_is_valid(const char *n) { + int r; + + if (!n) + return false; + + const char *colon = strchr(n, ':'); + if (!colon) + return dns_name_is_valid(n); + + _cleanup_free_ char *s = strndup(n, colon - n); + if (!s) + return -ENOMEM; + + r = dns_name_is_valid(s); + if (r <= 0) + return r; + + uint16_t port; + return safe_atou16(s, &port) >= 0 && port != 0; +} + +bool oci_tag_is_valid(const char *n) { + if (!n) + return false; + + /* As per https://github.com/opencontainers/distribution-spec/blob/main/spec.md, accept the following regex: + * + * [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} + */ + + if (!strchr(LETTERS DIGITS "_", n[0])) + return false; + + size_t l = strspn(n + 1, LETTERS DIGITS "._-"); + if (l > 126) + return false; + if (n[1+l] != 0) + return false; + + return true; +} + +int oci_ref_parse( + const char *ref, + char **ret_registry, + char **ret_image, + char **ret_tag) { + + int r; + + assert(ref); + + _cleanup_free_ char *without_tag = NULL, *tag = NULL; + const char *t = strrchr(ref, ':'); + if (t) { + tag = strdup(t + 1); + if (!tag) + return -ENOMEM; + if (!oci_tag_is_valid(tag)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI tag specification '%s' is not valid.", tag); + + without_tag = strndup(ref, t - ref); + if (!without_tag) + return -ENOMEM; + + ref = without_tag; + } + + _cleanup_free_ char *image = NULL, *registry = NULL; + t = strchr(ref, '/'); + if (t) { + registry = strndup(ref, t - ref); + if (!registry) + return -ENOMEM; + + r = oci_registry_is_valid(registry); + if (r < 0) + return r; + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI registry specification '%s' is not valid.", registry); + + image = strdup(t + 1); + } else + image = strdup(ref); + if (!image) + return -ENOMEM; + if (!oci_image_is_valid(image)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "OCI image specification '%s' is not valid.", registry); + + if (ret_registry) + *ret_registry = TAKE_PTR(registry); + if (ret_image) + *ret_image = TAKE_PTR(image); + if (ret_tag) + *ret_tag = TAKE_PTR(tag); + + return 0; +} + +int oci_ref_normalize(char **protocol, char **registry, char **image, char **tag) { + int r; + + assert(protocol); + assert(registry); + assert(image && *image); + assert(tag); + + /* OCI container reference are supposed to have the form /:. Except that it's + * all super messy, and for some registries the server name differs from the name people use in the + * references, and there are special rules for "short" container names (i.e. those which do not + * contain a "/"), and more. To deal with this, we devise a relatively simple scheme, to normalize + * such names. Specifically: + * + * If a registry is specified we look for + * /usr/lib/systemd/oci-registry/registry..oci-registry for registry-specific rules, to + * enforce on the reference. If no registry is specified, we look for an + * /usr/lib/systemd/oci-registry/image..oci-registry file, which contains image-specific rules + * instead. If this is not found we load /usr/lib/systemd/oci-registry/default.oci-registry + * instead. The files are encoded in JSON. + * + * The rules we apply are relatively simple: + * + * • defaultProtocol controls which protocol to use if none is known. This should always be https + * (since OCI images are authenticated purely via HTTPS), but for testing purposes "file" might be + * useful too. + * + * • overrideRegistry encodes which registry server to actually use, overriding what might have been + * specified. + * + * • overrideImage encodes which image name to actually use, overriding what might have been specified. + * + * • shortImagePrefix encodes a name prefix to prepend to "short" container names. This has no effect + * if overrideImage is set too. + * + * • defaultTag contains a tag to use as default, if none is specified. If not configured this + * defaults to "latest". + */ + + _cleanup_free_ char *fn = NULL; + if (*registry) { + /* If a registry is specified, we'll always respect it, and use it as only search key */ + _cleanup_free_ char *e = urlescape(*registry); + if (!e) + return -ENOMEM; + + fn = strjoin("registry.", e, ".oci-registry"); + } else { + /* If no registry is specified, let's go by image name */ + _cleanup_free_ char *e = urlescape(*image); + if (!e) + return -ENOMEM; + + fn = strjoin("image.", e, ".oci-registry"); + } + if (!fn) + return -ENOMEM; + + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *path = NULL; + r = search_and_fopen_nulstr(fn, "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path); + if (r == -ENOENT) + r = search_and_fopen_nulstr("default.oci-registry", "re", /* root= */ NULL, CONF_PATHS_NULSTR("systemd/oci-registry"), &f, &path); + if (r < 0 && r != -ENOENT) + return log_debug_errno(r, "Failed to find suitable OCI registry file: %m"); + + /* if ENOENT is seen, we use the defaults below! */ + + struct { + const char *default_protocol; + const char *override_registry; + const char *default_registry; + const char *override_image; + const char *short_image_prefix; + const char *default_tag; + } data = { + .default_protocol = "https", + .default_tag = "latest", + }; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + if (f) { + unsigned line = 0, column = 0; + r = sd_json_parse_file(f, path, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_debug_errno(r, "Parse failure at %s:%u:%u: %m", path, line, column); + + static const sd_json_dispatch_field table[] = { + { "defaultProtocol", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_protocol), 0 }, + { "overrideRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_registry), 0 }, + { "defaultRegistry", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_registry), 0 }, + { "overrideImage", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, override_image), 0 }, + { "shortImagePrefix", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, short_image_prefix), 0 }, + { "defaultTag", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(data, default_tag), 0 }, + {}, + }; + r = sd_json_dispatch(v, table, SD_JSON_ALLOW_EXTENSIONS, &data); + if (r < 0) + return r; + } + + _cleanup_free_ char *new_protocol = NULL; + if (data.default_protocol && isempty(*protocol)) { + new_protocol = strdup(data.default_protocol); + if (!new_protocol) + return -ENOMEM; + } + + _cleanup_free_ char *new_registry = NULL; + if (data.override_registry) { + if (!isempty(*registry)) + log_debug("Overriding registry to '%s' (was '%s') based on OCI registry database.", data.override_registry, *registry); + + new_registry = strdup(data.override_registry); + if (!new_registry) + return -ENOMEM; + } else if (data.default_registry && isempty(*registry)) { + new_registry = strdup(data.default_registry); + if (!new_registry) + return -ENOMEM; + } + + _cleanup_free_ char *new_image = NULL; + if (data.override_image) { + log_debug("Overriding image to '%s' (was '%s') based on OCI registry database.", data.override_registry, *image); + + new_image = strdup(data.override_image); + if (!new_image) + return -ENOMEM; + } else if (data.short_image_prefix && !strchr(*image, '/')) { + new_image = strjoin(data.short_image_prefix, *image); + if (!new_image) + return -ENOMEM; + } + + _cleanup_free_ char *new_tag = NULL; + if (data.default_tag && isempty(*tag)) { + new_tag = strdup(data.default_tag); + if (!new_tag) + return -ENOMEM; + } + + if (!new_registry && isempty(*registry)) + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "No suitable registry found."); + + if (new_protocol) + free_and_replace(*protocol, new_protocol); + if (new_registry) + free_and_replace(*registry, new_registry); + if (new_image) + free_and_replace(*image, new_image); + if (new_tag) + free_and_replace(*tag, new_tag); + + return 0; +} + +char* oci_digest_string(const struct iovec *iovec) { + assert(iovec); + + _cleanup_free_ char *h = hexmem(iovec->iov_base, iovec->iov_len); + if (!h) + return NULL; + + return strjoin("sha256:", h); +} + +int oci_make_manifest_url( + const char *protocol, + const char *repository, + const char *image, + const char *tag, + char **ret) { + + assert(protocol); + assert(repository); + assert(image); + assert(tag); + assert(ret); + + _cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/manifests/", tag); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +int oci_make_blob_url( + const char *protocol, + const char *repository, + const char *image, + const struct iovec *digest, + char **ret) { + + assert(protocol); + assert(repository); + assert(image); + assert(digest); + assert(ret); + + _cleanup_free_ char *d = oci_digest_string(digest); + if (!d) + return -ENOMEM; + + _cleanup_free_ char *url = strjoin(protocol, "://", repository, "/v2/", image, "/blobs/", d); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +/* OCI uses the Go architecture IDs */ +static const char *const go_arch_table[_ARCHITECTURE_MAX] = { + [ARCHITECTURE_ARM] = "arm", + [ARCHITECTURE_ARM64] = "arm64", + [ARCHITECTURE_MIPS] = "mips", + [ARCHITECTURE_MIPS64] = "mips64", + [ARCHITECTURE_MIPS64_LE] = "mips64le", + [ARCHITECTURE_MIPS_LE] = "mipsle", + [ARCHITECTURE_PPC64] = "ppc64", + [ARCHITECTURE_PPC64_LE] = "ppc64le", + [ARCHITECTURE_S390X] = "s390x", + [ARCHITECTURE_X86] = "386", + [ARCHITECTURE_X86_64] = "amd64", +}; + +DEFINE_STRING_TABLE_LOOKUP_FROM_STRING(go_arch, Architecture); + +char* urlescape(const char *s) { + size_t l = strlen_ptr(s); + + _cleanup_free_ char *t = new(char, l * 3 + 1); + if (!t) + return NULL; + + char *p = t; + for (; s && *s; s++) { + if (strchr(LETTERS DIGITS ".-_", *s)) + *(p++) = *s; + else { + *(p++) = '%'; + *(p++) = hexchar((uint8_t) *s >> 4); + *(p++) = hexchar((uint8_t) *s & 15); + } + } + + *p = 0; + return TAKE_PTR(t); +} diff --git a/src/import/oci-util.h b/src/import/oci-util.h new file mode 100644 index 0000000000..72b6370057 --- /dev/null +++ b/src/import/oci-util.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "basic-forward.h" + +bool oci_image_is_valid(const char *n); +int oci_registry_is_valid(const char *n); +bool oci_tag_is_valid(const char *n); + +int oci_ref_parse(const char *ref, char **ret_registry, char **ret_image, char **ret_tag); + +static inline int oci_ref_valid(const char *ref) { + int r; + r = oci_ref_parse(ref, NULL, NULL, NULL); + if (r == -EINVAL) + return false; + if (r < 0) + return r; + return true; +} + +int oci_ref_normalize(char **protocol, char **registry, char **image, char **tag); + +char* oci_digest_string(const struct iovec *iovec); + +int oci_make_manifest_url(const char *protocol, const char *repository, const char *image, const char *tag, char **ret); +int oci_make_blob_url(const char *protocol, const char *repository, const char *image, const struct iovec *digest, char **ret); + +Architecture go_arch_from_string(const char *s); + +char* urlescape(const char *s); diff --git a/src/import/org.freedesktop.import1.conf b/src/import/org.freedesktop.import1.conf index f99ec56c79..d8b82388d5 100644 --- a/src/import/org.freedesktop.import1.conf +++ b/src/import/org.freedesktop.import1.conf @@ -106,6 +106,10 @@ send_interface="org.freedesktop.import1.Manager" send_member="PullRawEx"/> + + diff --git a/src/import/pull-job.c b/src/import/pull-job.c index e495b56eab..dcdb1fe8fa 100644 --- a/src/import/pull-job.c +++ b/src/import/pull-job.c @@ -30,6 +30,10 @@ static int http_status_etag_exists(CURLcode status) { return status == 304; } +static int http_status_need_authentication(CURLcode status) { + return status == 401; +} + void pull_job_close_disk_fd(PullJob *j) { if (!j) return; @@ -60,10 +64,22 @@ PullJob* pull_job_unref(PullJob *j) { iovec_done(&j->payload); iovec_done(&j->checksum); iovec_done(&j->expected_checksum); + free(j->content_type); + + if (j->free_userdata) + j->free_userdata(j->userdata); + free(j->description); + free(j->authentication_challenge); return mfree(j); } +static const char* pull_job_description(PullJob *j) { + assert(j); + + return j->description ?: j->url; +} + static void pull_job_finish(PullJob *j, int ret) { assert(j); @@ -73,7 +89,7 @@ static void pull_job_finish(PullJob *j, int ret) { if (ret == 0) { j->state = PULL_JOB_DONE; j->progress_percent = 100; - log_info("Download of %s complete.", j->url); + log_info("Download of %s complete.", pull_job_description(j)); } else { j->state = PULL_JOB_FAILED; j->error = ret; @@ -83,15 +99,19 @@ static void pull_job_finish(PullJob *j, int ret) { j->on_finished(j); } -static int pull_job_restart(PullJob *j, const char *new_url) { +int pull_job_restart(PullJob *j, const char *new_url) { int r; assert(j); - assert(new_url); - r = free_and_strdup(&j->url, new_url); - if (r < 0) - return r; + /* If an URL is specified we retry the same request, just towards a different URL. If the URL is NULL + * then we'll fire the same request again (which is useful if some parameters have been changed) */ + + if (new_url) { + r = free_and_strdup(&j->url, new_url); + if (r < 0) + return r; + } j->state = PULL_JOB_INIT; j->error = 0; @@ -103,15 +123,17 @@ static int pull_job_restart(PullJob *j, const char *new_url) { j->etag_exists = false; j->mtime = 0; iovec_done(&j->checksum); - iovec_done(&j->expected_checksum); - j->expected_content_length = UINT64_MAX; + j->content_type = mfree(j->content_type); + + if (new_url) { + /* Reset expectations if the URL changes */ + iovec_done(&j->expected_checksum); + j->expected_content_length = UINT64_MAX; + } curl_glue_remove_and_free(j->glue, j->curl); j->curl = NULL; - curl_slist_free_all(j->request_header); - j->request_header = NULL; - import_compress_free(&j->compress); if (j->checksum_ctx) { @@ -192,6 +214,10 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { j->etag_exists = true; r = 0; goto finish; + } else if (http_status_need_authentication(status)) { + log_info("Access to image requires authentication."); + r = -ENOKEY; + goto finish; } else if (status >= 300) { if (status == 404 && j->on_not_found) { @@ -267,7 +293,7 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { goto finish; } - log_debug("%s of %s is %s.", EVP_MD_CTX_get0_name(j->checksum_ctx), j->url, h); + log_debug("%s of %s is %s.", EVP_MD_CTX_get0_name(j->checksum_ctx), pull_job_description(j), h); } if (iovec_is_set(&j->expected_checksum) && @@ -331,7 +357,7 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result) { } } - log_info("Acquired %s.", FORMAT_BYTES(j->written_uncompressed)); + log_info("Acquired %s for %s.", FORMAT_BYTES(j->written_uncompressed), pull_job_description(j)); r = 0; @@ -546,7 +572,7 @@ fail: } static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb, void *userdata) { - _cleanup_free_ char *length = NULL, *last_modified = NULL, *etag = NULL; + _cleanup_free_ char *length = NULL, *last_modified = NULL, *etag = NULL, *ct = NULL; size_t sz = size * nmemb; PullJob *j = ASSERT_PTR(userdata); CURLcode code; @@ -568,6 +594,19 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb goto fail; } + if (http_status_need_authentication(status)) { + _cleanup_free_ char *challenge = NULL; + + r = curl_header_strdup(contents, sz, "WWW-Authenticate:", &challenge); + if (r < 0) { + log_oom(); + goto fail; + } + if (r > 0) + free_and_replace(j->authentication_challenge, challenge); + return sz; + } + if (http_status_ok(status) || http_status_etag_exists(status)) { /* Check Etag on OK and etag exists responses. */ @@ -614,7 +653,7 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb goto fail; } - log_info("Downloading %s for %s.", FORMAT_BYTES(j->content_length), j->url); + log_info("Downloading %s for %s.", FORMAT_BYTES(j->content_length), pull_job_description(j)); } return sz; @@ -630,6 +669,16 @@ static size_t pull_job_header_callback(void *contents, size_t size, size_t nmemb return sz; } + r = curl_header_strdup(contents, sz, "Content-Type:", &ct); + if (r < 0) { + log_oom(); + goto fail; + } + if (r > 0) { + free_and_replace(j->content_type, ct); + return sz; + } + if (j->on_header) { r = j->on_header(j, contents, sz); if (r < 0) @@ -666,11 +715,11 @@ static int pull_job_progress_callback(void *userdata, curl_off_t dltotal, curl_o log_info("Got %u%% of %s. %s left at %s/s.", percent, - j->url, + pull_job_description(j), FORMAT_TIMESPAN(left, USEC_PER_SEC), FORMAT_BYTES((uint64_t) ((double) dlnow / ((double) done / (double) USEC_PER_SEC)))); } else - log_info("Got %u%% of %s.", percent, j->url); + log_info("Got %u%% of %s.", percent, pull_job_description(j)); j->progress_percent = percent; j->last_status_usec = n; @@ -724,6 +773,27 @@ int pull_job_new( return 0; } +int pull_job_add_request_header(PullJob *j, const char *hdr) { + assert(j); + assert(hdr); + + if (j->request_header) { + struct curl_slist *l; + + l = curl_slist_append(j->request_header, hdr); + if (!l) + return -ENOMEM; + + j->request_header = l; + } else { + j->request_header = curl_slist_new(hdr, NULL); + if (!j->request_header) + return -ENOMEM; + } + + return 0; +} + int pull_job_begin(PullJob *j) { int r; @@ -747,19 +817,9 @@ int pull_job_begin(PullJob *j) { if (!hdr) return -ENOMEM; - if (!j->request_header) { - j->request_header = curl_slist_new(hdr, NULL); - if (!j->request_header) - return -ENOMEM; - } else { - struct curl_slist *l; - - l = curl_slist_append(j->request_header, hdr); - if (!l) - return -ENOMEM; - - j->request_header = l; - } + r = pull_job_add_request_header(j, hdr); + if (r < 0) + return r; } if (j->request_header) { @@ -796,3 +856,30 @@ int pull_job_begin(PullJob *j) { return 0; } + +int pull_job_set_accept(PullJob *j, char * const *l) { + assert(j); + + if (strv_isempty(l)) + return 0; + + _cleanup_free_ char *joined = strv_join(l, ", "); + if (!joined) + return -ENOMEM; + + _cleanup_free_ char *f = strjoin("Accept: ", joined); + if (!f) + return -ENOMEM; + + return pull_job_add_request_header(j, f); +} + +int pull_job_set_bearer_token(PullJob *j, const char *token) { + assert(j); + + _cleanup_free_ char *f = strjoin("Authorization: Bearer ", token); + if (!f) + return -ENOMEM; + + return pull_job_add_request_header(j, f); +} diff --git a/src/import/pull-job.h b/src/import/pull-job.h index c4cda83a47..ea58b62f3b 100644 --- a/src/import/pull-job.h +++ b/src/import/pull-job.h @@ -36,6 +36,10 @@ typedef struct PullJob { char *url; void *userdata; + free_func_t free_userdata; + + char *description; + PullJobFinished on_finished; PullJobOpenDisk on_open_disk; PullJobHeader on_header; @@ -67,6 +71,7 @@ typedef struct PullJob { struct stat disk_stat; usec_t mtime; + char *content_type; ImportCompress compress; @@ -82,6 +87,8 @@ typedef struct PullJob { bool sync; bool force_memory; + + char *authentication_challenge; } PullJob; int pull_job_new(PullJob **ret, const char *url, CurlGlue *glue, void *userdata); @@ -93,4 +100,10 @@ void pull_job_curl_on_finished(CurlGlue *g, CURL *curl, CURLcode result); void pull_job_close_disk_fd(PullJob *j); +int pull_job_add_request_header(PullJob *j, const char *hdr); +int pull_job_set_accept(PullJob *j, char * const *l); +int pull_job_set_bearer_token(PullJob *j, const char *token); + +int pull_job_restart(PullJob *j, const char *new_url); + DEFINE_TRIVIAL_CLEANUP_FUNC(PullJob*, pull_job_unref); diff --git a/src/import/pull-oci.c b/src/import/pull-oci.c new file mode 100644 index 0000000000..3abb8777d1 --- /dev/null +++ b/src/import/pull-oci.c @@ -0,0 +1,1601 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-event.h" +#include "sd-json.h" +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "architecture.h" +#include "btrfs-util.h" +#include "cleanup-util.h" +#include "curl-util.h" +#include "dissect-image.h" +#include "errno-util.h" +#include "escape.h" +#include "extract-word.h" +#include "fd-util.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "install-file.h" +#include "io-util.h" +#include "json-util.h" +#include "mkdir-label.h" +#include "oci-util.h" +#include "ordered-set.h" +#include "path-util.h" +#include "pidref.h" +#include "process-util.h" +#include "pull-common.h" +#include "pull-oci.h" +#include "rm-rf.h" +#include "set.h" +#include "signal-util.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +#define LAYER_MAX 4096U +#define ACTIVE_LAYERS_MAX 5U + +typedef enum OciProgress { + OCI_DOWNLOADING_MANIFEST, + OCI_DOWNLOADING_LAYERS, + OCI_FINALIZING, + OCI_COPYING, +} OciProgress; + +typedef struct OciPull { + sd_event *event; + CurlGlue *glue; + + ImportFlags flags; + char *image_root; + + char *protocol; + char *repository; + char *image; + char *tag; + + PullJob *manifest_job; + PullJob *bearer_token_job; + PullJob *config_job; + OrderedSet *queued_layer_jobs; + Set *active_layer_jobs, *done_layer_jobs; + + OciPullFinished on_finished; + void *userdata; + + bool refuse_index; /* if true, refuse processing an OCI index, because we already processed one */ + + char *bearer_token; + + char *local; + + struct iovec *layer_digests; + size_t n_layers; + + struct iovec config; + int userns_fd; +} OciPull; + +typedef struct OciLayerState { + OciPull *pull; + + PidRef tar_pid; + char *temp_path; + char *final_path; + + int tree_fd; +} OciLayerState; + +static void oci_pull_job_on_finished_layer(PullJob *j); +static void oci_pull_job_on_finished_manifest(PullJob *j); +static void oci_pull_job_on_finished_config(PullJob *j); +static int oci_pull_work(OciPull *i); + +static OciLayerState* oci_layer_state_free(OciLayerState *st) { + if (!st) + return NULL; + + pidref_done_sigkill_wait(&st->tar_pid); + + if (st->temp_path) { + import_remove_tree(st->temp_path, st->pull ? &st->pull->userns_fd : NULL, st->pull->flags); + free(st->temp_path); + } + free(st->final_path); + + safe_close(st->tree_fd); + + return mfree(st); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(OciLayerState*, oci_layer_state_free); + +OciPull* oci_pull_unref(OciPull *i) { + if (!i) + return NULL; + + pull_job_unref(i->manifest_job); + pull_job_unref(i->bearer_token_job); + pull_job_unref(i->config_job); + ordered_set_free(i->queued_layer_jobs); + set_free(i->active_layer_jobs); + set_free(i->done_layer_jobs); + + curl_glue_unref(i->glue); + sd_event_unref(i->event); + + free(i->protocol); + free(i->repository); + free(i->image); + free(i->tag); + + free(i->image_root); + free(i->local); + free(i->bearer_token); + + iovec_done(&i->config); + + iovec_done_many_and_free(i->layer_digests, i->n_layers); + + safe_close(i->userns_fd); + + return mfree(i); +} + +int oci_pull_new( + OciPull **ret, + sd_event *event, + const char *image_root, + OciPullFinished on_finished, + void *userdata) { + + int r; + + assert(image_root); + assert(ret); + + _cleanup_free_ char *root = strdup(image_root); + if (!root) + return -ENOMEM; + + _cleanup_(sd_event_unrefp) sd_event *e = NULL; + if (event) + e = sd_event_ref(event); + else { + r = sd_event_default(&e); + if (r < 0) + return r; + } + + _cleanup_(curl_glue_unrefp) CurlGlue *g = NULL; + r = curl_glue_new(&g, e); + if (r < 0) + return r; + + _cleanup_(oci_pull_unrefp) OciPull *i = NULL; + i = new(OciPull, 1); + if (!i) + return -ENOMEM; + + *i = (OciPull) { + .on_finished = on_finished, + .userdata = userdata, + .image_root = TAKE_PTR(root), + .event = TAKE_PTR(e), + .glue = TAKE_PTR(g), + .userns_fd = -EBADF, + }; + + i->glue->on_finished = pull_job_curl_on_finished; + i->glue->userdata = i; + + *ret = TAKE_PTR(i); + + return 0; +} + +static int pull_job_payload_as_json(PullJob *j, sd_json_variant **ret) { + int r; + + assert(j); + assert(ret); + + /* The PullJob logic implicitly NUL terminates */ + assert(((char*) j->payload.iov_base)[j->payload.iov_len] == 0); + + if (memchr(j->payload.iov_base, 0, j->payload.iov_len)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded NUL byte in JSON data, refusing."); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse((char*) j->payload.iov_base, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON at position %u:%u: %m", line, column); + + if (DEBUG_LOGGING) + sd_json_variant_dump(v, SD_JSON_FORMAT_COLOR_AUTO|SD_JSON_FORMAT_PRETTY_AUTO, /* f= */ NULL, /* prefix= */ NULL); + + *ret = TAKE_PTR(v); + return 0; +} + +typedef struct OciIndexEntry { + char *media_type; + struct iovec digest; + uint64_t size; + char **url; + char *os; + char *architecture; + char *variant; +} OciIndexEntry; + +static void oci_index_entry_done(OciIndexEntry *entry) { + assert(entry); + + entry->media_type = mfree(entry->media_type); + iovec_done(&entry->digest); + entry->url = strv_free(entry->url); + entry->os = mfree(entry->os); + entry->architecture = mfree(entry->architecture); + entry->variant = mfree(entry->variant); +} + +static bool oci_index_entry_match(OciIndexEntry *entry) { + assert(entry); + + if (entry->os && !streq(entry->os, "linux")) + return false; + + if (entry->architecture && go_arch_from_string(entry->architecture) != native_architecture()) + return false; + + return true; +} + +static int json_dispatch_oci_digest(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + struct iovec *field = ASSERT_PTR(userdata); + int r; + + assert(variant); + + const char *s = NULL; + r = sd_json_dispatch_const_string(name, variant, flags, &s); + if (r < 0) + return r; + + const char *h = startswith(s, "sha256:"); + if (!h) + return json_log(variant, flags, SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported hash algorithm used in '%s', refusing.", s); + + _cleanup_(iovec_done) struct iovec d = {}; + r = unhexmem(h, &d.iov_base, &d.iov_len); + if (r < 0) + return json_log(variant, flags, r, "Failed to decode hash '%s', refusing.", s); + if (d.iov_len != SHA256_DIGEST_SIZE) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Hash '%s' has wrong size, refusing.", s); + + iovec_done(field); + *field = TAKE_STRUCT(d); + + return 0; +} + +static int json_dispatch_oci_platform(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + OciIndexEntry *entry = ASSERT_PTR(userdata); + + assert(variant); + + static const struct sd_json_dispatch_field table[] = { + { "os", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, os), 0 }, + { "architecture", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, architecture), 0 }, + { "variant", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, variant), 0 }, + {} + }; + + return sd_json_dispatch(variant, table, flags|SD_JSON_ALLOW_EXTENSIONS, entry); +} + +static int json_dispatch_oci_index_entry(sd_json_variant *v, OciIndexEntry *entry) { + int r; + + assert(v); + assert(entry); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciIndexEntry, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciIndexEntry, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciIndexEntry, size), SD_JSON_MANDATORY }, + { "urls", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciIndexEntry, url), 0 }, + { "platform", SD_JSON_VARIANT_OBJECT, json_dispatch_oci_platform, 0, 0 }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, entry); + if (r < 0) + return r; + + if (!streq_ptr(entry->media_type, "application/vnd.oci.image.manifest.v1+json")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest index entry with media type '%s', skipping.", entry->media_type); + return 0; + } + + return 1; +} + +static int oci_pull_redirect_manifest(OciPull *i, const OciIndexEntry *entry) { + int r; + + assert(i); + assert(entry); + + /* We acquired an index already and found the right manifest to select, hence let's acquire that one + * now */ + + _cleanup_free_ char *p = oci_digest_string(&entry->digest); + if (!p) + return -ENOMEM; + + _cleanup_free_ char *url = NULL; + r = oci_make_manifest_url(i->protocol, i->repository, i->image, p, &url); + if (r < 0) + return r; + + _cleanup_(pull_job_unrefp) PullJob *j = NULL; + r = pull_job_new(&j, url, i->glue, i); + if (r < 0) + return r; + + r = pull_job_set_accept( + j, + STRV_MAKE("application/vnd.oci.image.manifest.v1+json")); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(j, i->bearer_token); + if (r < 0) + return r; + } + + j->on_finished = oci_pull_job_on_finished_manifest; + j->calc_checksum = true; + if (!iovec_memdup(&entry->digest, &j->checksum)) + return -ENOMEM; + + j->description = strjoin("Image Manifest (", url, ")"); + if (!j->description) + return -ENOMEM; + + r = pull_job_begin(j); + if (r < 0) + return r; + + free_and_replace_full(i->manifest_job, j, pull_job_unref); + return 0; +} + +static int oci_pull_process_index(OciPull *i, PullJob *j) { + int r; + + assert(i); + assert(j); + + /* Processes a just downloaded OCI image index, as per: + * + * https://github.com/opencontainers/image-spec/blob/main/image-index.md */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = pull_job_payload_as_json(j, &v); + if (r < 0) + return r; + + struct { + unsigned schema_version; + const char *media_type; + sd_json_variant *manifests; + } index_data = { + .schema_version = UINT_MAX, + }; + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "schemaVersion", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, voffsetof(index_data, schema_version), SD_JSON_MANDATORY }, + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(index_data, media_type), 0 }, + { "manifests", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant_noref, voffsetof(index_data, manifests), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &index_data); + if (r < 0) + return r; + + if (index_data.schema_version != 2) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image index has unsupported schema version %u, refusing.", index_data.schema_version); + if (index_data.media_type && !streq(index_data.media_type, "application/vnd.oci.image.index.v1+json")) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image index has unexpected media type '%s', refusing.", index_data.media_type); + + sd_json_variant *m; + JSON_VARIANT_ARRAY_FOREACH(m, index_data.manifests) { + _cleanup_(oci_index_entry_done) OciIndexEntry entry = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_index_entry(m, &entry); + if (r < 0) + return r; + if (r == 0) /* skip? */ + continue; + + if (!oci_index_entry_match(&entry)) + continue; + + r = oci_pull_redirect_manifest(i, &entry); + if (r < 0) + return r; + + return 1; /* continue */ + } + + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No suitable OCI image manifest found for local system."); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(pull_job_hash_ops, void, trivial_hash_func, trivial_compare_func, PullJob, pull_job_unref); + +static int oci_pull_job_on_open_disk(PullJob *j) { + int r; + + assert(j); + + OciLayerState *st = ASSERT_PTR(j->userdata); + OciPull *i = st->pull; + + if (!st->temp_path) { + r = tempfn_random_child(i->image_root, "oci", &st->temp_path); + if (r < 0) + return log_oom(); + } + + (void) mkdir_parents_label(st->temp_path, 0700); + + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountfsd: %m"); + + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory( + mountfsd_link, + st->temp_path, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); + if (r < 0) + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); + + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + i->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &st->tree_fd); + if (r < 0) + return log_error_errno(r, "Failed to mount directory via mountsd: %m"); + } else { + if (i->flags & IMPORT_BTRFS_SUBVOL) + r = btrfs_subvol_make_fallback(AT_FDCWD, st->temp_path, 0755); + else + r = RET_NERRNO(mkdir(st->temp_path, 0755)); + if (r < 0) + return log_error_errno(r, "Failed to create directory/subvolume %s: %m", st->temp_path); + + if (r > 0 && (i->flags & IMPORT_BTRFS_QUOTA)) { /* actually btrfs subvol */ + (void) import_assign_pool_quota_and_warn(i->image_root); + (void) import_assign_pool_quota_and_warn(st->temp_path); + } + + st->tree_fd = open(st->temp_path, O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW); + if (st->tree_fd < 0) + return log_error_errno(errno, "Failed to open '%s': %m", st->temp_path); + } + + j->disk_fd = import_fork_tar_x(st->tree_fd, i->userns_fd, &st->tar_pid); + if (j->disk_fd < 0) + return j->disk_fd; + + return 0; +} + +static void oci_layer_state_free_wrapper(void *p) { + oci_layer_state_free(p); +} + +typedef struct OciManifestLayer { + char *media_type; + struct iovec digest; + uint64_t size; +} OciManifestLayer; + +static void oci_manifest_layer_done(OciManifestLayer *layer) { + assert(layer); + + layer->media_type = mfree(layer->media_type); + iovec_done(&layer->digest); +} + +static int oci_layer_dirname_for_digest(struct iovec *iovec, char **ret) { + assert(iovec); + assert(ret); + + _cleanup_free_ char *h = oci_digest_string(iovec); + if (!h) + return log_oom(); + + _cleanup_free_ char *fn = strjoin(".oci-", h); + if (!fn) + return log_oom(); + + *ret = TAKE_PTR(fn); + return 0; +} + +static int oci_pull_queue_layer(OciPull *i, OciManifestLayer *layer) { + int r; + + assert(i); + assert(layer); + + _cleanup_free_ char *url = NULL; + r = oci_make_blob_url(i->protocol, i->repository, i->image, &layer->digest, &url); + if (r < 0) + return r; + + _cleanup_free_ char *fn = NULL; + r = oci_layer_dirname_for_digest(&layer->digest, &fn); + if (r < 0) + return r; + + _cleanup_free_ char *final_path = path_join(i->image_root, fn); + if (!final_path) + return log_oom(); + + r = is_dir(final_path, /* follow= */ true); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to determine if directory '%s' exists: %m", final_path); + } else { + log_debug("Layer '%s' already exists, skipping download.", final_path); + return 0; + } + + _cleanup_(oci_layer_state_freep) OciLayerState *st = new(OciLayerState, 1); + if (!st) + return -ENOMEM; + + *st = (OciLayerState) { + .pull = i, + .tar_pid = PIDREF_NULL, + .tree_fd = -EBADF, + .final_path = TAKE_PTR(final_path), + }; + + /* Set up */ + _cleanup_(pull_job_unrefp) PullJob *j = NULL; + r = pull_job_new(&j, url, i->glue, st); + if (r < 0) + return r; + + j->free_userdata = oci_layer_state_free_wrapper; + TAKE_PTR(st); + + r = pull_job_set_accept(j, STRV_MAKE(layer->media_type)); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(j, i->bearer_token); + if (r < 0) + return r; + } + + j->on_finished = oci_pull_job_on_finished_layer; + j->on_open_disk = oci_pull_job_on_open_disk; + j->expected_content_length = layer->size; + j->calc_checksum = true; + if (!iovec_memdup(&layer->digest, &j->expected_checksum)) + return -ENOMEM; + + r = ordered_set_ensure_put(&i->queued_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) + return r; + + TAKE_PTR(j); + return 0; +} + +static int json_dispatch_oci_manifest_layer(sd_json_variant *v, OciManifestLayer *layer) { + int r; + + assert(v); + assert(layer); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciManifestLayer, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciManifestLayer, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciManifestLayer, size), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, layer); + if (r < 0) + return r; + + if (!STR_IN_SET(layer->media_type, + "application/vnd.oci.image.layer.v1.tar", + "application/vnd.oci.image.layer.v1.tar+gzip", + "application/vnd.oci.image.layer.v1.tar+zstd")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest layer with media type '%s', skipping.", layer->media_type); + return 0; + } + + return 1; +} + +typedef struct OciManifestConfig { + char *media_type; + struct iovec digest; + uint64_t size; + struct iovec data; +} OciManifestConfig; + +static void oci_manifest_config_done(OciManifestConfig *config) { + assert(config); + + config->media_type = mfree(config->media_type); + iovec_done(&config->digest); + iovec_done(&config->data); +} + +static int oci_pull_fetch_config(OciPull *i, OciManifestConfig *config) { + int r; + + assert(i); + assert(config); + + if (i->config_job) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Two configs requested, refusing."); + + _cleanup_free_ char *url = NULL; + r = oci_make_blob_url(i->protocol, i->repository, i->image, &config->digest, &url); + if (r < 0) + return r; + + _cleanup_free_ char *h = oci_digest_string(&config->digest); + if (!h) + return log_oom(); + + r = pull_job_new(&i->config_job, url, i->glue, i); + if (r < 0) + return r; + + r = pull_job_set_accept(i->config_job, STRV_MAKE(config->media_type)); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(i->config_job, i->bearer_token); + if (r < 0) + return r; + } + + i->config_job->on_finished = oci_pull_job_on_finished_config; + i->config_job->expected_content_length = config->size; + i->config_job->calc_checksum = true; + if (!iovec_memdup(&config->digest, &i->config_job->expected_checksum)) + return log_oom(); + + i->config_job->description = strjoin("Image configuration (", url, ")"); + + r = pull_job_begin(i->config_job); + if (r < 0) + return r; + + return 0; +} + +static int json_dispatch_oci_manifest_config(sd_json_variant *v, OciManifestConfig *config) { + int r; + + assert(v); + assert(config); + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(OciManifestConfig, media_type), SD_JSON_MANDATORY }, + { "digest", SD_JSON_VARIANT_STRING, json_dispatch_oci_digest, offsetof(OciManifestConfig, digest), SD_JSON_MANDATORY }, + { "size", SD_JSON_VARIANT_UNSIGNED, sd_json_dispatch_uint64, offsetof(OciManifestConfig, size), SD_JSON_MANDATORY }, + { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(OciManifestConfig, data), 0 }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, config); + if (r < 0) + return r; + + if (!STR_IN_SET(config->media_type, "application/vnd.oci.image.config.v1+json")) { + json_log(v, SD_JSON_LOG, /* error= */ 0, "Unexpected manifest config with media type '%s', skipping.", config->media_type); + return 0; + } + + if (iovec_is_set(&config->data)) { + if (config->data.iov_len != config->size) + return json_log(v, SD_JSON_LOG, SYNTHETIC_ERRNO(EBADMSG), "Manifest config size mismatch."); + + uint8_t h[SHA256_DIGEST_SIZE]; + if (memcmp_nn(sha256_direct(config->data.iov_base, config->data.iov_len, h), SHA256_DIGEST_SIZE, + config->digest.iov_base, config->digest.iov_len) != 0) + return json_log(v, SD_JSON_LOG, SYNTHETIC_ERRNO(EBADMSG), "Manifest data size mismatch."); + } + + return 1; +} + +static int oci_pull_fetch_layers(OciPull *i) { + int r; + + assert(i); + + while (set_size(i->active_layer_jobs) < ACTIVE_LAYERS_MAX) { + _cleanup_(pull_job_unrefp) PullJob *j = ordered_set_steal_first(i->queued_layer_jobs); + if (!j) + return 0; + + r = pull_job_begin(j); + if (r < 0) + return r; + + r = set_ensure_put(&i->active_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) + return r; + + TAKE_PTR(j); + } + + return 0; +} + +static int oci_pull_process_manifest(OciPull *i, PullJob *j) { + int r; + + assert(i); + assert(j); + + /* Processes a just downloaded OCI image manifest, as per: + * + * https://github.com/opencontainers/image-spec/blob/main/manifest.md */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = pull_job_payload_as_json(j, &v); + if (r < 0) + return r; + + struct { + unsigned schema_version; + const char *media_type; + sd_json_variant *config; + sd_json_variant *layers; + } manifest_data = { + .schema_version = UINT_MAX, + }; + + static const struct sd_json_dispatch_field dispatch_table[] = { + { "schemaVersion", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, voffsetof(manifest_data, schema_version), SD_JSON_MANDATORY }, + { "mediaType", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(manifest_data, media_type), 0 }, + { "config", SD_JSON_VARIANT_OBJECT, sd_json_dispatch_variant_noref, voffsetof(manifest_data, config), 0 }, + { "layers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant_noref, voffsetof(manifest_data, layers), SD_JSON_MANDATORY }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &manifest_data); + if (r < 0) + return r; + + if (manifest_data.schema_version != 2) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image manifest has unsupported schema version %u, refusing.", manifest_data.schema_version); + if (manifest_data.media_type && !streq(manifest_data.media_type, "application/vnd.oci.image.manifest.v1+json")) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "OCI image manifest has unexpected media type '%s', refusing.", manifest_data.media_type); + + if (manifest_data.config) { + _cleanup_(oci_manifest_config_done) OciManifestConfig config = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_manifest_config(manifest_data.config, &config); + if (r < 0) + return r; + + if (iovec_is_set(&config.data)) { + iovec_done(&i->config); + i->config = TAKE_STRUCT(config.data); + } else { + r = oci_pull_fetch_config(i, &config); + if (r < 0) + return r; + } + } + + sd_json_variant *m; + JSON_VARIANT_ARRAY_FOREACH(m, manifest_data.layers) { + _cleanup_(oci_manifest_layer_done) OciManifestLayer layer = { + .size = UINT64_MAX, + }; + + r = json_dispatch_oci_manifest_layer(m, &layer); + if (r < 0) + return r; + if (r == 0) /* skip? */ + continue; + + if (i->n_layers >= LAYER_MAX) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "Too many layers in manifest (> %zu), refusing.", i->n_layers); + + if (!GREEDY_REALLOC(i->layer_digests, i->n_layers + 1)) + return log_oom(); + + if (!iovec_memdup(&layer.digest, i->layer_digests + i->n_layers)) + return log_oom(); + + i->n_layers++; + + r = oci_pull_queue_layer(i, &layer); + if (r < 0) + return r; + } + + if (i->n_layers == 0) + return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Manifest has no recognized file system layers, refusing."); + + assert(i->n_layers >= ordered_set_size(i->queued_layer_jobs)); + size_t present_layers = i->n_layers - ordered_set_size(i->queued_layer_jobs); + + log_info("Image %s/%s:%s has %zu layers, %zu already present.", i->repository, i->image, i->tag, i->n_layers, present_layers); + + /* Assign nice descriptions that indicate which layer we are talking of here */ + PullJob *q; + size_t k = 0; + ORDERED_SET_FOREACH(q, i->queued_layer_jobs) { + _cleanup_free_ char *d = NULL; + + if (asprintf(&d, "Layer %zu/%zu (%s)", present_layers + k + 1, i->n_layers, q->url) < 0) + return log_oom(); + + free_and_replace(q->description, d); + k++; + } + + return oci_pull_work(i); +} + +static bool oci_pull_is_done(OciPull *i) { + assert(i); + assert(i->manifest_job); + + if (!PULL_JOB_IS_COMPLETE(i->manifest_job) || + (i->bearer_token_job && !PULL_JOB_IS_COMPLETE(i->bearer_token_job)) || + (i->config_job && !PULL_JOB_IS_COMPLETE(i->config_job))) + return false; + + return ordered_set_isempty(i->queued_layer_jobs) && + set_isempty(i->active_layer_jobs); +} + +typedef struct OciConfiguration { + char *user; + char **env; + char **entrypoint; + char **cmd; + char *working_dir; + int stop_signal; +} OciConfiguration; + +static void oci_configuration_done(OciConfiguration *c) { + assert(c); + + c->user = mfree(c->user); + c->env = strv_free(c->env); + c->entrypoint = strv_free(c->entrypoint); + c->cmd = strv_free(c->cmd); + c->working_dir = mfree(c->working_dir); +} + +static int print_pair_escaped(FILE *f, const char *field, const char *value) { + assert(f); + assert(field); + assert(value); + + _cleanup_free_ char *escaped = cescape(value); + if (!escaped) + return log_oom(); + + fputs(field, f); + fputc('=', f); + fputs(escaped, f); + fputc('\n', f); + + return 0; +} + +static int dispatch_unsupported(const char *name, sd_json_variant *v, sd_json_dispatch_flags_t flags, void *userdata) { + assert(v); + json_log(v, flags|SD_JSON_WARNING, /* error= */ 0, "OCI image specification field '%s' cannot be be translated to any corresponding .nspawn setting, skipping.", name); + return 0; +} + +static int oci_pull_save_nspawn_settings(OciPull *i) { + int r; + + assert(i); + assert(iovec_is_set(&i->config)); + + /* This translates the image spec "configuration" object into native .nspawn files for consumption by + * systemd-nspawn. It's vaguely related to + * https://github.com/opencontainers/image-spec/blob/v1.1.1/conversion.md except of course we don't + * translate to the OCI runtime spec here but rather to .nspawn knobs. */ + + /* The PullJob logic implicitly NUL terminates */ + assert(((char*) i->config.iov_base)[i->config.iov_len] == 0); + + if (memchr(i->config.iov_base, 0, i->config.iov_len)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Embedded NUL by in JSON config data, refusing."); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse((char*) i->config.iov_base, /* flags= */ 0, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse JSON config data at position %u:%u: %m", line, column); + + sd_json_variant *config = sd_json_variant_by_key(v, "config"); + + _cleanup_(oci_configuration_done) OciConfiguration config_data = { + .stop_signal = SIGTERM, + }; + static const struct sd_json_dispatch_field dispatch_table[] = { + { "User", SD_JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(OciConfiguration, user), 0 }, + { "Env", SD_JSON_VARIANT_ARRAY, json_dispatch_strv_environment, offsetof(OciConfiguration, env), 0 }, + { "Entrypoint", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciConfiguration, entrypoint), 0 }, + { "Cmd", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(OciConfiguration, cmd), 0 }, + { "WorkingDir", SD_JSON_VARIANT_STRING, json_dispatch_path, offsetof(OciConfiguration, working_dir), 0 }, + { "StopSignal", SD_JSON_VARIANT_STRING, sd_json_dispatch_signal, offsetof(OciConfiguration, stop_signal), 0 }, + + /* Currently unsupported fields that the OCI image spec defines but we cannot convert into + * .nspawn settings just yet. */ + { "ExposedPorts", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Volumes", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Labels", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "ArgsEscaped", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Memory", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "MemorySwap", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "CpuShares", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + { "Healthcheck", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_unsupported, 0, 0 }, + {} + }; + + r = sd_json_dispatch(config, dispatch_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &config_data); + if (r < 0) + return r; + + _cleanup_free_ char *fn = strjoin(i->local, ".nspawn"); + if (!fn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, fn); + if (!fn) + return log_oom(); + + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(unlink_and_freep) char *tmpfile = NULL; + r = fopen_tmpfile_linkable(j, O_WRONLY|O_CLOEXEC, &tmpfile, &f); + if (r < 0) + return log_error_errno(r, "Failed to create '%s': %m", j); + + fprintf(f, + "# Generated from OCI configuration object\n" + "[Exec]\n" + "Boot=no\n" + "KillSignal=%s\n", + signal_to_string(config_data.stop_signal)); + + if (config_data.user && !STR_IN_SET(config_data.user, "root", "0")) { + r = print_pair_escaped(f, "User", config_data.user); + if (r < 0) + return r; + } + + if (config_data.working_dir && !path_equal(config_data.working_dir, "/")) { + r = print_pair_escaped(f, "WorkingDirectory", config_data.working_dir); + if (r < 0) + return r; + } + + STRV_FOREACH(e, config_data.env) { + r = print_pair_escaped(f, "Environment", *e); + if (r < 0) + return r; + } + + _cleanup_strv_free_ char **cmdline = strv_copy(config_data.entrypoint); + if (!cmdline) + return log_oom(); + if (strv_extend_strv(&cmdline, config_data.cmd, /* filter_duplicates= */ false) < 0) + return log_oom(); + + if (!strv_isempty(cmdline)) { + _cleanup_free_ char *ej = NULL; + + STRV_FOREACH(z, cmdline) { + _cleanup_free_ char *q = NULL; + const char *a; + + if (isempty(*z)) + a = "\"\""; + else if (!in_charset(*z, ALPHANUMERICAL "=+-_/;.:,;")) { + _cleanup_free_ char *e = shell_escape(*z, "\""); + if (!e) + return log_oom(); + + q = strjoin("\"", e, "\""); + if (!q) + return log_oom(); + + a = q; + } else + a = *z; + + if (!strextend_with_separator(&ej, " ", a)) + return log_oom(); + } + + fprintf(f, "Parameters=%s\n", ej); + } + + r = flink_tmpfile(f, tmpfile, j, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", j); + + log_info("systemd-nspawn settings file written to '%s'.", j); + return 0; +} + +static int oci_pull_save_oci_config(OciPull *i) { + int r; + + assert(i); + assert(iovec_is_set(&i->config)); + + _cleanup_free_ char *fn = strjoin(i->local, ".oci-config"); + if (!fn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, fn); + if (!fn) + return log_oom(); + + _cleanup_close_ int fd = -EBADF; + _cleanup_(unlink_and_freep) char *tmpfile = NULL; + fd = open_tmpfile_linkable(j, O_WRONLY|O_CLOEXEC, &tmpfile); + if (fd < 0) + return log_error_errno(fd, "Failed to create '%s': %m", j); + + r = loop_write(fd, i->config.iov_base, i->config.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to write '%s': %m", j); + + r = link_tmpfile(fd, tmpfile, j, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to move '%s' into place: %m", j); + + log_info("OCI config written to '%s'.", j); + return 0; +} + +static int oci_pull_save_mstack(OciPull *i) { + int r; + + assert(i); + + _cleanup_free_ char *dn = strjoin(i->local, ".mstack"); + if (!dn) + return log_oom(); + + _cleanup_free_ char *j = path_join(i->image_root, dn); + if (!dn) + return log_oom(); + + log_notice("Creating '%s'", j); + + _cleanup_free_ char *_jt = NULL; + r = tempfn_random(j, "oci", &_jt); + if (r < 0) + return log_oom(); + + _cleanup_close_ int dir_fd = xopenat(AT_FDCWD, _jt, O_DIRECTORY|O_CREAT|O_CLOEXEC); + if (dir_fd < 0) + return log_error_errno(dir_fd, "Failed to create '%s': %m", j); + + _cleanup_(rm_rf_physical_and_freep) char *jt = TAKE_PTR(_jt); + for (size_t k = 0; k < i->n_layers; k++) { + _cleanup_free_ char *sl = NULL; + if (asprintf(&sl, "layer@%zu", k) < 0) + return log_oom(); + + _cleanup_free_ char *ldn = NULL; + if (oci_layer_dirname_for_digest(i->layer_digests + k, &ldn) < 0) + return log_oom(); + + _cleanup_free_ char *with_dot_dot = path_join("..", ldn); + if (!with_dot_dot) + return log_oom(); + + if (symlinkat(with_dot_dot, dir_fd, sl) < 0) + return log_error_errno(errno, "Failed to make symlink for layer '%s': %m", sl); + } + + if (!FLAGS_SET(i->flags, IMPORT_READ_ONLY)) { + /* If this shall be a mutable container let's also create an "rw" layer, that will become + * the upper layer in the overlayfs stack. */ + + if (FLAGS_SET(i->flags, IMPORT_FOREIGN_UID)) { + r = import_make_foreign_userns(&i->userns_fd); + if (r < 0) + return r; + + r = mountfsd_make_directory_fd( + /* vl= */ NULL, + dir_fd, + "rw", + 0755, + /* flags= */ 0, + /* ret_directory_fd= */ NULL); + if (r < 0) + return r; + } else { + _cleanup_close_ int rw_fd = open_mkdir_at(dir_fd, "rw", O_EXCL|O_CLOEXEC, 0755); + if (rw_fd < 0) + return log_error_errno(rw_fd, "Failed to create 'rw' layer: %m"); + } + } + + if (rename(jt, j) < 0) + return log_error_errno(errno, "Failed to move '%s' into place: %m", j); + + jt = mfree(jt); /* Disarm rm_rf_physical_and_free() */ + + log_info("Mount stack written to '%s'.", j); + return 0; +} + +static int oci_pull_make_local(OciPull *i) { + int r; + + assert(i); + assert(oci_pull_is_done(i)); + + r = oci_pull_save_mstack(i); + if (r < 0) + return r; + + if (!iovec_is_set(&i->config) || + iovec_memcmp(&i->config, &IOVEC_MAKE_STRING("{}")) == 0) + log_info("Image has no configuration, not saving."); + else { + r = oci_pull_save_nspawn_settings(i); + if (r < 0) + return r; + + r = oci_pull_save_oci_config(i); + if (r < 0) + return r; + } + + return 0; +} + +static int oci_pull_work(OciPull *i) { + int r; + + assert(i); + + r = oci_pull_fetch_layers(i); + if (r < 0) + return r; + + if (!oci_pull_is_done(i)) + return 1; /* continue */ + + r = oci_pull_make_local(i); + if (r < 0) + return r; + + log_info("Everything done."); + return 0; /* done */ +} + +static void oci_pull_finish(OciPull *i, int r) { + assert(i); + + if (i->on_finished) + i->on_finished(i, r, i->userdata); + else + sd_event_exit(i->event, r); +} + +static void oci_pull_job_on_finished_layer(PullJob *j) { + int r; + + assert(j); + + OciLayerState *st = ASSERT_PTR(j->userdata); + OciPull *i = st->pull; + + if (j->error != 0) { + r = log_error_errno(j->error, "Failed to retrieve layer file: %m"); + goto finish; + } + + pull_job_close_disk_fd(j); + + if (pidref_is_set(&st->tar_pid)) { + r = pidref_wait_for_terminate_and_check("tar", &st->tar_pid, WAIT_LOG); + if (r < 0) + goto finish; + + pidref_done(&st->tar_pid); + if (r != EXIT_SUCCESS) { + r = -EIO; + goto finish; + } + } + + assert(st->temp_path); + assert(st->final_path); + + r = install_file(AT_FDCWD, st->temp_path, + AT_FDCWD, st->final_path, + INSTALL_READ_ONLY|INSTALL_GRACEFUL); + if (r < 0) { + log_error_errno(r, "Failed to rename final image name to %s: %m", st->final_path); + goto finish; + } + + st->temp_path = mfree(st->temp_path); + + r = set_ensure_put(&i->done_layer_jobs, &pull_job_hash_ops, j); + if (r < 0) { + log_oom(); + goto finish; + } + + assert(set_remove(i->active_layer_jobs, j) == j); + + r = oci_pull_work(i); + if (r <= 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static void oci_pull_job_on_finished_config(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->config_job == j); + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve config object. (Wrong URL?)"); + else + r = log_error_errno(j->error, "Failed to retrieve config object: %m"); + goto finish; + } + + iovec_done(&i->config); + i->config = TAKE_STRUCT(j->payload); + + r = oci_pull_work(i); + if (r <= 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static void oci_pull_job_on_finished_bearer_token(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->bearer_token_job == j); + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve bearer token. (Wrong URL?)"); + else + r = log_error_errno(j->error, "Failed to retrieve bearer token: %m"); + goto finish; + } + + r = pull_job_payload_as_json(j, &v); + if (r < 0) + goto finish; + + sd_json_variant *tv = sd_json_variant_by_key(v, "token"); + if (!tv) { + r = log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Bearer token lacks 'token' field."); + goto finish; + } + + if (!sd_json_variant_is_string(tv)) { + r = log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'token' field of bearer token is not a string."); + goto finish; + } + + r = free_and_strdup_warn(&i->bearer_token, sd_json_variant_string(tv)); + if (r < 0) + goto finish; + + assert(i->manifest_job); + r = pull_job_set_bearer_token(i->manifest_job, i->bearer_token); + if (r < 0) { + log_error_errno(r, "Failed to set bearer token on manifest job: %m"); + goto finish; + } + + r = pull_job_restart(i->manifest_job, /* new_url= */ NULL); + if (r < 0) + goto finish; + + return; + +finish: + oci_pull_finish(i, r); +} + +static int make_bearer_token_url(const char *realm, const char *service, const char *scope, char **ret) { + assert(realm); + assert(service); + assert(scope); + assert(ret); + + _cleanup_free_ char *rs = urlescape(service); + if (!rs) + return -ENOMEM; + + _cleanup_free_ char *ss = urlescape(scope); + if (!ss) + return -ENOMEM; + + _cleanup_free_ char *url = strjoin(realm, "?service=", rs, "&scope=", ss); + if (!url) + return -ENOMEM; + + *ret = TAKE_PTR(url); + return 0; +} + +static int oci_pull_process_authentication_challenge(OciPull *i, const char *challenge) { + int r; + + assert(i); + assert(challenge); + + /* We only know what to do with the bearer token challenge */ + const char *e = startswith_no_case(challenge, "bearer "); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Authentication mechanism not recognized, cannot authenticate."); + + if (i->bearer_token_job) + return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Two bearer token challenges requested, refusing."); + + e += strspn(e, WHITESPACE); + + _cleanup_strv_free_ char **l = NULL; + r = strv_split_full(&l, e, ",", EXTRACT_KEEP_QUOTE); + if (r < 0) + return log_error_errno(r, "Failed to split bearer token paramaters: %m"); + + _cleanup_free_ char *realm = NULL, *scope = NULL, *service = NULL; + struct { + const char *name; + char **value; + } fields[] = { + { "realm=\"", &realm }, + { "scope=\"", &scope }, + { "service=\"", &service }, + }; + + STRV_FOREACH(k, l) { + bool found = false; + + FOREACH_ELEMENT(f, fields) { + const char *v = startswith_no_case(*k, f->name); + if (!v) + continue; + + const char *q = endswith(v,"\""); + if (!q) { + log_warning("Field isn't quoted properly: %s", *k); + continue; + } + + _cleanup_free_ char *s = strndup(v, q - v); + if (!s) + return log_oom(); + + free_and_replace(*f->value, s); + found = true; + break; + } + + if (!found) + log_debug("Ignoring bearer token challenge field: %s", *k); + } + + if (!realm || !service || !scope) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Incomplete bearer token fields"); + + if (!startswith_no_case(realm, "https://")) + return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "Authentication realm is not an URL, don't know how to proceed."); + + _cleanup_free_ char *url = NULL; + r = make_bearer_token_url(realm, service, scope, &url); + if (r < 0) + return log_error_errno(r, "Failed to make bearer token URL: %m"); + + r = pull_job_new(&i->bearer_token_job, url, i->glue, i); + if (r < 0) + return r; + + i->bearer_token_job->on_finished = oci_pull_job_on_finished_bearer_token; + i->bearer_token_job->description = strjoin("Bearer token (", url, ")"); + + r = pull_job_begin(i->bearer_token_job); + if (r < 0) + return r; + + return 1; /* continue running */ +} + +static void oci_pull_job_on_finished_manifest(PullJob *j) { + int r; + + assert(j); + OciPull *i = ASSERT_PTR(j->userdata); + assert(i->manifest_job == j); + + if (j->error != 0) { + if (j->error == -ENOMEDIUM) /* HTTP 404 */ + r = log_error_errno(j->error, "Failed to retrieve manifest/index file. (Wrong URL?)"); + else if (j->error == -ENOKEY) { /* HTTP 401 - Need authentication */ + + if (j->authentication_challenge) { + r = oci_pull_process_authentication_challenge(i, j->authentication_challenge); + if (r > 0) + return; + + goto finish; + } else + r = log_error_errno(j->error, "Failed to retrieve manifest/index file. (Needing authentication.)"); + } else + r = log_error_errno(j->error, "Failed to retrieve manifest/index file: %m"); + goto finish; + } + + /* If we have no content type, let's assume it's a manifest, not an index. This can happen in case we + * operate with file:// URLs, which in turn is really useful for testing purposes, to mock an OCI + * registry without having to set up an HTTP service. */ + if (streq_ptr(j->content_type, "application/vnd.oci.image.manifest.v1+json") || + !j->content_type) { + r = oci_pull_process_manifest(i, j); + if (r <= 0) + goto finish; + + return; + } + + if (streq_ptr(j->content_type, "application/vnd.oci.image.index.v1+json")) { + + if (i->refuse_index) { + r = log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Already processed an OCI index, refusing to process another one."); + goto finish; + } + + /* For now do not allow nested indexes */ + i->refuse_index = true; + + r = oci_pull_process_index(i, j); + if (r <= 0) + goto finish; + + return; + } + + r = log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Unexpected content type '%s', refusing.", strna(j->content_type)); + +finish: + oci_pull_finish(i, r); +} + +int oci_pull_start( + OciPull *i, + const char *ref, + const char *local, + ImportFlags flags) { + + int r; + + assert(i); + assert(ref); + + r = oci_ref_parse(ref, &i->repository, &i->image, &i->tag); + if (r < 0) + return r; + + r = oci_ref_normalize(&i->protocol, &i->repository, &i->image, &i->tag); + if (r < 0) + return r; + + if (local && !pull_validate_local(local, flags)) + return -EINVAL; + + if (i->manifest_job) + return -EBUSY; + + r = free_and_strdup(&i->local, local); + if (r < 0) + return r; + + i->flags = flags; + + _cleanup_free_ char *url = NULL; + r = oci_make_manifest_url(i->protocol, i->repository, i->image, i->tag, &url); + if (r < 0) + return r; + + /* Set up */ + r = pull_job_new(&i->manifest_job, url, i->glue, i); + if (r < 0) + return r; + + if (i->bearer_token) { + r = pull_job_set_bearer_token(i->manifest_job, i->bearer_token); + if (r < 0) + return r; + } + + r = pull_job_set_accept( + i->manifest_job, + STRV_MAKE("application/vnd.oci.image.manifest.v1+json", + "application/vnd.oci.image.index.v1+json")); + if (r < 0) + return r; + + i->manifest_job->on_finished = oci_pull_job_on_finished_manifest; + i->manifest_job->description = strjoin("Image Index (", url, ")"); + if (!i->manifest_job->description) + return -ENOMEM; + + return pull_job_begin(i->manifest_job); +} diff --git a/src/import/pull-oci.h b/src/import/pull-oci.h new file mode 100644 index 0000000000..86a37c33ec --- /dev/null +++ b/src/import/pull-oci.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" +#include "import-common.h" + +typedef struct OciPull OciPull; + +typedef void (*OciPullFinished)(OciPull *pull, int error, void *userdata); + +int oci_pull_new(OciPull **ret, sd_event *event, const char *image_root, OciPullFinished on_finished, void *userdata); +OciPull* oci_pull_unref(OciPull *i); + +DEFINE_TRIVIAL_CLEANUP_FUNC(OciPull*, oci_pull_unref); + +int oci_pull_start(OciPull *i, const char *ref, const char *local, ImportFlags flags); diff --git a/src/import/pull-tar.c b/src/import/pull-tar.c index b0cafd95f4..ae763879b7 100644 --- a/src/import/pull-tar.c +++ b/src/import/pull-tar.c @@ -2,6 +2,7 @@ #include "sd-daemon.h" #include "sd-event.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "btrfs-util.h" @@ -276,6 +277,11 @@ static int tar_pull_make_local_copy(TarPull *p) { if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to mountsd: %m"); + /* Usually, tar_pull_job_on_open_disk_tar() would allocate ->tree_fd for us, but if * already downloaded the image before, and are just making a copy of the original * download, we need to open ->tree_fd now */ @@ -294,20 +300,35 @@ static int tar_pull_make_local_copy(TarPull *p) { "Image tree '%s' is not owned by the foreign UID range, refusing.", p->final_path); - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &p->tree_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(t, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory( + mountfsd_link, + t, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); _cleanup_close_ int copy_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + ©_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); r = copy_tree_at_foreign(p->tree_fd, copy_fd, p->userns_fd); if (r < 0) @@ -611,14 +632,29 @@ static int tar_pull_job_on_open_disk_tar(PullJob *j) { if (r < 0) return r; - _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(where, MODE_INVALID, /* flags= */ 0, &directory_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); if (r < 0) - return r; + return log_error_errno(r, "Failed to connect to mountfsd: %m"); - r = mountfsd_mount_directory_fd(directory_fd, p->userns_fd, DISSECT_IMAGE_FOREIGN_UID, &p->tree_fd); + _cleanup_close_ int directory_fd = -EBADF; + r = mountfsd_make_directory( + mountfsd_link, + where, + MODE_INVALID, + /* flags= */ 0, + &directory_fd); if (r < 0) - return r; + return log_error_errno(r, "Failed to make directory via mountfsd: %m"); + + r = mountfsd_mount_directory_fd( + mountfsd_link, + directory_fd, + p->userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &p->tree_fd); + if (r < 0) + return log_error_errno(r, "Failed to mount directory via mountfsd: %m"); } else { if (p->flags & IMPORT_BTRFS_SUBVOL) r = btrfs_subvol_make_fallback(AT_FDCWD, where, 0755); diff --git a/src/import/pull.c b/src/import/pull.c index a57c1c039d..e2f8161526 100644 --- a/src/import/pull.c +++ b/src/import/pull.c @@ -18,9 +18,11 @@ #include "iovec-util.h" #include "log.h" #include "main-func.h" +#include "oci-util.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" +#include "pull-oci.h" #include "pull-raw.h" #include "pull-tar.h" #include "runtime-scope.h" @@ -244,6 +246,71 @@ static int pull_raw(int argc, char *argv[], void *userdata) { return -r; } +static void on_oci_finished(OciPull *pull, int error, void *userdata) { + sd_event *event = userdata; + assert(pull); + + if (error == 0) + log_info("Operation completed successfully."); + + sd_event_exit(event, ABS(error)); +} + +static int pull_oci(int argc, char *argv[], void *userdata) { + int r; + + const char *ref = argv[1]; + + _cleanup_free_ char *image = NULL; + r = oci_ref_parse(ref, /* ret_registry= */ NULL, &image, /* ret_tag= */ NULL); + if (r == -EINVAL) + return log_error_errno(r, "OCI ref '%s' is invalid.", ref); + if (r < 0) + return log_error_errno(r, "Failed to check of OCI ref '%s' is valid: %m", ref); + + _cleanup_free_ char *l = NULL; + const char *local; + if (argc >= 3) + local = empty_or_dash_to_null(argv[2]); + else { + r = path_extract_filename(image, &l); + if (r < 0) + return log_error_errno(r, "Failed to get extract final component of '%s': %m", image); + + local = l; + } + + _cleanup_free_ char *normalized = NULL; + r = normalize_local(local, ref, &normalized); + if (r < 0) + return r; + + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + r = import_allocate_event_with_signals(&event); + if (r < 0) + return r; + + _cleanup_(oci_pull_unrefp) OciPull *pull = NULL; + r = oci_pull_new(&pull, event, arg_image_root, on_oci_finished, event); + if (r < 0) + return log_error_errno(r, "Failed to allocate puller: %m"); + + r = oci_pull_start( + pull, + ref, + normalized, + arg_import_flags & IMPORT_PULL_FLAGS_MASK_OCI); + if (r < 0) + return log_error_errno(r, "Failed to pull image: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + log_info("Exiting."); + return -r; +} + static int help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] {COMMAND} ...\n" @@ -251,6 +318,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%2$sCommands:%3$s\n" " tar URL [NAME] Download a TAR image\n" " raw URL [NAME] Download a RAW image\n" + " oci REF [NAME] Download an OCI image\n" "\n%2$sOptions:%3$s\n" " -h --help Show this help\n" " --version Show package version\n" @@ -595,6 +663,7 @@ static int pull_main(int argc, char *argv[]) { { "help", VERB_ANY, VERB_ANY, 0, help }, { "tar", 2, 3, 0, pull_tar }, { "raw", 2, 3, 0, pull_raw }, + { "oci", 2, 3, 0, pull_oci }, {} }; diff --git a/src/import/test-oci-util.c b/src/import/test-oci-util.c new file mode 100644 index 0000000000..395b622b42 --- /dev/null +++ b/src/import/test-oci-util.c @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "tests.h" +#include "oci-util.h" + +static void test_urlescape_one(const char *s, const char *expected) { + _cleanup_free_ char *t = ASSERT_PTR(urlescape(s)); + + ASSERT_STREQ(t, expected); +} + +TEST(urlescape) { + test_urlescape_one(NULL, ""); + test_urlescape_one("", ""); + test_urlescape_one("a", "a"); + test_urlescape_one(" ", "%20"); + test_urlescape_one(" ", "%20%20%20%20%20"); + test_urlescape_one("foo\tfoo\aqux", "foo%09foo%07qux"); + test_urlescape_one("müffel", "m%c3%bcffel"); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 7b8812f03a..25f1a84551 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -662,6 +662,7 @@ static int vl_method_mount_image( if (r < 0) return r; + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL; for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { DissectedPartition *pp = di->partitions + d; int fd_idx; @@ -673,7 +674,14 @@ static int vl_method_mount_image( continue; if (userns_fd >= 0) { - r = nsresource_add_mount(userns_fd, pp->fsmount_fd); + + if (!nsresource_link) { + r = nsresource_connect(&nsresource_link); + if (r < 0) + return r; + } + + r = nsresource_add_mount(nsresource_link, userns_fd, pp->fsmount_fd); if (r < 0) return r; } @@ -1206,7 +1214,7 @@ static int vl_method_mount_directory( } if (userns_fd >= 0) { - r = nsresource_add_mount(userns_fd, mount_fd); + r = nsresource_add_mount(/* vl= */ NULL, userns_fd, mount_fd); if (r < 0) return r; } diff --git a/src/mstack/meson.build b/src/mstack/meson.build new file mode 100644 index 0000000000..27a618f371 --- /dev/null +++ b/src/mstack/meson.build @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-mstack', + 'public' : true, + 'sources' : files('mstack-tool.c'), + }, +] + +install_symlink('mount.mstack', + pointing_to : sbin_to_bin + 'systemd-mstack', + install_dir : sbindir) diff --git a/src/mstack/mstack-tool.c b/src/mstack/mstack-tool.c new file mode 100644 index 0000000000..bc86d8565a --- /dev/null +++ b/src/mstack/mstack-tool.c @@ -0,0 +1,437 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "argv-util.h" +#include "build.h" +#include "chase.h" +#include "errno-util.h" +#include "extract-word.h" +#include "fd-util.h" +#include "format-table.h" +#include "image-policy.h" +#include "main-func.h" +#include "mount-util.h" +#include "mountpoint-util.h" +#include "mstack.h" +#include "parse-argument.h" +#include "pretty-print.h" +#include "string-util.h" + +static enum { + ACTION_INSPECT, + ACTION_MOUNT, + ACTION_UMOUNT, +} arg_action = ACTION_INSPECT; +static char *arg_what = NULL; +static char *arg_where = NULL; +static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +static int arg_legend = true; +static MStackFlags arg_mstack_flags = 0; +static bool arg_rmdir = false; +static ImagePolicy *arg_image_policy = NULL; +static ImageFilter *arg_image_filter = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_what, freep); +STATIC_DESTRUCTOR_REGISTER(arg_where, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("systemd-mstack", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] WHAT\n" + "%1$s [OPTIONS...] --mount WHAT WHERE\n" + "%1$s [OPTIONS...] --umount WHERE\n" + "\n%5$sInspect or apply mount stack.%6$s\n\n" + "%3$sOptions:%4$s\n" + " --no-pager Do not pipe output into a pager\n" + " --no-legend Do not print the column headers\n" + " --json=pretty|short|off Generate JSON output\n" + " -r --read-only Mount read-only\n" + " --mkdir Make mount directory before mounting, if missing\n" + " --rmdir Remove mount directory after unmounting\n" + " --image-policy=POLICY\n" + " Specify image dissection policy\n" + " --image-filter=FILTER\n" + " Specify image dissection filter\n" + "\n%3$sCommands:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -m --mount Mount the mstack to the specified directory\n" + " -M Shortcut for --mount --mkdir\n" + " -u --umount Unmount the image from the specified directory\n" + " -U Shortcut for --umount --rmdir\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), ansi_normal(), + ansi_highlight(), ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_NO_PAGER, + ARG_NO_LEGEND, + ARG_JSON, + ARG_MKDIR, + ARG_RMDIR, + ARG_IMAGE_POLICY, + ARG_IMAGE_FILTER, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "mount", no_argument, NULL, 'm' }, + { "umount", no_argument, NULL, 'u' }, + { "json", required_argument, NULL, ARG_JSON }, + { "read-only", no_argument, NULL, 'r' }, + { "rmdir", no_argument, NULL, ARG_RMDIR }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "image-filter", required_argument, NULL, ARG_IMAGE_FILTER }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "hmMuUr", options, NULL)) >= 0) { + + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'r': + arg_mstack_flags |= MSTACK_RDONLY; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case ARG_IMAGE_FILTER: { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + r = image_filter_parse(optarg, &f); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter expression: %s", optarg); + + image_filter_free(arg_image_filter); + arg_image_filter = TAKE_PTR(f); + break; + } + + case ARG_MKDIR: + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case ARG_RMDIR: + arg_rmdir = true; + break; + + case 'm': + arg_action = ACTION_MOUNT; + break; + + case 'M': + /* Shortcut combination of --mkdir + --mount */ + arg_action = ACTION_MOUNT; + arg_mstack_flags |= MSTACK_MKDIR; + break; + + case 'u': + arg_action = ACTION_UMOUNT; + break; + + case 'U': + /* Shortcut combination of --rmdir + --umount */ + arg_action = ACTION_UMOUNT; + arg_rmdir = true; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + } + + switch (arg_action) { + + case ACTION_INSPECT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + break; + + case ACTION_MOUNT: + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + case ACTION_UMOUNT: + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one argument."); + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + break; + + default: + assert_not_reached(); + } + + return 1; +} + +static int parse_argv_as_mount_helper(int argc, char *argv[]) { + const char *options = NULL; + bool fake = false; + int c, r; + + /* Implements util-linux "external helper" command line interface, as per mount(8) man page. */ + + while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) { + switch (c) { + + case 'f': + fake = true; + break; + + case 'o': + options = optarg; + break; + + case 't': + if (!streq(optarg, "mstack")) + log_debug("Unexpected file system type '%s', ignoring.", optarg); + break; + + case 's': /* sloppy mount options */ + case 'n': /* aka --no-mtab */ + case 'v': /* aka --verbose */ + log_debug("Ignoring option -%c, not implemented.", c); + break; + + case 'N': /* aka --namespace= */ + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c); + + case '?': + return -EINVAL; + } + } + + if (optind + 2 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected an image file path and target directory as only argument."); + + for (const char *p = options;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE); + if (r < 0) + return log_error_errno(r, "Failed to extract mount option: %m"); + if (r == 0) + break; + + if (streq(word, "ro")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, true); + else if (streq(word, "rw")) + SET_FLAG(arg_mstack_flags, MSTACK_RDONLY, false); + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown mount option '%s'.", word); + } + + if (fake) + return 0; + + r = parse_path_argument(argv[optind], /* suppress_root= */ false, &arg_what); + if (r < 0) + return r; + + r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &arg_where); + if (r < 0) + return r; + + arg_action = ACTION_MOUNT; + return 1; +} + +static int inspect_mstack(void) { + _cleanup_(mstack_freep) MStack *mstack = NULL; + int r; + + assert(arg_what); + + r = mstack_load(arg_what, /* dir_fd= */ -EBADF, &mstack); + if (r < 0) + return log_debug_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_what); + + _cleanup_(table_unrefp) Table *t = NULL; + + t = table_new("type", "name", "image", "what", "where", "sort"); + if (!t) + return log_oom(); + + table_set_ersatz_string(t, TABLE_ERSATZ_DASH); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + _cleanup_free_ char *w = NULL; + r = fd_get_path(m->what_fd, &w); + if (r < 0) + return log_error_errno(r, "Failed to get path of what file descriptor: %m"); + + r = table_add_many( + t, + TABLE_STRING, mstack_mount_type_to_string(m->mount_type), + TABLE_STRING, m->what, + TABLE_STRING, image_type_to_string(m->image_type), + TABLE_PATH, w, + TABLE_PATH, m->where ?: ((mstack->root_mount && mstack->root_mount != m) ? "/usr" : "/"), + TABLE_STRING, m->sort_key); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int mount_mstack(void) { + int r; + + assert(arg_what); + assert(arg_where); + + r = mstack_apply( + arg_what, + /* dir_fd= */ -EBADF, + arg_where, + /* temp_mount_dir= */ NULL, /* auto-create temporary directory */ + /* mountfsd_link= */ NULL, + /* userns_fd= */ -EBADF, + arg_image_policy, + arg_image_filter, + arg_mstack_flags, + /* ret_root_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to apply .mstack/ directory '%s': %m", arg_what); + + return 0; +} + +static int umount_mstack(void) { + int r; + + assert(arg_where); + + _cleanup_free_ char *canonical = NULL; + _cleanup_close_ int fd = chase_and_open(arg_where, /* root= */ NULL, /* chase_flags= */ 0, O_DIRECTORY, &canonical); + if (fd == -ENOTDIR) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "'%s' is not a directory", arg_where); + if (fd < 0) + return log_error_errno(fd, "Failed to resolve path '%s': %m", arg_where); + + r = is_mount_point_at(fd, /* path= */ NULL, /* flags= */ 0); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'%s' is not a mount point", canonical); + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", canonical); + + fd = safe_close(fd); + + r = umount_recursive(canonical, 0); + if (r < 0) + return log_error_errno(r, "Failed to unmount '%s': %m", canonical); + + if (arg_rmdir) { + r = RET_NERRNO(rmdir(canonical)); + if (r < 0) + return log_error_errno(r, "Failed to remove mount directory '%s': %m", canonical); + } + + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + if (invoked_as(argv, "mount.mstack")) + r = parse_argv_as_mount_helper(argc, argv); + else + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + switch (arg_action) { + + case ACTION_INSPECT: + return inspect_mstack(); + + case ACTION_MOUNT: + return mount_mstack(); + + case ACTION_UMOUNT: + return umount_mstack(); + + default: + assert_not_reached(); + } +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/nspawn/nspawn-cgroup.c b/src/nspawn/nspawn-cgroup.c index 4206fe94e5..f1d45bbb9b 100644 --- a/src/nspawn/nspawn-cgroup.c +++ b/src/nspawn/nspawn-cgroup.c @@ -108,7 +108,10 @@ int create_subcgroup( if (r < 0) return log_error_errno(r, "Failed to add process " PID_FMT " to cgroup %s: %m", pid->pid, payload); - r = nsresource_add_cgroup(userns_fd, cgroup_fd); + r = nsresource_add_cgroup( + /* vl= */ NULL, + userns_fd, + cgroup_fd); if (r < 0) return log_error_errno(r, "Failed to add cgroup %s to userns: %m", payload); } else { diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 296b12bb90..08afa171ae 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -19,6 +19,7 @@ #include "sd-id128.h" #include "sd-netlink.h" #include "sd-path.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "barrier.h" @@ -69,6 +70,7 @@ #include "mkdir.h" #include "mount-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "netlink-internal.h" #include "notify-recv.h" @@ -204,6 +206,7 @@ struct ether_addr arg_network_provided_mac = {}; static PagerFlags arg_pager_flags = 0; static unsigned long arg_personality = PERSONALITY_INVALID; static char *arg_image = NULL; +static char *arg_mstack = NULL; static char *arg_oci_bundle = NULL; static VolatileMode arg_volatile_mode = VOLATILE_NO; static ExposePort *arg_expose_ports = NULL; @@ -272,6 +275,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_network_bridge, freep); STATIC_DESTRUCTOR_REGISTER(arg_network_zone, freep); STATIC_DESTRUCTOR_REGISTER(arg_network_namespace_path, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_mstack, freep); STATIC_DESTRUCTOR_REGISTER(arg_oci_bundle, freep); STATIC_DESTRUCTOR_REGISTER(arg_property, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_property_message, sd_bus_message_unrefp); @@ -738,6 +742,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_BACKGROUND, ARG_CLEANUP, ARG_NO_ASK_PASSWORD, + ARG_MSTACK, }; static const struct option options[] = { @@ -817,6 +822,7 @@ static int parse_argv(int argc, char *argv[]) { { "background", required_argument, NULL, ARG_BACKGROUND }, { "cleanup", no_argument, NULL, ARG_CLEANUP }, { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD }, + { "mstack", required_argument, NULL, ARG_MSTACK }, {} }; @@ -863,6 +869,14 @@ static int parse_argv(int argc, char *argv[]) { arg_settings_mask |= SETTING_DIRECTORY; break; + case ARG_MSTACK: + r = parse_path_argument(optarg, false, &arg_mstack); + if (r < 0) + return r; + + arg_settings_mask |= SETTING_DIRECTORY; + break; + case ARG_OCI_BUNDLE: r = parse_path_argument(optarg, false, &arg_oci_bundle); if (r < 0) @@ -1654,11 +1668,11 @@ static int verify_arguments(void) { if (has_custom_root_mount(arg_custom_mounts, arg_n_custom_mounts)) arg_read_only = true; - if (arg_directory && arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--directory= and --image= may not be combined."); + if (!!arg_directory + !!arg_image + !!arg_mstack > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--directory=, --image= --mstack= may not be combined."); - if (arg_template && arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= and --image= may not be combined."); + if (arg_template && (arg_image || arg_mstack)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= and --image=/--mstack= may not be combined."); if (arg_template && !(arg_directory || arg_machine)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--template= needs --directory= or --machine=."); @@ -1666,6 +1680,9 @@ static int verify_arguments(void) { if (arg_ephemeral && arg_template) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ephemeral and --template= may not be combined."); + if (arg_ephemeral && arg_mstack) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--ephemeral and --mstack= may not be combined."); + /* Permit --ephemeral with --link-journal=try-* to satisfy principle of the least astonishment * (by common sense, "try" means "do not fail if not possible") */ if (arg_ephemeral && !IN_SET(arg_link_journal, LINK_NO, LINK_AUTO) && !arg_link_journal_try) @@ -3054,6 +3071,24 @@ static int pick_paths(void) { arg_architecture = result.architecture; } + if (arg_mstack) { + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + PickFilter filter = *pick_filter_image_mstack; + + filter.architecture = arg_architecture; + + r = path_pick_update_warn( + &arg_mstack, + &filter, + /* n_filters= */ 1, + PICK_ARCHITECTURE|PICK_TRIES, + &result); + if (r < 0) + return r; + + arg_architecture = result.architecture; + } + if (arg_template) { _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; PickFilter filter = *pick_filter_image_dir; @@ -3088,7 +3123,7 @@ static int determine_names(void) { return log_oom(); } - if (!arg_image && !arg_directory) { + if (!arg_image && !arg_directory && !arg_mstack) { if (arg_machine) { _cleanup_(image_unrefp) Image *i = NULL; @@ -3099,10 +3134,24 @@ static int determine_names(void) { if (r < 0) return log_error_errno(r, "Failed to find image for machine '%s': %m", arg_machine); - if (IN_SET(i->type, IMAGE_RAW, IMAGE_BLOCK)) + switch (i->type) { + case IMAGE_RAW: + case IMAGE_BLOCK: r = free_and_strdup(&arg_image, i->path); - else + break; + + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: r = free_and_strdup(&arg_directory, i->path); + break; + + case IMAGE_MSTACK: + r = free_and_strdup(&arg_mstack, i->path); + break; + + default: + assert_not_reached(); + } if (r < 0) return log_oom(); @@ -3114,31 +3163,40 @@ static int determine_names(void) { return log_error_errno(r, "Failed to determine current directory: %m"); } - if (!arg_directory && !arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine path, please use -D or -i."); + if (!arg_directory && !arg_image && !arg_mstack) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine path, please use --directory=, --image= or --mstack=."); } if (!arg_machine) { - if (arg_directory && path_equal(arg_directory, "/")) { - arg_machine = gethostname_malloc(); - if (!arg_machine) - return log_oom(); + if (arg_directory) { + if (path_equal(arg_directory, "/")) { + arg_machine = gethostname_malloc(); + if (!arg_machine) + return log_oom(); + } else { + r = path_extract_filename(arg_directory, &arg_machine); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); + } } else if (arg_image) { - char *e; - r = path_extract_filename(arg_image, &arg_machine); if (r < 0) return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_image); /* Truncate suffix if there is one */ - e = endswith(arg_machine, ".raw"); + char *e = endswith(arg_machine, ".raw"); if (e) *e = 0; - } else { - r = path_extract_filename(arg_directory, &arg_machine); + } else if (arg_mstack) { + r = path_extract_filename(arg_mstack, &arg_machine); if (r < 0) - return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); - } + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_mstack); + + char *e = endswith(arg_machine, ".mstack"); + if (e) + *e = 0; + } else + assert_not_reached(); hostname_cleanup(arg_machine); if (!hostname_is_valid(arg_machine, 0)) @@ -3873,6 +3931,7 @@ static int outer_child( const char *directory, int mount_fd, DissectedImage *dissected_image, + MStack *mstack, int fd_outer_socket, int fd_inner_socket, FDSet *fds, @@ -3928,6 +3987,7 @@ static int outer_child( if (mount_fd >= 0) { assert(arg_directory); assert(!arg_image); + assert(!arg_mstack); if (move_mount(mount_fd, "", AT_FDCWD, directory, MOVE_MOUNT_F_EMPTY_PATH) < 0) return log_error_errno(errno, "Failed to attach root directory: %m"); @@ -3938,6 +3998,7 @@ static int outer_child( } else if (dissected_image) { assert(!arg_directory); assert(arg_image); + assert(!arg_mstack); /* If we are operating on a disk image, then mount its root directory now, but leave out the * rest. We can read the UID shift from it if we need to. Further down we'll mount the rest, @@ -3955,9 +4016,38 @@ static int outer_child( (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0)); if (r < 0) return r; + + } else if (arg_mstack) { + assert(!arg_directory); + assert(!arg_image); + assert(arg_mstack); + + MStackFlags mstack_flags = arg_read_only ? MSTACK_RDONLY : 0; + + /* This creates the needed overlayfs or tmpfs, owned by our target userns. Note that we pass + * the target mount dir as temporary mount dir here. We after all just need some dir here + * that definitely exists, and the temporary mounts on it are not going to be visible + * outside. */ + r = mstack_make_mounts( + mstack, + /* temp_mount_dir= */ directory, /* !! */ + mstack_flags); + if (r < 0) + return log_error_errno(r, "Failed to make .mstack/ mounts: %m"); + + /* And then attaches all mounts to the directory */ + r = mstack_bind_mounts( + mstack, + directory, + /* where_fd= */ -EBADF, + mstack_flags, + /* ret_root_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed bind mount .mstack/ mounts: %m"); } else { assert(arg_directory); assert(!arg_image); + assert(!arg_mstack); r = mount_nofollow_verbose(LOG_ERR, arg_directory, directory, /* fstype= */ NULL, MS_BIND|MS_REC, /* options= */ NULL); if (r < 0) @@ -5023,6 +5113,10 @@ static int load_settings(void) { r = file_in_same_dir(arg_directory, arg_settings_filename, &p); if (r < 0 && r != -EADDRNOTAVAIL) /* if directory is root fs, don't complain */ return log_error_errno(r, "Failed to generate settings path from directory path: %m"); + } else if (arg_mstack) { + r = file_in_same_dir(arg_mstack, arg_settings_filename, &p); + if (r < 0) + return log_error_errno(r, "Failed to generate settings path from mstack path: %m"); } if (p) { @@ -5073,6 +5167,7 @@ static int run_container( const char *directory, int mount_fd, DissectedImage *dissected_image, + MStack *mstack, int userns_fd, FDSet *fds, char veth_name[IFNAMSIZ], @@ -5223,6 +5318,7 @@ static int run_container( directory, mount_fd, dissected_image, + mstack, fd_outer_socket_pair[1], fd_inner_socket_pair[1], fds, @@ -5347,7 +5443,13 @@ static int run_container( } else { _cleanup_free_ char *host_ifname = NULL; - r = nsresource_add_netif_veth(userns_fd, child_netns_fd, /* namespace_ifname= */ NULL, &host_ifname, /* ret_namespace_ifname= */ NULL); + r = nsresource_add_netif_veth( + /* vl= */ NULL, + 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"); @@ -5932,8 +6034,10 @@ static int run(int argc, char *argv[]) { _cleanup_(rm_rf_subvolume_and_freep) char *snapshot_dir = NULL; _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; + _cleanup_(mstack_freep) MStack *mstack = NULL; _cleanup_(sd_netlink_unrefp) sd_netlink *nfnl = NULL; _cleanup_(pidref_done) PidRef pid = PIDREF_NULL; + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL, *mountfsd_link = NULL; log_setup(); @@ -6036,13 +6140,28 @@ static int run(int argc, char *argv[]) { if (arg_userns_mode == USER_NAMESPACE_MANAGED) { /* Let's allocate a 64K userns first, if managed mode is chosen */ + r = nsresource_connect(&nsresource_link); + if (r < 0) { + log_error_errno(r, "Failed to connect to nsresourced: %m"); + goto finish; + } + + r = mountfsd_connect(&mountfsd_link); + if (r < 0) { + log_error_errno(r, "Failed to connect to mountsd: %m"); + goto finish; + } + _cleanup_free_ char *userns_name = NULL; if (asprintf(&userns_name, "nspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0) { r = log_oom(); goto finish; } - userns_fd = nsresource_allocate_userns(userns_name, NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ + userns_fd = nsresource_allocate_userns( + nsresource_link, + userns_name, + NSRESOURCE_UIDS_64K); /* allocate 64K UIDs */ if (userns_fd < 0) { r = log_error_errno(userns_fd, "Failed to allocate user namespace with 64K users: %m"); goto finish; @@ -6195,20 +6314,23 @@ static int run(int argc, char *argv[]) { } } - if (arg_userns_mode == USER_NAMESPACE_MANAGED) { + if (userns_fd >= 0) { r = mountfsd_mount_directory( + mountfsd_link, arg_directory, userns_fd, determine_dissect_image_flags(), &mount_fd); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to mount directory via mountfsd: %m"); goto finish; + } } - } else { + + } else if (arg_image) { DissectImageFlags dissect_image_flags = determine_dissect_image_flags(); - assert(arg_image); assert(!arg_template); r = chase_and_update(&arg_image, 0); @@ -6345,6 +6467,7 @@ static int run(int argc, char *argv[]) { goto finish; } else { r = mountfsd_mount_image( + mountfsd_link, arg_image, userns_fd, /* options= */ NULL, @@ -6352,8 +6475,10 @@ static int run(int argc, char *argv[]) { &arg_verity_settings, dissect_image_flags, &dissected_image); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to mount image via mountfsd: %m"); goto finish; + } } /* Now that we mounted the image, let's try to remove it again, if it is ephemeral */ @@ -6362,7 +6487,40 @@ static int run(int argc, char *argv[]) { if (arg_architecture < 0) arg_architecture = dissected_image_architecture(dissected_image); - } + + } else if (arg_mstack) { + assert(!arg_template); + assert(!arg_ephemeral); + + r = chase_and_update(&arg_mstack, CHASE_MUST_BE_DIRECTORY); + if (r < 0) + goto finish; + + if (!IN_SET(arg_userns_mode, USER_NAMESPACE_NO, USER_NAMESPACE_MANAGED)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--mstack= requires managed user namespacing, or user namespace turned off."); + + MStackFlags mstack_flags = arg_read_only ? MSTACK_RDONLY : 0; + r = mstack_load(arg_mstack, + /* dir_fd= */ -EBADF, + &mstack); + if (r < 0) { + log_error_errno(r, "Failed to load .mstack/ directory '%s': %m", arg_mstack); + goto finish; + } + + r = mstack_open_images( + mstack, + mountfsd_link, + userns_fd, + arg_image_policy, + /* image_filter= */ NULL, + mstack_flags); + if (r < 0) { + log_error_errno(r, "Failed to open .mstack/ layer images '%s': %m", arg_mstack); + goto finish; + } + } else + assert_not_reached(); /* Create a temporary place to mount stuff. */ r = mkdtemp_malloc("/tmp/nspawn-root-XXXXXX", &rootdir); @@ -6375,8 +6533,11 @@ static int run(int argc, char *argv[]) { if (r < 0) goto finish; + mountfsd_link = sd_varlink_unref(mountfsd_link); + nsresource_link = sd_varlink_unref(nsresource_link); + if (!arg_quiet) { - const char *t = arg_image ?: arg_directory; + const char *t = arg_mstack ?: arg_image ?: arg_directory; _cleanup_free_ char *u = NULL; (void) terminal_urlify_path(t, t, &u); @@ -6412,9 +6573,11 @@ static int run(int argc, char *argv[]) { rootdir, mount_fd, dissected_image, + mstack, userns_fd, fds, - veth_name, &veth_created, + veth_name, + &veth_created, &expose_args, &master, &pid, &ret); if (r <= 0) diff --git a/src/portable/portable.c b/src/portable/portable.c index 403a05005c..c86a6c2722 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -6,6 +6,7 @@ #include "sd-bus.h" #include "sd-messages.h" +#include "sd-varlink.h" #include "bus-common-errors.h" #include "bus-error.h" @@ -470,7 +471,7 @@ static int portable_extract_by_path( _cleanup_close_ int rfd = open(path, O_PATH|O_CLOEXEC); if (rfd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); struct stat st; if (fstat(rfd, &st) < 0) @@ -480,17 +481,25 @@ static int portable_extract_by_path( _cleanup_free_ char *image_name = NULL; r = path_extract_filename(path, &image_name); if (r < 0) - return log_error_errno(r, "Failed to extract image name from path '%s': %m", path); + return log_debug_errno(r, "Failed to extract image name from path '%s': %m", path); if (scope == RUNTIME_SCOPE_USER && uid_is_foreign(st.st_uid)) { - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); _cleanup_close_ int mfd = -EBADF; - r = mountfsd_mount_directory_fd(rfd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mfd); + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + rfd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &mfd); if (r < 0) - return log_debug_errno(r, "Failed to open '%s' via mountfsd: %m", path); + return r; _cleanup_close_pair_ int seq[2] = EBADF_PAIR; if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0) @@ -587,7 +596,7 @@ static int portable_extract_by_path( /* root_hash_path= */ NULL, /* root_hash_sig_path= */ NULL); if (r < 0) - return log_error_errno(r, "Failed to read verity artifacts for %s: %m", path); + return log_debug_errno(r, "Failed to read verity artifacts for %s: %m", path); if (verity.data_path) flags |= DISSECT_IMAGE_NO_PARTITION_TABLE; @@ -604,11 +613,15 @@ static int portable_extract_by_path( return log_debug_errno(r, "Failed to create temporary directory: %m"); if (scope == RUNTIME_SCOPE_USER) { - userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); r = mountfsd_mount_image_fd( + /* vl= */ NULL, rfd, userns_fd, /* options= */ NULL, @@ -1267,11 +1280,36 @@ void portable_changes_free(PortableChange *changes, size_t n_changes) { } static const char *root_setting_from_image(ImageType type) { - return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "RootDirectory=" : "RootImage="; + switch (type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + return "RootDirectory="; + + case IMAGE_RAW: + case IMAGE_BLOCK: + return "RootImage="; + + case IMAGE_MSTACK: + return "RootMStack="; + + default: + return NULL; + } } static const char *extension_setting_from_image(ImageType type) { - return IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "ExtensionDirectories=" : "ExtensionImages="; + switch (type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + return "ExtensionDirectories="; + + case IMAGE_RAW: + case IMAGE_BLOCK: + return "ExtensionImages="; + + default: + return NULL; + } } static int make_marker_text(const char *image_path, OrderedHashmap *extension_images, char **ret_text) { @@ -1401,6 +1439,8 @@ static int install_chroot_dropin( Image *ext; root_type = root_setting_from_image(type); + if (!root_type) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported as portable service.", image_type_to_string(type)); r = path_extract_filename(m->image_path ?: image_path, &base_name); if (r < 0) @@ -1453,15 +1493,19 @@ static int install_chroot_dropin( if (m->image_path && !path_equal(m->image_path, image_path)) ORDERED_HASHMAP_FOREACH(ext, extension_images) { - _cleanup_free_ char *extension_base_name = NULL; + const char *extension_setting = extension_setting_from_image(ext->type); + if (!extension_setting) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported for extensions: %m", image_type_to_string(ext->type)); + + _cleanup_free_ char *extension_base_name = NULL; r = path_extract_filename(ext->path, &extension_base_name); if (r < 0) return log_debug_errno(r, "Failed to extract basename from '%s': %m", ext->path); if (!strextend(&text, "\n", - extension_setting_from_image(ext->type), + extension_setting, ext->path, /* With --force tell PID1 to avoid enforcing that the image and * extension-release. have to match. */ @@ -1777,33 +1821,46 @@ static int install_image( if (flags & PORTABLE_MIXED_COPY_LINK) { if (scope == RUNTIME_SCOPE_USER) { - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m"); _cleanup_close_ int fd = open(image_path, O_DIRECTORY|O_CLOEXEC); if (fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", image_path); + return log_debug_errno(errno, "Failed to open '%s': %m", image_path); struct stat st; if (fstat(fd, &st) < 0) - return log_error_errno(errno, "Failed to stat '%s': %m", image_path); + return log_debug_errno(errno, "Failed to stat '%s': %m", image_path); + + _cleanup_(sd_varlink_unrefp) sd_varlink *mountfsd_link = NULL; + r = mountfsd_connect(&mountfsd_link); + if (r < 0) + return r; _cleanup_close_ int tree_fd = -EBADF; if (uid_is_foreign(st.st_uid)) { - r = mountfsd_mount_directory_fd(fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &tree_fd); + r = mountfsd_mount_directory_fd( + mountfsd_link, + fd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &tree_fd); if (r < 0) return r; } else tree_fd = TAKE_FD(fd); _cleanup_close_ int directory_fd = -EBADF; - r = mountfsd_make_directory(target, MODE_INVALID, /* flags= */ 0, &directory_fd); + r = mountfsd_make_directory(mountfsd_link, target, MODE_INVALID, /* flags= */ 0, &directory_fd); if (r < 0) return r; _cleanup_close_ int copy_fd = -EBADF; - r = mountfsd_mount_directory_fd(directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); + r = mountfsd_mount_directory_fd(mountfsd_link, directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, ©_fd); if (r < 0) return r; diff --git a/src/run/run.c b/src/run/run.c index 04e643b209..e614f27c28 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -3053,7 +3053,7 @@ static bool shall_make_executable_absolute(void) { if (!arg_root_directory && running_in_chroot() > 0) return false; - FOREACH_STRING(f, "RootDirectory=", "RootImage=", "ExecSearchPath=", "MountImages=", "ExtensionImages=") + FOREACH_STRING(f, "RootDirectory=", "RootImage=", "RootMStack=", "ExecSearchPath=", "MountImages=", "ExtensionImages=") if (strv_find_startswith(arg_property, f)) return false; diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 057a89b8ab..84de3478a7 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -2468,6 +2468,7 @@ static const BusProperty execute_properties[] = { { "SELinuxContext", bus_append_string }, { "RootImage", bus_append_string }, { "RootVerity", bus_append_string }, + { "RootMStack", bus_append_string }, { "RuntimeDirectoryPreserve", bus_append_string }, { "Personality", bus_append_string }, { "KeyringMode", bus_append_string }, diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 8352e5a821..4c0752c979 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -11,6 +11,7 @@ #include "sd-json.h" #include "sd-path.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "blockdev-util.h" @@ -36,6 +37,7 @@ #include "log.h" #include "loop-util.h" #include "mountpoint-util.h" +#include "mstack.h" #include "namespace-util.h" #include "nsresource.h" #include "nulstr-util.h" @@ -43,6 +45,7 @@ #include "path-lookup.h" #include "path-util.h" #include "process-util.h" +#include "recurse-dir.h" #include "rm-rf.h" #include "runtime-scope.h" #include "stat-util.h" @@ -478,12 +481,67 @@ static int image_make( on_mount_id = _mnt_id; if (S_ISDIR(st->st_mode)) { - unsigned file_attr = 0; - usec_t crtime = 0; if (!ret) return 0; + if (endswith(path, ".mstack")) { + usec_t crtime = 0; + r = fd_getcrtime(fd, &crtime); + if (r < 0) + log_debug_errno(r, "Unable to read creation time of '%s', ignoring: %m", path); + + if (!pretty) { + r = extract_image_basename( + path, + image_class_suffix_to_string(c), + STRV_MAKE(".mstack"), + &pretty_buffer, + /* ret_suffix= */ NULL); + if (r < 0) + return r; + + pretty = pretty_buffer; + } + + _cleanup_(mstack_freep) MStack *mstack = NULL; + r = mstack_load(path, fd, &mstack); + if (r < 0) { + log_debug_errno(r, "Failed to load mstack '%s', ignoring: %m", path); + read_only = true; + } else if (!read_only) { + r = mstack_is_read_only(mstack); + if (r < 0) + log_debug_errno(r, "Failed to determine if mstack '%s' is read-only, assuming it is: %m", path); + + read_only = r != 0; + } + + r = image_new(IMAGE_MSTACK, + c, + pretty, + path, + read_only, + crtime, + /* mtime= */ 0, + fh, + on_mount_id, + (uint64_t) st->st_ino, + ret); + if (r < 0) + return r; + + if (mstack) { + r = mstack_is_foreign_uid_owned(mstack); + if (r < 0) + log_debug_errno(r, "Failed to determine if mstack '%s' is foreign UID owned, assuming it is not: %m", path); + if (r > 0) + (*ret)->foreign_uid_owned = true; + } + + return 0; + } + if (!pretty) { r = extract_image_basename( path, @@ -532,10 +590,12 @@ static int image_make( } /* Get directory creation time (not available everywhere, but that's OK */ + usec_t crtime = 0; (void) fd_getcrtime(fd, &crtime); /* If the IMMUTABLE bit is set, we consider the directory read-only. Since the ioctl is not * supported everywhere we ignore failures. */ + unsigned file_attr = 0; (void) read_attr_fd(fd, &file_attr); /* It's just a normal directory. */ @@ -767,7 +827,7 @@ static char** make_possible_filenames(ImageClass class, const char *image_name) assert(image_name); FOREACH_STRING(v_suffix, "", ".v") - FOREACH_STRING(format_suffix, "", ".raw") { + FOREACH_STRING(format_suffix, "", ".raw", ".mstack") { _cleanup_free_ char *j = NULL; const char *class_suffix; @@ -864,6 +924,12 @@ int image_find(RuntimeScope scope, log_debug("Ignoring non-regular file '%s' with .raw suffix.", fname); continue; } + } else if (endswith(fname, ".mstack")) { + + if (!S_ISDIR(st.st_mode)) { + log_debug("Ignoring non-directory '%s' with .mstack suffix.", fname); + continue; + } } else if (endswith(fname, ".v")) { @@ -1088,7 +1154,7 @@ int image_discover( r = extract_image_basename( nov, image_class_suffix_to_string(class), - STRV_MAKE(".raw", ""), + STRV_MAKE(".raw", ".mstack", ""), &pretty, &suffix); if (r < 0) { @@ -1139,7 +1205,7 @@ int image_discover( r = extract_image_basename( fname, image_class_suffix_to_string(class), - /* format_suffixes= */ NULL, + STRV_MAKE(".mstack", ""), &pretty, /* ret_suffix= */ NULL); if (r < 0) { @@ -1210,60 +1276,112 @@ int image_discover( return 0; } +static int unpriv_remove_cb( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + int r, userns_fd = PTR_TO_FD(userdata); + + assert(sx); + + if (event == RECURSE_DIR_ENTER && + S_ISDIR(sx->stx_mode) && + uid_is_foreign(sx->stx_uid)) { + + /* This is owned by the foreign UID range, and a dir, let's remove it via mountfsd userns + * shenanigans. */ + + _cleanup_close_ int tree_fd = -EBADF; + r = mountfsd_mount_directory_fd( + /* vl= */ NULL, + inode_fd, + userns_fd, + DISSECT_IMAGE_FOREIGN_UID, + &tree_fd); + if (r < 0) + return r; + + /* Fork off child that moves into userns and does the copying */ + r = pidref_safe_fork_full( + "rm-tree", + /* stdio_fds= */ NULL, + (int[]) { userns_fd, tree_fd, }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, + /* ret= */ NULL); + if (r < 0) + log_debug_errno(r, "Process that was supposed to remove subtree '%s' failed, ignoring: %m", empty_to_root(path)); + else if (r == 0) { + /* child */ + + r = namespace_enter( + /* pidns_fd= */ -EBADF, + /* mntns_fd= */ -EBADF, + /* netns_fd= */ -EBADF, + userns_fd, + /* root_fd= */ -EBADF); + if (r < 0) { + log_debug_errno(r, "Failed to join user namespace: %m"); + _exit(EXIT_FAILURE); + } + + _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); + if (dfd < 0) { + log_error_errno(r, "Failed to reopen tree fd: %m"); + _exit(EXIT_FAILURE); + } + + r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); + if (r < 0) { + log_error_errno(r, "Failed to empty '%s' directory in foreign UID mode: %m", empty_to_root(path)); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + + /* Don't descent further into this one, and delete it immediately */ + return RECURSE_DIR_UNLINK_GRACEFUL; + } + + /* Everything else try to remove */ + if (event == RECURSE_DIR_LEAVE) + return RECURSE_DIR_UNLINK_GRACEFUL; + + return RECURSE_DIR_CONTINUE; +} + static int unprivileged_remove(Image *i) { int r; assert(i); - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, /* size= */ NSRESOURCE_UIDS_64K); + /* We want this to work in complex .mstack/ hierarchies, where the main directory (and maybe a .v/ + * directory below or two) might be owned by the user themselves, but some subdirs might be owned by + * the foreign UID range. We deal with this by recursively descending down the tree, and removing + * foreign-owned ranges via userns shenanigans, and the rest just like that. */ + + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + /* size= */ NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate transient user namespace: %m"); - _cleanup_close_ int tree_fd = -EBADF; - r = mountfsd_mount_directory( + r = recurse_dir_at( + AT_FDCWD, i->path, - userns_fd, - DISSECT_IMAGE_FOREIGN_UID, - &tree_fd); + /* statx_mask= */ STATX_TYPE|STATX_UID, + /* n_depth_max= */ UINT_MAX, + RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SAME_MOUNT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL, + unpriv_remove_cb, + FD_TO_PTR(userns_fd)); if (r < 0) return r; - /* Fork off child that moves into userns and does the copying */ - r = pidref_safe_fork_full( - "rm-tree", - /* stdio_fds= */ NULL, - (int[]) { userns_fd, tree_fd, }, 2, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, - /* ret= */ NULL); - if (r < 0) - return log_debug_errno(r, "Process that was supposed to remove tree failed: %m"); - if (r == 0) { - /* child */ - - r = namespace_enter( - /* pidns_fd= */ -EBADF, - /* mntns_fd= */ -EBADF, - /* netns_fd= */ -EBADF, - userns_fd, - /* root_fd= */ -EBADF); - if (r < 0) { - log_debug_errno(r, "Failed to join user namespace: %m"); - _exit(EXIT_FAILURE); - } - - _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); - if (dfd < 0) { - log_error_errno(r, "Failed to reopen tree fd: %m"); - _exit(EXIT_FAILURE); - } - - r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); - if (r < 0) { - log_error_errno(r, "Failed to empty '%s' directory in foreign UID mode: %m", i->path); - _exit(EXIT_FAILURE); - } - - _exit(EXIT_SUCCESS); - } return 0; } @@ -1305,6 +1423,9 @@ int image_remove(Image *i, RuntimeScope scope) { /* Allow deletion of read-only directories */ (void) chattr_path(i->path, 0, FS_IMMUTABLE_FL); + _fallthrough_; + + case IMAGE_MSTACK: /* If this is foreign owned, try an unprivileged remove first, but accept if that doesn't work, and do it directly either way, maybe it works */ if (i->foreign_uid_owned) (void) unprivileged_remove(i); @@ -1559,13 +1680,22 @@ static int unprivileged_clone(Image *i, const char *new_path) { assert(i); assert(new_path); - _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, /* size= */ NSRESOURCE_UIDS_64K); + _cleanup_close_ int userns_fd = nsresource_allocate_userns( + /* vl= */ NULL, + /* name= */ NULL, + /* size= */ NSRESOURCE_UIDS_64K); if (userns_fd < 0) return log_debug_errno(userns_fd, "Failed to allocate transient user namespace: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = mountfsd_connect(&link); + if (r < 0) + return r; + /* Map original image */ _cleanup_close_ int tree_fd = -EBADF; r = mountfsd_mount_directory( + link, i->path, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -1576,6 +1706,7 @@ static int unprivileged_clone(Image *i, const char *new_path) { /* Make new image */ _cleanup_close_ int new_fd = -EBADF; r = mountfsd_make_directory( + link, new_path, MODE_INVALID, /* flags= */ 0, @@ -1586,6 +1717,7 @@ static int unprivileged_clone(Image *i, const char *new_path) { /* Mount new image */ _cleanup_close_ int target_fd = -EBADF; r = mountfsd_mount_directory_fd( + link, new_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -1593,6 +1725,8 @@ static int unprivileged_clone(Image *i, const char *new_path) { if (r < 0) return r; + link = sd_varlink_unref(link); + /* Fork off child that moves into userns and does the copying */ return copy_tree_at_foreign(tree_fd, target_fd, userns_fd); } @@ -2367,6 +2501,7 @@ static const char* const image_type_table[_IMAGE_TYPE_MAX] = { [IMAGE_SUBVOLUME] = "subvolume", [IMAGE_RAW] = "raw", [IMAGE_BLOCK] = "block", + [IMAGE_MSTACK] = "mstack", }; DEFINE_STRING_TABLE_LOOKUP(image_type, ImageType); diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index c2c1a5e93d..5dac81b9c5 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -11,6 +11,7 @@ typedef enum ImageType { IMAGE_SUBVOLUME, IMAGE_RAW, IMAGE_BLOCK, + IMAGE_MSTACK, _IMAGE_TYPE_MAX, _IMAGE_TYPE_INVALID = -EINVAL, } ImageType; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 4156903774..eb4b6bd609 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4958,6 +4958,7 @@ int verity_dissect_and_mount( return log_debug_errno(userns_fd, "Failed to open our own user namespace: %m"); r = mountfsd_mount_image( + /* vl= */ NULL, src_fd >= 0 ? FORMAT_PROC_FD_PATH(src_fd) : src, userns_fd, options, @@ -5125,7 +5126,30 @@ static void mount_image_reply_parameters_done(MountImageReplyParameters *p) { #endif +int mountfsd_connect(sd_varlink **ret) { + int r; + + assert(ret); + + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); + if (r < 0) + return log_debug_errno(r, "Failed to connect to mountfsd: %m"); + + r = sd_varlink_set_allow_fd_passing_input(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for read: %m"); + + r = sd_varlink_set_allow_fd_passing_output(vl, true); + if (r < 0) + return log_debug_errno(r, "Failed to enable varlink fd passing for write: %m"); + + *ret = TAKE_PTR(vl); + return 0; +} + int mountfsd_mount_image_fd( + sd_varlink *vl, int image_fd, int userns_fd, const MountOptions *options, @@ -5149,7 +5173,6 @@ int mountfsd_mount_image_fd( _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; _cleanup_close_ int verity_data_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; _cleanup_free_ char *ps = NULL; const char *error_id; int r; @@ -5157,48 +5180,45 @@ int mountfsd_mount_image_fd( assert(image_fd >= 0); assert(ret); - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_varlink_set_allow_fd_passing_input(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); - - r = sd_varlink_set_allow_fd_passing_output(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + vl = _vl; + } _cleanup_close_ int reopened_fd = -EBADF; image_fd = fd_reopen_condition(image_fd, O_CLOEXEC|O_NOCTTY|O_NONBLOCK|(FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY) ? O_RDONLY : O_RDWR), O_PATH, &reopened_fd); if (image_fd < 0) - return log_error_errno(image_fd, "Failed to reopen fd: %m"); + return log_debug_errno(image_fd, "Failed to reopen fd: %m"); r = sd_varlink_push_dup_fd(vl, image_fd); if (r < 0) - return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push image fd into varlink connection: %m"); if (userns_fd >= 0) { r = sd_varlink_push_dup_fd(vl, userns_fd); if (r < 0) - return log_error_errno(r, "Failed to push image fd into varlink connection: %m"); + return log_debug_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 to format image policy to string: %m"); + return log_debug_errno(r, "Failed to format image policy to string: %m"); } if (verity && verity->data_path) { verity_data_fd = open(verity->data_path, O_RDONLY|O_CLOEXEC); if (verity_data_fd < 0) - return log_error_errno(errno, "Failed to open verity data file '%s': %m", verity->data_path); + return log_debug_errno(errno, "Failed to open verity data file '%s': %m", verity->data_path); r = sd_varlink_push_dup_fd(vl, verity_data_fd); if (r < 0) - return log_error_errno(r, "Failed to push verity data fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push verity data fd into varlink connection: %m"); } _cleanup_(sd_json_variant_unrefp) sd_json_variant *mount_options = NULL; @@ -5220,7 +5240,7 @@ int mountfsd_mount_image_fd( /* ret_values= */ NULL, &filtered); if (r < 0) - return log_error_errno(r, "Failed to filter mount options: %m"); + return log_debug_errno(r, "Failed to filter mount options: %m"); if (isempty(filtered)) continue; @@ -5230,7 +5250,7 @@ int mountfsd_mount_image_fd( &mount_options, SD_JSON_BUILD_PAIR_STRING(partition_designator_to_string(i), filtered ?: o)); if (r < 0) - return log_error_errno(r, "Failed to build mount options array: %m"); + return log_debug_errno(r, "Failed to build mount options array: %m"); } sd_json_variant *reply = NULL; @@ -5256,7 +5276,7 @@ int mountfsd_mount_image_fd( r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); log_debug("Effective image policy: %s", p.image_policy); @@ -5289,7 +5309,7 @@ int mountfsd_mount_image_fd( r = sd_json_dispatch(i, partition_dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &pp); if (r < 0) - return log_error_errno(r, "Failed to parse partition data: %m"); + return log_debug_errno(r, "Failed to parse partition data: %m"); if (pp.fsmount_fd_idx != UINT_MAX) { fsmount_fd = sd_varlink_take_fd(vl, pp.fsmount_fd_idx); @@ -5302,11 +5322,11 @@ int mountfsd_mount_image_fd( if (!di) { r = dissected_image_new(/* path= */ NULL, &di); if (r < 0) - return log_error_errno(r, "Failed to allocated new dissected image structure: %m"); + return log_debug_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)); + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Duplicate partition data for '%s'.", partition_designator_to_string(pp.designator)); di->partitions[pp.designator] = (DissectedPartition) { .found = true, @@ -5337,6 +5357,7 @@ int mountfsd_mount_image_fd( } int mountfsd_mount_image( + sd_varlink *vl, const char *path, int userns_fd, const MountOptions *options, @@ -5352,10 +5373,10 @@ int mountfsd_mount_image( _cleanup_close_ int image_fd = open(path, O_RDONLY|O_CLOEXEC); if (image_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; - r = mountfsd_mount_image_fd(image_fd, userns_fd, options, image_policy, verity, flags, &di); + r = mountfsd_mount_image_fd(vl, image_fd, userns_fd, options, image_policy, verity, flags, &di); if (r < 0) return r; @@ -5370,6 +5391,7 @@ int mountfsd_mount_image( } int mountfsd_mount_directory_fd( + sd_varlink *vl, int directory_fd, int userns_fd, DissectImageFlags flags, @@ -5383,27 +5405,23 @@ int mountfsd_mount_directory_fd( /* Pick one identity, not both, that makes no sense. */ assert(!FLAGS_SET(flags, DISSECT_IMAGE_FOREIGN_UID|DISSECT_IMAGE_IDENTITY_UID)); - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_varlink_set_allow_fd_passing_input(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); - - r = sd_varlink_set_allow_fd_passing_output(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + vl = _vl; + } r = sd_varlink_push_dup_fd(vl, directory_fd); if (r < 0) - return log_error_errno(r, "Failed to push directory fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push directory fd into varlink connection: %m"); if (userns_fd >= 0) { r = sd_varlink_push_dup_fd(vl, userns_fd); if (r < 0) - return log_error_errno(r, "Failed to push user namespace fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push user namespace fd into varlink connection: %m"); } sd_json_variant *reply = NULL; @@ -5430,17 +5448,18 @@ int mountfsd_mount_directory_fd( unsigned fsmount_fd_idx = UINT_MAX; r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &fsmount_fd_idx); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); _cleanup_close_ int fsmount_fd = sd_varlink_take_fd(vl, fsmount_fd_idx); if (fsmount_fd < 0) - return log_error_errno(fsmount_fd, "Failed to take mount fd from Varlink connection: %m"); + return log_debug_errno(fsmount_fd, "Failed to take mount fd from Varlink connection: %m"); *ret_mount_fd = TAKE_FD(fsmount_fd); return 0; } int mountfsd_mount_directory( + sd_varlink *vl, const char *path, int userns_fd, DissectImageFlags flags, @@ -5451,12 +5470,13 @@ int mountfsd_mount_directory( _cleanup_close_ int directory_fd = open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC|O_PATH); if (directory_fd < 0) - return log_error_errno(errno, "Failed to open '%s': %m", path); + return log_debug_errno(errno, "Failed to open '%s': %m", path); - return mountfsd_mount_directory_fd(directory_fd, userns_fd, flags, ret_mount_fd); + return mountfsd_mount_directory_fd(vl, directory_fd, userns_fd, flags, ret_mount_fd); } int mountfsd_make_directory_fd( + sd_varlink *vl, int parent_fd, const char *name, mode_t mode, @@ -5468,22 +5488,18 @@ int mountfsd_make_directory_fd( assert(parent_fd >= 0); assert(name); - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; - r = sd_varlink_connect_address(&vl, "/run/systemd/io.systemd.MountFileSystem"); - if (r < 0) - return log_error_errno(r, "Failed to connect to mountfsd: %m"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = mountfsd_connect(&_vl); + if (r < 0) + return r; - r = sd_varlink_set_allow_fd_passing_input(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for read: %m"); - - r = sd_varlink_set_allow_fd_passing_output(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + vl = _vl; + } r = sd_varlink_push_dup_fd(vl, parent_fd); if (r < 0) - return log_error_errno(r, "Failed to push parent fd into varlink connection: %m"); + return log_debug_errno(r, "Failed to push parent fd into varlink connection: %m"); sd_json_variant *reply = NULL; const char *error_id = NULL; @@ -5507,11 +5523,11 @@ int mountfsd_make_directory_fd( unsigned directory_fd_idx = UINT_MAX; r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &directory_fd_idx); if (r < 0) - return log_error_errno(r, "Failed to parse MountImage() reply: %m"); + return log_debug_errno(r, "Failed to parse MountImage() reply: %m"); _cleanup_close_ int directory_fd = sd_varlink_take_fd(vl, directory_fd_idx); if (directory_fd < 0) - return log_error_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); + return log_debug_errno(directory_fd, "Failed to take directory fd from Varlink connection: %m"); if (ret_directory_fd) *ret_directory_fd = TAKE_FD(directory_fd); @@ -5519,6 +5535,7 @@ int mountfsd_make_directory_fd( } int mountfsd_make_directory( + sd_varlink *vl, const char *path, mode_t mode, DissectImageFlags flags, @@ -5529,18 +5546,18 @@ int mountfsd_make_directory( _cleanup_free_ char *parent = NULL; r = path_extract_directory(path, &parent); if (r < 0) - return log_error_errno(r, "Failed to extract parent directory from '%s': %m", path); + return log_debug_errno(r, "Failed to extract parent directory from '%s': %m", path); _cleanup_free_ char *dirname = NULL; r = path_extract_filename(path, &dirname); if (r < 0) - return log_error_errno(r, "Failed to extract directory name from '%s': %m", path); + return log_debug_errno(r, "Failed to extract directory name from '%s': %m", path); _cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC); if (fd < 0) - return log_error_errno(r, "Failed to open '%s': %m", parent); + return log_debug_errno(r, "Failed to open '%s': %m", parent); - return mountfsd_make_directory_fd(fd, dirname, mode, flags, ret_directory_fd); + return mountfsd_make_directory_fd(vl, fd, dirname, mode, flags, ret_directory_fd); } int copy_tree_at_foreign(int source_fd, int target_fd, int userns_fd) { @@ -5600,6 +5617,7 @@ int remove_tree_foreign(const char *path, int userns_fd) { _cleanup_close_ int tree_fd = -EBADF; r = mountfsd_mount_directory( + /* vl= */ NULL, path, userns_fd, DISSECT_IMAGE_FOREIGN_UID, @@ -5611,7 +5629,7 @@ int remove_tree_foreign(const char *path, int userns_fd) { "rm-tree", /* stdio_fds= */ NULL, (int[]) { userns_fd, tree_fd }, 2, - FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_REOPEN_LOG, /* ret= */ NULL); if (r < 0) return r; @@ -5625,19 +5643,19 @@ int remove_tree_foreign(const char *path, int userns_fd) { userns_fd, /* root_fd= */ -EBADF); if (r < 0) { - log_error_errno(r, "Failed to join user namespace: %m"); + log_debug_errno(r, "Failed to join user namespace: %m"); _exit(EXIT_FAILURE); } _cleanup_close_ int dfd = fd_reopen(tree_fd, O_DIRECTORY|O_CLOEXEC); if (dfd < 0) { - log_error_errno(r, "Failed to reopen tree fd: %m"); + log_debug_errno(r, "Failed to reopen tree fd: %m"); _exit(EXIT_FAILURE); } r = rm_rf_children(dfd, REMOVE_PHYSICAL|REMOVE_SUBVOLUME|REMOVE_CHMOD, /* root_dev= */ NULL); if (r < 0) - log_warning_errno(r, "Failed to empty '%s' directory in foreign UID mode, ignoring: %m", path); + log_debug_errno(r, "Failed to empty '%s' directory in foreign UID mode, ignoring: %m", path); _exit(EXIT_SUCCESS); } diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index aa8c6a2737..09d7db5952 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -269,13 +269,23 @@ static inline const char* dissected_partition_fstype(const DissectedPartition *m int get_common_dissect_directory(char **ret); -int mountfsd_mount_image_fd(int image_fd, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); -int mountfsd_mount_image(const char *path, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); -int mountfsd_mount_directory_fd(int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); -int mountfsd_mount_directory(const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); +int mountfsd_connect(sd_varlink **ret); -int mountfsd_make_directory_fd(int parent_fd, const char *name, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); -int mountfsd_make_directory(const char *path, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); +/* All the calls below take a 'link' parameter, that may be an already established Varlink connection object + * towards systemd-mountfsd, previously created via mountfsd_connect(). This serves two purposes: first of + * all allows more efficient resource usage, as this allows recycling already allocated resources for + * multiple calls. Secondly, the user credentials are pinned at time of mountfsd_connect(), and the caller + * hence can drop privileges afterwards while keeping open the connection and still execute relevant + * operations under the original identity, until the connection is closed. The 'link' parameter may be passed + * as NULL in which case a short-lived connection is created, just to execute the requested operation. */ + +int mountfsd_mount_image_fd(sd_varlink *vl, int image_fd, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_image(sd_varlink *vl, const char *path, int userns_fd, const MountOptions *options, const ImagePolicy *image_policy, const VeritySettings *verity, DissectImageFlags flags, DissectedImage **ret); +int mountfsd_mount_directory_fd(sd_varlink *vl, int directory_fd, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); +int mountfsd_mount_directory(sd_varlink *vl, const char *path, int userns_fd, DissectImageFlags flags, int *ret_mount_fd); + +int mountfsd_make_directory_fd(sd_varlink *vl, int parent_fd, const char *name, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); +int mountfsd_make_directory(sd_varlink *vl, const char *path, mode_t mode, DissectImageFlags flags, int *ret_directory_fd); int copy_tree_at_foreign(int source_fd, int target_fd, int userns_fd); int remove_tree_foreign(const char *path, int userns_fd); diff --git a/src/shared/import-util.c b/src/shared/import-util.c index 2644f59fda..35079a6537 100644 --- a/src/shared/import-util.c +++ b/src/shared/import-util.c @@ -125,6 +125,7 @@ int import_url_change_suffix( static const char* const import_type_table[_IMPORT_TYPE_MAX] = { [IMPORT_RAW] = "raw", [IMPORT_TAR] = "tar", + [IMPORT_OCI] = "oci", }; DEFINE_STRING_TABLE_LOOKUP(import_type, ImportType); diff --git a/src/shared/import-util.h b/src/shared/import-util.h index 2a594e1e61..a982d159ca 100644 --- a/src/shared/import-util.h +++ b/src/shared/import-util.h @@ -6,6 +6,7 @@ typedef enum ImportType { IMPORT_RAW, IMPORT_TAR, + IMPORT_OCI, _IMPORT_TYPE_MAX, _IMPORT_TYPE_INVALID = -EINVAL, } ImportType; diff --git a/src/shared/meson.build b/src/shared/meson.build index 8b49ad9a52..5becbf3b4f 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -132,6 +132,7 @@ shared_sources = files( 'module-util.c', 'mount-setup.c', 'mount-util.c', + 'mstack.c', 'net-condition.c', 'netif-naming-scheme.c', 'netif-sriov.c', diff --git a/src/shared/mstack.c b/src/shared/mstack.c new file mode 100644 index 0000000000..9cbc316319 --- /dev/null +++ b/src/shared/mstack.c @@ -0,0 +1,1203 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "chase.h" +#include "dissect-image.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "log.h" +#include "loop-util.h" +#include "macro.h" +#include "mount-util.h" +#include "mstack.h" +#include "path-util.h" +#include "process-util.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "sort-util.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "tmpfile-util.h" +#include "uid-classification.h" +#include "unit-name.h" +#include "vpick.h" + +static void mstack_mount_done(MStackMount *m) { + assert(m); + + m->where = mfree(m->where); + m->what = mfree(m->what); + m->what_fd = safe_close(m->what_fd); + m->mount_fd = safe_close(m->mount_fd); + m->sort_key = mfree(m->sort_key); + m->dissected_image = dissected_image_unref(m->dissected_image); +} + +static void mstack_done(MStack *mstack) { + assert(mstack); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + mstack_mount_done(m); + + mstack->mounts = mfree(mstack->mounts); + mstack->n_mounts = 0; + mstack->root_mount = NULL; + mstack->has_tmpfs_root = mstack->has_overlayfs = false; + mstack->path = mfree(mstack->path); + safe_close(mstack->root_mount_fd); + safe_close(mstack->usr_mount_fd); +} + +MStack *mstack_free(MStack *mstack) { + if (!mstack) + return NULL; + + mstack_done(mstack); + + return mfree(mstack); +} + +static int validate_prefix_name(const char *name, const char *prefix, char **ret_parameter) { + _cleanup_free_ char *p = NULL; + + assert(name); + assert(prefix); + + const char *a = startswith(name, prefix); + if (isempty(a)) { + if (ret_parameter) + *ret_parameter = NULL; + + return false; + } + + p = strdup(a); + if (!p) + return -ENOMEM; + + if (ret_parameter) + *ret_parameter = TAKE_PTR(p); + + return true; +} + +static MStackMount *mstack_find(MStack *mstack, MStackMountType t, const char *sort_key, const char *where) { + assert(mstack); + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (t >= 0 && m->mount_type != t) + continue; + + if (sort_key && !streq_ptr(m->sort_key, sort_key)) + continue; + + if (where && !path_equal(m->where, where)) + continue; + + return m; + } + + return NULL; +} + +static int mstack_load_one(MStack *mstack, const char *dir, int dir_fd, const char *fname) { + int r; + + assert(mstack); + assert(dir_fd >= 0); + assert(fname); + + _cleanup_close_ int what_fd = openat(dir_fd, fname, O_PATH|O_CLOEXEC); + if (what_fd < 0) + return log_debug_errno(errno, "Failed to open %s/%s: %m", dir, fname); + + struct stat st; + if (fstat(what_fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat %s/%s: %m", dir, fname); + + ImageType image_type = _IMAGE_TYPE_INVALID; + _cleanup_free_ char *what = NULL, *unsuffixed = NULL; + if (S_ISDIR(st.st_mode)) { + + const char *dotv = endswith(fname, ".v"); + if (dotv) { + const char *dotrawv = endswith(fname, ".raw.v"); + + PickFilter filter = { + .type_mask = dotrawv ? (1U << DT_REG) : ((1U << DT_DIR) | (1U << DT_BLK)), + .suffix = dotrawv ? ".raw" : NULL, + .architecture = _ARCHITECTURE_INVALID, + }; + + _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; + r = path_pick(dir, dir_fd, fname, &filter, /* n_filters= */ 1, PICK_ARCHITECTURE, &result); + if (r < 0) + return log_debug_errno(r, "Failed to resolve '%s' directory: %m", fname); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Found no suitable entry in '%s': %m", fname); + + what = TAKE_PTR(result.path); + close_and_replace(what_fd, result.fd); + st = result.st; + + unsuffixed = strndup(fname, (dotrawv ?: dotv) - fname); + if (!unsuffixed) + return log_oom(); + + image_type = S_ISDIR(st.st_mode) ? IMAGE_DIRECTORY : + S_ISREG(st.st_mode) ? IMAGE_RAW : + S_ISBLK(st.st_mode) ? IMAGE_BLOCK : _IMAGE_TYPE_INVALID; + + assert(image_type >= 0); + } else + image_type = IMAGE_DIRECTORY; + + } else if (S_ISREG(st.st_mode)) { + const char *e = endswith(fname, ".raw"); + if (!e) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected suffix of '%s/%s', refusing.", dir, fname); + + unsuffixed = strndup(fname, e - fname); + if (!unsuffixed) + return -ENOMEM; + + image_type = IMAGE_RAW; + + } else if (S_ISBLK(st.st_mode)) + image_type = IMAGE_BLOCK; + else + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unexpected inode type of '%s/%s', refusing.", dir, fname); + + if (!what) { + what = strdup(fname); + if (!what) + return -ENOMEM; + } + + if (!unsuffixed) { + unsuffixed = strdup(what); + if (!unsuffixed) + return -ENOMEM; + } + + if (!GREEDY_REALLOC(mstack->mounts, mstack->n_mounts+1)) + return -ENOMEM; + + MStackMount *m = mstack->mounts + mstack->n_mounts; + + _cleanup_free_ char *parameter = NULL; + r = validate_prefix_name(unsuffixed, "layer@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) { + /* Paranoia: let's refuse two layers that have the same sort key. Howe can that happen? + * People might have a .raw layer and one dir layer with the same name. Or one with .v and + * one without. */ + if (mstack_find(mstack, MSTACK_LAYER, parameter, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate layer '%s', refusing.", parameter); + + *m = (MStackMount) { + .mount_type = MSTACK_LAYER, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .sort_key = TAKE_PTR(parameter), + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack layer '%s' ('%s', owned by UID " UID_FMT ")", m->sort_key, m->what, st.st_uid); + return 0; + } + + if (streq(unsuffixed, "rw")) { + if (mstack_find(mstack, MSTACK_RW, /* sort_key= */ NULL, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate rw entry, refusing."); + + *m = (MStackMount) { + .mount_type = MSTACK_RW, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack rw layer ('%s')", m->what); + return 0; + } + + MStackMountType bind_type = _MSTACK_MOUNT_TYPE_INVALID; + r = validate_prefix_name(unsuffixed, "bind@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) + bind_type = MSTACK_BIND; + else { + r = validate_prefix_name(unsuffixed, "robind@", ¶meter); + if (r < 0) + return log_debug_errno(r, "Failed to check prefix of %s/%s: %m", dir, fname); + if (r > 0) + bind_type = MSTACK_ROBIND; + } + if (bind_type >= 0) { + _cleanup_free_ char *where = NULL; + r = unit_name_path_unescape(parameter, &where); + if (r < 0) + return log_debug_errno(r, "Cannot unescape path '%s' of '%s/%s'", parameter, dir, fname); + + if (mstack_find(mstack, MSTACK_BIND, /* sort_key= */ NULL, /* where= */ where) || + mstack_find(mstack, MSTACK_ROBIND, /* sort_key= */ NULL, /* where= */ where)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate bind entry, refusing"); + + *m = (MStackMount) { + .mount_type = bind_type, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .where = TAKE_PTR(where), + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack bind layer '%s' ('%s')", empty_to_root(m->where), m->what); + return 0; + } + + if (streq(unsuffixed, "root")) { + if (mstack_find(mstack, MSTACK_ROOT, /* sort_key= */ NULL, /* where= */ NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Duplicate root entry, refusing"); + + *m = (MStackMount) { + .mount_type = MSTACK_ROOT, + .what = TAKE_PTR(what), + .what_fd = TAKE_FD(what_fd), + .mount_fd = -EBADF, + .image_type = image_type, + }; + + mstack->n_mounts++; + log_debug("Found mstack root layer ('%s')", m->what); + return 0; + } + + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Unrecognized entry '%s/%s', refusing", dir, fname); +} + +static int mount_compare_func(const MStackMount *a, const MStackMount *b) { + int r; + + assert(a); + assert(b); + + /* If we apply this mstack in read-only mode then we'll convert the 'rw' layer which normally is an + * upperdir into the topmost lowerdir. When sorting the mstack it is hence essential, that the "rw" + * layer ends up *after* the regular layers. Enforce this here via a compile-time check. */ + assert_cc(MSTACK_RW > MSTACK_LAYER); + + r = CMP(a->mount_type, b->mount_type); + if (r != 0) + return r; + + r = path_compare(a->where, b->where); + if (r != 0) + return r; + + r = strverscmp_improved(a->sort_key, b->sort_key); + if (r != 0) + return r; + + return 0; +} + +static void mstack_remove(MStack *mstack, MStackMountType t) { + assert(mstack); + + size_t z = 0; + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + if (m->mount_type == t) + mstack_mount_done(m); + else + mstack->mounts[z++] = *m; + } + + mstack->n_mounts = z; +} + +static int mstack_normalize(MStack *mstack) { + int r; + + assert(mstack); + + typesafe_qsort(mstack->mounts, mstack->n_mounts, mount_compare_func); + + size_t n_layers = 0; + bool has_rw = false, has_root_bind = false, has_usr_bind = false, has_root = false; + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + switch (m->mount_type) { + case MSTACK_LAYER: + n_layers++; + break; + + case MSTACK_RW: + assert(!has_rw); + has_rw = true; + break; + + case MSTACK_BIND: + case MSTACK_ROBIND: + if (empty_or_root(m->where)) + has_root_bind = true; + else if (path_equal(m->where, "/usr")) + has_usr_bind = true; + break; + + case MSTACK_ROOT: + assert(!has_root); + has_root = true; + break; + + default: + assert_not_reached(); + } + } + + /* If the overlayfs stack is fully obstructed, kill it */ + if (has_root_bind || (has_root && has_usr_bind)) { + mstack_remove(mstack, MSTACK_LAYER); + mstack_remove(mstack, MSTACK_RW); + + n_layers = 0; + has_rw = false; + } + + /* Only a single read-only or read-write layer? Turn into bind mount! */ + if (n_layers + has_rw == 1) { + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + if (m->mount_type == MSTACK_LAYER) + m->mount_type = MSTACK_ROBIND; + else if (m->mount_type == MSTACK_RW) + m->mount_type = MSTACK_BIND; + else + continue; + + if (has_root) { + /* If there's a root dir, let's only bind mount the /usr/ subdir */ + _cleanup_close_ int usr_fd = openat(m->what_fd, "usr", O_CLOEXEC|O_PATH|O_NOFOLLOW|O_DIRECTORY); + if (usr_fd < 0) + return log_debug_errno(errno, "Failed to open /usr/ subdir: %m"); + + _cleanup_free_ char *usr = path_join(m->what, "usr"); + if (!usr) + return log_oom(); + + r = free_and_strdup_warn(&m->where, "/usr"); + if (r < 0) + return r; + + close_and_replace(m->what_fd, usr_fd); + free_and_replace(m->what, usr); + } else { + r = free_and_strdup_warn(&m->where, "/"); + if (r < 0) + return r; + + has_root_bind = true; + } + } + + n_layers = 0; + has_rw = false; + } + + /* If the root dir is overmounted, we can drop the original root */ + if (has_root_bind) { + mstack_remove(mstack, MSTACK_ROOT); + has_root = false; + } + + /* After converting, let's sort things again */ + typesafe_qsort(mstack->mounts, mstack->n_mounts, mount_compare_func); + + /* Find root mount (unless it's the overlayfs stack) */ + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if ((m->mount_type == MSTACK_ROOT) || + (IN_SET(m->mount_type, MSTACK_BIND, MSTACK_ROBIND) && empty_or_root(m->where))) { + assert(!mstack->root_mount); + mstack->root_mount = m; + } + assert((has_root || has_root_bind) == !!mstack->root_mount); + + mstack->has_tmpfs_root = n_layers == 0 && !has_rw && !has_root_bind && !has_root; + mstack->has_overlayfs = n_layers > 0 || has_rw; + return 0; +} + +static int mstack_load_now(MStack *mstack, const char *dir, int dir_fd, MStackFlags flags) { + _cleanup_close_ int _dir_fd = -EBADF; + int r; + + assert(mstack); + + r = free_and_strdup_warn(&mstack->path, dir); + if (r < 0) + return r; + + /* Expects dir_fd already opened. If not, then we'll open it based on 'dir' */ + if (dir_fd < 0) { + _dir_fd = openat(AT_FDCWD, isempty(dir) ? "." : dir, O_DIRECTORY|O_CLOEXEC); + if (_dir_fd < 0) + return log_debug_errno(errno, "Failed to to open '%s': %m", dir); + + dir_fd = _dir_fd; + } else { + /* Possibly convert an O_PATH fd to a real one */ + dir_fd = fd_reopen_condition(dir_fd, O_DIRECTORY|O_CLOEXEC, O_PATH|O_DIRECTORY, &_dir_fd); + if (dir_fd < 0) + return log_debug_errno(dir_fd, "Failed to reopen '%s': %m", dir); + } + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(dir_fd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return r; + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + r = mstack_load_one(mstack, dir, dir_fd, (*i)->d_name); + if (r < 0) + return r; + } + + return mstack_normalize(mstack); +} + +static int mount_get_fd(MStackMount *m) { + assert(m); + + if (m->dissected_image) { + assert(m->dissected_image->partitions[PARTITION_ROOT].found); + return ASSERT_FD(m->dissected_image->partitions[PARTITION_ROOT].fsmount_fd); + } + + if (m->mount_fd >= 0) + return m->mount_fd; + + return m->what_fd; +} + +static bool mount_is_ro(MStackMount *m, MStackFlags flags) { + assert(m); + + return FLAGS_SET(flags, MSTACK_RDONLY) || + IN_SET(m->mount_type, MSTACK_LAYER, MSTACK_ROBIND); +} + +static const char *mount_name(MStackMount *m) { + assert(m); + + /* Returns some vaguely useful identifier for this layer, for showing in debug output */ + + if (m->sort_key) + return m->sort_key; + + if (m->where) + return m->where; + + return mstack_mount_type_to_string(m->mount_type); +} + +int mstack_open_images( + MStack *mstack, + sd_varlink *mountfsd_link, + int userns_fd, + const ImagePolicy *image_policy, + const ImageFilter *image_filter, + MStackFlags flags) { + + int r; + + assert(mstack); + + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (userns_fd >= 0 && !mountfsd_link) { + /* User a single connection for all mounts */ + r = mountfsd_connect(&_vl); + if (r < 0) + return r; + + mountfsd_link = _vl; + } + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + DissectImageFlags dissect_image_flags = + DISSECT_IMAGE_DISCARD| + DISSECT_IMAGE_GENERIC_ROOT| + DISSECT_IMAGE_REQUIRE_ROOT| + DISSECT_IMAGE_MOUNT_ROOT_ONLY| + DISSECT_IMAGE_FSCK| + DISSECT_IMAGE_USR_NO_ROOT| + DISSECT_IMAGE_GROWFS| + DISSECT_IMAGE_ADD_PARTITION_DEVICES| + DISSECT_IMAGE_PIN_PARTITION_DEVICES| + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY; + + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_READ_ONLY, mount_is_ro(m, flags)); + SET_FLAG(dissect_image_flags, DISSECT_IMAGE_FOREIGN_UID, userns_fd >= 0); + + switch (m->image_type) { + + case IMAGE_RAW: + case IMAGE_BLOCK: + assert(!m->dissected_image); + + if (userns_fd >= 0) { + r = mountfsd_mount_image_fd( + mountfsd_link, + m->what_fd, + userns_fd, + /* options= */ NULL, + image_policy, + /* verity= */ NULL, + dissect_image_flags, + &m->dissected_image); + if (r < 0) + return r; + } else { + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(dissected_image_unrefp) DissectedImage *dissected_image = NULL; + + r = loop_device_make( + m->what_fd, + FLAGS_SET(flags, MSTACK_RDONLY) ? O_RDONLY : O_RDWR, + /* offset= */ 0, + /* size= */ UINT64_MAX, + /* sector_size= */ UINT32_MAX, + LO_FLAGS_PARTSCAN, + LOCK_SH, + &loop_device); + if (r < 0) + return log_debug_errno(r, "Failed to allocate loopback device for '%s': %m", m->what); + + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + r = dissect_loop_device_and_warn( + loop_device, + &verity, + /* mount_options= */ NULL, + image_policy, + image_filter, + dissect_image_flags, + &dissected_image); + if (r < 0) + return r; + + if (!dissected_image->partitions[PARTITION_ROOT].found) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Currently images withot root partition are not supported: %m"); + + r = dissected_image_load_verity_sig_partition( + dissected_image, + loop_device->fd, + &verity); + if (r < 0) + return log_debug_errno(r, "Failed to load Verity signature partition of '%s': %m", m->what); + + r = dissected_image_guess_verity_roothash( + dissected_image, + &verity); + if (r < 0) + return log_debug_errno(r, "Failed to guess Verity root hash of '%s': %m", m->what); + + r = dissected_image_decrypt( + dissected_image, + /* root= */ NULL, + /* passphrase= */ NULL, + &verity, + image_policy, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to decrypt image '%s': %m", m->what); + + r = dissected_image_mount( + dissected_image, + /* where= */ NULL, /* allocate as mount fds, do not attach anywhere */ + /* uid_shift= */ UID_INVALID, + /* uid_range= */ UID_INVALID, + /* userns_fd = */ -EBADF, + dissect_image_flags); + if (r < 0) + return log_debug_errno(r, "Failed to mount image '%s': %m", m->what); + + r = loop_device_flock(loop_device, LOCK_UN); + if (r < 0) + return log_debug_errno(r, "Failed to unlock loopback block device: %m"); + + r = dissected_image_relinquish(dissected_image); + if (r < 0) + return log_debug_errno(r, "Failed to relinquish DM and loopback block devices: %m"); + + m->dissected_image = TAKE_PTR(dissected_image); + } + + log_debug("Acquired mstack DDI layer '%s'", mount_name(m)); + break; + + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + assert(m->mount_fd < 0); + + if (userns_fd >= 0) { + r = mountfsd_mount_directory_fd( + mountfsd_link, + m->what_fd, + userns_fd, + dissect_image_flags, + &m->mount_fd); + if (r < 0) + return r; + } else { + m->mount_fd = open_tree_attr_with_fallback( + mount_get_fd(m), + /* path= */ "", + OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = mount_is_ro(m, flags) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = mount_is_ro(m, flags) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_PRIVATE, /* disconnect us from bind mount source */ + }); + if (m->mount_fd < 0) + return log_debug_errno(m->mount_fd, "Failed to create bind mount inode '%s': %m", m->where); + } + + log_debug("Acquired bind mount for layer '%s'.", mount_name(m)); + break; + + default: + assert_not_reached(); + } + } + + return 0; +} + +static int mstack_has_writable_layers(MStack *mstack, MStackFlags flags) { + assert(mstack); + + if (FLAGS_SET(flags, MSTACK_RDONLY)) + return false; + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if (m->mount_type == MSTACK_RW) + return true; + + return false; +} + +static int fsconfig_add_layer(int sb_fd, const char *key, int layer_fd) { + int r; + + assert(sb_fd >= 0); + assert(key); + assert(layer_fd >= 0); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *pretty = NULL; + (void) fd_get_path(layer_fd, &pretty); + log_debug("Adding '%s' as layer '%s' to overlayfs.", key, pretty); + } + + r = RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_FD, key, /* value= */ NULL, layer_fd)); + if (r != -EBADF && !ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return r; + + /* overlayfs learnt support for FSCONFIG_SET_FD only with linux 6.13, hence provide a fallback here via /proc/self/fd/ */ + + // FIXME: This compatibility code path shall be removed once kernel 6.13 + // becomes the new minimal baseline + + const char *layer_path = FORMAT_PROC_FD_PATH(layer_fd); + log_debug_errno(r, "FSCONFIG_SET_FD for layer '%s' failed, falling back to FSCONFIG_SET with '%s': %m", key, layer_path); + return RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_STRING, key, layer_path, /* aux= */ 0)); +} + +static int mstack_make_overlayfs( + MStack *mstack, + const char *temp_mount_dir, + MStackFlags flags, + int *ret_overlayfs_mnt_fd) { + + int r; + + assert(mstack); + assert(temp_mount_dir); + assert(ret_overlayfs_mnt_fd); + + if (!mstack->has_overlayfs) { + *ret_overlayfs_mnt_fd = -EBADF; + return 0; + } + + bool writable = mstack_has_writable_layers(mstack, flags); + + _cleanup_close_ int sb_fd = fsopen("overlay", FSOPEN_CLOEXEC); + if (sb_fd < 0) + return log_debug_errno(errno, "Failed to create overlayfs: %m"); + + _cleanup_close_pair_ int errno_pipe_fds[2] = EBADF_PAIR; + if (pipe2(errno_pipe_fds, O_CLOEXEC) < 0) + return log_debug_errno(errno, "Failed to open pipe: %m"); + + /* If we operate unpriv, we have to attach the layers to a place in the fs, before we can pass them + * to overlayfs (see comments below), hence fork off a child with a private mount namespace, so that + * noone else sees that. */ + r = pidref_safe_fork("(layerfd)", + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_REOPEN_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, + /* ret= */ NULL); + if (r < 0) { + errno_pipe_fds[1] = safe_close(errno_pipe_fds[1]); + + int q = read_errno(errno_pipe_fds[0]); + if (q < 0 && q != -EIO) + return q; + + return r; + } + if (r == 0) { + /* child */ + + /* Kernel expects the stack in reverse order, hence go from back to front */ + for (size_t i = mstack->n_mounts; i > 0; i--) { + MStackMount *m = mstack->mounts + i - 1; + + if (!IN_SET(m->mount_type, MSTACK_RW, MSTACK_LAYER)) + continue; + + /* overlayfs refuses to work with layers on mounts not owned by our userns, hence create a + * clone that is owned by our userns */ + _cleanup_close_ int cloned_fd = mount_fd_clone(ASSERT_FD(mount_get_fd(m)), /* recursive= */ false, /* replacement_fd= */ NULL); + if (cloned_fd < 0) + report_errno_and_exit(errno_pipe_fds[1], cloned_fd); + + /* When working with detached mounts overlayfs (which requires kernel 6.14) currently + * insists on upperdir being the root inode of the mount. But that collides with the + * requirement that upperdir/workdir are on the same mount and siblings. Bummer. To + * work around this we'll temporarily attach the thing, which relaxes the rules + * sufficiently. */ + if (move_mount(cloned_fd, "", -EBADF, temp_mount_dir, MOVE_MOUNT_F_EMPTY_PATH) < 0) + report_errno_and_exit(errno_pipe_fds[1], -errno); + + /* Open the layer immediately after attaching */ + _cleanup_close_ int temp_fd = open(temp_mount_dir, O_PATH|O_CLOEXEC); + if (temp_fd < 0) + report_errno_and_exit(errno_pipe_fds[1], -errno); + + switch (m->mount_type) { + + case MSTACK_RW: { + if (mount_is_ro(m, flags)) { + /* If invoked in read-only mode we'll not create the data dir, but use it if it exists */ + _cleanup_close_ int data_fd = openat(temp_fd, "data", O_CLOEXEC|O_NOFOLLOW|O_DIRECTORY); + if (data_fd < 0) { + if (errno == ENOENT) /* If the 'data' dir doesn't exist, just skip + * over it, it apparently was never created, but + * that's fine for a read-only invocation */ + break; + + log_debug_errno(errno, "Failed to open 'data' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* Downgrade to regular lowerdir if read-only is requested */ + r = fsconfig_add_layer(sb_fd, "lowerdir+", data_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer lowerdir+=%s/data: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + } else { + /* If invoked in writable mode, let's create the data dir if it is missing */ + _cleanup_close_ int data_fd = open_mkdir_at(temp_fd, "data", O_CLOEXEC|O_NOFOLLOW, 0755); + if (data_fd < 0) { + log_debug_errno(data_fd, "Failed to open 'data' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], data_fd); + } + + r = fsconfig_add_layer(sb_fd, "upperdir", data_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer upperdir=%s/data: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + /* Similar, create the work directory */ + _cleanup_close_ int work_fd = open_mkdir_at(temp_fd, "work", O_CLOEXEC|O_NOFOLLOW, 0755); + if (work_fd < 0) { + log_debug_errno(work_fd, "Failed to open 'work' directory below 'rw' layer: %m"); + report_errno_and_exit(errno_pipe_fds[1], work_fd); + } + + /* rm_rf_children() takes possession of the fd no matter what, let's dup it here */ + int dup_fd = fcntl(work_fd, F_DUPFD_CLOEXEC, 3); + if (dup_fd < 0) { + log_debug_errno(errno, "Failed to duplicate work fd: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* Empty the work directory, just in case it existed before. It's supposed to be empty. */ + r = rm_rf_children(dup_fd, REMOVE_PHYSICAL, /* root_dev= */ NULL); + if (r < 0) + log_debug_errno(r, "Failed to empty 'work' directory below 'rw' layer, ignoring: %m"); + + r = fsconfig_add_layer(sb_fd, "workdir", work_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer workdir=%s/work: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + break; + } + break; + } + + case MSTACK_LAYER: + r = fsconfig_add_layer(sb_fd, "lowerdir+", temp_fd); + if (r < 0) { + log_debug_errno(r, "Failed to set mount layer lowerdir+=%s: %m", m->what); + report_errno_and_exit(errno_pipe_fds[1], r); + } + + break; + + default: + break; + } + } + + if (!writable && fsconfig(sb_fd, FSCONFIG_SET_FLAG, "ro", /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set read-only mount flag: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + if (fsconfig(sb_fd, FSCONFIG_SET_FLAG, "userxattr", /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set userxattr mount flag: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "source", mstack->path, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to set mount source: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + /* This is where the superblock is materialized. It must be called from the child's + * namespace, where the mounts are attached as described above, otherwise overlayfs is + * unhappy and will refuse the superblock to be created. */ + if (fsconfig(sb_fd, FSCONFIG_CMD_CREATE, /* key= */ NULL, /* value= */ NULL, /* aux= */ 0) < 0) { + log_debug_errno(errno, "Failed to realize overlayfs: %m"); + report_errno_and_exit(errno_pipe_fds[1], -errno); + } + + report_errno_and_exit(errno_pipe_fds[1], 0); + } + + _cleanup_close_ int overlayfs_mnt_fd = fsmount(sb_fd, FSMOUNT_CLOEXEC, 0); + if (overlayfs_mnt_fd < 0) + return log_debug_errno(errno, "Failed to create mount fd: %m"); + + if (mount_setattr(overlayfs_mnt_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = writable ? 0 : MOUNT_ATTR_RDONLY, + .attr_clr = writable ? MOUNT_ATTR_RDONLY : 0, + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark root bind mount read-only: %m"); + + *ret_overlayfs_mnt_fd = TAKE_FD(overlayfs_mnt_fd); + return 1; +} + +int mstack_make_mounts( + MStack *mstack, + const char *temp_mount_dir, + MStackFlags flags) { + + int r; + + assert(mstack); + assert(temp_mount_dir); + + _cleanup_close_ int overlayfs_mnt_fd = -EBADF; + r = mstack_make_overlayfs(mstack, temp_mount_dir, flags, &overlayfs_mnt_fd); + if (r < 0) + return r; + if (r > 0) + log_debug("Acquired mstack overlayfs mount."); + + assert(mstack->root_mount_fd < 0); + if (mstack->root_mount) { + assert(!mstack->has_tmpfs_root); + + mstack->root_mount_fd = fcntl(mount_get_fd(mstack->root_mount), F_DUPFD_CLOEXEC, 3); + if (mstack->root_mount_fd < 0) + return log_debug_errno(errno, "Failed to create root bind mount: %m"); + + log_debug("Acquired mstack root bind mount."); + + } else if (mstack->has_tmpfs_root) { + _cleanup_close_ int sb_fd = fsopen("tmpfs", FSOPEN_CLOEXEC); + if (sb_fd < 0) + return log_debug_errno(errno, "Failed to create tmpfs: %m"); + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "source", mstack->path, 0) < 0) + return log_debug_errno(errno, "Failed to set mount source: %m"); + + if (fsconfig(sb_fd, FSCONFIG_SET_STRING, "mode", "0755", 0) < 0) + return log_debug_errno(errno, "Failed to set mount source: %m"); + + if (fsconfig(sb_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0) < 0) + return log_debug_errno(errno, "Failed to realize tmpfs: %m"); + + mstack->root_mount_fd = fsmount(sb_fd, FSMOUNT_CLOEXEC, 0); + if (mstack->root_mount_fd < 0) + return log_debug_errno(errno, "Failed to create mount fd: %m"); + + log_debug("Acquired root tmpfs mount."); + } + + if (mstack->root_mount_fd >= 0 && overlayfs_mnt_fd >= 0) { + /* If we have an overlayfs and a root fs, then the overlayfs should be placed on /usr/. */ + mstack->usr_mount_fd = open_tree(overlayfs_mnt_fd, "usr", OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW); + if (mstack->usr_mount_fd < 0) + return log_debug_errno(errno, "Failed to create bind mount inode '/usr/': %m"); + + if (mount_setattr(mstack->usr_mount_fd, "", AT_EMPTY_PATH, + &(struct mount_attr) { + .attr_set = mount_is_ro(mstack->root_mount, flags) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = mount_is_ro(mstack->root_mount, flags) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_PRIVATE, /* disconnect us from bind mount source */ + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark usr bind mount read-only: %m"); + + log_debug("Acquired mstack overlayfs '/usr/' submount."); + } + + /* If we acquired no other root fs, then the overlayfs is our root */ + if (mstack->root_mount_fd < 0) + mstack->root_mount_fd = TAKE_FD(overlayfs_mnt_fd); + + return 0; +} + +int mstack_bind_mounts( + MStack *mstack, + const char *where, + int where_fd, + MStackFlags flags, + int *ret_root_fd) { + + int r; + + assert(mstack); + + _cleanup_close_ int _where_fd = -EBADF; + if (where_fd == AT_FDCWD) { + _where_fd = open(".", O_CLOEXEC|O_PATH|O_DIRECTORY); + if (_where_fd < 0) + return log_debug_errno(errno, "Failed to open current working directory: %m"); + where_fd = _where_fd; + } else if (where_fd < 0) { + r = chase(where, + /* root= */ NULL, + (FLAGS_SET(flags, MSTACK_MKDIR) ? CHASE_MKDIR_0755 : 0)|CHASE_MUST_BE_DIRECTORY, + /* ret_path= */ NULL, + &_where_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open '%s': %m", where); + + where_fd = _where_fd; + } + + assert(mstack->root_mount_fd >= 0); + if (move_mount(mstack->root_mount_fd, "", where_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach mstack root mount to '%s': %m", where); + + log_debug("Attached mstack root mount to '%s'.", where); + + _cleanup_close_ int root_fd = open(where, O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW); + if (root_fd < 0) + return log_debug_errno(errno, "Failed to mount root mount '%s': %m", where); + + if (mstack->usr_mount_fd >= 0) { + _cleanup_close_ int subdir_fd = -EBADF; + r = chaseat(root_fd, "usr", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open mount point inode '%s': %m", where); + + if (move_mount(mstack->usr_mount_fd, "", subdir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach bind mount to '/usr/' subdir: %m"); + + log_debug("Attached mstack '/usr/' mount to '%s/usr/'.", where); + } + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (!IN_SET(m->mount_type, MSTACK_BIND, MSTACK_ROBIND) || + m == mstack->root_mount) + continue; + + assert(m->mount_fd >= 0); + + _cleanup_close_ int subdir_fd = -EBADF; + r = chaseat(root_fd, m->where, CHASE_AT_RESOLVE_IN_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &subdir_fd); + if (r < 0) + return log_debug_errno(r, "Failed to open mount point inode '%s': %m", m->where); + + if (move_mount(m->mount_fd, "", subdir_fd, "", MOVE_MOUNT_F_EMPTY_PATH|MOVE_MOUNT_T_EMPTY_PATH) < 0) + return log_debug_errno(errno, "Failed to attach bind mount to '%s' subdir: %m", m->where); + + log_debug("Attached mstack '%s/' mount to '%s/%s/'.", m->where, where, m->where); + } + + /* If we have a tmpfs root, the above might have created mount point inodes. Hence we left the tmpfs + * writable for that. Let's fix that now. Also, let's enable propagation for the future. (Reminder: + * we disconnect propagation from the host, but we *want* propagation by default for everything + * created further down the tree. Hence we'll set MS_SHARED here right-away.) */ + if (mount_setattr(root_fd, "", AT_EMPTY_PATH|AT_RECURSIVE, + &(struct mount_attr) { + .attr_set = FLAGS_SET(flags, MSTACK_RDONLY) ? MOUNT_ATTR_RDONLY : 0, + .attr_clr = FLAGS_SET(flags, MSTACK_RDONLY) ? 0 : MOUNT_ATTR_RDONLY, + .propagation = MS_SHARED, + }, sizeof(struct mount_attr)) < 0) + return log_debug_errno(errno, "Failed to mark root bind mount read-only: %m"); + + if (ret_root_fd) + *ret_root_fd = TAKE_FD(root_fd); + + return 0; +} + +int mstack_apply( + const char *dir, + int dir_fd, + const char *where, + const char *temp_mount_dir, + sd_varlink *link, + int userns_fd, + const ImagePolicy *image_policy, + const ImageFilter *image_filter, + MStackFlags flags, + int *ret_root_fd) { + int r; + + assert(where); + + _cleanup_(mstack_done) MStack mstack = MSTACK_INIT; + r = mstack_load_now(&mstack, dir, dir_fd, flags); + if (r < 0) + return r; + + r = mstack_open_images(&mstack, link, userns_fd, image_policy, image_filter, flags); + if (r < 0) + return r; + + _cleanup_(rmdir_and_freep) char *t = NULL; + if (!temp_mount_dir) { + r = mkdtemp_malloc("/tmp/mstack-temporary-XXXXXX", &t); + if (r < 0) + return r; + + temp_mount_dir = t; + } + + r = mstack_make_mounts(&mstack, temp_mount_dir, flags); + if (r < 0) + return r; + + return mstack_bind_mounts(&mstack, where, /* where_fd= */ -EBADF, flags, ret_root_fd); +} + +int mstack_load(const char *dir, + int dir_fd, + MStack **ret) { + + int r; + + /* Well-known errors: + * + * -ENOTUNIQ → Multiple conflicting layers for the same path defined + * -EBADMSG → Bad file suffix, inode type for layer, or unrecognized entry + */ + + MStack *mstack = new(MStack, 1); + if (!mstack) + return -ENOMEM; + + *mstack = MSTACK_INIT; + + r = mstack_load_now(mstack, dir, dir_fd, /* flags= */ 0); + if (r < 0) + return r; + + if (ret) + *ret = TAKE_PTR(mstack); + + return 0; +} + +int mstack_is_read_only(MStack *mstack) { + assert(mstack); + + /* Checks if the mstack consists of only read-only layers and bind mounts */ + + if (mstack->has_tmpfs_root) + return false; + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) + if (IN_SET(m->mount_type, MSTACK_ROOT, MSTACK_RW, MSTACK_BIND)) + return false; + + return true; +} + +int mstack_is_foreign_uid_owned(MStack *mstack) { + int r; + + assert(mstack); + + /* Checks if any of the layers are owned by the host's foreign UID range */ + + FOREACH_ARRAY(m, mstack->mounts, mstack->n_mounts) { + + if (!IN_SET(m->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) + continue; + + assert(m->what_fd >= 0); + + struct stat st; + if (fstat(m->what_fd, &st) < 0) + return -errno; + + r = stat_verify_directory(&st); + if (r < 0) + return r; + + if (uid_is_foreign(st.st_uid)) + return true; + } + + return false; +} + +static const char *const mstack_mount_type_table[] = { + [MSTACK_ROOT] = "root", + [MSTACK_LAYER] = "layer", + [MSTACK_RW] = "rw", + [MSTACK_BIND] = "bind", + [MSTACK_ROBIND] = "robind", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(mstack_mount_type, MStackMountType); diff --git a/src/shared/mstack.h b/src/shared/mstack.h new file mode 100644 index 0000000000..b71ff86940 --- /dev/null +++ b/src/shared/mstack.h @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "discover-image.h" +#include "shared-forward.h" + +typedef enum MStackFlags { + MSTACK_MKDIR = 1 << 0, /* when mounting, create top-level inode to mount on top */ + MSTACK_RDONLY = 1 << 1, +} MStackFlags; + +typedef enum MStackMountType { + MSTACK_ROOT, /* optional "root" entry used as root, with the layer@/rw layers only used for /usr/ */ + MSTACK_LAYER, /* "layer@…" entries that are the lower (read-only) layers of an overlayfs stack */ + MSTACK_RW, /* "rw" entry that is the upper (writable) layer of an overlayfs stack (contains two subdirs: 'data' + 'work') */ + MSTACK_BIND, /* "bind@…" entries that are (writable) bind mounted on top of the overlayfs */ + MSTACK_ROBIND, /* "robind@…" similar, but read-only */ + _MSTACK_MOUNT_TYPE_MAX, + _MSTACK_MOUNT_TYPE_INVALID = -EINVAL, +} MStackMountType; + +typedef struct MStackMount { + MStackMountType mount_type; + char *what; + int what_fd; + int mount_fd; + char *sort_key; + char *where; + ImageType image_type; + DissectedImage *dissected_image; +} MStackMount; + +typedef struct MStack { + char *path; + MStackMount *mounts; + size_t n_mounts; + bool has_tmpfs_root; /* If true, we need a throw-away tmpfs as root */ + bool has_overlayfs; /* Indicates whether we need overlayfs (i.e. if there are more than a single layer */ + MStackMount *root_mount; /* If there's a MOUNT_BIND/MOUNT_ROBIND/MOUNT_ROOT mount, this points to it */ + int root_mount_fd; + int usr_mount_fd; +} MStack; + +#define MSTACK_INIT \ + (MStack) { \ + .root_mount_fd = -EBADF, \ + .usr_mount_fd = -EBADF, \ + } + +MStack *mstack_free(MStack *mstack); +DEFINE_TRIVIAL_CLEANUP_FUNC(MStack*, mstack_free); + +int mstack_load(const char *dir, int dir_fd, MStack **ret); +int mstack_open_images(MStack *mstack, sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags); +int mstack_make_mounts(MStack *mstack, const char *temp_mount_dir, MStackFlags flags); +int mstack_bind_mounts(MStack *mstack, const char *where, int where_fd, MStackFlags flags, int *ret_root_fd); + +/* The four calls above in one */ +int mstack_apply(const char *dir, int dir_fd, const char *where, const char *temp_mount_dir, sd_varlink *mountfsd_link, int userns_fd, const ImagePolicy *image_policy, const ImageFilter *image_filter, MStackFlags flags, int *ret_root_fd); + +int mstack_is_read_only(MStack *mstack); +int mstack_is_foreign_uid_owned(MStack *mstack); + +DECLARE_STRING_TABLE_LOOKUP_TO_STRING(mstack_mount_type, MStackMountType); diff --git a/src/shared/nsresource.c b/src/shared/nsresource.c index 31d96bad7c..615f99eff1 100644 --- a/src/shared/nsresource.c +++ b/src/shared/nsresource.c @@ -57,8 +57,25 @@ static int make_pid_name(char **ret) { return 0; } -int nsresource_allocate_userns(const char *name, uint64_t size) { +int nsresource_connect(sd_varlink **ret) { + int r; + + assert(ret); + _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; + r = sd_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 = sd_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"); + + *ret = TAKE_PTR(vl); + return 0; +} + +int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size) { _cleanup_close_ int userns_fd = -EBADF; _cleanup_free_ char *_name = NULL; const char *error_id; @@ -77,13 +94,14 @@ int nsresource_allocate_userns(const char *name, uint64_t size) { if (size <= 0 || size > UINT64_C(0x100000000)) /* Note: the server actually only allows allocating 1 or 64K right now */ return -EINVAL; - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd = userns_acquire_empty(); if (userns_fd < 0) @@ -113,8 +131,7 @@ int nsresource_allocate_userns(const char *name, uint64_t size) { return TAKE_FD(userns_fd); } -int nsresource_register_userns(const char *name, int userns_fd) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_register_userns(sd_varlink *vl, const char *name, int userns_fd) { _cleanup_close_ int _userns_fd = -EBADF; _cleanup_free_ char *_name = NULL; const char *error_id; @@ -138,13 +155,14 @@ int nsresource_register_userns(const char *name, int userns_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -169,8 +187,7 @@ int nsresource_register_userns(const char *name, int userns_fd) { return 0; } -int nsresource_add_mount(int userns_fd, int mount_fd) { - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_add_mount(sd_varlink *vl, int userns_fd, int mount_fd) { _cleanup_close_ int _userns_fd = -EBADF; int r, userns_fd_idx, mount_fd_idx; const char *error_id; @@ -185,21 +202,22 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_varlink_set_allow_fd_passing_output(vl, true); - if (r < 0) - return log_error_errno(r, "Failed to enable varlink fd passing for write: %m"); + vl = _vl; + } userns_fd_idx = sd_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"); + return log_debug_errno(userns_fd_idx, "Failed to push userns fd into varlink connection: %m"); mount_fd_idx = sd_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"); + return log_debug_errno(mount_fd_idx, "Failed to push mount fd into varlink connection: %m"); sd_json_variant *reply = NULL; r = sd_varlink_callbo( @@ -210,19 +228,18 @@ int nsresource_add_mount(int userns_fd, int mount_fd) { SD_JSON_BUILD_PAIR_UNSIGNED("userNamespaceFileDescriptor", userns_fd_idx), SD_JSON_BUILD_PAIR_UNSIGNED("mountFileDescriptor", mount_fd_idx)); if (r < 0) - return log_error_errno(r, "Failed to call AddMountToUserNamespace() varlink call: %m"); + return log_debug_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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding mount to registration."); return 0; } if (error_id) - return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to mount image: %s", error_id); + return log_debug_errno(sd_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_(sd_varlink_unrefp) sd_varlink *vl = NULL; +int nsresource_add_cgroup(sd_varlink *vl, int userns_fd, int cgroup_fd) { _cleanup_close_ int _userns_fd = -EBADF; int r, userns_fd_idx, cgroup_fd_idx; const char *error_id; @@ -237,13 +254,14 @@ int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -264,7 +282,7 @@ int nsresource_add_cgroup(int userns_fd, int cgroup_fd) { 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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding cgroup to registration."); return 0; } if (error_id) @@ -287,6 +305,7 @@ static void interface_params_done(InterfaceParams *p) { } int nsresource_add_netif_veth( + sd_varlink *vl, int userns_fd, int netns_fd, const char *namespace_ifname, @@ -294,7 +313,6 @@ int nsresource_add_netif_veth( char **ret_namespace_ifname) { _cleanup_close_ int _userns_fd = -EBADF, _netns_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r, userns_fd_idx, netns_fd_idx; const char *error_id; @@ -314,13 +332,14 @@ int nsresource_add_netif_veth( netns_fd = _netns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } userns_fd_idx = sd_varlink_push_dup_fd(vl, userns_fd); if (userns_fd_idx < 0) @@ -343,7 +362,7 @@ int nsresource_add_netif_veth( 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."); + log_debug("User namespace has not been allocated via namespace resource registry, not adding network to registration."); return 0; } if (error_id) @@ -368,11 +387,11 @@ int nsresource_add_netif_veth( } int nsresource_add_netif_tap( + sd_varlink *vl, int userns_fd, char **ret_host_ifname) { _cleanup_close_ int _userns_fd = -EBADF; - _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; int r, userns_fd_idx; const char *error_id; @@ -384,13 +403,14 @@ int nsresource_add_netif_tap( userns_fd = _userns_fd; } - r = sd_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"); + _cleanup_(sd_varlink_unrefp) sd_varlink *_vl = NULL; + if (!vl) { + r = nsresource_connect(&_vl); + if (r < 0) + return r; - r = sd_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"); + vl = _vl; + } r = sd_varlink_set_allow_fd_passing_input(vl, true); if (r < 0) diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h index 136b1f85ac..93957a10c8 100644 --- a/src/shared/nsresource.h +++ b/src/shared/nsresource.h @@ -7,9 +7,19 @@ #define NSRESOURCE_UIDS_64K 0x10000U #define NSRESOURCE_UIDS_1 1U -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_veth(int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); -int nsresource_add_netif_tap(int userns_fd, char **ret_host_ifname); +int nsresource_connect(sd_varlink **ret); + +/* All the calls below take a 'link' parameter, that may be an already established Varlink connection object + * towards systemd-nsresourced, previously created via nsresource_connect(). This serves two purposes: first + * of all allows more efficient resource usage, as this allows recycling already allocated resources for + * multiple calls. Secondly, the user credentials are pinned at time of nsresource_connect(), and the caller + * hence can drop privileges afterwards while keeping open the connection and still execute relevant + * operations under the original identity, until the connection is closed. The 'link' parameter may be passed + * as NULL in which case a short-lived connection is created, just to execute the requested operation. */ + +int nsresource_allocate_userns(sd_varlink *vl, const char *name, uint64_t size); +int nsresource_register_userns(sd_varlink *vl, const char *name, int userns_fd); +int nsresource_add_mount(sd_varlink *vl, int userns_fd, int mount_fd); +int nsresource_add_cgroup(sd_varlink *vl, int userns_fd, int cgroup_fd); +int nsresource_add_netif_veth(sd_varlink *vl, int userns_fd, int netns_fd, const char *namespace_ifname, char **ret_host_ifname, char **ret_namespace_ifname); +int nsresource_add_netif_tap(sd_varlink *vl, int userns_fd, char **ret_host_ifname); diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index f35287746b..09ce806ad2 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -53,6 +53,7 @@ typedef struct Condition Condition; typedef struct ConfigSection ConfigSection; typedef struct ConfigTableItem ConfigTableItem; typedef struct CPUSet CPUSet; +typedef struct DissectedImage DissectedImage; typedef struct DnsAnswer DnsAnswer; typedef struct DnsPacket DnsPacket; typedef struct DnsQuestion DnsQuestion; @@ -64,6 +65,7 @@ typedef struct FDSet FDSet; typedef struct Fido2HmacSalt Fido2HmacSalt; typedef struct GroupRecord GroupRecord; typedef struct Image Image; +typedef struct ImageFilter ImageFilter; typedef struct ImagePolicy ImagePolicy; typedef struct InstallChange InstallChange; typedef struct InstallInfo InstallInfo; @@ -72,6 +74,7 @@ typedef struct LoopDevice LoopDevice; typedef struct MachineBindUserContext MachineBindUserContext; typedef struct MachineCredentialContext MachineCredentialContext; typedef struct MountOptions MountOptions; +typedef struct MStack MStack; typedef struct OpenFile OpenFile; typedef struct Pkcs11EncryptedKey Pkcs11EncryptedKey; typedef struct Table Table; diff --git a/src/shared/tar-util.c b/src/shared/tar-util.c index 27f3212fb3..17c03e910b 100644 --- a/src/shared/tar-util.c +++ b/src/shared/tar-util.c @@ -147,8 +147,8 @@ static int open_inode_finalize(OpenInode *of) { /* We adjust the UID/GID right before the mode, since doing this might affect the mode (drops * suid/sgid bits). * - * We adjust the mode only when leaving a dir, because if we are unpriv we might lose the - * ability to enter it once we do this. */ + * We adjust the mode only when leaving a dir, because if we are unprivileged we might lose + * the ability to enter it once we do this. */ if (uid_is_valid(of->uid) || gid_is_valid(of->gid) || of->mode != MODE_INVALID) { k = fchmod_and_chown_with_fallback(of->fd, /* path= */ NULL, of->mode, of->uid, of->gid); @@ -277,6 +277,91 @@ static int archive_unpack_regular( return TAKE_FD(fd); } +static int overlayfs_fsetfattr( + const char *path, /* purely decorative, for log purposes */ + int fd, + const char *name, /* xattr key name */ + const char *value /* xattr value */) { + int r; + + assert(fd >= 0); + assert(path); + assert(name); + assert(value); + + /* overlayfs knows magic {user|trusted}.overlay.* xattrs for whiteouts and opaque directories. The + * 'user.overlay.*' ones are only checked if overlayfs is mounted with "userxattr". We only set that + * one because we want to operate unprivileged. Ideally, we'd set both here, to maximize the chance + * that things work both in privileged and unprivileged scenarios, but unfortunately this has the + * effect that the privileged ones are ignored (and visible in the overlayfs mount). */ + _cleanup_free_ char *n = strjoin("user.overlay.", name); + if (!n) + return log_oom(); + + r = xsetxattr(fd, /* path= */ NULL, AT_EMPTY_PATH, n, value); + if (r < 0) + return log_error_errno(r, "Failed to set '%s' xattr on file '%s': %m", n, path); + + return 0; +} + +static int archive_unpack_whiteout( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path, /* Full path of 'parent_fd', purely decorative for log purposes */ + const char *filename, /* Just the filename we are supposed to whiteout */ + const char *path /* Full path of the whiteout file, purely decorative for log purposes */) { + + int r; + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + assert(filename); + assert(path); + + _cleanup_free_ char *tmp = NULL; + _cleanup_close_ int fd = open_tmpfile_linkable_at(parent_fd, filename, O_CLOEXEC|O_WRONLY, &tmp); + if (fd < 0) + return log_error_errno(fd, "Failed to create whiteout file for '%s': %m", path); + + CLEANUP_TMPFILE_AT(parent_fd, tmp); + + r = overlayfs_fsetfattr(path, fd, "whiteout", "y"); + if (r < 0) + return r; + + /* As per https://docs.kernel.org/filesystems/overlayfs.html also mark the parent */ + r = overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "x"); + if (r < 0) + return r; + + r = link_tmpfile_at(fd, parent_fd, tmp, filename, LINK_TMPFILE_REPLACE); + if (r < 0) + return log_error_errno(r, "Failed to install regular file '%s': %m", path); + + tmp = mfree(tmp); /* disarm CLEANUP_TMPFILE_AT */ + return 0; /* we do not return an fd here, because this kills an inode, and doesn't synthesize one */ +} + +static int archive_unpack_opaque( + struct archive *a, + struct archive_entry *entry, + int parent_fd, + const char *parent_path) { + + assert(a); + assert(entry); + assert(parent_fd >= 0); + assert(parent_path); + + /* we do not return an fd here either */ + + return overlayfs_fsetfattr(parent_path, parent_fd, "opaque", "y"); +} + static int archive_unpack_directory( struct archive *a, struct archive_entry *entry, @@ -908,15 +993,45 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { switch (filetype) { case S_IFREG: + if (FLAGS_SET(flags, TAR_OCI_WHITEOUTS)) { + if (streq(e, ".wh..wh..opq")) { + r = archive_unpack_opaque(a, entry, parent_fd, empty_to_root(parent_path)); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + + const char *w = startswith(e, ".wh."); + if (w) { + if (!filename_is_valid(w)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Invalid whiteout file entry '%s', refusing.", e); + + r = archive_unpack_whiteout(a, entry, parent_fd, empty_to_root(parent_path), w, j); + if (r < 0) + return r; + + /* NB: this does not create an inode! */ + break; + } + } + fd = archive_unpack_regular(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFDIR: fd = archive_unpack_directory(a, entry, parent_fd, e, j, fflags); + if (fd < 0) + return fd; break; case S_IFLNK: fd = archive_unpack_symlink(a, entry, parent_fd, e, j); + if (fd < 0) + return fd; break; case S_IFCHR: @@ -924,6 +1039,8 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { case S_IFIFO: case S_IFSOCK: fd = archive_unpack_special_inode(a, entry, parent_fd, e, j, filetype); + if (fd < 0) + return fd; break; default: @@ -931,9 +1048,6 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Unexpected file type %i of '%s', refusing.", (int) filetype, j); } - if (fd < 0) - return fd; - } else { /* This is some intermediary node in the path that we haven't opened yet. Create it with default attributes */ fd = open_mkdir_at(parent_fd, e, O_CLOEXEC, 0700); @@ -949,22 +1063,24 @@ int tar_x(int input_fd, int tree_fd, TarFlags flags) { * fully done with the inode (i.e. after creating further inodes inside of dir inodes * for example), due to permission problems this might create or that the mtime * changes we do might still be affected by our changes. */ - open_inodes[n_open_inodes++] = (OpenInode) { - .fd = TAKE_FD(fd), - .path = TAKE_PTR(j), - .filetype = filetype, - .mode = mode, - .mtime = mtime, - .uid = uid, - .gid = gid, - .fflags = fflags, - .acl_access = TAKE_PTR(acl_access), - .acl_default = TAKE_PTR(acl_default), - .xattr = TAKE_PTR(xa), - .n_xattr = n_xa, - }; + if (fd >= 0) { + open_inodes[n_open_inodes++] = (OpenInode) { + .fd = TAKE_FD(fd), + .path = TAKE_PTR(j), + .filetype = filetype, + .mode = mode, + .mtime = mtime, + .uid = uid, + .gid = gid, + .fflags = fflags, + .acl_access = TAKE_PTR(acl_access), + .acl_default = TAKE_PTR(acl_default), + .xattr = TAKE_PTR(xa), + .n_xattr = n_xa, + }; - n_xa = 0; + n_xa = 0; + } } } diff --git a/src/shared/tar-util.h b/src/shared/tar-util.h index 10c60be8a8..4ce8c57377 100644 --- a/src/shared/tar-util.h +++ b/src/shared/tar-util.h @@ -4,6 +4,7 @@ typedef enum TarFlags { TAR_SELINUX = 1 << 0, /* Include SELinux xattr in tarball, or unpack it */ TAR_SQUASH_UIDS_ABOVE_64K = 1 << 1, /* Squash UIDs/GIDs above 64K when packing/unpacking to the nobody user */ + TAR_OCI_WHITEOUTS = 1 << 2, /* Turn OCI/aufs whiteout inodes into overlayfs whiteouts */ } TarFlags; int tar_x(int input_fd, int tree_fd, TarFlags flags); diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c index a7d56bb5e9..a008b506e9 100644 --- a/src/shared/varlink-io.systemd.Unit.c +++ b/src/shared/varlink-io.systemd.Unit.c @@ -409,6 +409,8 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD(RootDirectory, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootImage="), SD_VARLINK_DEFINE_FIELD(RootImage, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootMStack="), + SD_VARLINK_DEFINE_FIELD(RootMStack, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootImageOptions="), SD_VARLINK_DEFINE_FIELD_BY_TYPE(RootImageOptions, PartitionMountOptions, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("https://www.freedesktop.org/software/systemd/man"PROJECT_VERSION_STR"systemd.exec.html#RootEphemeral="), diff --git a/src/shared/vpick.c b/src/shared/vpick.c index 69e8be1b16..38ceb225cd 100644 --- a/src/shared/vpick.c +++ b/src/shared/vpick.c @@ -798,3 +798,11 @@ const PickFilter pick_filter_image_dir[1] = { .architecture = _ARCHITECTURE_INVALID, }, }; + +const PickFilter pick_filter_image_mstack[1] = { + { + .type_mask = UINT32_C(1) << DT_DIR, + .architecture = _ARCHITECTURE_INVALID, + .suffix = ".mstack", + }, +}; diff --git a/src/shared/vpick.h b/src/shared/vpick.h index 1ebcd5c8ab..55596d24df 100644 --- a/src/shared/vpick.h +++ b/src/shared/vpick.h @@ -64,8 +64,10 @@ int path_uses_vpick(const char *path); extern const PickFilter pick_filter_image_raw[1]; extern const PickFilter pick_filter_image_dir[1]; +extern const PickFilter pick_filter_image_mstack[1]; #define pick_filter_image_any (const PickFilter[]) { \ pick_filter_image_raw[0], \ + pick_filter_image_mstack[0], \ pick_filter_image_dir[0], \ } diff --git a/src/test/meson.build b/src/test/meson.build index 3ca614db36..adbcd3c0d4 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -149,6 +149,7 @@ simple_tests += files( 'test-mkdir.c', 'test-modhex.c', 'test-mountpoint-util.c', + 'test-mstack.c', 'test-namespace-util.c', 'test-net-naming-scheme.c', 'test-notify-recv.c', diff --git a/src/test/test-mstack.c b/src/test/test-mstack.c new file mode 100644 index 0000000000..7c370fcbe9 --- /dev/null +++ b/src/test/test-mstack.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#include "capability-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "fs-util.h" +#include "mountpoint-util.h" +#include "mstack.h" +#include "path-util.h" +#include "process-util.h" +#include "rm-rf.h" +#include "tests.h" +#include "tmpfile-util.h" +#include "virt.h" + +static bool overlayfs_set_fd_lowerdir_plus_supported(void) { + int r; + + _cleanup_close_ int sb_fd = fsopen("overlay", FSOPEN_CLOEXEC); + if (sb_fd < 0 && (ERRNO_IS_NOT_SUPPORTED(errno) || errno == ENODEV)) + return false; + ASSERT_OK_ERRNO(sb_fd); + + _cleanup_close_ int layer_fd = open("/", O_DIRECTORY|O_CLOEXEC); + ASSERT_OK_ERRNO(layer_fd); + + r = RET_NERRNO(fsconfig(sb_fd, FSCONFIG_SET_FD, "lowerdir+", /* value= */ NULL, layer_fd)); + if (r < 0 && (ERRNO_IS_NEG_NOT_SUPPORTED(r) || r == -EINVAL)) + return false; + + ASSERT_OK_ERRNO(r); + return true; +} + +TEST(mstack) { + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_close_ int tfd = -EBADF; + int r; + + tfd = mkdtemp_open("/tmp/mstack-what-XXXXXX", O_PATH, &t); + ASSERT_OK(tfd); + + ASSERT_OK_ERRNO(mkdirat(tfd, "rw", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "rw/data", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "rw/data/check1", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/check2", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/zzz", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@1", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@1/check3", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "layer@0/yyy", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "bind@zzz", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "bind@zzz/check4", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "robind@yyy", 0755)); + ASSERT_OK_ERRNO(mkdirat(tfd, "robind@yyy/check5", 0755)); + + _cleanup_(mstack_freep) MStack *mstack = NULL; + ASSERT_OK(mstack_load(t, tfd, &mstack)); + + ASSERT_OK_ZERO(mstack_is_read_only(mstack)); + ASSERT_OK_ZERO(mstack_is_foreign_uid_owned(mstack)); + + if (!have_effective_cap(CAP_SYS_ADMIN)) + return (void) log_tests_skipped("not attaching mstack, lacking privs"); + if (!mount_new_api_supported()) + return (void) log_tests_skipped("kernel does not support new mount API, skipping mstack attachment test."); + if (!overlayfs_set_fd_lowerdir_plus_supported()) + return (void) log_tests_skipped("overlayfs does not support FSCONFIG_SET_FD with lowerdir+, skipping mstack attachment test."); + if (running_in_chroot() > 0) /* we cannot disable mount prop if we are in a chroot without the root inode being a proper mount point */ + return (void) log_tests_skipped("running in chroot(), skipping mstack attachment test."); + + mstack = mstack_free(mstack); + + /* For with a new mountns */ + r = pidref_safe_fork("(mstack-test", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, /* ret= */ NULL); + ASSERT_OK(r); + + if (r == 0) { + MStackFlags flags = 0; + + /* Close the original temporary fd, it still points to an inode of the original mountns, + * which we cannot use to generate mounts from */ + tfd = safe_close(tfd); + + { + ASSERT_OK(mstack_load(t, -EBADF, &mstack)); + + ASSERT_OK(mstack_open_images( + mstack, + /* mountfsd_link= */ NULL, + /* userns_fd= */ -EBADF, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + flags)); + + _cleanup_(rmdir_and_freep) char *m = NULL; + ASSERT_OK(mkdtemp_malloc("/tmp/mstack-temporary-XXXXXX", &m)); + + ASSERT_OK(mstack_make_mounts(mstack, m, flags)); + + _cleanup_(rmdir_and_freep) char *w = NULL; + ASSERT_OK(mkdtemp_malloc("/tmp/mstack-where-XXXXXX", &w)); + + _cleanup_close_ int rfd = -EBADF; + ASSERT_OK(mstack_bind_mounts(mstack, w, /* where_fd= */ -EBADF, flags, &rfd)); + + _cleanup_close_ int ofd = open(w, O_PATH|O_CLOEXEC); + ASSERT_OK_ERRNO(ofd); + + ASSERT_OK_ERRNO(faccessat(ofd, "check1", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "check2/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "check3/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "zzz/check4/", F_OK, AT_SYMLINK_NOFOLLOW)); + ASSERT_OK_ERRNO(faccessat(ofd, "yyy/check5/", F_OK, AT_SYMLINK_NOFOLLOW)); + + _cleanup_free_ char *j = ASSERT_PTR(path_join(w, "zzz")); + ASSERT_OK_ERRNO(umount2(j, MNT_DETACH)); + _cleanup_free_ char *jj = ASSERT_PTR(path_join(w, "yyy")); + ASSERT_OK_ERRNO(umount2(jj, MNT_DETACH)); + ASSERT_OK_ERRNO(umount2(w, MNT_DETACH)); + } + + mstack = mstack_free(mstack); + + _exit(EXIT_SUCCESS); + } +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index c487fc54ec..5b67cd9f96 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -198,7 +198,6 @@ TEST(protect_kernel_logs) { static const NamespaceParameters p = { .runtime_scope = RUNTIME_SCOPE_SYSTEM, .protect_kernel_logs = true, - .root_directory_fd = -EBADF, }; int r; diff --git a/src/test/test-ns.c b/src/test/test-ns.c index c6d6f2e423..9a17ed4f36 100644 --- a/src/test/test-ns.c +++ b/src/test/test-ns.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include @@ -77,11 +78,20 @@ int main(int argc, char *argv[]) { else log_info("Not chrooted"); + _cleanup_(pinned_resource_done) PinnedResource pr = PINNED_RESOURCE_NULL; + + if (root_directory) { + pr.directory_fd = open(root_directory, O_PATH|O_CLOEXEC|O_DIRECTORY); + assert_se(pr.directory_fd >= 0); + + pr.directory = strdup(root_directory); + assert_se(pr.directory); + } + NamespaceParameters p = { .runtime_scope = RUNTIME_SCOPE_SYSTEM, - .root_directory = root_directory, - .root_directory_fd = -EBADF, + .rootfs = &pr, .read_write_paths = (char**) writable, .read_only_paths = (char**) readonly, diff --git a/src/test/test-nsresource.c b/src/test/test-nsresource.c index aacd776d7f..b8845b8ffe 100644 --- a/src/test/test-nsresource.c +++ b/src/test/test-nsresource.c @@ -2,6 +2,8 @@ #include +#include "sd-varlink.h" + #include "errno-util.h" #include "fd-util.h" #include "namespace-util.h" @@ -16,13 +18,18 @@ TEST(delegatetap) { return (void) log_tests_skipped_errno(userns_fd, "User namespaces not available"); ASSERT_OK(userns_fd); - r = nsresource_register_userns("foobar", userns_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = nsresource_connect(&link); if (ERRNO_IS_NEG_DISCONNECT(r) || r == -ENOENT || ERRNO_IS_NEG_NOT_SUPPORTED(r)) return (void) log_tests_skipped_errno(r, "systemd-nsresourced cannot be reached"); + + r = nsresource_register_userns(link, "foobar", userns_fd); + if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return (void) log_tests_skipped_errno(r, "systemd-nsresourced does not work"); ASSERT_OK(r); _cleanup_free_ char *ifname = NULL; - _cleanup_close_ int tap_fd = nsresource_add_netif_tap(userns_fd, &ifname); + _cleanup_close_ int tap_fd = nsresource_add_netif_tap(link, userns_fd, &ifname); if (ERRNO_IS_NEG_NOT_SUPPORTED(tap_fd)) return (void) log_tests_skipped_errno(tap_fd, "tap device support not available"); ASSERT_OK(tap_fd); diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 0c957b48aa..7cbe7fd5f9 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -12,6 +12,7 @@ #include "sd-daemon.h" #include "sd-event.h" #include "sd-id128.h" +#include "sd-varlink.h" #include "alloc-util.h" #include "architecture.h" @@ -2041,11 +2042,16 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (asprintf(&userns_name, "vmspawn-" PID_FMT "-%s", getpid_cached(), arg_machine) < 0) return log_oom(); - r = nsresource_register_userns(userns_name, delegate_userns_fd); + _cleanup_(sd_varlink_unrefp) sd_varlink *nsresource_link = NULL; + r = nsresource_connect(&nsresource_link); + if (r < 0) + return log_error_errno(r, "Failed to connect to nsresourced: %m"); + + r = nsresource_register_userns(nsresource_link, userns_name, delegate_userns_fd); if (r < 0) return log_error_errno(r, "Failed to register user namespace with systemd-nsresourced: %m"); - tap_fd = nsresource_add_netif_tap(delegate_userns_fd, /* ret_host_ifname= */ NULL); + tap_fd = nsresource_add_netif_tap(nsresource_link, delegate_userns_fd, /* ret_host_ifname= */ NULL); if (tap_fd < 0) return log_error_errno(tap_fd, "Failed to allocate network tap device: %m"); diff --git a/test/units/TEST-07-PID1.delegate-namespaces.sh b/test/units/TEST-07-PID1.delegate-namespaces.sh index 61d0986685..0221c5a9c3 100755 --- a/test/units/TEST-07-PID1.delegate-namespaces.sh +++ b/test/units/TEST-07-PID1.delegate-namespaces.sh @@ -88,14 +88,14 @@ testcase_multiple_features() { -p PrivateTmp=yes \ -p PrivateDevices=yes \ -p PrivateNetwork=yes \ - -p PrivateUsersEx=self \ + -p PrivateUsers=self \ -p PrivateIPC=yes \ -p ProtectHostname=yes \ -p ProtectClock=yes \ -p ProtectKernelTunables=yes \ -p ProtectKernelModules=yes \ -p ProtectKernelLogs=yes \ - -p ProtectControlGroupsEx=private \ + -p ProtectControlGroups=private \ -p LockPersonality=yes \ -p Environment=ABC=QED \ -p DelegateNamespaces=yes \ diff --git a/test/units/TEST-13-NSPAWN.pull-oci.sh b/test/units/TEST-13-NSPAWN.pull-oci.sh new file mode 100755 index 0000000000..e673b711f0 --- /dev/null +++ b/test/units/TEST-13-NSPAWN.pull-oci.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2016 +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if ! can_do_rootless_nspawn; then + echo "Skipping unpriv nspawn test" + exit 0 +fi + +# We need FSCONFIG_SET_FD support in overlayfs for .mstack to work. Let's skip +# this test on old kernels, that didn't have that yet. Ideally we'd check for +# the feature itself here, but I couldn't figure out a nice way to detect +# support for this from shell, hence let's do a version check instead. +if systemd-analyze condition 'ConditionVersion= < 6.13' ; then + echo "Kernel too old for FSCONFIG_SET_FD support on overlayfs, skipping pull-oci test". + exit 0 +fi + +export SYSTEMD_LOG_LEVEL=debug +export SYSTEMD_LOG_TARGET=journal + +at_exit() { + rm -rf /var/tmp/pull-oci-test + rm -rf /home/testuser/.local/state/machines/ocibasic + rm -rf /home/testuser/.local/state/machines/ocilayer +} + +trap at_exit EXIT + +# Install a PK rule that allows 'testuser' user to register a machine even +# though they are not on an fg console, just for testing +mkdir -p /etc/polkit-1/rules.d +cat >/etc/polkit-1/rules.d/registermachinetest.rules <<'EOF' +polkit.addRule(function(action, subject) { + if (action.id == "org.freedesktop.machine1.register-machine" && + subject.user == "testuser") { + return polkit.Result.YES; + } +}); +EOF + +run0 -u testuser mkdir -p .local/state/machines + +create_dummy_container /home/testuser/.local/state/machines/ocibasic +cat >/home/testuser/.local/state/machines/ocibasic/sbin/init <home/testuser/.local/state/machines/ocilayer/etc/waldo </var/tmp/pull-oci-test/ocibasic.tar.gz +run0 --pipe -u testuser importctl -m --user export-tar --format=gzip ocilayer - >/var/tmp/pull-oci-test/ocilayer.tar.gz + +OCIBASIC_SHA256="$(sha256sum /var/tmp/pull-oci-test/ocibasic.tar.gz | cut -d' ' -f1)" +OCIBASIC_SIZE="$(stat -c %s /var/tmp/pull-oci-test/ocibasic.tar.gz)" +OCILAYER_SHA256="$(sha256sum /var/tmp/pull-oci-test/ocilayer.tar.gz | cut -d' ' -f1)" +OCILAYER_SIZE="$(stat -c %s /var/tmp/pull-oci-test/ocilayer.tar.gz)" + +# Let's now put together a simple, fake, static OCI registry that sits on +# file:// rather than https://, so that we don't have to spawn an HTTP +# server. After all we don't want to test the server side code, but only the +# client side code, and libcurl nicely abstracts https:// or ftp:// from us. + +mkdir -p /var/tmp/pull-oci-test/v2/ocicombo/manifests +cat >/var/tmp/pull-oci-test/v2/ocicombo/manifests/latest < /usr/lib/systemd/oci-registry/registry.localfile.oci-registry <