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 /sbin/mount.mstack
+
+
+
+ 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 <