diff --git a/TODO b/TODO
index bdb8a18de9..d6d9901e64 100644
--- a/TODO
+++ b/TODO
@@ -28,6 +28,12 @@ Features:
* nspawn: greater control over selinux label?
+* sd-event: implement inotify events, as we can safely and robustly do that now
+ for any inode without fearing confusion by inodes appearing at multiple
+ places: we can open it with O_PATH first, then store its inode in a hash
+ table, to recognize duplicate watches before creating (and thus corrupting
+ pre-existing ones) them, and using /proc/self/fd/ to add it right after.
+
* the error paths in usbffs_dispatch_ep() leak memory
* cgroups: figure out if we can somehow communicate in a cleaner way whether a
@@ -45,6 +51,17 @@ Features:
that our log messages could contain clickable links for example for unit
files and suchlike we operate on.
+* introduce a new SystemCallFilters= group called "@system-service" with a
+ sensible default set for system services, then make use of them in portable
+ profiles
+
+* add support for "portablectl attach http://foobar.com/waaa.raw (i.e. importd integration)
+
+* add attach --enable and attach --now (for attach+enable+start)
+
+* sync dynamic uids/gids between host+portable srvice (i.e. if DynamicUser=1 is set for a service, make sure that the
+ selected user is resolvable in the service even if it ships its own /etc/passwd)
+
* Fix DECIMAL_STR_MAX or DECIMAL_STR_WIDTH. One includes a trailing NUL, the
other doesn't. What a desaster. Probably to exclude it. Also
DECIMAL_STR_WIDTH should probably add an extra "-" into account for negative
diff --git a/doc/PORTABLE_SERVICES.md b/doc/PORTABLE_SERVICES.md
new file mode 100644
index 0000000000..b989a4e549
--- /dev/null
+++ b/doc/PORTABLE_SERVICES.md
@@ -0,0 +1,251 @@
+# Portable Services Introduction
+
+This systemd version includes a preview of the "portable service"
+concept. "Portable Services" are supposed to be an incremental improvement over
+traditional system services, making two specific facets of container management
+available to system services more readily. Specifically:
+
+1. The bundling of applications, i.e. packing up multiple services, their
+ binaries and all their dependencies in a single image, and running them
+ directly from it.
+
+2. Stricter default security policies, i.e. sand-boxing of applications.
+
+The primary tool for interfacing with "portable services" is the new
+"portablectl" program. It's currently shipped in /usr/lib/systemd/portablectl
+(i.e. not in the `$PATH`), since it's not yet considered part of the officially
+supported systemd interfaces — it's a preview still after all.
+
+Portable services don't bring anything inherently new to the table. All they do
+is put together known concepts in a slightly nicer way to cover a specific set
+of use-cases in a nicer way.
+
+# So, what *is* a "Portable Service"?
+
+A portable service is ultimately just an OS tree, either inside of a directory
+tree, or inside a raw disk image containing a Linux file system. This tree is
+called the "image". It can be "attached" or "detached" from the system. When
+"attached" specific systemd units from the image are made available on the host
+system, then behaving pretty much exactly like locally installed system
+services. When "detached" these units are removed again from the host, leaving
+no artifacts around (except maybe messages they might have logged).
+
+The OS tree/image can be created with any tool of your choice. For example, you
+can use `dnf --installroot=` if you like, or `debootstrap`, the image format is
+entirely generic, and doesn't have to carry any specific metadata beyond what
+distribution images carry anyway. Or to say this differently: the image format
+doesn't define any new metadata as unit files and OS tree directories or disk
+images are already sufficient, and pretty universally available these days. One
+particularly nice tool for creating suitable images is
+[mkosi](https://github.com/systemd/mkosi), but many other existing tools will
+do too.
+
+If you so will, "Portable Services" are a nicer way to manage chroot()
+environments, with better security, tooling and behavior.
+
+# Where's the difference to a "Container"?
+
+"Container" is a very vague term, after all it is used for
+systemd-nspawn/LXC-type OS containers, for Docker/rkt-like micro service
+containers, and even certain 'lightweight' VM runtimes.
+
+The "portable service" concept ultimately will not provide a fully isolated
+environment to the payload, like containers mostly intend to. Instead they are
+from the beginning more alike regular system services, can be controlled with
+the same tools, are exposed the same way in all infrastructure and so on. Their
+main difference is that the use a different root directory than the rest of the
+system. Hence, the intention is not to run code in a different, isolated world
+from the host — like most containers would do it —, but to run it in the same
+world, but with stricter access controls on what the service can see and do.
+
+As one point of differentiation: as programs run as "portable services" are
+pretty much regular system services, they won't run as PID 1 (like Docker would
+do it), but as normal process. A corollary of that is that they aren't supposed
+to manage anything in their own environment (such as the network) as the
+execution environment is mostly shared with the rest of the system.
+
+The primary focus use-case of "portable services" is to extend the host system
+with encapsulated extensions, but provide almost full integration with the rest
+of the system, though possibly restricted by effective security knobs. This
+focus includes system extensions otherwise sometimes called "super-privileged
+containers".
+
+Note that portable services are only available for system services, not for
+user services. i.e. the functionality cannot be used for the stuff
+bubblewrap/flatpak is focusing on.
+
+# Mode of Operation
+
+If you have portable service image, maybe in a raw disk image called
+`foobar_0.7.23.raw`, then attaching the services to the host is as easy as:
+
+```
+# /usr/lib/systemd/portablectl attach foobar_0.7.23.raw
+```
+
+This command does the following:
+
+1. It dissects the image, checks and validates the `/etc/os-release` data of
+ the image, and looks for all included unit files.
+
+2. It copies out all unit files with a suffix of `.service`, `.socket`,
+ `.target`, `.timer` and `.path`. whose name begins with the image's name
+ (with the .raw removed), truncated at the first underscore (if there is
+ one). This prefix name generated from the image name must be followed by a
+ ".", "-" or "@" character in the unit name. Or in other words, given the
+ image name of `foobar_0.7.23.raw` all unit files matching
+ `foobar-*.{service|socket|target|timer|path}`,
+ `foobar@.{service|socket|target|timer|path}` as well as
+ `foobar.*.{service|socket|target|timer|path}` and
+ `foobar.{service|socket|target|timer|path}` are copied out. These unit files
+ are placed in `/etc/systemd/system/` like regular unit files. Within the
+ images the unit files are looked for at the usual locations, i.e. in
+ `/usr/lib/systemd/system/` and `/etc/systemd/system/` and so on, relative to
+ the image's root.
+
+3. For each such unit file a drop-in file is created. Let's say
+ `foobar-waldo.service` was one of the unit files copied to
+ `/etc/systemd/system/`, then a drop-in file
+ `/etc/systemd/system/foobar-waldo.service.d/20-portable.conf` is created,
+ containing a few lines of additional configuration:
+
+ ```
+ [Service]
+ RootImage=/path/to/foobar.raw
+ Environment=PORTABLE=foobar
+ LogExtraFields=PORTABLE=foobar
+ ```
+
+4. For each such unit a "profile" drop-in is linked in. This "profile" drop-in
+ generally contains security options that lock down the service. By default
+ the `default` profile is used, which provides a medium level of
+ security. There's also `trusted` which runs the service at the highest
+ privileges, i.e. host's root and everything. The `strict' profile comes with
+ the toughest security restrictions. Finally, `nonetwork` is like `default`
+ but without network access. Users may define their own profiles too (or
+ modify the existing ones)
+
+And that's already it.
+
+Note that the images need to stay around (and the same location) as long as the
+portable service is attached. If an image is moved, the `RootImage=` line
+written to the unit drop-in would point to an non-existing place, and break the
+logic.
+
+The `portablectl detach` command executes the reverse operation: it looks for
+the drop-ins and the unit files associated with the image, and removes them
+again.
+
+Note that `portable attach` won't enable or start any of the units it copies
+out. This still has to take place in a second, separate step. (That said We
+might add options to do this automatically later on.).
+
+# Requirements on Images
+
+Note that portable services don't introduce any new image format, but most OS
+images should just work the way they are. Specifically, the following
+requirements are made for an image that can be attached/detached with
+`portablectl`.
+
+1. It must contain a binary (and its dependencies) that shall be invoked,
+ including all its dependencies. If binary code, the code needs to be
+ compiled for an architecture compatible with the host.
+
+2. The image must either be a plain sub-directory (or btrfs subvolume)
+ containing the binaries and its dependencies in a classic Linux OS tree, or
+ must be a raw disk image either containing only one, naked file system, or
+ an image with a partition table understood by the Linux kernel with only a
+ single partition defined, or alternatively, a GPT partition table with a set
+ of properly marked partitions following the [Discoverable Partitions
+ Specification](https://www.freedesktop.org/wiki/Specifications/DiscoverablePartitionsSpec/).
+
+3. The image must at least contain one matching unit file, with the right name
+ prefix and suffix (see above). The unit file is searched in the usual paths,
+ i.e. primarily /etc/systemd/system/ and /usr/lib/systemd/system/ within the
+ image. (The implementation will check a couple of other paths too, but it's
+ recommended to use these two paths.)
+
+4. The image must contain an os-release file, either in /etc/os-release or
+ /usr/lib/os-release. The file should follow the standard format.
+
+Note that generally images created by tools such as `debootstrap`, `dnf
+--installroot=` or `mkosi` qualify for all of the above in one way or
+another. If you wonder what the most minimal image would be that complies with
+the requirements above, it could consist of this:
+
+```
+/usr/bin/minimald # a statically compiled binary
+/usr/lib/systemd/minimal-test.service # the unit file for the service, with ExecStart=/usr/bin/minimald
+/usr/lib/os-release # an os-release file explaining what this is
+```
+
+And that's it.
+
+Note that qualifying images do not have to contain an init system of their
+own. If they do, it's fine, it will be ignored by the portable service logic,
+but they generally don't have to, and it might make sense to avoid any, to keep
+images minimal.
+
+Note that as no new image format or metadata is defined, it's very
+straight-forward to define images than can be made use of it a number of
+different ways. For example, by using `mkosi -b` you can trivially build a
+single, unified image that:
+
+1. Can be attached as portable service, to run any container services natively
+ on the host.
+
+2. Can be run as OS container, using `systemd-nspawn`, by booting the image
+ with `systemd-nspawn -i -b`.
+
+3. Can be booted directly as VM image, using a generic VM executor such as
+ `virtualbox`/`qemu`/`kvm`
+
+4. Can be booted directly on bare-metal systems.
+
+Of course, to facilitate 2, 3 and 4 you need to include an init system in the
+image. To facility 3 and 4 you also need to include a boot loader in the
+image. As mentioned `mkosi -b` takes care of all of that for you, but any other
+image generator should work too.
+
+# Execution Environment
+
+Note that the code in portable service images is run exactly like regular
+services. Hence there's no new execution environment to consider. Oh, unlike
+Docker would do it, as these are regular system services they aren't run as PID
+1 either, but with regular PID values.
+
+# Access to host resources
+
+If services shipped with this mechanism shall be able to access host resources
+(such as files or AF_UNIX sockets for IPC), use the normal `BindPaths=` and
+`BindReadOnlyPaths=` settings in unit files to mount them in. In fact the
+`default` profile mentioned above makes use of this to ensure
+`/etc/resolv.conf`, the D-Bus system bus socket or write access to the logging
+subsystem are available to the service.
+
+# Instantiation
+
+Sometimes it makes sense to instantiate the same set of services multiple
+times. The portable service concept does not introduce a new logic for this. It
+is recommended to use the regular unit templating of systemd for this, i.e. to
+include template units such as `foobar@.service`, so that instantiation is as
+simple as:
+
+```
+# /usr/lib/systemd/portablectl attach foobar_0.7.23.raw
+# systemctl enable --now foobar@instancea.service
+# systemctl enable --now foobar@instanceb.service
+…
+```
+
+The benefit of this approach is that templating works exactly the same for
+units shipped with the OS itself as for attached portable services.
+
+# Immutable images with local data
+
+It's a good idea to keep portable service images read-only during normal
+operation. In fact all but the `trusted` profile will default to this kind of
+behaviour, by setting the `ProtectSystem=strict` option. In this case writable
+service data may be placed on the host file system. Use `StateDirectory=` in
+the unit files to enable such behaviour and add a local data directory to the
+services copied onto the host.
diff --git a/man/portablectl.xml b/man/portablectl.xml
new file mode 100644
index 0000000000..5472397d48
--- /dev/null
+++ b/man/portablectl.xml
@@ -0,0 +1,404 @@
+
+
+
+
+
+
+
+
+ portablectl
+ systemd
+
+
+
+ Developer
+ Lennart
+ Poettering
+ lennart@poettering.net
+
+
+
+
+
+ portablectl
+ 1
+
+
+
+ portablectl
+ Attach, detach or inspect portable service images
+
+
+
+
+ portablectl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+
+ Description
+
+ portablectl may be used to attach, detach or inspect portable service images. It's
+ primarily a command interfacing with
+ systemd-portabled.service 8 .
+
+ Portable service images contain an OS file system tree along with
+ systemd 1 unit file
+ information. A service image may be "attached" to the local system. If attached, a set of unit files are copied
+ from the image to the host, and extended with RootDirectory= or RootImage=
+ assignments (in case of service units) pointing to the image file or directory, ensuring the services will run
+ within the file system context of the image.
+
+ Portable service images are an efficient way to bundle multiple related services and other units together,
+ and transfer them as a whole between systems. When these images are attached the local system the contained units
+ may run in most ways like regular system-provided units, either with full privileges or inside strict sandboxing,
+ depending on the selected configuration.
+
+ Specifically portable service images may be of the following kind:
+
+
+ Directory trees containing an OS, including the top-level directories /usr/ ,
+ /etc/ , and so on.
+
+ btrfs subvolumes containing OS trees, similar to normal directory trees.
+
+ Binary "raw" disk images containing MBR or GPT partition tables and Linux file system
+ partitions.
+
+
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+ -q
+ --quiet
+
+ Suppresses additional informational output while running.
+
+
+
+ -p PROFILE
+ --profile= PROFILE
+
+ When attaching an image, select the profile to use. By default the default
+ profile is used. For details about profiles, see below.
+
+
+
+ --copy=
+
+ When attaching an image, select whether to prefer copying or symlinking of files installed into
+ the host system. Takes one of copy (to prefer copying of files), symlink
+ (to prefer creation of symbolic links) or auto for an intermediary mode where security
+ profile drop-ins are symlinked while unit files are copied. Note that this option expresses a preference only,
+ in cases where symbolic links cannot be created — for example when the image operated on is a raw disk image,
+ and hence not directly referentiable from the host file system — copying of files is used
+ unconditionally.
+
+
+
+ --runtime
+
+ When specified the unit and drop-in files are placed in
+ /run/systemd/system/ instead of /etc/systemd/system/ . Images attached
+ with this option set hence remain attached only until the next reboot, while they are normally attached
+ persistently.
+
+
+
+ --no-reload
+
+ Don't reload the service manager after attaching or detaching a portable service
+ image. Normally the service manager is reloaded to ensure it is aware of added or removed unit
+ files.
+
+
+
+ --cat
+
+ When inspecting portable service images, show the (unprocessed) contents of the metadata files
+ pulled from the image, instead of brief summaries. Specifically, this will show the
+ os-release 5 and unit file
+ contents of the image.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+
+ list
+
+ List available portable service images. This will list all portable service images discovered
+ in the portable image search paths (see below), along with brief metadata and state information. Note that many
+ of the commands below may both operate on images inside and outside of the search paths. This command is hence
+ mostly a convenience option, the commands are generally not restricted to what this list
+ shows.
+
+
+
+ attach IMAGE [PREFIX… ]
+
+ Attach a portable service image to the host system. Expects a file system path to a portable
+ service image file or directory as first argument. If the specified path contains no slash character
+ (/ ) it is understood as image filename that is searched for in the portable service image
+ search paths (see below). To reference a file in the current working directory prefix the filename with
+ ./ to avoid this search path logic.
+
+ When a portable service is attached four operations are executed:
+
+
+
+ All unit files of types .service , .socket ,
+ .target , .timer and .path which match the
+ indicated unit file name prefix are copied from the image to the host's
+ /etc/systemd/system/ directory (or /run/systemd/system/ — depending
+ whether --runtime is specified, see above).
+
+ For unit files of type .service a drop-in is added to these copies that
+ adds RootDirectory= or RootImage= settings (see
+ systemd.unit 5 for
+ details), that ensures these services are run within the file system of the originating portable service
+ image.
+
+ A second drop-in is created: the "profile" drop-in, that may contain additional security
+ settings (and other settings). A number of profiles are available by default but administrators may define
+ their own ones. See below.
+
+ If the portable service image file is not already in the search path (see below), a symbolic
+ link to it is created in /etc/portables/ or
+ /run/portables/ , to make sure it is included in it.
+
+
+ By default all unit files whose names start with a prefix generated from the image's file name are copied
+ out. Specifically, the prefix is determined from the image file name with any suffix such as
+ .raw removed, truncated at the first occurence of and underscore character
+ (_ ), if there is one. The underscore logic is supposed to be used to versioning so that the
+ an image file foobar_47.11.raw will result in a unit file matching prefix of
+ foobar . This prefix is then compared with all unit files names contained in the image in
+ the usual directories, but only unit file names where the prefix is followed by - ,
+ . or @ are considered. Example: if a portable service image file is named
+ foobar_47.11.raw then by default all its unit files with names such as
+ foobar-quux-waldi.service , foobar.service or
+ foobar@.service will be considered. It's possible to override the matching prefix: all
+ strings listed on the command line after the image file name are considered prefixes, overriding the implicit
+ logic where the prefix is derived from the image file name.
+
+ By default, after the unit files are attached the service manager's configuration is reloaded, except
+ when --no-reload is specified (see above). This ensures that the new units made available to
+ the service manager are seen by it.
+
+
+
+
+ detach IMAGE
+
+ Detaches a portable service image from the host. This undoes the operations executed by the
+ attach command above, and removes the unit file copies, drop-ins and image symlink
+ again. This command expects an image name or path as parameter. Note that if a path is specified only the last
+ component of it (i.e. the file or directory name itself, not the path to it) is used for finding matching unit
+ files. This is a convencience feature to allow all arguments passed as attach also to
+ detach .
+
+
+
+ inspect IMAGE [PREFIX… ]
+
+ Extracts various metadata from a portable service image and presents it to the
+ caller. Specifically, the
+ os-release 5 file of the
+ image is retrieved as well as all matching unit files. By default a short summary showing the most relevant
+ metadata in combination with a list of matching unit files is shown (that is the unit files
+ attach would install to the host system). If combined with --cat (see
+ above), the os-release data and the units files' contents is displayed unprocessed. This
+ command is useful to determine whether an image qualifies as portable service image, and which unit files are
+ included. This command expects the path to the image as parameter, optionally followed by a list of unit file
+ prefixes to consider, similar to the attach command described above.
+
+
+
+
+ is-attached IMAGE
+
+ Determines whether the specified image is currently attached or not. Unless combined with the
+ --quiet switch this will show a short state identifier for the image. Specifically:
+
+
+ Image attachment states
+
+
+
+
+
+ State
+ Description
+
+
+
+
+ detached
+ The image is currently not attached.
+
+
+ attached
+ The image is currently attached, i.e. its unit files have been made available to the host system.
+
+
+ attached-runtime
+ Like attached , but the the unit files have been made available transiently only, i.e. the attach command has been invoked with the --runtime option.
+
+
+ enabled
+ The image is currently attached, and at least one unit file associated with it has been enabled.
+
+
+ enabled-runtime
+ Like enabled , but the the unit files have been made available transiently only, i.e. the attach command has been invoked with the --runtime option.
+
+
+ running
+ The image is currently attached, and at least one unit file associated with it is running.
+
+
+ running-runtime
+ The image is currently attached transiently, and at least one unit file associated with it is running.
+
+
+
+
+
+
+
+
+
+ read-only IMAGE [BOOL ]
+
+ Marks or (unmarks) a portable service image read-only. Takes an image name, followed by a
+ boolean as arguments. If the boolean is omitted, positive is implied, i.e. the image is marked
+ read-only.
+
+
+
+ remove IMAGE …
+
+ Removes one or more portable service images. Note that this command will only remove the
+ specified image path itself — it it refers to a symbolic link then the symbolic link is removed and not the
+ image it points to.
+
+
+
+ set-limit [NAME ] BYTES
+
+ Sets the maximum size in bytes that a specific portable service image, or all images, may grow
+ up to on disk (disk quota). Takes either one or two parameters. The first, optional parameter refers to a
+ portable service image name. If specified, the size limit of the specified image is changed. If omitted, the
+ overall size limit of the sum of all images stored locally is changed. The final argument specifies the size
+ limit in bytes, possibly suffixed by the usual K, M, G, T units. If the size limit shall be disabled, specify
+ - as size.
+
+ Note that per-image size limits are only supported on btrfs file systems. Also, depending on
+ BindPaths= settings in the portable service's unit files directories from the host might be
+ visible in the image environment during runtime which are not affected by this setting, as only the image
+ itself is counted against this limit.
+
+
+
+
+
+
+
+ Files and Directories
+
+ Portable service images are preferably stored in /var/lib/portables/ , but are also
+ searched for in /etc/portables/ , /run/systemd/portables/ ,
+ /usr/local/lib/portables/ and /usr/lib/portables/ . It's recommended not
+ to place image files directly in /etc/portables/ or
+ /run/systemd/portables/ (as these are generally not suitable for storing large or non-textual
+ data), but use these directories only for linking images located elsewhere into the image search path.
+
+
+
+ Profiles
+
+ When portable service images are attached a "profile" drop-in is linked in, which may be used to enforce
+ additional security (and other) restrictions locally. Four profile drop-ins are defined by default, and shipped in
+ /usr/lib/systemd/portable/profile/ . Additional, local profiles may be defined by placing them
+ in /etc/systemd/portable/profile/ . The default profiles are:
+
+
+ Profiles
+
+
+
+
+
+ Name
+ Description
+
+
+
+
+ default
+ This is the default profile if no other profile name is set via the --profile= (see above). It's fairly restrictive, but should be useful for common, unprivileged system workloads. This includes write access to the logging framework, as well as IPC access to the D-Bus system.
+
+
+ nonetwork
+ Very similar to default , but networking is turned off for any services of the portable service image.
+
+
+ strict
+ A profile with very strict settings. This profile excludes IPC (D-Bus) and network access.
+
+
+ trusted
+ A profile with very relaxed settings. In this profile the services run with full privileges.
+
+
+
+
+
+ For details on this profiles, and their effects please have a look at their precise definitions,
+ e.g. /usr/lib/systemd/portable/profile/default/service.conf and similar.
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ See Also
+
+ systemd 1 ,
+ systemd-portabled.service 8
+
+
+
+
diff --git a/man/rules/meson.build b/man/rules/meson.build
index e9759f16a1..d3ed9f8a6a 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -43,6 +43,7 @@ manpages = [
['nss-systemd', '8', ['libnss_systemd.so.2'], 'ENABLE_NSS_SYSTEMD'],
['os-release', '5', [], ''],
['pam_systemd', '8', [], 'HAVE_PAM'],
+ ['portablectl', '1', [], 'ENABLE_PORTABLED'],
['resolvectl', '1', ['resolvconf'], 'ENABLE_RESOLVE'],
['resolved.conf', '5', ['resolved.conf.d'], 'ENABLE_RESOLVE'],
['runlevel', '8', [], 'ENABLE_UTMP'],
@@ -611,6 +612,7 @@ manpages = [
['systemd-notify', '1', [], ''],
['systemd-nspawn', '1', [], ''],
['systemd-path', '1', [], ''],
+ ['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'],
['systemd-quotacheck.service',
'8',
['systemd-quotacheck'],
diff --git a/man/systemd-portabled.service.xml b/man/systemd-portabled.service.xml
new file mode 100644
index 0000000000..7b1c3ae68c
--- /dev/null
+++ b/man/systemd-portabled.service.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+ systemd-portabled.service
+ systemd
+
+
+
+ Developer
+ Lennart
+ Poettering
+ lennart@poettering.net
+
+
+
+
+
+ systemd-portabled.service
+ 8
+
+
+
+ systemd-portabled.service
+ systemd-portabled
+ Portable service manager
+
+
+
+ systemd-portabled.service
+ /usr/lib/systemd/systemd-portabled
+
+
+
+ Description
+
+ systemd-portabled is a system service that may be used to attach, detach and inspect
+ portable service images.
+
+ Most of systemd-portabled 's functionality is accessible through the
+ portablectl 1 command.
+
+
+ See the Portable
+ Services Documentation for details about the concepts this service implements.
+
+
+
+ See Also
+
+ systemd 1 ,
+ portablectl 1
+
+
+
+
diff --git a/meson.build b/meson.build
index ca8ffabee2..48851d4af2 100644
--- a/meson.build
+++ b/meson.build
@@ -138,6 +138,7 @@ testsdir = join_paths(prefixdir, 'lib/systemd/tests')
systemdstatedir = join_paths(localstatedir, 'lib/systemd')
catalogstatedir = join_paths(systemdstatedir, 'catalog')
randomseeddir = join_paths(localstatedir, 'lib/systemd')
+profiledir = join_paths(rootlibexecdir, 'portable', 'profile')
docdir = get_option('docdir')
if docdir == ''
@@ -1177,6 +1178,7 @@ foreach term : ['utmp',
'hostnamed',
'localed',
'machined',
+ 'portabled',
'networkd',
'timedated',
'timesyncd',
@@ -1355,6 +1357,7 @@ subdir('src/import')
subdir('src/kernel-install')
subdir('src/locale')
subdir('src/machine')
+subdir('src/portable')
subdir('src/nspawn')
subdir('src/resolve')
subdir('src/timedate')
@@ -1716,6 +1719,26 @@ exe = executable('systemctl', 'src/systemctl/systemctl.c',
install_dir : rootbindir)
public_programs += [exe]
+if conf.get('ENABLE_PORTABLED') == 1
+ executable('systemd-portabled',
+ systemd_portabled_sources,
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+
+ exe = executable('portablectl', 'src/portable/portablectl.c',
+ include_directories : includes,
+ link_with : [libshared],
+ dependencies : [threads],
+ install_rpath : rootlibexecdir,
+ install : true,
+ install_dir : rootlibexecdir)
+ public_programs += [exe]
+endif
+
foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit']
meson.add_install_script(meson_make_symlink,
join_paths(rootbindir, 'systemctl'),
@@ -2895,6 +2918,7 @@ foreach tuple : [
['rfkill'],
['logind'],
['machined'],
+ ['portabled'],
['importd'],
['hostnamed'],
['timedated'],
diff --git a/meson_options.txt b/meson_options.txt
index e3dea3ebc5..85d794b08b 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -79,6 +79,8 @@ option('localed', type : 'boolean',
description : 'install the systemd-localed stack')
option('machined', type : 'boolean',
description : 'install the systemd-machined stack')
+option('portabled', type : 'boolean',
+ description : 'install the systemd-portabled stack')
option('networkd', type : 'boolean',
description : 'install the systemd-networkd stack')
option('timedated', type : 'boolean',
diff --git a/src/basic/conf-files.c b/src/basic/conf-files.c
index 1112dbf2f8..2e65a6e74f 100644
--- a/src/basic/conf-files.c
+++ b/src/basic/conf-files.c
@@ -21,18 +21,28 @@
#include "macro.h"
#include "missing.h"
#include "path-util.h"
+#include "set.h"
#include "stat-util.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "util.h"
-static int files_add(Hashmap *h, const char *suffix, const char *root, unsigned flags, const char *path) {
+static int files_add(
+ Hashmap *h,
+ Set *masked,
+ const char *suffix,
+ const char *root,
+ unsigned flags,
+ const char *path) {
+
_cleanup_closedir_ DIR *dir = NULL;
const char *dirpath;
struct dirent *de;
int r;
+ assert(h);
+ assert((flags & CONF_FILES_FILTER_MASKED) == 0 || masked);
assert(path);
dirpath = prefix_roota(root, path);
@@ -41,62 +51,89 @@ static int files_add(Hashmap *h, const char *suffix, const char *root, unsigned
if (!dir) {
if (errno == ENOENT)
return 0;
- return -errno;
+
+ return log_debug_errno(errno, "Failed to open directory '%s': %m", dirpath);
}
FOREACH_DIRENT(de, dir, return -errno) {
- char *p;
+ struct stat st;
+ char *p, *key;
- if (!dirent_is_file_with_suffix(de, suffix)) {
- log_debug("Ignoring %s/%s, because it's not a regular file with suffix %s.", dirpath, de->d_name, strna(suffix));
+ /* Does this match the suffix? */
+ if (suffix && !endswith(de->d_name, suffix))
+ continue;
+
+ /* Has this file already been found in an earlier directory? */
+ if (hashmap_contains(h, de->d_name)) {
+ log_debug("Skipping overridden file '%s/%s'.", dirpath, de->d_name);
continue;
}
- if (flags & CONF_FILES_EXECUTABLE) {
- struct stat st;
-
- /* As requested: check if the file is marked exectuable. Note that we don't check access(X_OK)
- * here, as we care about whether the file is marked executable at all, and not whether it is
- * executable for us, because if such errors are stuff we should log about. */
+ /* Has this been masked in an earlier directory? */
+ if ((flags & CONF_FILES_FILTER_MASKED) && set_contains(masked, de->d_name)) {
+ log_debug("File '%s/%s' is masked by previous entry.", dirpath, de->d_name);
+ continue;
+ }
+ /* Read file metadata if we shall validate the check for file masks, for node types or whether the node is marked executable. */
+ if (flags & (CONF_FILES_FILTER_MASKED|CONF_FILES_REGULAR|CONF_FILES_DIRECTORY|CONF_FILES_EXECUTABLE))
if (fstatat(dirfd(dir), de->d_name, &st, 0) < 0) {
- log_debug_errno(errno, "Failed to stat %s/%s, ignoring: %m", dirpath, de->d_name);
+ log_debug_errno(errno, "Failed to stat '%s/%s', ignoring: %m", dirpath, de->d_name);
continue;
}
- if (!null_or_empty(&st)) {
- /* A mask is a symlink to /dev/null or an empty file. It does not even
- * have to be executable. Other entries must be regular executable files
- * or symlinks to them. */
- if (S_ISREG(st.st_mode)) {
- if ((st.st_mode & 0111) == 0) { /* not executable */
- log_debug("Ignoring %s/%s, as it is not marked executable.",
- dirpath, de->d_name);
- continue;
- }
- } else {
- log_debug("Ignoring %s/%s, as it is neither a regular file nor a mask.",
- dirpath, de->d_name);
- continue;
- }
+ /* Is this a masking entry? */
+ if ((flags & CONF_FILES_FILTER_MASKED))
+ if (null_or_empty(&st)) {
+ /* Mark this one as masked */
+ r = set_put_strdup(masked, de->d_name);
+ if (r < 0)
+ return r;
+
+ log_debug("File '%s/%s' is a mask.", dirpath, de->d_name);
+ continue;
}
+
+ /* Does this node have the right type? */
+ if (flags & (CONF_FILES_REGULAR|CONF_FILES_DIRECTORY))
+ if (!((flags & CONF_FILES_DIRECTORY) && S_ISDIR(st.st_mode)) &&
+ !((flags & CONF_FILES_REGULAR) && S_ISREG(st.st_mode))) {
+ log_debug("Ignoring '%s/%s', as it is not a of the right type.", dirpath, de->d_name);
+ continue;
+ }
+
+ /* Does this node have the executable bit set? */
+ if (flags & CONF_FILES_EXECUTABLE)
+ /* As requested: check if the file is marked exectuable. Note that we don't check access(X_OK)
+ * here, as we care about whether the file is marked executable at all, and not whether it is
+ * executable for us, because if so, such errors are stuff we should log about. */
+
+ if ((st.st_mode & 0111) == 0) { /* not executable */
+ log_debug("Ignoring '%s/%s', as it is not marked executable.", dirpath, de->d_name);
+ continue;
+ }
+
+ if (flags & CONF_FILES_BASENAME) {
+ p = strdup(de->d_name);
+ if (!p)
+ return -ENOMEM;
+
+ key = p;
+ } else {
+ p = strjoin(dirpath, "/", de->d_name);
+ if (!p)
+ return -ENOMEM;
+
+ key = basename(p);
}
- p = strjoin(dirpath, "/", de->d_name);
- if (!p)
- return -ENOMEM;
-
- r = hashmap_put(h, basename(p), p);
- if (r == -EEXIST) {
- log_debug("Skipping overridden file: %s.", p);
- free(p);
- } else if (r < 0) {
- free(p);
- return r;
- } else if (r == 0) {
- log_debug("Duplicate file %s", p);
+ r = hashmap_put(h, key, p);
+ if (r < 0) {
free(p);
+ return log_debug_errno(r, "Failed to add item to hashmap: %m");
}
+
+ assert(r > 0);
}
return 0;
@@ -112,6 +149,7 @@ static int base_cmp(const void *a, const void *b) {
static int conf_files_list_strv_internal(char ***strv, const char *suffix, const char *root, unsigned flags, char **dirs) {
_cleanup_hashmap_free_ Hashmap *fh = NULL;
+ _cleanup_set_free_free_ Set *masked = NULL;
char **files, **p;
int r;
@@ -121,12 +159,18 @@ static int conf_files_list_strv_internal(char ***strv, const char *suffix, const
if (!path_strv_resolve_uniq(dirs, root))
return -ENOMEM;
- fh = hashmap_new(&string_hash_ops);
+ fh = hashmap_new(&path_hash_ops);
if (!fh)
return -ENOMEM;
+ if (flags & CONF_FILES_FILTER_MASKED) {
+ masked = set_new(&path_hash_ops);
+ if (!masked)
+ return -ENOMEM;
+ }
+
STRV_FOREACH(p, dirs) {
- r = files_add(fh, suffix, root, flags, *p);
+ r = files_add(fh, masked, suffix, root, flags, *p);
if (r == -ENOMEM)
return r;
if (r < 0)
diff --git a/src/basic/conf-files.h b/src/basic/conf-files.h
index 2dd1b272ca..78d03e6e0d 100644
--- a/src/basic/conf-files.h
+++ b/src/basic/conf-files.h
@@ -9,7 +9,11 @@
***/
enum {
- CONF_FILES_EXECUTABLE = 1,
+ CONF_FILES_EXECUTABLE = 1U << 0,
+ CONF_FILES_REGULAR = 1U << 1,
+ CONF_FILES_DIRECTORY = 1U << 2,
+ CONF_FILES_BASENAME = 1U << 3,
+ CONF_FILES_FILTER_MASKED = 1U << 4,
};
int conf_files_list(char ***ret, const char *suffix, const char *root, unsigned flags, const char *dir, ...);
diff --git a/src/basic/exec-util.c b/src/basic/exec-util.c
index d5d0edfb5f..e9203fe578 100644
--- a/src/basic/exec-util.c
+++ b/src/basic/exec-util.c
@@ -89,7 +89,7 @@ static int do_execute(
* If callbacks is nonnull, execution is serial. Otherwise, we default to parallel.
*/
- r = conf_files_list_strv(&paths, NULL, NULL, CONF_FILES_EXECUTABLE, (const char* const*) directories);
+ r = conf_files_list_strv(&paths, NULL, NULL, CONF_FILES_EXECUTABLE|CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED, (const char* const*) directories);
if (r < 0)
return r;
diff --git a/src/basic/fd-util.c b/src/basic/fd-util.c
index 9536a50b74..dc63cb0d7c 100644
--- a/src/basic/fd-util.c
+++ b/src/basic/fd-util.c
@@ -12,10 +12,13 @@
#include
#include
+#include "alloc-util.h"
+#include "copy.h"
#include "dirent-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "fs-util.h"
+#include "io-util.h"
#include "macro.h"
#include "memfd-util.h"
#include "missing.h"
@@ -562,6 +565,202 @@ try_dev_shm_without_o_tmpfile:
return -EOPNOTSUPP;
}
+/* When the data is smaller or equal to 64K, try to place the copy in a memfd/pipe */
+#define DATA_FD_MEMORY_LIMIT (64U*1024U)
+
+/* If memfd/pipe didn't work out, then let's use a file in /tmp up to a size of 1M. If it's large than that use /var/tmp instead. */
+#define DATA_FD_TMP_LIMIT (1024U*1024U)
+
+int fd_duplicate_data_fd(int fd) {
+
+ _cleanup_close_ int copy_fd = -1, tmp_fd = -1;
+ _cleanup_free_ void *remains = NULL;
+ _cleanup_free_ char *t = NULL;
+ size_t remains_size = 0;
+ const char *td;
+ struct stat st;
+ int r;
+
+ /* Creates a 'data' fd from the specified source fd, containing all the same data in a read-only fashion, but
+ * independent of it (i.e. the source fd can be closed and unmounted after this call succeeded). Tries to be
+ * somewhat smart about where to place the data. In the best case uses a memfd(). If memfd() are not supported
+ * uses a pipe instead. For larger data will use an unlinked file in /tmp, and for even larger data one in
+ * /var/tmp. */
+
+ if (fstat(fd, &st) < 0)
+ return -errno;
+
+ /* For now, let's only accept regular files, sockets, pipes and char devices */
+ if (S_ISDIR(st.st_mode))
+ return -EISDIR;
+ if (S_ISLNK(st.st_mode))
+ return -ELOOP;
+ if (!S_ISREG(st.st_mode) && !S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode) && !S_ISCHR(st.st_mode))
+ return -EBADFD;
+
+ /* If we have reason to believe the data is bounded in size, then let's use memfds or pipes as backing fd. Note
+ * that we use the reported regular file size only as a hint, given that there are plenty special files in
+ * /proc and /sys which report a zero file size but can be read from. */
+
+ if (!S_ISREG(st.st_mode) || st.st_size < DATA_FD_MEMORY_LIMIT) {
+
+ /* Try a memfd first */
+ copy_fd = memfd_new("data-fd");
+ if (copy_fd >= 0) {
+ off_t f;
+
+ r = copy_bytes(fd, copy_fd, DATA_FD_MEMORY_LIMIT, 0);
+ if (r < 0)
+ return r;
+
+ f = lseek(copy_fd, 0, SEEK_SET);
+ if (f != 0)
+ return -errno;
+
+ if (r == 0) {
+ /* Did it fit into the limit? If so, we are done. */
+ r = memfd_set_sealed(copy_fd);
+ if (r < 0)
+ return r;
+
+ return TAKE_FD(copy_fd);
+ }
+
+ /* Hmm, pity, this didn't fit. Let's fall back to /tmp then, see below */
+
+ } else {
+ _cleanup_(close_pairp) int pipefds[2] = { -1, -1 };
+ int isz;
+
+ /* If memfds aren't available, use a pipe. Set O_NONBLOCK so that we will get EAGAIN rather
+ * then block indefinitely when we hit the pipe size limit */
+
+ if (pipe2(pipefds, O_CLOEXEC|O_NONBLOCK) < 0)
+ return -errno;
+
+ isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
+ if (isz < 0)
+ return -errno;
+
+ /* Try to enlarge the pipe size if necessary */
+ if ((size_t) isz < DATA_FD_MEMORY_LIMIT) {
+
+ (void) fcntl(pipefds[1], F_SETPIPE_SZ, DATA_FD_MEMORY_LIMIT);
+
+ isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
+ if (isz < 0)
+ return -errno;
+ }
+
+ if ((size_t) isz >= DATA_FD_MEMORY_LIMIT) {
+
+ r = copy_bytes_full(fd, pipefds[1], DATA_FD_MEMORY_LIMIT, 0, &remains, &remains_size);
+ if (r < 0 && r != -EAGAIN)
+ return r; /* If we get EAGAIN it could be because of the source or because of
+ * the destination fd, we can't know, as sendfile() and friends won't
+ * tell us. Hence, treat this as reason to fall back, just to be
+ * sure. */
+ if (r == 0) {
+ /* Everything fit in, yay! */
+ (void) fd_nonblock(pipefds[0], false);
+
+ return TAKE_FD(pipefds[0]);
+ }
+
+ /* Things didn't fit in. But we read data into the pipe, let's remember that, so that
+ * when writing the new file we incorporate this first. */
+ copy_fd = TAKE_FD(pipefds[0]);
+ }
+ }
+ }
+
+ /* If we have reason to believe this will fit fine in /tmp, then use that as first fallback. */
+ if ((!S_ISREG(st.st_mode) || st.st_size < DATA_FD_TMP_LIMIT) &&
+ (DATA_FD_MEMORY_LIMIT + remains_size) < DATA_FD_TMP_LIMIT) {
+ off_t f;
+
+ tmp_fd = open_tmpfile_unlinkable(NULL /* NULL as directory means /tmp */, O_RDWR|O_CLOEXEC);
+ if (tmp_fd < 0)
+ return tmp_fd;
+
+ if (copy_fd >= 0) {
+ /* If we tried a memfd/pipe first and it ended up being too large, then copy this into the
+ * temporary file first. */
+
+ r = copy_bytes(copy_fd, tmp_fd, UINT64_MAX, 0);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ }
+
+ if (remains_size > 0) {
+ /* If there were remaining bytes (i.e. read into memory, but not written out yet) from the
+ * failed copy operation, let's flush them out next. */
+
+ r = loop_write(tmp_fd, remains, remains_size, false);
+ if (r < 0)
+ return r;
+ }
+
+ r = copy_bytes(fd, tmp_fd, DATA_FD_TMP_LIMIT - DATA_FD_MEMORY_LIMIT - remains_size, COPY_REFLINK);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ goto finish; /* Yay, it fit in */
+
+ /* It didn't fit in. Let's not forget to use what we already used */
+ f = lseek(tmp_fd, 0, SEEK_SET);
+ if (f != 0)
+ return -errno;
+
+ safe_close(copy_fd);
+ copy_fd = TAKE_FD(tmp_fd);
+
+ remains = mfree(remains);
+ remains_size = 0;
+ }
+
+ /* As last fallback use /var/tmp */
+ r = var_tmp_dir(&td);
+ if (r < 0)
+ return r;
+
+ tmp_fd = open_tmpfile_unlinkable(td, O_RDWR|O_CLOEXEC);
+ if (tmp_fd < 0)
+ return tmp_fd;
+
+ if (copy_fd >= 0) {
+ /* If we tried a memfd/pipe first, or a file in /tmp, and it ended up being too large, than copy this
+ * into the temporary file first. */
+ r = copy_bytes(copy_fd, tmp_fd, UINT64_MAX, COPY_REFLINK);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+ }
+
+ if (remains_size > 0) {
+ /* Then, copy in any read but not yet written bytes. */
+ r = loop_write(tmp_fd, remains, remains_size, false);
+ if (r < 0)
+ return r;
+ }
+
+ /* Copy in the rest */
+ r = copy_bytes(fd, tmp_fd, UINT64_MAX, COPY_REFLINK);
+ if (r < 0)
+ return r;
+
+ assert(r == 0);
+
+finish:
+ /* Now convert the O_RDWR file descriptor into an O_RDONLY one (and as side effect seek to the beginning of the
+ * file again */
+
+ return fd_reopen(tmp_fd, O_RDONLY|O_CLOEXEC);
+}
+
int fd_move_above_stdio(int fd) {
int flags, copy;
PROTECT_ERRNO;
diff --git a/src/basic/fd-util.h b/src/basic/fd-util.h
index 89c3f34c7b..2ae4e67ed0 100644
--- a/src/basic/fd-util.h
+++ b/src/basic/fd-util.h
@@ -81,6 +81,8 @@ enum {
int acquire_data_fd(const void *data, size_t size, unsigned flags);
+int fd_duplicate_data_fd(int fd);
+
/* Hint: ENETUNREACH happens if we try to connect to "non-existing" special IP addresses, such as ::5 */
#define ERRNO_IS_DISCONNECT(r) \
IN_SET(r, ENOTCONN, ECONNRESET, ECONNREFUSED, ECONNABORTED, EPIPE, ENETUNREACH)
diff --git a/src/basic/fileio.c b/src/basic/fileio.c
index 5cab4c1024..6dd00e0698 100644
--- a/src/basic/fileio.c
+++ b/src/basic/fileio.c
@@ -263,29 +263,35 @@ int verify_file(const char *fn, const char *blob, bool accept_extra_nl) {
}
int read_full_stream(FILE *f, char **contents, size_t *size) {
- size_t n, l;
_cleanup_free_ char *buf = NULL;
struct stat st;
+ size_t n, l;
+ int fd;
assert(f);
assert(contents);
- if (fstat(fileno(f), &st) < 0)
- return -errno;
-
n = LINE_MAX;
- if (S_ISREG(st.st_mode)) {
+ fd = fileno(f);
+ if (fd >= 0) { /* If the FILE* object is backed by an fd (as opposed to memory or such, see fmemopen(), let's
+ * optimize our buffering) */
- /* Safety check */
- if (st.st_size > READ_FULL_BYTES_MAX)
- return -E2BIG;
+ if (fstat(fileno(f), &st) < 0)
+ return -errno;
- /* Start with the right file size, but be prepared for files from /proc which generally report a file
- * size of 0. Note that we increase the size to read here by one, so that the first read attempt
- * already makes us notice the EOF. */
- if (st.st_size > 0)
- n = st.st_size + 1;
+ if (S_ISREG(st.st_mode)) {
+
+ /* Safety check */
+ if (st.st_size > READ_FULL_BYTES_MAX)
+ return -E2BIG;
+
+ /* Start with the right file size, but be prepared for files from /proc which generally report a file
+ * size of 0. Note that we increase the size to read here by one, so that the first read attempt
+ * already makes us notice the EOF. */
+ if (st.st_size > 0)
+ n = st.st_size + 1;
+ }
}
l = 0;
@@ -676,21 +682,41 @@ static int parse_env_file_push(
return 0;
}
-int parse_env_file(
+int parse_env_filev(
+ FILE *f,
const char *fname,
- const char *newline, ...) {
+ const char *newline,
+ va_list ap) {
- va_list ap;
int r, n_pushed = 0;
+ va_list aq;
if (!newline)
newline = NEWLINE;
+ va_copy(aq, ap);
+ r = parse_env_file_internal(f, fname, newline, parse_env_file_push, &aq, &n_pushed);
+ va_end(aq);
+ if (r < 0)
+ return r;
+
+ return n_pushed;
+}
+
+int parse_env_file(
+ FILE *f,
+ const char *fname,
+ const char *newline,
+ ...) {
+
+ va_list ap;
+ int r;
+
va_start(ap, newline);
- r = parse_env_file_internal(NULL, fname, newline, parse_env_file_push, &ap, &n_pushed);
+ r = parse_env_filev(f, fname, newline, ap);
va_end(ap);
- return r < 0 ? r : n_pushed;
+ return r;
}
static int load_env_file_push(
diff --git a/src/basic/fileio.h b/src/basic/fileio.h
index bed7c8fb7e..addd9bade4 100644
--- a/src/basic/fileio.h
+++ b/src/basic/fileio.h
@@ -45,7 +45,8 @@ int read_full_stream(FILE *f, char **contents, size_t *size);
int verify_file(const char *fn, const char *blob, bool accept_extra_nl);
-int parse_env_file(const char *fname, const char *separator, ...) _sentinel_;
+int parse_env_filev(FILE *f, const char *fname, const char *separator, va_list ap);
+int parse_env_file(FILE *f, const char *fname, const char *separator, ...) _sentinel_;
int load_env_file(FILE *f, const char *fname, const char *separator, char ***l);
int load_env_file_pairs(FILE *f, const char *fname, const char *separator, char ***l);
diff --git a/src/basic/meson.build b/src/basic/meson.build
index 69c525dcdc..0b2106807c 100644
--- a/src/basic/meson.build
+++ b/src/basic/meson.build
@@ -131,6 +131,8 @@ basic_sources = files('''
ordered-set.h
pager.c
pager.h
+ os-util.c
+ os-util.h
parse-util.c
parse-util.h
path-util.c
diff --git a/src/basic/os-util.c b/src/basic/os-util.c
new file mode 100644
index 0000000000..207594cef8
--- /dev/null
+++ b/src/basic/os-util.c
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "os-util.h"
+#include "strv.h"
+#include "fileio.h"
+#include "string-util.h"
+
+int path_is_os_tree(const char *path) {
+ int r;
+
+ assert(path);
+
+ /* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
+ * always results in -ENOENT, and we can properly distuingish the case where the whole root doesn't exist from
+ * the case where just the os-release file is missing. */
+ if (laccess(path, F_OK) < 0)
+ return -errno;
+
+ /* We use {/etc|/usr/lib}/os-release as flag file if something is an OS */
+ r = open_os_release(path, NULL, NULL);
+ if (r == -ENOENT) /* We got nothing */
+ return 0;
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
+int open_os_release(const char *root, char **ret_path, int *ret_fd) {
+ _cleanup_free_ char *q = NULL;
+ const char *p;
+ int k;
+
+ FOREACH_STRING(p, "/etc/os-release", "/usr/lib/os-release") {
+ k = chase_symlinks(p, root, CHASE_PREFIX_ROOT|(ret_fd ? CHASE_OPEN : 0), (ret_path ? &q : NULL));
+ if (k != -ENOENT)
+ break;
+ }
+ if (k < 0)
+ return k;
+
+ if (ret_fd) {
+ int real_fd;
+
+ /* Convert the O_PATH fd into a proper, readable one */
+ real_fd = fd_reopen(k, O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ safe_close(k);
+ if (real_fd < 0)
+ return real_fd;
+
+ *ret_fd = real_fd;
+ }
+
+ if (ret_path)
+ *ret_path = TAKE_PTR(q);
+
+ return 0;
+}
+
+int fopen_os_release(const char *root, char **ret_path, FILE **ret_file) {
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -1;
+ FILE *f;
+ int r;
+
+ if (!ret_file)
+ return open_os_release(root, ret_path, NULL);
+
+ r = open_os_release(root, ret_path ? &p : NULL, &fd);
+ if (r < 0)
+ return r;
+
+ f = fdopen(fd, "re");
+ if (!f)
+ return -errno;
+ fd = -1;
+
+ *ret_file = f;
+
+ if (ret_path)
+ *ret_path = TAKE_PTR(p);
+
+ return 0;
+}
+
+int parse_os_release(const char *root, ...) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ va_list ap;
+ int r;
+
+ r = fopen_os_release(root, &p, &f);
+ if (r < 0)
+ return r;
+
+ va_start(ap, root);
+ r = parse_env_filev(f, p, NEWLINE, ap);
+ va_end(ap);
+
+ return r;
+}
+
+int load_os_release_pairs(const char *root, char ***ret) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ int r;
+
+ r = fopen_os_release(root, &p, &f);
+ if (r < 0)
+ return r;
+
+ return load_env_file_pairs(f, p, NEWLINE, ret);
+}
diff --git a/src/basic/os-util.h b/src/basic/os-util.h
new file mode 100644
index 0000000000..6b9e033941
--- /dev/null
+++ b/src/basic/os-util.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+int path_is_os_tree(const char *path);
+
+int open_os_release(const char *root, char **ret_path, int *ret_fd);
+int fopen_os_release(const char *root, char **ret_path, FILE **ret_file);
+
+int parse_os_release(const char *root, ...);
+int load_os_release_pairs(const char *root, char ***ret);
diff --git a/src/basic/process-util.c b/src/basic/process-util.c
index ce56ce0735..65abaaf888 100644
--- a/src/basic/process-util.c
+++ b/src/basic/process-util.c
@@ -17,6 +17,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -1344,6 +1345,16 @@ int safe_fork_full(
}
}
+ if ((flags & (FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE)) == (FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE)) {
+
+ /* Optionally, make sure we never propagate mounts to the host. */
+
+ if (mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) < 0) {
+ log_full_errno(prio, errno, "Failed to remount root directory as MS_SLAVE: %m");
+ _exit(EXIT_FAILURE);
+ }
+ }
+
if (flags & FORK_CLOSE_ALL_FDS) {
/* Close the logs here in case it got reopened above, as close_all_fds() would close them for us */
log_close();
diff --git a/src/basic/process-util.h b/src/basic/process-util.h
index 0cec7c4c5c..ba6fea6891 100644
--- a/src/basic/process-util.h
+++ b/src/basic/process-util.h
@@ -163,6 +163,7 @@ typedef enum ForkFlags {
FORK_LOG = 1U << 5,
FORK_WAIT = 1U << 6,
FORK_NEW_MOUNTNS = 1U << 7,
+ FORK_MOUNTNS_SLAVE = 1U << 8,
} ForkFlags;
int safe_fork_full(const char *name, const int except_fds[], size_t n_except_fds, ForkFlags flags, pid_t *ret_pid);
diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c
index 61cc6f5738..1f0a75a0e8 100644
--- a/src/basic/stat-util.c
+++ b/src/basic/stat-util.c
@@ -132,32 +132,6 @@ int path_is_read_only_fs(const char *path) {
return false;
}
-int path_is_os_tree(const char *path) {
- int r;
-
- assert(path);
-
- /* Does the path exist at all? If not, generate an error immediately. This is useful so that a missing root dir
- * always results in -ENOENT, and we can properly distuingish the case where the whole root doesn't exist from
- * the case where just the os-release file is missing. */
- if (laccess(path, F_OK) < 0)
- return -errno;
-
- /* We use /usr/lib/os-release as flag file if something is an OS */
- r = chase_symlinks("/usr/lib/os-release", path, CHASE_PREFIX_ROOT, NULL);
- if (r == -ENOENT) {
-
- /* Also check for the old location in /etc, just in case. */
- r = chase_symlinks("/etc/os-release", path, CHASE_PREFIX_ROOT, NULL);
- if (r == -ENOENT)
- return 0; /* We got nothing */
- }
- if (r < 0)
- return r;
-
- return 1;
-}
-
int files_same(const char *filea, const char *fileb, int flags) {
struct stat a, b;
diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h
index d1e8d33001..02b1c47123 100644
--- a/src/basic/stat-util.h
+++ b/src/basic/stat-util.h
@@ -35,7 +35,6 @@ int null_or_empty_path(const char *fn);
int null_or_empty_fd(int fd);
int path_is_read_only_fs(const char *path);
-int path_is_os_tree(const char *path);
int files_same(const char *filea, const char *fileb, int flags);
diff --git a/src/basic/util.c b/src/basic/util.c
index 68aaa21a8d..d158089538 100644
--- a/src/basic/util.c
+++ b/src/basic/util.c
@@ -264,7 +264,7 @@ int container_get_leader(const char *machine, pid_t *pid) {
return -EINVAL;
p = strjoina("/run/systemd/machines/", machine);
- r = parse_env_file(p, NEWLINE, "LEADER", &s, "CLASS", &class, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "LEADER", &s, "CLASS", &class, NULL);
if (r == -ENOENT)
return -EHOSTDOWN;
if (r < 0)
diff --git a/src/core/dbus-manager.c b/src/core/dbus-manager.c
index 13d7bd476f..f7535e38fc 100644
--- a/src/core/dbus-manager.c
+++ b/src/core/dbus-manager.c
@@ -26,6 +26,7 @@
#include "fs-util.h"
#include "install.h"
#include "log.h"
+#include "os-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "selinux-access.h"
diff --git a/src/core/locale-setup.c b/src/core/locale-setup.c
index 705dd95de1..dcb70d6fe3 100644
--- a/src/core/locale-setup.c
+++ b/src/core/locale-setup.c
@@ -24,7 +24,7 @@ int locale_setup(char ***environment) {
int r = 0, i;
if (detect_container() <= 0) {
- r = parse_env_file("/proc/cmdline", WHITESPACE,
+ r = parse_env_file(NULL, "/proc/cmdline", WHITESPACE,
"locale.LANG", &variables[VARIABLE_LANG],
"locale.LANGUAGE", &variables[VARIABLE_LANGUAGE],
"locale.LC_CTYPE", &variables[VARIABLE_LC_CTYPE],
@@ -48,7 +48,7 @@ int locale_setup(char ***environment) {
/* Hmm, nothing set on the kernel cmd line? Then let's
* try /etc/locale.conf */
if (r <= 0) {
- r = parse_env_file("/etc/locale.conf", NEWLINE,
+ r = parse_env_file(NULL, "/etc/locale.conf", NEWLINE,
"LANG", &variables[VARIABLE_LANG],
"LANGUAGE", &variables[VARIABLE_LANGUAGE],
"LC_CTYPE", &variables[VARIABLE_LC_CTYPE],
diff --git a/src/core/main.c b/src/core/main.c
index d70c1b8160..7e5a7c22b7 100644
--- a/src/core/main.c
+++ b/src/core/main.c
@@ -57,6 +57,7 @@
#include "manager.h"
#include "missing.h"
#include "mount-setup.h"
+#include "os-util.h"
#include "pager.h"
#include "parse-util.h"
#include "path-util.h"
@@ -1215,23 +1216,18 @@ static int enforce_syscall_archs(Set *archs) {
static int status_welcome(void) {
_cleanup_free_ char *pretty_name = NULL, *ansi_color = NULL;
- const char *fn;
int r;
if (arg_show_status <= 0)
return 0;
- FOREACH_STRING(fn, "/etc/os-release", "/usr/lib/os-release") {
- r = parse_env_file(fn, NEWLINE,
- "PRETTY_NAME", &pretty_name,
- "ANSI_COLOR", &ansi_color,
- NULL);
-
- if (r != -ENOENT)
- break;
- }
- if (r < 0 && r != -ENOENT)
- log_warning_errno(r, "Failed to read os-release file, ignoring: %m");
+ r = parse_os_release(NULL,
+ "PRETTY_NAME", &pretty_name,
+ "ANSI_COLOR", &ansi_color,
+ NULL);
+ if (r < 0)
+ log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to read os-release file, ignoring: %m");
if (log_get_show_color())
return status_printf(NULL, false, false,
diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c
index 7895e3832a..0fc8bb891b 100644
--- a/src/firstboot/firstboot.c
+++ b/src/firstboot/firstboot.c
@@ -33,6 +33,7 @@
#include "hostname-util.h"
#include "locale-util.h"
#include "mkdir.h"
+#include "os-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "proc-cmdline.h"
@@ -79,27 +80,16 @@ static bool press_any_key(void) {
static void print_welcome(void) {
_cleanup_free_ char *pretty_name = NULL;
- const char *os_release = NULL;
static bool done = false;
int r;
if (done)
return;
- os_release = prefix_roota(arg_root, "/etc/os-release");
- r = parse_env_file(os_release, NEWLINE,
- "PRETTY_NAME", &pretty_name,
- NULL);
- if (r == -ENOENT) {
-
- os_release = prefix_roota(arg_root, "/usr/lib/os-release");
- r = parse_env_file(os_release, NEWLINE,
- "PRETTY_NAME", &pretty_name,
- NULL);
- }
-
- if (r < 0 && r != -ENOENT)
- log_warning_errno(r, "Failed to read os-release file: %m");
+ r = parse_os_release(arg_root, "PRETTY_NAME", &pretty_name, NULL);
+ if (r < 0)
+ log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
+ "Failed to read os-release file, ignoring: %m");
printf("\nWelcome to your new installation of %s!\nPlease configure a few basic system settings:\n\n",
isempty(pretty_name) ? "Linux" : pretty_name);
diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c
index 88a8938c31..28fc628742 100644
--- a/src/hostname/hostnamed.c
+++ b/src/hostname/hostnamed.c
@@ -16,6 +16,7 @@
#include "env-util.h"
#include "fileio-label.h"
#include "hostname-util.h"
+#include "os-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "selinux-util.h"
@@ -88,7 +89,7 @@ static int context_read_data(Context *c) {
if (r < 0 && r != -ENOENT)
return r;
- r = parse_env_file("/etc/machine-info", NEWLINE,
+ r = parse_env_file(NULL, "/etc/machine-info", NEWLINE,
"PRETTY_HOSTNAME", &c->data[PROP_PRETTY_HOSTNAME],
"ICON_NAME", &c->data[PROP_ICON_NAME],
"CHASSIS", &c->data[PROP_CHASSIS],
@@ -98,18 +99,11 @@ static int context_read_data(Context *c) {
if (r < 0 && r != -ENOENT)
return r;
- r = parse_env_file("/etc/os-release", NEWLINE,
- "PRETTY_NAME", &c->data[PROP_OS_PRETTY_NAME],
- "CPE_NAME", &c->data[PROP_OS_CPE_NAME],
- "HOME_URL", &c->data[PROP_HOME_URL],
- NULL);
- if (r == -ENOENT)
- r = parse_env_file("/usr/lib/os-release", NEWLINE,
- "PRETTY_NAME", &c->data[PROP_OS_PRETTY_NAME],
- "CPE_NAME", &c->data[PROP_OS_CPE_NAME],
- "HOME_URL", &c->data[PROP_HOME_URL],
- NULL);
-
+ r = parse_os_release(NULL,
+ "PRETTY_NAME", &c->data[PROP_OS_PRETTY_NAME],
+ "CPE_NAME", &c->data[PROP_OS_CPE_NAME],
+ "HOME_URL", &c->data[PROP_HOME_URL],
+ NULL);
if (r < 0 && r != -ENOENT)
return r;
diff --git a/src/import/export.c b/src/import/export.c
index 5464b85840..5ff219b033 100644
--- a/src/import/export.c
+++ b/src/import/export.c
@@ -69,13 +69,11 @@ static int export_tar(int argc, char *argv[], void *userdata) {
int r, fd;
if (machine_name_is_valid(argv[1])) {
- r = image_find(argv[1], &image);
+ r = image_find(IMAGE_MACHINE, argv[1], &image);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Machine image %s not found.", argv[1]);
if (r < 0)
return log_error_errno(r, "Failed to look for machine %s: %m", argv[1]);
- if (r == 0) {
- log_error("Machine image %s not found.", argv[1]);
- return -ENOENT;
- }
local = image->path;
} else
@@ -148,13 +146,11 @@ static int export_raw(int argc, char *argv[], void *userdata) {
int r, fd;
if (machine_name_is_valid(argv[1])) {
- r = image_find(argv[1], &image);
+ r = image_find(IMAGE_MACHINE, argv[1], &image);
+ if (r == -ENOENT)
+ return log_error_errno(r, "Machine image %s not found.", argv[1]);
if (r < 0)
return log_error_errno(r, "Failed to look for machine %s: %m", argv[1]);
- if (r == 0) {
- log_error("Machine image %s not found.", argv[1]);
- return -ENOENT;
- }
local = image->path;
} else
diff --git a/src/import/import.c b/src/import/import.c
index 5f459b0b3d..5cef899226 100644
--- a/src/import/import.c
+++ b/src/import/import.c
@@ -75,10 +75,11 @@ static int import_tar(int argc, char *argv[], void *userdata) {
}
if (!arg_force) {
- r = image_find(local, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
- else if (r > 0) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
log_error("Image '%s' already exists.", local);
return -EEXIST;
}
@@ -170,10 +171,11 @@ static int import_raw(int argc, char *argv[], void *userdata) {
}
if (!arg_force) {
- r = image_find(local, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
- else if (r > 0) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
log_error("Image '%s' already exists.", local);
return -EEXIST;
}
diff --git a/src/import/pull.c b/src/import/pull.c
index d2a4d94f2b..dfbdda79f1 100644
--- a/src/import/pull.c
+++ b/src/import/pull.c
@@ -83,10 +83,11 @@ static int pull_tar(int argc, char *argv[], void *userdata) {
}
if (!arg_force) {
- r = image_find(local, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
- else if (r > 0) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
log_error("Image '%s' already exists.", local);
return -EEXIST;
}
@@ -169,10 +170,11 @@ static int pull_raw(int argc, char *argv[], void *userdata) {
}
if (!arg_force) {
- r = image_find(local, NULL);
- if (r < 0)
- return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
- else if (r > 0) {
+ r = image_find(IMAGE_MACHINE, local, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return log_error_errno(r, "Failed to check whether image '%s' exists: %m", local);
+ } else {
log_error("Image '%s' already exists.", local);
return -EEXIST;
}
diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c
index 7399d3315f..c29b65764a 100644
--- a/src/journal-remote/journal-gatewayd.c
+++ b/src/journal-remote/journal-gatewayd.c
@@ -24,6 +24,7 @@
#include "log.h"
#include "logs-show.h"
#include "microhttpd-util.h"
+#include "os-util.h"
#include "parse-util.h"
#include "sigbus.h"
#include "util.h"
@@ -777,10 +778,8 @@ static int request_handler_machine(
if (r < 0)
return mhd_respondf(connection, r, MHD_HTTP_INTERNAL_SERVER_ERROR, "Failed to determine disk usage: %m");
- if (parse_env_file("/etc/os-release", NEWLINE, "PRETTY_NAME", &os_name, NULL) == -ENOENT)
- (void) parse_env_file("/usr/lib/os-release", NEWLINE, "PRETTY_NAME", &os_name, NULL);
-
- get_virtualization(&v);
+ (void) parse_os_release(NULL, "PRETTY_NAME", &os_name, NULL);
+ (void) get_virtualization(&v);
r = asprintf(&json,
"{ \"machine_id\" : \"" SD_ID128_FORMAT_STR "\","
diff --git a/src/journal-remote/journal-upload.c b/src/journal-remote/journal-upload.c
index 5194b3136f..4a351079e9 100644
--- a/src/journal-remote/journal-upload.c
+++ b/src/journal-remote/journal-upload.c
@@ -152,7 +152,7 @@ static int load_cursor_state(Uploader *u) {
if (!u->state_file)
return 0;
- r = parse_env_file(u->state_file, NEWLINE,
+ r = parse_env_file(NULL, u->state_file, NEWLINE,
"LAST_CURSOR", &u->last_cursor,
NULL);
diff --git a/src/journal/journald-stream.c b/src/journal/journald-stream.c
index 35c664455e..93a0cc2288 100644
--- a/src/journal/journald-stream.c
+++ b/src/journal/journald-stream.c
@@ -646,7 +646,7 @@ static int stdout_stream_load(StdoutStream *stream, const char *fname) {
return log_oom();
}
- r = parse_env_file(stream->state_file, NEWLINE,
+ r = parse_env_file(NULL, stream->state_file, NEWLINE,
"PRIORITY", &priority,
"LEVEL_PREFIX", &level_prefix,
"FORWARD_TO_SYSLOG", &forward_to_syslog,
diff --git a/src/journal/sd-journal.c b/src/journal/sd-journal.c
index 2e81799add..7d81790356 100644
--- a/src/journal/sd-journal.c
+++ b/src/journal/sd-journal.c
@@ -1894,7 +1894,7 @@ _public_ int sd_journal_open_container(sd_journal **ret, const char *machine, in
assert_return(machine_name_is_valid(machine), -EINVAL);
p = strjoina("/run/systemd/machines/", machine);
- r = parse_env_file(p, NEWLINE, "ROOT", &root, "CLASS", &class, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "ROOT", &root, "CLASS", &class, NULL);
if (r == -ENOENT)
return -EHOSTDOWN;
if (r < 0)
diff --git a/src/libsystemd-network/sd-dhcp-lease.c b/src/libsystemd-network/sd-dhcp-lease.c
index 505d02b50d..5bc7454957 100644
--- a/src/libsystemd-network/sd-dhcp-lease.c
+++ b/src/libsystemd-network/sd-dhcp-lease.c
@@ -1034,7 +1034,7 @@ int dhcp_lease_load(sd_dhcp_lease **ret, const char *lease_file) {
if (r < 0)
return r;
- r = parse_env_file(lease_file, NEWLINE,
+ r = parse_env_file(NULL, lease_file, NEWLINE,
"ADDRESS", &address,
"ROUTER", &router,
"NETMASK", &netmask,
diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h
index 239c96bc1c..c67015c2c9 100644
--- a/src/libsystemd/sd-bus/bus-common-errors.h
+++ b/src/libsystemd/sd-bus/bus-common-errors.h
@@ -41,6 +41,8 @@
#define BUS_ERROR_NO_SUCH_USER_MAPPING "org.freedesktop.machine1.NoSuchUserMapping"
#define BUS_ERROR_NO_SUCH_GROUP_MAPPING "org.freedesktop.machine1.NoSuchGroupMapping"
+#define BUS_ERROR_NO_SUCH_PORTABLE_IMAGE "org.freedesktop.portable1.NoSuchImage"
+
#define BUS_ERROR_NO_SUCH_SESSION "org.freedesktop.login1.NoSuchSession"
#define BUS_ERROR_NO_SESSION_FOR_PID "org.freedesktop.login1.NoSessionForPID"
#define BUS_ERROR_NO_SUCH_USER "org.freedesktop.login1.NoSuchUser"
diff --git a/src/libsystemd/sd-login/sd-login.c b/src/libsystemd/sd-login/sd-login.c
index 3f73dadc2e..fdbc29cc38 100644
--- a/src/libsystemd/sd-login/sd-login.c
+++ b/src/libsystemd/sd-login/sd-login.c
@@ -273,7 +273,7 @@ _public_ int sd_uid_get_state(uid_t uid, char**state) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "STATE", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "STATE", &s, NULL);
if (r == -ENOENT) {
free(s);
s = strdup("offline");
@@ -304,7 +304,7 @@ _public_ int sd_uid_get_display(uid_t uid, char **session) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "DISPLAY", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "DISPLAY", &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
@@ -359,7 +359,7 @@ _public_ int sd_uid_is_on_seat(uid_t uid, int require_active, const char *seat)
variable = require_active ? "ACTIVE_UID" : "UIDS";
- r = parse_env_file(p, NEWLINE, variable, &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, variable, &s, NULL);
if (r == -ENOENT)
return 0;
if (r < 0)
@@ -388,7 +388,7 @@ static int uid_get_array(uid_t uid, const char *variable, char ***array) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, variable, &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, variable, &s, NULL);
if (r == -ENOENT || (r >= 0 && isempty(s))) {
if (array)
*array = NULL;
@@ -466,7 +466,7 @@ _public_ int sd_session_is_active(const char *session) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "ACTIVE", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "ACTIVE", &s, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -485,7 +485,7 @@ _public_ int sd_session_is_remote(const char *session) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "REMOTE", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "REMOTE", &s, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -506,7 +506,7 @@ _public_ int sd_session_get_state(const char *session, char **state) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "STATE", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "STATE", &s, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -529,7 +529,7 @@ _public_ int sd_session_get_uid(const char *session, uid_t *uid) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, "UID", &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "UID", &s, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -551,7 +551,7 @@ static int session_get_string(const char *session, const char *field, char **val
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE, field, &s, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, field, &s, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -643,7 +643,7 @@ _public_ int sd_seat_get_active(const char *seat, char **session, uid_t *uid) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE,
+ r = parse_env_file(NULL, p, NEWLINE,
"ACTIVE", &s,
"ACTIVE_UID", &t,
NULL);
@@ -681,7 +681,7 @@ _public_ int sd_seat_get_sessions(const char *seat, char ***sessions, uid_t **ui
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE,
+ r = parse_env_file(NULL, p, NEWLINE,
"SESSIONS", &s,
"UIDS", &t,
NULL);
@@ -750,7 +750,7 @@ static int seat_get_can(const char *seat, const char *variable) {
if (r < 0)
return r;
- r = parse_env_file(p, NEWLINE,
+ r = parse_env_file(NULL, p, NEWLINE,
variable, &s,
NULL);
if (r == -ENOENT)
@@ -899,7 +899,7 @@ _public_ int sd_machine_get_class(const char *machine, char **class) {
assert_return(class, -EINVAL);
p = strjoina("/run/systemd/machines/", machine);
- r = parse_env_file(p, NEWLINE, "CLASS", &c, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "CLASS", &c, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
@@ -923,7 +923,7 @@ _public_ int sd_machine_get_ifindices(const char *machine, int **ifindices) {
assert_return(ifindices, -EINVAL);
p = strjoina("/run/systemd/machines/", machine);
- r = parse_env_file(p, NEWLINE, "NETIF", &netif, NULL);
+ r = parse_env_file(NULL, p, NEWLINE, "NETIF", &netif, NULL);
if (r == -ENOENT)
return -ENXIO;
if (r < 0)
diff --git a/src/libsystemd/sd-network/sd-network.c b/src/libsystemd/sd-network/sd-network.c
index 286e92bc2c..4f3f6f9e1d 100644
--- a/src/libsystemd/sd-network/sd-network.c
+++ b/src/libsystemd/sd-network/sd-network.c
@@ -30,7 +30,7 @@ _public_ int sd_network_get_operational_state(char **state) {
assert_return(state, -EINVAL);
- r = parse_env_file("/run/systemd/netif/state", NEWLINE, "OPER_STATE", &s, NULL);
+ r = parse_env_file(NULL, "/run/systemd/netif/state", NEWLINE, "OPER_STATE", &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
@@ -50,7 +50,7 @@ static int network_get_strv(const char *key, char ***ret) {
assert_return(ret, -EINVAL);
- r = parse_env_file("/run/systemd/netif/state", NEWLINE, key, &s, NULL);
+ r = parse_env_file(NULL, "/run/systemd/netif/state", NEWLINE, key, &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
@@ -98,7 +98,7 @@ static int network_link_get_string(int ifindex, const char *field, char **ret) {
xsprintf(path, "/run/systemd/netif/links/%i", ifindex);
- r = parse_env_file(path, NEWLINE, field, &s, NULL);
+ r = parse_env_file(NULL, path, NEWLINE, field, &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
@@ -121,7 +121,7 @@ static int network_link_get_strv(int ifindex, const char *key, char ***ret) {
assert_return(ret, -EINVAL);
xsprintf(path, "/run/systemd/netif/links/%i", ifindex);
- r = parse_env_file(path, NEWLINE, key, &s, NULL);
+ r = parse_env_file(NULL, path, NEWLINE, key, &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
@@ -218,7 +218,7 @@ static int network_link_get_ifindexes(int ifindex, const char *key, int **ret) {
assert_return(ret, -EINVAL);
xsprintf(path, "/run/systemd/netif/links/%i", ifindex);
- r = parse_env_file(path, NEWLINE, key, &s, NULL);
+ r = parse_env_file(NULL, path, NEWLINE, key, &s, NULL);
if (r == -ENOENT)
return -ENODATA;
if (r < 0)
diff --git a/src/locale/keymap-util.c b/src/locale/keymap-util.c
index 1aca627658..7cc2eb4407 100644
--- a/src/locale/keymap-util.c
+++ b/src/locale/keymap-util.c
@@ -93,7 +93,7 @@ static int locale_read_data(Context *c) {
context_free_locale(c);
- r = parse_env_file("/etc/locale.conf", NEWLINE,
+ r = parse_env_file(NULL, "/etc/locale.conf", NEWLINE,
"LANG", &c->locale[VARIABLE_LANG],
"LANGUAGE", &c->locale[VARIABLE_LANGUAGE],
"LC_CTYPE", &c->locale[VARIABLE_LC_CTYPE],
@@ -137,7 +137,7 @@ static int vconsole_read_data(Context *c) {
context_free_vconsole(c);
- r = parse_env_file("/etc/vconsole.conf", NEWLINE,
+ r = parse_env_file(NULL, "/etc/vconsole.conf", NEWLINE,
"KEYMAP", &c->vc_keymap,
"KEYMAP_TOGGLE", &c->vc_keymap_toggle,
NULL);
diff --git a/src/locale/localectl.c b/src/locale/localectl.c
index 473b28de4d..49a44ae8bb 100644
--- a/src/locale/localectl.c
+++ b/src/locale/localectl.c
@@ -61,7 +61,7 @@ static void print_overridden_variables(void) {
if (detect_container() > 0 || arg_host)
return;
- r = parse_env_file("/proc/cmdline", WHITESPACE,
+ r = parse_env_file(NULL, "/proc/cmdline", WHITESPACE,
"locale.LANG", &variables[VARIABLE_LANG],
"locale.LANGUAGE", &variables[VARIABLE_LANGUAGE],
"locale.LC_CTYPE", &variables[VARIABLE_LC_CTYPE],
diff --git a/src/login/logind-inhibit.c b/src/login/logind-inhibit.c
index 0938f2f101..8bf3e72d22 100644
--- a/src/login/logind-inhibit.c
+++ b/src/login/logind-inhibit.c
@@ -198,7 +198,7 @@ int inhibitor_load(Inhibitor *i) {
char *cc;
int r;
- r = parse_env_file(i->state_file, NEWLINE,
+ r = parse_env_file(NULL, i->state_file, NEWLINE,
"WHAT", &what,
"UID", &uid,
"PID", &pid,
diff --git a/src/login/logind-session.c b/src/login/logind-session.c
index eb131ae50c..a82498c92e 100644
--- a/src/login/logind-session.c
+++ b/src/login/logind-session.c
@@ -365,7 +365,7 @@ int session_load(Session *s) {
assert(s);
- r = parse_env_file(s->state_file, NEWLINE,
+ r = parse_env_file(NULL, s->state_file, NEWLINE,
"REMOTE", &remote,
"SCOPE", &s->scope,
"SCOPE_JOB", &s->scope_job,
diff --git a/src/login/logind-user.c b/src/login/logind-user.c
index 8d3af6948c..1dfc590147 100644
--- a/src/login/logind-user.c
+++ b/src/login/logind-user.c
@@ -287,7 +287,7 @@ int user_load(User *u) {
assert(u);
- r = parse_env_file(u->state_file, NEWLINE,
+ r = parse_env_file(NULL, u->state_file, NEWLINE,
"SERVICE_JOB", &u->service_job,
"SLICE_JOB", &u->slice_job,
"DISPLAY", &display,
diff --git a/src/machine/image-dbus.c b/src/machine/image-dbus.c
index 0ee2247167..d2f5dc4593 100644
--- a/src/machine/image-dbus.c
+++ b/src/machine/image-dbus.c
@@ -435,8 +435,10 @@ int image_object_find(sd_bus *bus, const char *path, const char *interface, void
if (r < 0)
return r;
- r = image_find(e, &image);
- if (r <= 0)
+ r = image_find(IMAGE_MACHINE, e, &image);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
return r;
image->userdata = m;
@@ -478,7 +480,7 @@ int image_node_enumerator(sd_bus *bus, const char *path, void *userdata, char **
if (!images)
return -ENOMEM;
- r = image_discover(images);
+ r = image_discover(IMAGE_MACHINE, images);
if (r < 0)
return r;
diff --git a/src/machine/machine-dbus.c b/src/machine/machine-dbus.c
index 058543fbb2..d7921cf8cf 100644
--- a/src/machine/machine-dbus.c
+++ b/src/machine/machine-dbus.c
@@ -32,6 +32,7 @@
#include "machine-dbus.h"
#include "machine.h"
#include "mkdir.h"
+#include "os-util.h"
#include "path-util.h"
#include "process-util.h"
#include "signal-util.h"
@@ -330,7 +331,7 @@ int bus_machine_method_get_os_release(sd_bus_message *message, void *userdata, s
switch (m->class) {
case MACHINE_HOST:
- r = load_env_file_pairs(NULL, "/etc/os-release", NULL, &l);
+ r = load_os_release_pairs(NULL, &l);
if (r < 0)
return r;
@@ -361,13 +362,10 @@ int bus_machine_method_get_os_release(sd_bus_message *message, void *userdata, s
if (r < 0)
_exit(EXIT_FAILURE);
- fd = open("/etc/os-release", O_RDONLY|O_CLOEXEC|O_NOCTTY);
- if (fd < 0 && errno == ENOENT) {
- fd = open("/usr/lib/os-release", O_RDONLY|O_CLOEXEC|O_NOCTTY);
- if (fd < 0 && errno == ENOENT)
- _exit(EXIT_NOT_FOUND);
- }
- if (fd < 0)
+ r = open_os_release(NULL, NULL, &fd);
+ if (r == -ENOENT)
+ _exit(EXIT_NOT_FOUND);
+ if (r < 0)
_exit(EXIT_FAILURE);
r = copy_bytes(fd, pair[1], (uint64_t) -1, 0);
diff --git a/src/machine/machine-dbus.h b/src/machine/machine-dbus.h
index 50cdda45f0..6b9cdef8c8 100644
--- a/src/machine/machine-dbus.h
+++ b/src/machine/machine-dbus.h
@@ -31,5 +31,3 @@ int bus_machine_method_get_uid_shift(sd_bus_message *message, void *userdata, sd
int machine_send_signal(Machine *m, bool new_machine);
int machine_send_create_reply(Machine *m, sd_bus_error *error);
-
-int bus_reply_pair_array(sd_bus_message *m, char **l);
diff --git a/src/machine/machine.c b/src/machine/machine.c
index 008bbbc3f4..f57984a0c2 100644
--- a/src/machine/machine.c
+++ b/src/machine/machine.c
@@ -255,7 +255,7 @@ int machine_load(Machine *m) {
if (!m->state_file)
return 0;
- r = parse_env_file(m->state_file, NEWLINE,
+ r = parse_env_file(NULL, m->state_file, NEWLINE,
"SCOPE", &m->unit,
"SCOPE_JOB", &m->scope_job,
"SERVICE", &m->service,
diff --git a/src/machine/machined-dbus.c b/src/machine/machined-dbus.c
index 2ac4689ba1..cb46718696 100644
--- a/src/machine/machined-dbus.c
+++ b/src/machine/machined-dbus.c
@@ -145,8 +145,8 @@ static int method_get_image(sd_bus_message *message, void *userdata, sd_bus_erro
if (r < 0)
return r;
- r = image_find(name, NULL);
- if (r == 0)
+ r = image_find(IMAGE_MACHINE, name, NULL);
+ if (r == -ENOENT)
return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
if (r < 0)
return r;
@@ -450,14 +450,14 @@ static int method_register_machine(sd_bus_message *message, void *userdata, sd_b
return method_register_machine_internal(message, false, userdata, error);
}
-static int method_terminate_machine(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
+static int redirect_method_to_machine(sd_bus_message *message, Manager *m, sd_bus_error *error, sd_bus_message_handler_t method) {
Machine *machine;
const char *name;
int r;
assert(message);
assert(m);
+ assert(method);
r = sd_bus_message_read(message, "s", &name);
if (r < 0)
@@ -467,67 +467,23 @@ static int method_terminate_machine(sd_bus_message *message, void *userdata, sd_
if (!machine)
return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
- return bus_machine_method_terminate(message, machine, error);
+ return method(message, machine, error);
+}
+
+static int method_terminate_machine(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_terminate);
}
static int method_kill_machine(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return sd_bus_error_set_errno(error, r);
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_kill(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_kill);
}
static int method_get_machine_addresses(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return sd_bus_error_set_errno(error, r);
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_get_addresses(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_get_addresses);
}
static int method_get_machine_os_release(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return sd_bus_error_set_errno(error, r);
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_get_os_release(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_get_os_release);
}
static int method_list_images(sd_bus_message *message, void *userdata, sd_bus_error *error) {
@@ -545,7 +501,7 @@ static int method_list_images(sd_bus_message *message, void *userdata, sd_bus_er
if (!images)
return -ENOMEM;
- r = image_discover(images);
+ r = image_discover(IMAGE_MACHINE, images);
if (r < 0)
return r;
@@ -584,336 +540,89 @@ static int method_list_images(sd_bus_message *message, void *userdata, sd_bus_er
}
static int method_open_machine_pty(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return sd_bus_error_set_errno(error, r);
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_open_pty(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_open_pty);
}
static int method_open_machine_login(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_open_login(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_open_login);
}
static int method_open_machine_shell(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
-
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_open_shell(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_open_shell);
}
static int method_bind_mount_machine(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_bind_mount(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_bind_mount);
}
static int method_copy_machine(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_copy(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_copy);
}
static int method_open_machine_root_directory(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
- const char *name;
- int r;
-
- assert(message);
- assert(m);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
-
- return bus_machine_method_open_root_directory(message, machine, error);
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_open_root_directory);
}
static int method_get_machine_uid_shift(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- Manager *m = userdata;
- Machine *machine;
+ return redirect_method_to_machine(message, userdata, error, bus_machine_method_get_uid_shift);
+}
+
+static int redirect_method_to_image(sd_bus_message *message, Manager *m, sd_bus_error *error, sd_bus_message_handler_t method) {
+ _cleanup_(image_unrefp) Image* i = NULL;
const char *name;
int r;
assert(message);
assert(m);
+ assert(method);
r = sd_bus_message_read(message, "s", &name);
if (r < 0)
return r;
- machine = hashmap_get(m->machines, name);
- if (!machine)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_MACHINE, "No machine '%s' known", name);
+ if (!image_name_is_valid(name))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
- return bus_machine_method_get_uid_shift(message, machine, error);
+ r = image_find(IMAGE_MACHINE, name, &i);
+ if (r == -ENOENT)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
+ if (r < 0)
+ return r;
+
+ i->userdata = m;
+ return method(message, i, error);
}
static int method_remove_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image* i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_remove(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_remove);
}
static int method_rename_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image* i = NULL;
- const char *old_name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &old_name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(old_name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", old_name);
-
- r = image_find(old_name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", old_name);
-
- i->userdata = userdata;
- return bus_image_method_rename(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_rename);
}
static int method_clone_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *old_name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &old_name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(old_name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", old_name);
-
- r = image_find(old_name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", old_name);
-
- i->userdata = userdata;
- return bus_image_method_clone(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_clone);
}
static int method_mark_image_read_only(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_mark_read_only(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_mark_read_only);
}
static int method_get_image_hostname(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_get_hostname(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_get_hostname);
}
static int method_get_image_machine_id(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_get_machine_id(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_get_machine_id);
}
static int method_get_image_machine_info(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_get_machine_info(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_get_machine_info);
}
static int method_get_image_os_release(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_get_os_release(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_get_os_release);
}
static int clean_pool_done(Operation *operation, int ret, sd_bus_error *error) {
@@ -1070,7 +779,7 @@ static int method_clean_pool(sd_bus_message *message, void *userdata, sd_bus_err
goto child_fail;
}
- r = image_discover(images);
+ r = image_discover(IMAGE_MACHINE, images);
if (r < 0)
goto child_fail;
@@ -1198,27 +907,7 @@ static int method_set_pool_limit(sd_bus_message *message, void *userdata, sd_bus
}
static int method_set_image_limit(sd_bus_message *message, void *userdata, sd_bus_error *error) {
- _cleanup_(image_unrefp) Image *i = NULL;
- const char *name;
- int r;
-
- assert(message);
-
- r = sd_bus_message_read(message, "s", &name);
- if (r < 0)
- return r;
-
- if (!image_name_is_valid(name))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is invalid.", name);
-
- r = image_find(name, &i);
- if (r < 0)
- return r;
- if (r == 0)
- return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_IMAGE, "No image '%s' known", name);
-
- i->userdata = userdata;
- return bus_image_method_set_limit(message, i, error);
+ return redirect_method_to_image(message, userdata, error, bus_image_method_set_limit);
}
static int method_map_from_machine_user(sd_bus_message *message, void *userdata, sd_bus_error *error) {
@@ -1899,30 +1588,3 @@ int manager_add_machine(Manager *m, const char *name, Machine **_machine) {
return 0;
}
-
-int bus_reply_pair_array(sd_bus_message *m, char **l) {
- _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
- char **k, **v;
- int r;
-
- r = sd_bus_message_new_method_return(m, &reply);
- if (r < 0)
- return r;
-
- r = sd_bus_message_open_container(reply, 'a', "{ss}");
- if (r < 0)
- return r;
-
- STRV_FOREACH_PAIR(k, v, l) {
- r = sd_bus_message_append(reply, "{ss}", *k, *v);
- if (r < 0)
- return r;
- }
-
- r = sd_bus_message_close_container(reply);
- if (r < 0)
- return r;
-
- return sd_bus_send(NULL, reply, NULL);
-
-}
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
index be0742e47b..8cc817ac00 100644
--- a/src/network/networkd-link.c
+++ b/src/network/networkd-link.c
@@ -2866,7 +2866,7 @@ static int link_load(Link *link) {
assert(link);
- r = parse_env_file(link->state_file, NEWLINE,
+ r = parse_env_file(NULL, link->state_file, NEWLINE,
"NETWORK_FILE", &network_file,
"ADDRESSES", &addresses,
"ROUTES", &routes,
diff --git a/src/network/networkd-lldp-tx.c b/src/network/networkd-lldp-tx.c
index 6cccfc4be0..a2121eb5ff 100644
--- a/src/network/networkd-lldp-tx.c
+++ b/src/network/networkd-lldp-tx.c
@@ -249,7 +249,7 @@ static int link_send_lldp(Link *link) {
return r;
(void) gethostname_strict(&hostname);
- (void) parse_env_file("/etc/machine-info", NEWLINE, "PRETTY_HOSTNAME", &pretty_hostname, NULL);
+ (void) parse_env_file(NULL, "/etc/machine-info", NEWLINE, "PRETTY_HOSTNAME", &pretty_hostname, NULL);
assert_cc(LLDP_TX_INTERVAL_USEC * LLDP_TX_HOLD + 1 <= (UINT16_MAX - 1) * USEC_PER_SEC);
ttl = DIV_ROUND_UP(LLDP_TX_INTERVAL_USEC * LLDP_TX_HOLD + 1, USEC_PER_SEC);
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index 009ecf4e4a..be2f1715ad 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -77,6 +77,7 @@
#include "nspawn-settings.h"
#include "nspawn-setuid.h"
#include "nspawn-stub-pid1.h"
+#include "os-util.h"
#include "pager.h"
#include "parse-util.h"
#include "path-util.h"
@@ -2398,13 +2399,11 @@ static int determine_names(void) {
if (arg_machine) {
_cleanup_(image_unrefp) Image *i = NULL;
- r = image_find(arg_machine, &i);
+ r = image_find(IMAGE_MACHINE, arg_machine, &i);
+ if (r == -ENOENT)
+ return log_error_errno(r, "No image for machine '%s'.", arg_machine);
if (r < 0)
return log_error_errno(r, "Failed to find image for machine '%s': %m", arg_machine);
- if (r == 0) {
- log_error("No image for machine '%s'.", arg_machine);
- return -ENOENT;
- }
if (IN_SET(i->type, IMAGE_RAW, IMAGE_BLOCK))
r = free_and_strdup(&arg_image, i->path);
@@ -2883,7 +2882,9 @@ static int outer_child(
* makes sure ESP partitions and userns are compatible. */
r = dissected_image_mount(dissected_image, directory, arg_uid_shift,
- DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_DISCARD_ON_LOOP|(arg_read_only ? DISSECT_IMAGE_READ_ONLY : 0));
+ DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_DISCARD_ON_LOOP|
+ (arg_read_only ? DISSECT_IMAGE_READ_ONLY : 0)|
+ (arg_start_mode == START_BOOT ? DISSECT_IMAGE_VALIDATE_OS : 0));
if (r < 0)
return r;
}
diff --git a/src/portable/meson.build b/src/portable/meson.build
new file mode 100644
index 0000000000..dc57163460
--- /dev/null
+++ b/src/portable/meson.build
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+systemd_portabled_sources = files('''
+ portable.c
+ portable.h
+ portabled-bus.c
+ portabled-image-bus.c
+ portabled-image-bus.h
+ portabled-image.c
+ portabled-image.h
+ portabled-operation.c
+ portabled-operation.h
+ portabled.c
+ portabled.h
+'''.split())
+
+if conf.get('ENABLE_PORTABLED') == 1
+ install_data('org.freedesktop.portable1.conf',
+ install_dir : dbuspolicydir)
+ install_data('org.freedesktop.portable1.service',
+ install_dir : dbussystemservicedir)
+ install_data('org.freedesktop.portable1.policy',
+ install_dir : polkitpolicydir)
+
+ install_data('profile/default/service.conf', install_dir : join_paths(profiledir, 'default'))
+ install_data('profile/nonetwork/service.conf', install_dir : join_paths(profiledir, 'nonetwork'))
+ install_data('profile/strict/service.conf', install_dir : join_paths(profiledir, 'strict'))
+ install_data('profile/trusted/service.conf', install_dir : join_paths(profiledir, 'trusted'))
+endif
diff --git a/src/portable/org.freedesktop.portable1.conf b/src/portable/org.freedesktop.portable1.conf
new file mode 100644
index 0000000000..4c20a886e6
--- /dev/null
+++ b/src/portable/org.freedesktop.portable1.conf
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/portable/org.freedesktop.portable1.policy b/src/portable/org.freedesktop.portable1.policy
new file mode 100644
index 0000000000..17fd0a7a20
--- /dev/null
+++ b/src/portable/org.freedesktop.portable1.policy
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+ The systemd Project
+ http://www.freedesktop.org/wiki/Software/systemd
+
+
+ Inspect a portable service
+ Authentication is required to inspect a portable service.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
+
+ Attach or detach a portable service
+ Authentication is required to attach or detach a portable service.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+ org.freedesktop.systemd1.reload-daemon
+
+
+
+ Delete or modify portable service image
+ Authentication is required to delete or modify a portable service image.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
+
diff --git a/src/portable/org.freedesktop.portable1.service b/src/portable/org.freedesktop.portable1.service
new file mode 100644
index 0000000000..eff37b0183
--- /dev/null
+++ b/src/portable/org.freedesktop.portable1.service
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+[D-BUS Service]
+Name=org.freedesktop.portable1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.portable1.service
diff --git a/src/portable/portable.c b/src/portable/portable.c
new file mode 100644
index 0000000000..aaabe9fb36
--- /dev/null
+++ b/src/portable/portable.c
@@ -0,0 +1,1427 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "bus-common-errors.h"
+#include "bus-error.h"
+#include "conf-files.h"
+#include "copy.h"
+#include "def.h"
+#include "dirent-util.h"
+#include "dissect-image.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "io-util.h"
+#include "locale-util.h"
+#include "loop-util.h"
+#include "machine-image.h"
+#include "mkdir.h"
+#include "os-util.h"
+#include "path-lookup.h"
+#include "portable.h"
+#include "process-util.h"
+#include "set.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "user-util.h"
+
+static const char profile_dirs[] = CONF_PATHS_NULSTR("systemd/portable/profile");
+
+/* Markers used in the first line of our 20-portable.conf unit file drop-in to determine, that a) the unit file was
+ * dropped there by the portable service logic and b) for which image it was dropped there. */
+#define PORTABLE_DROPIN_MARKER_BEGIN "# Drop-in created for image '"
+#define PORTABLE_DROPIN_MARKER_END "', do not edit."
+
+static bool prefix_match(const char *unit, const char *prefix) {
+ const char *p;
+
+ p = startswith(unit, prefix);
+ if (!p)
+ return false;
+
+ /* Only respect prefixes followed by dash or dot or when there's a complete match */
+ return IN_SET(*p, '-', '.', '@', 0);
+}
+
+static bool unit_match(const char *unit, char **matches) {
+ const char *dot;
+ char **i;
+
+ dot = strrchr(unit, '.');
+ if (!dot)
+ return false;
+
+ if (!STR_IN_SET(dot, ".service", ".socket", ".target", ".timer", ".path"))
+ return false;
+
+ /* Empty match expression means: everything */
+ if (strv_isempty(matches))
+ return true;
+
+ /* Otherwise, at least one needs to match */
+ STRV_FOREACH(i, matches)
+ if (prefix_match(unit, *i))
+ return true;
+
+ return false;
+}
+
+static PortableMetadata *portable_metadata_new(const char *name, int fd) {
+ PortableMetadata *m;
+
+ m = malloc0(offsetof(PortableMetadata, name) + strlen(name) + 1);
+ if (!m)
+ return NULL;
+
+ strcpy(m->name, name);
+ m->fd = fd;
+
+ return m;
+}
+
+PortableMetadata *portable_metadata_unref(PortableMetadata *i) {
+ if (!i)
+ return NULL;
+
+ safe_close(i->fd);
+ free(i->source);
+
+ return mfree(i);
+}
+
+Hashmap *portable_metadata_hashmap_unref(Hashmap *h) {
+
+ for (;;) {
+ PortableMetadata *i;
+
+ i = hashmap_steal_first(h);
+ if (!i)
+ break;
+
+ portable_metadata_unref(i);
+ }
+
+ return hashmap_free(h);
+}
+
+static int compare_metadata(PortableMetadata *const *x, PortableMetadata *const *y) {
+ return strcmp((*x)->name, (*y)->name);
+}
+
+int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret) {
+
+ _cleanup_free_ PortableMetadata **sorted = NULL;
+ Iterator iterator;
+ PortableMetadata *item;
+ size_t k = 0;
+
+ sorted = new(PortableMetadata*, hashmap_size(unit_files));
+ if (!sorted)
+ return -ENOMEM;
+
+ HASHMAP_FOREACH(item, unit_files, iterator)
+ sorted[k++] = item;
+
+ assert(k == hashmap_size(unit_files));
+
+ typesafe_qsort(sorted, k, compare_metadata);
+
+ *ret = TAKE_PTR(sorted);
+ return 0;
+}
+
+static int send_item(
+ int socket_fd,
+ const char *name,
+ int fd) {
+
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(int))];
+ } control = {};
+ struct iovec iovec;
+ struct msghdr mh = {
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ .msg_iov = &iovec,
+ .msg_iovlen = 1,
+ };
+ struct cmsghdr *cmsg;
+ _cleanup_close_ int data_fd = -1;
+
+ assert(socket_fd >= 0);
+ assert(name);
+ assert(fd >= 0);
+
+ data_fd = fd_duplicate_data_fd(fd);
+ if (data_fd < 0)
+ return data_fd;
+
+ cmsg = CMSG_FIRSTHDR(&mh);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &data_fd, sizeof(int));
+
+ mh.msg_controllen = CMSG_SPACE(sizeof(int));
+ iovec = IOVEC_MAKE_STRING(name);
+
+ if (sendmsg(socket_fd, &mh, MSG_NOSIGNAL) < 0)
+ return -errno;
+
+ return 0;
+}
+
+static int recv_item(
+ int socket_fd,
+ char **ret_name,
+ int *ret_fd) {
+
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(int))];
+ } control = {};
+ char buffer[PATH_MAX+2];
+ struct iovec iov = IOVEC_INIT(buffer, sizeof(buffer)-1);
+ struct msghdr mh = {
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+ struct cmsghdr *cmsg;
+ _cleanup_close_ int found_fd = -1;
+ char *copy;
+ ssize_t n;
+
+ assert(socket_fd >= 0);
+ assert(ret_name);
+ assert(ret_fd);
+
+ n = recvmsg(socket_fd, &mh, MSG_NOSIGNAL|MSG_CMSG_CLOEXEC);
+ if (n < 0)
+ return -errno;
+
+ CMSG_FOREACH(cmsg, &mh) {
+ if (cmsg->cmsg_level == SOL_SOCKET &&
+ cmsg->cmsg_type == SCM_RIGHTS) {
+
+ if (cmsg->cmsg_len == CMSG_LEN(sizeof(int))) {
+ assert(found_fd < 0);
+ found_fd = *(int*) CMSG_DATA(cmsg);
+ break;
+ }
+
+ cmsg_close_all(&mh);
+ return -EIO;
+ }
+ }
+
+ buffer[n] = 0;
+
+ copy = strdup(buffer);
+ if (!copy)
+ return -ENOMEM;
+
+ *ret_name = copy;
+ *ret_fd = TAKE_FD(found_fd);
+
+ return 0;
+}
+
+static int extract_now(
+ const char *where,
+ char **matches,
+ int socket_fd,
+ PortableMetadata **ret_os_release,
+ Hashmap **ret_unit_files) {
+
+ _cleanup_(portable_metadata_hashmap_unrefp) Hashmap *unit_files = NULL;
+ _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
+ _cleanup_(lookup_paths_free) LookupPaths paths = {};
+ _cleanup_close_ int os_release_fd = -1;
+ _cleanup_free_ char *os_release_path = NULL;
+ char **i;
+ int r;
+
+ /* Extracts the metadata from a directory tree 'where'. Extracts two kinds of information: the /etc/os-release
+ * data, and all unit files matching the specified expression. Note that this function is called in two very
+ * different but also similar contexts. When the tool gets invoked on a directory tree, we'll process it
+ * directly, and in-process, and thus can return the requested data directly, via 'ret_os_release' and
+ * 'ret_unit_files'. However, if the tool is invoked on a raw disk image — which needs to be mounted first — we
+ * are invoked in a child process with private mounts and then need to send the collected data to our
+ * parent. To handle both cases in one call this function also gets a 'socket_fd' parameter, which when >= 0 is
+ * used to send the data to the parent. */
+
+ assert(where);
+
+ /* First, find /etc/os-release and send it upstream (or just save it). */
+ r = open_os_release(where, &os_release_path, &os_release_fd);
+ if (r < 0)
+ log_debug_errno(r, "Couldn't acquire os-release file, ignoring: %m");
+ else {
+ if (socket_fd >= 0) {
+ r = send_item(socket_fd, "/etc/os-release", os_release_fd);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to send os-release file: %m");
+ }
+
+ if (ret_os_release) {
+ os_release = portable_metadata_new("/etc/os-release", os_release_fd);
+ if (!os_release)
+ return -ENOMEM;
+
+ os_release_fd = -1;
+ os_release->source = TAKE_PTR(os_release_path);
+ }
+ }
+
+ /* Then, send unit file data to the parent (or/and add it to the hashmap). For that we use our usual unit
+ * discovery logic. Note that we force looking inside of /lib/systemd/system/ for units too, as we mightbe
+ * compiled for a split-usr system but the image might be a legacy-usr one. */
+ r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, where);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to acquire lookup paths: %m");
+
+ unit_files = hashmap_new(&string_hash_ops);
+ if (!unit_files)
+ return -ENOMEM;
+
+ STRV_FOREACH(i, paths.search_path) {
+ _cleanup_free_ char *resolved = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ struct dirent *de;
+
+ r = chase_symlinks_and_opendir(*i, where, 0, &resolved, &d);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to open unit path '%s', ignoring: %m", *i);
+ continue;
+ }
+
+ FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to read directory: %m")) {
+ _cleanup_(portable_metadata_unrefp) PortableMetadata *m = NULL;
+ _cleanup_close_ int fd = -1;
+
+ if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY))
+ continue;
+
+ if (!unit_match(de->d_name, matches))
+ continue;
+
+ /* Filter out duplicates */
+ if (hashmap_get(unit_files, de->d_name))
+ continue;
+
+ dirent_ensure_type(d, de);
+ if (!IN_SET(de->d_type, DT_LNK, DT_REG))
+ continue;
+
+ fd = openat(dirfd(d), de->d_name, O_CLOEXEC|O_RDONLY);
+ if (fd < 0) {
+ log_debug_errno(errno, "Failed to open unit file '%s', ignoring: %m", de->d_name);
+ continue;
+ }
+
+ if (socket_fd >= 0) {
+ r = send_item(socket_fd, de->d_name, fd);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to send unit metadata to parent: %m");
+ }
+
+ m = portable_metadata_new(de->d_name, fd);
+ if (!m)
+ return -ENOMEM;
+ fd = -1;
+
+ m->source = strjoin(resolved, "/", de->d_name);
+ if (!m->source)
+ return -ENOMEM;
+
+ r = hashmap_put(unit_files, m->name, m);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add unit to hashmap: %m");
+ m = NULL;
+ }
+ }
+
+ if (ret_os_release)
+ *ret_os_release = TAKE_PTR(os_release);
+ if (ret_unit_files)
+ *ret_unit_files = TAKE_PTR(unit_files);
+
+ return 0;
+}
+
+static int portable_extract_by_path(
+ const char *path,
+ char **matches,
+ PortableMetadata **ret_os_release,
+ Hashmap **ret_unit_files,
+ sd_bus_error *error) {
+
+ _cleanup_(portable_metadata_hashmap_unrefp) Hashmap *unit_files = NULL;
+ _cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL;
+ _cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
+ int r;
+
+ assert(path);
+
+ r = loop_device_make_by_path(path, O_RDONLY, &d);
+ if (r == -EISDIR) {
+ /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory
+ * tree and not a raw device. It's easy then. */
+
+ r = extract_now(path, matches, -1, &os_release, &unit_files);
+ if (r < 0)
+ return r;
+
+ } else if (r < 0)
+ return log_debug_errno(r, "Failed to set up loopback device: %m");
+ else {
+ _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
+ _cleanup_(rmdir_and_freep) char *tmpdir = NULL;
+ _cleanup_(close_pairp) int seq[2] = { -1, -1 };
+ _cleanup_(sigkill_waitp) pid_t child = 0;
+
+ /* We now have a loopback block device, let's fork off a child in its own mount namespace, mount it
+ * there, and extract the metadata we need. The metadata is sent from the child back to us. */
+
+ BLOCK_SIGNALS(SIGCHLD);
+
+ r = mkdtemp_malloc("/tmp/inspect-XXXXXX", &tmpdir);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to create temporary directory: %m");
+
+ r = dissect_image(d->fd, NULL, 0, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_REQUIRE_ROOT|DISSECT_IMAGE_DISCARD_ON_LOOP, &m);
+ if (r == -ENOPKG)
+ sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Couldn't identify a suitable partition table or file system in '%s'.", path);
+ else if (r == -EADDRNOTAVAIL)
+ sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No root partition for specified root hash found in '%s'.", path);
+ else if (r == -ENOTUNIQ)
+ sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Multiple suitable root partitions found in image '%s'.", path);
+ else if (r == -ENXIO)
+ sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "No suitable root partition found in image '%s'.", path);
+ else if (r == -EPROTONOSUPPORT)
+ sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Device '%s' is loopback block device with partition scanning turned off, please turn it on.", path);
+ if (r < 0)
+ return r;
+
+ if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0)
+ return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m");
+
+ r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE|FORK_LOG, &child);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ seq[0] = safe_close(seq[0]);
+
+ r = dissected_image_mount(m, tmpdir, UID_INVALID, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_VALIDATE_OS);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to mount dissected image: %m");
+ goto child_finish;
+ }
+
+ r = extract_now(tmpdir, matches, seq[1], NULL, NULL);
+
+ child_finish:
+ _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
+ }
+
+ seq[1] = safe_close(seq[1]);
+
+ unit_files = hashmap_new(&string_hash_ops);
+ if (!unit_files)
+ return -ENOMEM;
+
+ for (;;) {
+ _cleanup_(portable_metadata_unrefp) PortableMetadata *add = NULL;
+ _cleanup_free_ char *name = NULL;
+ _cleanup_close_ int fd = -1;
+
+ r = recv_item(seq[0], &name, &fd);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to receive item: %m");
+
+ /* We can't really distuingish a zero-length datagram without any fds from EOF (both are signalled the
+ * same way by recvmsg()). Hence, accept either as end notification. */
+ if (isempty(name) && fd < 0)
+ break;
+
+ if (isempty(name) || fd < 0) {
+ log_debug("Invalid item sent from child.");
+ return -EINVAL;
+ }
+
+ add = portable_metadata_new(name, fd);
+ if (!add)
+ return -ENOMEM;
+ fd = -1;
+
+ /* Note that we do not initialize 'add->source' here, as the source path is not usable here as
+ * it refers to a path only valid in the short-living namespaced child process we forked
+ * here. */
+
+ if (PORTABLE_METADATA_IS_UNIT(add)) {
+ r = hashmap_put(unit_files, add->name, add);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add item to unit file list: %m");
+
+ add = NULL;
+
+ } else if (PORTABLE_METADATA_IS_OS_RELEASE(add)) {
+
+ assert(!os_release);
+ os_release = TAKE_PTR(add);
+ } else
+ assert_not_reached("Unexpected metadata item from child.");
+ }
+
+ r = wait_for_terminate_and_check("(sd-dissect)", child, 0);
+ if (r < 0)
+ return r;
+ child = 0;
+ }
+
+ if (!os_release)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image '%s' lacks os-release data, refusing.", path);
+
+ if (hashmap_isempty(unit_files))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Couldn't find any matching unit files in image '%s', refusing.", path);
+
+ if (ret_unit_files)
+ *ret_unit_files = TAKE_PTR(unit_files);
+
+ if (ret_os_release)
+ *ret_os_release = TAKE_PTR(os_release);
+
+ return 0;
+}
+
+int portable_extract(
+ const char *name_or_path,
+ char **matches,
+ PortableMetadata **ret_os_release,
+ Hashmap **ret_unit_files,
+ sd_bus_error *error) {
+
+ _cleanup_(image_unrefp) Image *image = NULL;
+ int r;
+
+ assert(name_or_path);
+
+ r = image_find_harder(IMAGE_PORTABLE, name_or_path, &image);
+ if (r < 0)
+ return r;
+
+ return portable_extract_by_path(image->path, matches, ret_os_release, ret_unit_files, error);
+}
+
+static int unit_file_is_active(
+ sd_bus *bus,
+ const char *name,
+ sd_bus_error *error) {
+
+ static const char *const active_states[] = {
+ "activating",
+ "active",
+ "reloading",
+ "deactivating",
+ NULL,
+ };
+ int r;
+
+ if (!bus)
+ return false;
+
+ /* If we are looking at a plain or instance things are easy, we can just query the state */
+ if (unit_name_is_valid(name, UNIT_NAME_PLAIN|UNIT_NAME_INSTANCE)) {
+ _cleanup_free_ char *path = NULL, *buf = NULL;
+
+ path = unit_dbus_path_from_name(name);
+ if (!path)
+ return -ENOMEM;
+
+ r = sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.systemd1",
+ path,
+ "org.freedesktop.systemd1.Unit",
+ "ActiveState",
+ error,
+ &buf);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to retrieve unit state: %s", bus_error_message(error, r));
+
+ return strv_contains((char**) active_states, buf);
+ }
+
+ /* Otherwise we need to enumerate. But let's build the most restricted query we can */
+ if (unit_name_is_valid(name, UNIT_NAME_TEMPLATE)) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ const char *at, *prefix, *joined;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "ListUnitsByPatterns");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append_strv(m, (char**) active_states);
+ if (r < 0)
+ return r;
+
+ at = strchr(name, '@');
+ assert(at);
+
+ prefix = strndupa(name, at + 1 - name);
+ joined = strjoina(prefix, "*", at + 1);
+
+ r = sd_bus_message_append_strv(m, STRV_MAKE(joined));
+ if (r < 0)
+ return r;
+
+ r = sd_bus_call(bus, m, 0, error, &reply);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to list units: %s", bus_error_message(error, r));
+
+ r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_ARRAY, "(ssssssouso)");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_enter_container(reply, SD_BUS_TYPE_STRUCT, "ssssssouso");
+ if (r < 0)
+ return r;
+
+ return r > 0;
+ }
+
+ return -EINVAL;
+}
+
+static int portable_changes_add(
+ PortableChange **changes,
+ size_t *n_changes,
+ PortableChangeType type,
+ const char *path,
+ const char *source) {
+
+ _cleanup_free_ char *p = NULL, *s = NULL;
+ PortableChange *c;
+
+ assert(path);
+ assert(!changes == !n_changes);
+
+ if (!changes)
+ return 0;
+
+ c = reallocarray(*changes, *n_changes + 1, sizeof(PortableChange));
+ if (!c)
+ return -ENOMEM;
+ *changes = c;
+
+ p = strdup(path);
+ if (!p)
+ return -ENOMEM;
+
+ path_kill_slashes(p);
+
+ if (source) {
+ s = strdup(source);
+ if (!s)
+ return -ENOMEM;
+
+ path_kill_slashes(s);
+ }
+
+ c[(*n_changes)++] = (PortableChange) {
+ .type = type,
+ .path = TAKE_PTR(p),
+ .source = TAKE_PTR(s),
+ };
+
+ return 0;
+}
+
+static int portable_changes_add_with_prefix(
+ PortableChange **changes,
+ size_t *n_changes,
+ PortableChangeType type,
+ const char *prefix,
+ const char *path,
+ const char *source) {
+
+ assert(path);
+ assert(!changes == !n_changes);
+
+ if (!changes)
+ return 0;
+
+ if (prefix) {
+ path = strjoina(prefix, "/", path);
+
+ if (source)
+ source = strjoina(prefix, "/", source);
+ }
+
+ return portable_changes_add(changes, n_changes, type, path, source);
+}
+
+void portable_changes_free(PortableChange *changes, size_t n_changes) {
+ size_t i;
+
+ assert(changes || n_changes == 0);
+
+ for (i = 0; i < n_changes; i++) {
+ free(changes[i].path);
+ free(changes[i].source);
+ }
+
+ free(changes);
+}
+
+static int install_chroot_dropin(
+ const char *image_path,
+ ImageType type,
+ const PortableMetadata *m,
+ const char *dropin_dir,
+ char **ret_dropin,
+ PortableChange **changes,
+ size_t *n_changes) {
+
+ _cleanup_free_ char *text = NULL, *dropin = NULL;
+ int r;
+
+ assert(image_path);
+ assert(m);
+ assert(dropin_dir);
+
+ dropin = strjoin(dropin_dir, "/20-portable.conf");
+ if (!dropin)
+ return -ENOMEM;
+
+ text = strjoin(PORTABLE_DROPIN_MARKER_BEGIN, image_path, PORTABLE_DROPIN_MARKER_END "\n");
+ if (!text)
+ return -ENOMEM;
+
+ if (endswith(m->name, ".service"))
+ if (!strextend(&text,
+ "\n"
+ "[Service]\n",
+ IN_SET(type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) ? "RootDirectory=" : "RootImage=", image_path, "\n"
+ "Environment=PORTABLE=", basename(image_path), "\n"
+ "LogExtraFields=PORTABLE=", basename(image_path), "\n",
+ NULL))
+
+ return -ENOMEM;
+
+ r = write_string_file(dropin, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to write '%s': %m", dropin);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_WRITE, dropin, NULL);
+
+ if (ret_dropin)
+ *ret_dropin = TAKE_PTR(dropin);
+
+ return 0;
+}
+
+static int find_profile(const char *name, const char *unit, char **ret) {
+ const char *p, *dot;
+
+ assert(name);
+ assert(ret);
+
+ assert_se(dot = strrchr(unit, '.'));
+
+ NULSTR_FOREACH(p, profile_dirs) {
+ _cleanup_free_ char *joined;
+
+ joined = strjoin(p, "/", name, "/", dot + 1, ".conf");
+ if (!joined)
+ return -ENOMEM;
+
+ if (laccess(joined, F_OK) >= 0) {
+ *ret = TAKE_PTR(joined);
+ return 0;
+ }
+
+ if (errno != ENOENT)
+ return -errno;
+ }
+
+ return -ENOENT;
+}
+
+static int install_profile_dropin(
+ const char *image_path,
+ const PortableMetadata *m,
+ const char *dropin_dir,
+ const char *profile,
+ PortableFlags flags,
+ char **ret_dropin,
+ PortableChange **changes,
+ size_t *n_changes) {
+
+ _cleanup_free_ char *dropin = NULL, *from = NULL;
+ int r;
+
+ assert(image_path);
+ assert(m);
+ assert(dropin_dir);
+
+ if (!profile)
+ return 0;
+
+ r = find_profile(profile, m->name, &from);
+ if (r < 0) {
+ if (r != ENOENT)
+ return log_debug_errno(errno, "Profile '%s' is not accessible: %m", profile);
+
+ log_debug_errno(errno, "Skipping link to profile '%s', as it does not exist: %m", profile);
+ return 0;
+ }
+
+ dropin = strjoin(dropin_dir, "/10-profile.conf");
+ if (!dropin)
+ return -ENOMEM;
+
+ if (flags & PORTABLE_PREFER_COPY) {
+
+ r = copy_file_atomic(from, dropin, 0644, 0, COPY_REFLINK);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to copy %s %s %s: %m", from, special_glyph(ARROW), dropin);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, dropin, from);
+
+ } else {
+
+ if (symlink(from, dropin) < 0)
+ return log_debug_errno(errno, "Failed to link %s %s %s: %m", from, special_glyph(ARROW), dropin);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, dropin, from);
+ }
+
+ if (ret_dropin)
+ *ret_dropin = TAKE_PTR(dropin);
+
+ return 0;
+}
+
+static const char *config_path(const LookupPaths *paths, PortableFlags flags) {
+ const char *where;
+
+ assert(paths);
+
+ if (flags & PORTABLE_RUNTIME)
+ where = paths->runtime_config;
+ else
+ where = paths->persistent_config;
+
+ assert(where);
+ return where;
+}
+
+static int attach_unit_file(
+ const LookupPaths *paths,
+ const char *image_path,
+ ImageType type,
+ const PortableMetadata *m,
+ const char *profile,
+ PortableFlags flags,
+ PortableChange **changes,
+ size_t *n_changes) {
+
+ _cleanup_(unlink_and_freep) char *chroot_dropin = NULL, *profile_dropin = NULL;
+ _cleanup_(rmdir_and_freep) char *dropin_dir = NULL;
+ const char *where, *path;
+ int r;
+
+ assert(paths);
+ assert(image_path);
+ assert(m);
+ assert(PORTABLE_METADATA_IS_UNIT(m));
+
+ where = config_path(paths, flags);
+ path = strjoina(where, "/", m->name);
+
+ dropin_dir = strjoin(path, ".d");
+ if (!dropin_dir)
+ return -ENOMEM;
+
+ (void) mkdir_p(dropin_dir, 0755);
+ (void) portable_changes_add(changes, n_changes, PORTABLE_MKDIR, dropin_dir, NULL);
+
+ /* We install the drop-ins first, and the actual unit file last to achieve somewhat atomic behaviour if PID 1
+ * is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at
+ * all for PID 1. */
+
+ r = install_chroot_dropin(image_path, type, m, dropin_dir, &chroot_dropin, changes, n_changes);
+ if (r < 0)
+ return r;
+
+ r = install_profile_dropin(image_path, m, dropin_dir, profile, flags, &profile_dropin, changes, n_changes);
+ if (r < 0)
+ return r;
+
+ if ((flags & PORTABLE_PREFER_SYMLINK) && m->source) {
+
+ if (symlink(m->source, path) < 0)
+ return log_debug_errno(errno, "Failed to symlink unit file '%s': %m", path);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, path, m->source);
+
+ } else {
+ _cleanup_(unlink_and_freep) char *tmp = NULL;
+ _cleanup_close_ int fd = -1;
+
+ fd = open_tmpfile_linkable(where, O_WRONLY|O_CLOEXEC, &tmp);
+ if (fd < 0)
+ return log_debug_errno(fd, "Failed to create unit file '%s': %m", path);
+
+ r = copy_bytes(m->fd, fd, UINT64_MAX, COPY_REFLINK);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to copy unit file '%s': %m", path);
+
+ if (fchmod(fd, 0644) < 0)
+ return log_debug_errno(errno, "Failed to change unit file access mode for '%s': %m", path);
+
+ r = link_tmpfile(fd, tmp, path);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to install unit file '%s': %m", path);
+
+ tmp = mfree(tmp);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_COPY, path, m->source);
+ }
+
+ /* All is established now, now let's disable any rollbacks */
+ chroot_dropin = mfree(chroot_dropin);
+ profile_dropin = mfree(profile_dropin);
+ dropin_dir = mfree(dropin_dir);
+
+ return 0;
+}
+
+static int image_symlink(
+ const char *image_path,
+ PortableFlags flags,
+ char **ret) {
+
+ const char *fn, *where;
+ char *joined = NULL;
+
+ assert(image_path);
+ assert(ret);
+
+ fn = last_path_component(image_path);
+
+ if (flags & PORTABLE_RUNTIME)
+ where = "/run/portables/";
+ else
+ where = "/etc/portables/";
+
+ joined = strjoin(where, fn);
+ if (!joined)
+ return -ENOMEM;
+
+ *ret = joined;
+ return 0;
+}
+
+static int install_image_symlink(
+ const char *image_path,
+ PortableFlags flags,
+ PortableChange **changes,
+ size_t *n_changes) {
+
+ _cleanup_free_ char *sl = NULL;
+ int r;
+
+ assert(image_path);
+
+ /* If the image is outside of the image search also link it into it, so that it can be found with short image
+ * names and is listed among the images. */
+
+ if (image_in_search_path(IMAGE_PORTABLE, image_path))
+ return 0;
+
+ r = image_symlink(image_path, flags, &sl);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to generate image symlink path: %m");
+
+ (void) mkdir_parents(sl, 0755);
+
+ if (symlink(image_path, sl) < 0)
+ return log_debug_errno(errno, "Failed to link %s %s %s: %m", image_path, special_glyph(ARROW), sl);
+
+ (void) portable_changes_add(changes, n_changes, PORTABLE_SYMLINK, sl, image_path);
+ return 0;
+}
+
+int portable_attach(
+ sd_bus *bus,
+ const char *name_or_path,
+ char **matches,
+ const char *profile,
+ PortableFlags flags,
+ PortableChange **changes,
+ size_t *n_changes,
+ sd_bus_error *error) {
+
+ _cleanup_(portable_metadata_hashmap_unrefp) Hashmap *unit_files = NULL;
+ _cleanup_(lookup_paths_free) LookupPaths paths = {};
+ _cleanup_(image_unrefp) Image *image = NULL;
+ PortableMetadata *item;
+ Iterator iterator;
+ int r;
+
+ assert(name_or_path);
+
+ r = image_find_harder(IMAGE_PORTABLE, name_or_path, &image);
+ if (r < 0)
+ return r;
+
+ r = portable_extract_by_path(image->path, matches, NULL, &unit_files, error);
+ if (r < 0)
+ return r;
+
+ r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(item, unit_files, iterator) {
+ r = unit_file_exists(UNIT_FILE_SYSTEM, &paths, item->name);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to determine whether unit '%s' exists on the host: %m", item->name);
+ if (r > 0)
+ return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' exists on the host already, refusing.", item->name);
+
+ r = unit_file_is_active(bus, item->name, error);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active already, refusing.", item->name);
+ }
+
+ HASHMAP_FOREACH(item, unit_files, iterator) {
+ r = attach_unit_file(&paths, image->path, image->type, item, profile, flags, changes, n_changes);
+ if (r < 0)
+ return r;
+ }
+
+ /* We don't care too much for the image symlink, it's just a convenience thing, it's not necessary for proper
+ * operation otherwise. */
+ (void) install_image_symlink(image->path, flags, changes, n_changes);
+
+ return 0;
+}
+
+static bool marker_matches_image(const char *marker, const char *name_or_path) {
+ const char *a;
+
+ assert(marker);
+ assert(name_or_path);
+
+ a = last_path_component(marker);
+
+ if (image_name_is_valid(name_or_path)) {
+ const char *e;
+
+ /* We shall match against an image name. In that case let's compare the last component, and optionally
+ * allow either a suffix of ".raw" or a series of "/". */
+
+ e = startswith(a, name_or_path);
+ if (!e)
+ return false;
+
+ return
+ e[strspn(e, "/")] == 0 ||
+ streq(e, ".raw");
+ } else {
+ const char *b;
+ size_t l;
+
+ /* We shall match against a path. Let's ignore any prefix here though, as often there are many ways to
+ * reach the same file. However, in this mode, let's validate any file suffix. */
+
+ l = strcspn(a, "/");
+ b = last_path_component(name_or_path);
+
+ if (strcspn(b, "/") != l)
+ return false;
+
+ return memcmp(a, b, l) == 0;
+ }
+}
+
+static int test_chroot_dropin(
+ DIR *d,
+ const char *where,
+ const char *fname,
+ const char *name_or_path,
+ char **ret_marker) {
+
+ _cleanup_free_ char *line = NULL, *marker = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_close_ int fd = -1;
+ const char *p, *e, *k;
+ int r;
+
+ assert(d);
+ assert(where);
+ assert(fname);
+
+ /* We recognize unis created from portable images via the drop-in we created for them */
+
+ p = strjoina(fname, ".d/20-portable.conf");
+ fd = openat(dirfd(d), p, O_RDONLY|O_CLOEXEC);
+ if (fd < 0) {
+ if (errno == ENOENT)
+ return 0;
+
+ return log_debug_errno(errno, "Failed to open %s/%s: %m", where, p);
+ }
+
+ f = fdopen(fd, "re");
+ if (!f)
+ return log_debug_errno(errno, "Failed to convert file handle: %m");
+ fd = -1;
+
+ (void) __fsetlocking(f, FSETLOCKING_BYCALLER);
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read from %s/%s: %m", where, p);
+
+ e = startswith(line, PORTABLE_DROPIN_MARKER_BEGIN);
+ if (!e)
+ return 0;
+
+ k = endswith(e, PORTABLE_DROPIN_MARKER_END);
+ if (!k)
+ return 0;
+
+ marker = strndup(e, k - e);
+ if (!marker)
+ return -ENOMEM;
+
+ if (!name_or_path)
+ r = true;
+ else
+ r = marker_matches_image(marker, name_or_path);
+
+ if (ret_marker)
+ *ret_marker = TAKE_PTR(marker);
+
+ return r;
+}
+
+int portable_detach(
+ sd_bus *bus,
+ const char *name_or_path,
+ PortableFlags flags,
+ PortableChange **changes,
+ size_t *n_changes,
+ sd_bus_error *error) {
+
+ _cleanup_(lookup_paths_free) LookupPaths paths = {};
+ _cleanup_set_free_free_ Set *unit_files = NULL, *markers = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ const char *where, *item;
+ Iterator iterator;
+ struct dirent *de;
+ int ret = 0;
+ int r;
+
+ assert(name_or_path);
+
+ r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL);
+ if (r < 0)
+ return r;
+
+ where = config_path(&paths, flags);
+
+ d = opendir(where);
+ if (!d)
+ return log_debug_errno(errno, "Failed to open '%s' directory: %m", where);
+
+ unit_files = set_new(&string_hash_ops);
+ if (!unit_files)
+ return -ENOMEM;
+
+ markers = set_new(&path_hash_ops);
+ if (!markers)
+ return -ENOMEM;
+
+ FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) {
+ _cleanup_free_ char *marker = NULL;
+ UnitFileState state;
+
+ if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY))
+ continue;
+
+ /* Filter out duplicates */
+ if (set_get(unit_files, de->d_name))
+ continue;
+
+ dirent_ensure_type(d, de);
+ if (!IN_SET(de->d_type, DT_LNK, DT_REG))
+ continue;
+
+ r = test_chroot_dropin(d, where, de->d_name, name_or_path, &marker);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ r = unit_file_lookup_state(UNIT_FILE_SYSTEM, &paths, de->d_name, &state);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name);
+ if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_RUNTIME))
+ return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is in state '%s', can't detach.", de->d_name, unit_file_state_to_string(state));
+
+ r = unit_file_is_active(bus, de->d_name, error);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return sd_bus_error_setf(error, BUS_ERROR_UNIT_EXISTS, "Unit file '%s' is active, can't detach.", de->d_name);
+
+ r = set_put_strdup(unit_files, de->d_name);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name);
+
+ if (path_is_absolute(marker) &&
+ !image_in_search_path(IMAGE_PORTABLE, marker)) {
+
+ r = set_ensure_allocated(&markers, &path_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = set_put(markers, marker);
+ if (r >= 0)
+ marker = NULL;
+ else if (r != -EEXIST)
+ return r;
+ }
+ }
+
+ if (set_isempty(unit_files)) {
+ log_debug("No unit files associated with '%s' found. Image not attached?", name_or_path);
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_UNIT, "No unit files associated with '%s' found. Image not attached?", name_or_path);
+ }
+
+ SET_FOREACH(item, unit_files, iterator) {
+ _cleanup_free_ char *md = NULL;
+ const char *suffix;
+
+ if (unlinkat(dirfd(d), item, 0) < 0) {
+ log_debug_errno(errno, "Can't remove unit file %s/%s: %m", where, item);
+
+ if (errno != ENOENT && ret >= 0)
+ ret = -errno;
+ } else
+ portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, item, NULL);
+
+ FOREACH_STRING(suffix, ".d/10-profile.conf", ".d/20-portable.conf") {
+ _cleanup_free_ char *dropin = NULL;
+
+ dropin = strjoin(item, suffix);
+ if (!dropin)
+ return -ENOMEM;
+
+ if (unlinkat(dirfd(d), dropin, 0) < 0) {
+ log_debug_errno(errno, "Can't remove drop-in %s/%s: %m", where, dropin);
+
+ if (errno != ENOENT && ret >= 0)
+ ret = -errno;
+ } else
+ portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, dropin, NULL);
+ }
+
+ md = strjoin(item, ".d");
+ if (!md)
+ return -ENOMEM;
+
+ if (unlinkat(dirfd(d), md, AT_REMOVEDIR) < 0) {
+ log_debug_errno(errno, "Can't remove drop-in directory %s/%s: %m", where, md);
+
+ if (errno != ENOENT && ret >= 0)
+ ret = -errno;
+ } else
+ portable_changes_add_with_prefix(changes, n_changes, PORTABLE_UNLINK, where, md, NULL);
+ }
+
+ /* Now, also drop any image symlink, for images outside of the sarch path */
+ SET_FOREACH(item, markers, iterator) {
+ _cleanup_free_ char *sl = NULL;
+ struct stat st;
+
+ r = image_symlink(item, flags, &sl);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to determine image symlink for '%s', ignoring: %m", item);
+ continue;
+ }
+
+ if (lstat(sl, &st) < 0) {
+ log_debug_errno(errno, "Failed to stat '%s', ignoring: %m", sl);
+ continue;
+ }
+
+ if (!S_ISLNK(st.st_mode)) {
+ log_debug("Image '%s' is not a symlink, ignoring.", sl);
+ continue;
+ }
+
+ if (unlink(sl) < 0) {
+ log_debug_errno(errno, "Can't remove image symlink '%s': %m", sl);
+
+ if (errno != ENOENT && ret >= 0)
+ ret = -errno;
+ } else
+ portable_changes_add(changes, n_changes, PORTABLE_UNLINK, sl, NULL);
+ }
+
+ return ret;
+}
+
+static int portable_get_state_internal(
+ sd_bus *bus,
+ const char *name_or_path,
+ PortableFlags flags,
+ PortableState *ret,
+ sd_bus_error *error) {
+
+ _cleanup_(lookup_paths_free) LookupPaths paths = {};
+ bool found_enabled = false, found_running = false;
+ _cleanup_set_free_free_ Set *unit_files = NULL;
+ _cleanup_closedir_ DIR *d = NULL;
+ const char *where;
+ struct dirent *de;
+ int r;
+
+ assert(name_or_path);
+ assert(ret);
+
+ r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL);
+ if (r < 0)
+ return r;
+
+ where = config_path(&paths, flags);
+
+ d = opendir(where);
+ if (!d)
+ return log_debug_errno(errno, "Failed to open '%s' directory: %m", where);
+
+ unit_files = set_new(&string_hash_ops);
+ if (!unit_files)
+ return -ENOMEM;
+
+ FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) {
+ UnitFileState state;
+
+ if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY))
+ continue;
+
+ /* Filter out duplicates */
+ if (set_get(unit_files, de->d_name))
+ continue;
+
+ dirent_ensure_type(d, de);
+ if (!IN_SET(de->d_type, DT_LNK, DT_REG))
+ continue;
+
+ r = test_chroot_dropin(d, where, de->d_name, name_or_path, NULL);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ r = unit_file_lookup_state(UNIT_FILE_SYSTEM, &paths, de->d_name, &state);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name);
+ if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME))
+ found_enabled = true;
+
+ r = unit_file_is_active(bus, de->d_name, error);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ found_running = true;
+
+ r = set_put_strdup(unit_files, de->d_name);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name);
+ }
+
+ *ret = found_running ? (!set_isempty(unit_files) && (flags & PORTABLE_RUNTIME) ? PORTABLE_RUNNING_RUNTIME : PORTABLE_RUNNING) :
+ found_enabled ? (flags & PORTABLE_RUNTIME ? PORTABLE_ENABLED_RUNTIME : PORTABLE_ENABLED) :
+ !set_isempty(unit_files) ? (flags & PORTABLE_RUNTIME ? PORTABLE_ATTACHED_RUNTIME : PORTABLE_ATTACHED) : PORTABLE_DETACHED;
+
+ return 0;
+}
+
+int portable_get_state(
+ sd_bus *bus,
+ const char *name_or_path,
+ PortableFlags flags,
+ PortableState *ret,
+ sd_bus_error *error) {
+
+ PortableState state;
+ int r;
+
+ assert(name_or_path);
+ assert(ret);
+
+ /* We look for matching units twice: once in the regular directories, and once in the runtime directories — but
+ * the latter only if we didn't find anything in the former. */
+
+ r = portable_get_state_internal(bus, name_or_path, flags & ~PORTABLE_RUNTIME, &state, error);
+ if (r < 0)
+ return r;
+
+ if (state == PORTABLE_DETACHED) {
+ r = portable_get_state_internal(bus, name_or_path, flags | PORTABLE_RUNTIME, &state, error);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = state;
+ return 0;
+}
+
+int portable_get_profiles(char ***ret) {
+ assert(ret);
+
+ return conf_files_list_nulstr(ret, NULL, NULL, CONF_FILES_DIRECTORY|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED, profile_dirs);
+}
+
+static const char* const portable_change_type_table[_PORTABLE_CHANGE_TYPE_MAX] = {
+ [PORTABLE_COPY] = "copy",
+ [PORTABLE_MKDIR] = "mkdir",
+ [PORTABLE_SYMLINK] = "symlink",
+ [PORTABLE_UNLINK] = "unlink",
+ [PORTABLE_WRITE] = "write",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(portable_change_type, PortableChangeType);
+
+static const char* const portable_state_table[_PORTABLE_STATE_MAX] = {
+ [PORTABLE_DETACHED] = "detached",
+ [PORTABLE_ATTACHED] = "attached",
+ [PORTABLE_ATTACHED_RUNTIME] = "attached-runtime",
+ [PORTABLE_ENABLED] = "enabled",
+ [PORTABLE_ENABLED_RUNTIME] = "enabled-runtime",
+ [PORTABLE_RUNNING] = "running",
+ [PORTABLE_RUNNING_RUNTIME] = "running-runtime",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(portable_state, PortableState);
diff --git a/src/portable/portable.h b/src/portable/portable.h
new file mode 100644
index 0000000000..a0959abb64
--- /dev/null
+++ b/src/portable/portable.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "hashmap.h"
+#include "macro.h"
+#include "set.h"
+#include "string-util.h"
+
+typedef struct PortableMetadata {
+ int fd;
+ char *source;
+ char name[];
+} PortableMetadata;
+
+#define PORTABLE_METADATA_IS_OS_RELEASE(m) (streq((m)->name, "/etc/os-release"))
+#define PORTABLE_METADATA_IS_UNIT(m) (!IN_SET((m)->name[0], 0, '/'))
+
+typedef enum PortableFlags {
+ PORTABLE_PREFER_COPY = 1U << 0,
+ PORTABLE_PREFER_SYMLINK = 1U << 1,
+ PORTABLE_RUNTIME = 1U << 2,
+} PortableFlags;
+
+typedef enum PortableChangeType {
+ PORTABLE_COPY,
+ PORTABLE_SYMLINK,
+ PORTABLE_UNLINK,
+ PORTABLE_WRITE,
+ PORTABLE_MKDIR,
+ _PORTABLE_CHANGE_TYPE_MAX,
+ _PORTABLE_CHANGE_TYPE_INVALID = INT_MIN,
+} PortableChangeType;
+
+typedef enum PortableState {
+ PORTABLE_DETACHED,
+ PORTABLE_ATTACHED,
+ PORTABLE_ATTACHED_RUNTIME,
+ PORTABLE_ENABLED,
+ PORTABLE_ENABLED_RUNTIME,
+ PORTABLE_RUNNING,
+ PORTABLE_RUNNING_RUNTIME,
+ _PORTABLE_STATE_MAX,
+ _PORTABLE_STATE_INVALID = -1
+} PortableState;
+
+typedef struct PortableChange {
+ int type; /* PortableFileChangeType or negative error number */
+ char *path;
+ char *source;
+} PortableChange;
+
+PortableMetadata *portable_metadata_unref(PortableMetadata *i);
+DEFINE_TRIVIAL_CLEANUP_FUNC(PortableMetadata*, portable_metadata_unref);
+
+Hashmap *portable_metadata_hashmap_unref(Hashmap *h);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Hashmap*, portable_metadata_hashmap_unref);
+
+int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret);
+
+int portable_extract(const char *image, char **matches, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, sd_bus_error *error);
+
+int portable_attach(sd_bus *bus, const char *name_or_path, char **matches, const char *profile, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
+int portable_detach(sd_bus *bus, const char *name_or_path, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
+
+int portable_get_state(sd_bus *bus, const char *name_or_path, PortableFlags flags, PortableState *ret, sd_bus_error *error);
+
+int portable_get_profiles(char ***ret);
+
+void portable_changes_free(PortableChange *changes, size_t n_changes);
+
+const char *portable_change_type_to_string(PortableChangeType t) _const_;
+PortableChangeType portable_change_type_from_string(const char *t) _pure_;
+
+const char *portable_state_to_string(PortableState t) _const_;
+PortableState portable_state_from_string(const char *t) _pure_;
diff --git a/src/portable/portablectl.c b/src/portable/portablectl.c
new file mode 100644
index 0000000000..8e44d34d27
--- /dev/null
+++ b/src/portable/portablectl.c
@@ -0,0 +1,965 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+#include
+
+#include "sd-bus.h"
+
+#include "alloc-util.h"
+#include "bus-error.h"
+#include "bus-util.h"
+#include "def.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "fs-util.h"
+#include "locale-util.h"
+#include "machine-image.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "spawn-polkit-agent.h"
+#include "string-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "verbs.h"
+
+static bool arg_no_pager = false;
+static bool arg_legend = true;
+static bool arg_ask_password = true;
+static bool arg_quiet = false;
+static const char *arg_profile = "default";
+static const char* arg_copy_mode = NULL;
+static bool arg_runtime = false;
+static bool arg_reload = true;
+static bool arg_cat = false;
+static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
+static char *arg_host = NULL;
+
+static int determine_image(const char *image, bool permit_non_existing, char **ret) {
+ int r;
+
+ /* If the specified name is a valid image name, we pass it as-is to portabled, which will search for it in the
+ * usual search directories. Otherwise we presume it's a path, and will normalize it on the client's side
+ * (among other things, to make the path independent of the client's working directory) before passing it
+ * over. */
+
+ if (image_name_is_valid(image)) {
+ char *c;
+
+ if (!arg_quiet && laccess(image, F_OK) >= 0)
+ log_warning("Ambiguous invocation: current working directory contains file matching non-path argument '%s', ignoring. "
+ "Prefix argument with './' to force reference to file in current working directory.", image);
+
+ c = strdup(image);
+ if (!c)
+ return log_oom();
+
+ *ret = c;
+ return 0;
+ }
+
+ if (arg_transport != BUS_TRANSPORT_LOCAL) {
+ log_error("Operations on images by path not supported when connecting to remote systems.");
+ return -EOPNOTSUPP;
+ }
+
+ r = chase_symlinks(image, NULL, CHASE_TRAIL_SLASH | (permit_non_existing ? CHASE_NONEXISTENT : 0), ret);
+ if (r < 0)
+ return log_error_errno(r, "Cannot normalize specified image path '%s': %m", image);
+
+ return 0;
+}
+
+static int extract_prefix(const char *path, char **ret) {
+ _cleanup_free_ char *name = NULL;
+ const char *bn, *underscore;
+ size_t m;
+
+ bn = basename(path);
+
+ underscore = strchr(bn, '_');
+ if (underscore)
+ m = underscore - bn;
+ else {
+ const char *e;
+
+ e = endswith(bn, ".raw");
+ if (!e)
+ e = strchr(bn, 0);
+
+ m = e - bn;
+ }
+
+ name = strndup(bn, m);
+ if (!name)
+ return -ENOMEM;
+
+ /* A slightly reduced version of what's permitted in unit names. With ':' and '\' are removed, as well as '_'
+ * which we use as delimiter for the second part of the image string, which we ignore for now. */
+ if (!in_charset(name, DIGITS LETTERS "-."))
+ return -EINVAL;
+
+ if (!filename_is_valid(name))
+ return -EINVAL;
+
+ *ret = name;
+ name = NULL;
+
+ return 0;
+}
+
+static int determine_matches(const char *image, char **l, bool allow_any, char ***ret) {
+ char **k;
+ int r;
+
+ /* Determine the matches to apply. If the list is empty we derive the match from the image name. If the list
+ * contains exactly the "-" we return a wildcard list (which is the empty list), but only if this is expressly
+ * permitted. */
+
+ if (strv_isempty(l)) {
+ char *prefix;
+
+ r = extract_prefix(image, &prefix);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract prefix of image name '%s': %m", image);
+
+ if (!arg_quiet)
+ log_info("(Matching unit files with prefix '%s'.)", prefix);
+
+ k = NULL;
+ r = strv_consume(&k, prefix);
+ if (r < 0)
+ return log_oom();
+
+ } else if (strv_equal(l, STRV_MAKE("-"))) {
+
+ if (!allow_any) {
+ log_error("Refusing all unit file match.");
+ return -EINVAL;
+ }
+
+ if (!arg_quiet)
+ log_info("(Matching all unit files.)");
+ k = NULL;
+ } else {
+ _cleanup_free_ char *joined = NULL;
+
+ k = strv_copy(l);
+ if (!k)
+ return log_oom();
+
+ joined = strv_join(k, "', '");
+ if (!joined)
+ return log_oom();
+
+ if (!arg_quiet)
+ log_info("(Matching unit files with prefixes '%s'.)", joined);
+ }
+
+ *ret = k;
+
+ return 0;
+}
+
+static int acquire_bus(sd_bus **bus) {
+ int r;
+
+ assert(bus);
+
+ if (*bus)
+ return 0;
+
+ r = bus_connect_transport(arg_transport, arg_host, false, bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to bus: %m");
+
+ (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password);
+
+ return 0;
+}
+
+static int maybe_reload(sd_bus **bus) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ int r;
+
+ if (!arg_reload)
+ return 0;
+
+ r = acquire_bus(bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ *bus,
+ &m,
+ "org.freedesktop.systemd1",
+ "/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ "Reload");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* Reloading the daemon may take long, hence set a longer timeout here */
+ r = sd_bus_call(*bus, m, DEFAULT_TIMEOUT_USEC * 2, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to reload daemon: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int inspect_image(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **matches = NULL;
+ _cleanup_free_ char *image = NULL;
+ bool nl = false, header = false;
+ const void *data;
+ const char *path;
+ size_t sz;
+ int r;
+
+ r = determine_image(argv[1], false, &image);
+ if (r < 0)
+ return r;
+
+ r = determine_matches(argv[1], argv + 2, true, &matches);
+ if (r < 0)
+ return r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "GetImageMetadata");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", image);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, matches);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, 0, &error, &reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to inspect image metadata: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "s", &path);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &data, &sz);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ (void) pager_open(arg_no_pager, false);
+
+ if (arg_cat) {
+ printf("%s-- OS Release: --%s\n", ansi_highlight(), ansi_normal());
+ fwrite(data, sz, 1, stdout);
+ fflush(stdout);
+ nl = true;
+ } else {
+ const char *pretty_portable = NULL, *pretty_os = NULL;
+
+ _cleanup_fclose_ FILE *f;
+
+ f = fmemopen((void*) data, sz, "re");
+ if (!f)
+ return log_error_errno(errno, "Failed to open /etc/os-release buffer: %m");
+
+ r = parse_env_file(f, "/etc/os-release", NEWLINE,
+ "PORTABLE_PRETTY_NAME", &pretty_portable,
+ "PRETTY_NAME", &pretty_os,
+ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse /etc/os-release: %m");
+
+ printf("Image:\n\t%s\n"
+ "Portable Service:\n\t%s\n"
+ "Operating System:\n\t%s\n",
+ path,
+ strna(pretty_portable),
+ strna(pretty_os));
+ }
+
+ r = sd_bus_message_enter_container(reply, 'a', "{say}");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ for (;;) {
+ const char *name;
+
+ r = sd_bus_message_enter_container(reply, 'e', "say");
+ if (r < 0)
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
+
+ r = sd_bus_message_read(reply, "s", &name);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ r = sd_bus_message_read_array(reply, 'y', &data, &sz);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (arg_cat) {
+ if (nl)
+ fputc('\n', stdout);
+
+ printf("%s-- Unit file: %s --%s\n", ansi_highlight(), name, ansi_normal());
+ fwrite(data, sz, 1, stdout);
+ fflush(stdout);
+ nl = true;
+ } else {
+ if (!header) {
+ fputs("Unit files:\n", stdout);
+ header = true;
+ }
+
+ fputc('\t', stdout);
+ fputs(name, stdout);
+ fputc('\n', stdout);
+ }
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ }
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ return 0;
+}
+
+static int print_changes(sd_bus_message *m) {
+ int r;
+
+ if (arg_quiet)
+ return 0;
+
+ r = sd_bus_message_enter_container(m, 'a', "(sss)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ for (;;) {
+ const char *type, *path, *source;
+
+ r = sd_bus_message_read(m, "(sss)", &type, &path, &source);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
+
+ if (streq(type, "symlink"))
+ log_info("Created symlink %s %s %s.", path, special_glyph(ARROW), source);
+ else if (streq(type, "copy")) {
+ if (isempty(source))
+ log_info("Copied %s.", path);
+ else
+ log_info("Copied %s %s %s.", source, special_glyph(ARROW), path);
+ } else if (streq(type, "unlink"))
+ log_info("Removed %s.", path);
+ else if (streq(type, "write"))
+ log_info("Written %s.", path);
+ else if (streq(type, "mkdir"))
+ log_info("Created directory %s.", path);
+ else
+ log_error("Unexpected change: %s/%s/%s", type, path, source);
+ }
+
+ r = sd_bus_message_exit_container(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int attach_image(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **matches = NULL;
+ _cleanup_free_ char *image = NULL;
+ int r;
+
+ r = determine_image(argv[1], false, &image);
+ if (r < 0)
+ return r;
+
+ r = determine_matches(argv[1], argv + 2, false, &matches);
+ if (r < 0)
+ return r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "AttachImage");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", image);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, matches);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "sbs", arg_profile, arg_runtime, arg_copy_mode);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_call(bus, m, 0, &error, &reply);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach image: %s", bus_error_message(&error, r));
+
+ (void) maybe_reload(&bus);
+
+ print_changes(reply);
+ return 0;
+}
+
+static int detach_image(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *image = NULL;
+ int r;
+
+ r = determine_image(argv[1], true, &image);
+ if (r < 0)
+ return r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "DetachImage",
+ &error,
+ &reply,
+ "sb", image, arg_runtime);
+ if (r < 0)
+ return log_error_errno(r, "Failed to detach image: %s", bus_error_message(&error, r));
+
+ (void) maybe_reload(&bus);
+
+ print_changes(reply);
+ return 0;
+}
+
+static int list_images(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;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_(table_unrefp) Table *table = NULL;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "ListImages",
+ &error,
+ &reply,
+ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to list images: %s", bus_error_message(&error, r));
+
+ table = table_new("NAME", "TYPE", "RO", "CRTIME", "MTIME", "USAGE", "STATE");
+ if (!table)
+ return log_oom();
+
+ r = sd_bus_message_enter_container(reply, 'a', "(ssbtttso)");
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ for (;;) {
+ const char *name, *type, *state, *object;
+ uint64_t crtime, mtime, usage;
+ TableCell *cell;
+ bool ro_bool;
+ int ro_int;
+
+ r = sd_bus_message_read(reply, "(ssbtttso)", &name, &type, &ro_int, &crtime, &mtime, &usage, &state, &object);
+ if (r < 0)
+ return bus_log_parse_error(r);
+ if (r == 0)
+ break;
+
+ r = table_add_many(table,
+ TABLE_STRING, name,
+ TABLE_STRING, type);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ ro_bool = ro_int;
+ r = table_add_cell(table, &cell, TABLE_BOOLEAN, &ro_bool);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ if (ro_bool) {
+ r = table_set_color(table, cell, ansi_highlight_red());
+ if (r < 0)
+ return log_error_errno(r, "Failed to set table cell color: %m");
+ }
+
+ r = table_add_many(table,
+ TABLE_TIMESTAMP, crtime,
+ TABLE_TIMESTAMP, mtime,
+ TABLE_SIZE, usage);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ r = table_add_cell(table, &cell, TABLE_STRING, state);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add row to table: %m");
+
+ if (!streq(state, "detached")) {
+ r = table_set_color(table, cell, ansi_highlight_green());
+ if (r < 0)
+ return log_error_errno(r, "Failed to set table cell color: %m");
+ }
+ }
+
+ r = sd_bus_message_exit_container(reply);
+ if (r < 0)
+ return bus_log_parse_error(r);
+
+ if (table_get_rows(table) > 1) {
+ r = table_set_sort(table, (size_t) 0, (size_t) -1);
+ if (r < 0)
+ return log_error_errno(r, "Failed to sort table: %m");
+
+ table_set_header(table, arg_legend);
+
+ r = table_print(table, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to show table: %m");
+ }
+
+ if (arg_legend) {
+ if (table_get_rows(table) > 1)
+ printf("\n%zu images listed.\n", table_get_rows(table) - 1);
+ else
+ printf("No images.\n");
+ }
+
+ return 0;
+}
+
+static int remove_image(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r, i;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ for (i = 1; i < argc; i++) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+
+ r = sd_bus_message_new_method_call(
+ bus,
+ &m,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "RemoveImage");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append(m, "s", argv[i]);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ /* This is a slow operation, hence turn off any method call timeouts */
+ r = sd_bus_call(bus, m, USEC_INFINITY, &error, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Could not remove image: %s", bus_error_message(&error, r));
+ }
+
+ return 0;
+}
+
+static int read_only_image(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int b = true, r;
+
+ if (argc > 2) {
+ b = parse_boolean(argv[2]);
+ if (b < 0)
+ return log_error_errno(b, "Failed to parse boolean argument: %s", argv[2]);
+ }
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "MarkImageReadOnly",
+ &error,
+ NULL,
+ "sb", argv[1], b);
+ if (r < 0)
+ return log_error_errno(r, "Could not mark image read-only: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int set_limit(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ uint64_t limit;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ if (STR_IN_SET(argv[argc-1], "-", "none", "infinity"))
+ limit = (uint64_t) -1;
+ else {
+ r = parse_size(argv[argc-1], 1024, &limit);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse size: %s", argv[argc-1]);
+ }
+
+ if (argc > 2)
+ /* With two arguments changes the quota limit of the specified image */
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "SetImageLimit",
+ &error,
+ NULL,
+ "st", argv[1], limit);
+ else
+ /* With one argument changes the pool quota limit */
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "SetPoolLimit",
+ &error,
+ NULL,
+ "t", limit);
+
+ if (r < 0)
+ return log_error_errno(r, "Could not set limit: %s", bus_error_message(&error, r));
+
+ return 0;
+}
+
+static int is_image_attached(int argc, char *argv[], void *userdata) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_free_ char *image = NULL;
+ const char *state;
+ int r;
+
+ r = determine_image(argv[1], true, &image);
+ if (r < 0)
+ return r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_call_method(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "GetImageState",
+ &error,
+ &reply,
+ "s", image);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get image state: %s", bus_error_message(&error, r));
+
+ r = sd_bus_message_read(reply, "s", &state);
+ if (r < 0)
+ return r;
+
+ if (!arg_quiet)
+ puts(state);
+
+ return streq(state, "detached");
+}
+
+static int dump_profiles(void) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_(closedirp) DIR *d = NULL;
+ char **i;
+ int r;
+
+ r = acquire_bus(&bus);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_get_property_strv(
+ bus,
+ "org.freedesktop.portable1",
+ "/org/freedesktop/portable1",
+ "org.freedesktop.portable1.Manager",
+ "Profiles",
+ &error,
+ &l);
+ if (r < 0)
+ return log_error_errno(r, "Failed to acquire list of profiles: %s", bus_error_message(&error, r));
+
+ if (arg_legend)
+ log_info("Available unit profiles:");
+
+ STRV_FOREACH(i, l) {
+ fputs(*i, stdout);
+ fputc('\n', stdout);
+ }
+
+ return 0;
+}
+
+static int help(int argc, char *argv[], void *userdata) {
+
+ (void) pager_open(arg_no_pager, false);
+
+ printf("%s [OPTIONS...] {COMMAND} ...\n\n"
+ "Attach or detach portable services from the local system.\n\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " --no-pager Do not pipe output into a pager\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --no-ask-password Do not ask for system passwords\n"
+ " -H --host=[USER@]HOST Operate on remote host\n"
+ " -M --machine=CONTAINER Operate on local container\n"
+ " -q --quiet Suppress informational messages\n"
+ " -p --profile=PROFILE Pick security profile for portable service\n"
+ " --copy=copy|auto|symlink Prefer copying or symlinks if possible\n"
+ " --runtime Attach portable service until next reboot only\n"
+ " --no-reload Don't reload the system and service manager\n"
+ " --cat When inspecting include unit and os-release file\n"
+ " contents\n\n"
+ "Commands:\n"
+ " list List available portable service images\n"
+ " attach NAME|PATH [PREFIX...]\n"
+ " Attach the specified portable service image\n"
+ " detach NAME|PATH Detach the specified portable service image\n"
+ " inspect NAME|PATH [PREFIX...]\n"
+ " Show details of specified portable service image\n"
+ " is-attached NAME|PATH Query if portable service image is attached\n"
+ " read-only NAME|PATH [BOOL] Mark or unmark portable service image read-only\n"
+ " remove NAME|PATH... Remove a portable service image\n"
+ " set-limit [NAME|PATH] Set image or pool size limit (disk quota)\n"
+ , program_invocation_short_name);
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_PAGER,
+ ARG_NO_LEGEND,
+ ARG_NO_ASK_PASSWORD,
+ ARG_COPY,
+ ARG_RUNTIME,
+ ARG_NO_RELOAD,
+ ARG_CAT,
+ };
+
+ 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 },
+ { "no-ask-password", no_argument, NULL, ARG_NO_ASK_PASSWORD },
+ { "host", required_argument, NULL, 'H' },
+ { "machine", required_argument, NULL, 'M' },
+ { "quiet", no_argument, NULL, 'q' },
+ { "profile", required_argument, NULL, 'p' },
+ { "copy", required_argument, NULL, ARG_COPY },
+ { "runtime", no_argument, NULL, ARG_RUNTIME },
+ { "no-reload", no_argument, NULL, ARG_NO_RELOAD },
+ { "cat", no_argument, NULL, ARG_CAT },
+ {}
+ };
+
+ assert(argc >= 0);
+ assert(argv);
+
+ for (;;) {
+ int c;
+
+ c = getopt_long(argc, argv, "hH:M:qp:", options, NULL);
+ if (c < 0)
+ break;
+
+ switch (c) {
+
+ case 'h':
+ help(0, NULL, NULL);
+ return 0;
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_PAGER:
+ arg_no_pager = true;
+ break;
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case ARG_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ case 'H':
+ arg_transport = BUS_TRANSPORT_REMOTE;
+ arg_host = optarg;
+ break;
+
+ case 'M':
+ arg_transport = BUS_TRANSPORT_MACHINE;
+ arg_host = optarg;
+ break;
+
+ case 'q':
+ arg_quiet = true;
+ break;
+
+ case 'p':
+ if (!filename_is_valid(optarg)) {
+ log_error("Unit profile name not valid: %s", optarg);
+ return -EINVAL;
+ }
+
+ if (streq(optarg, "help"))
+ return dump_profiles();
+
+ arg_profile = optarg;
+ break;
+
+ case ARG_COPY:
+ if (streq(optarg, "auto"))
+ arg_copy_mode = NULL;
+ else if (STR_IN_SET(optarg, "copy", "symlink"))
+ arg_copy_mode = optarg;
+ else {
+ log_error("Failed to parse --copy= argument: %s", optarg);
+ return -EINVAL;
+ }
+
+ break;
+
+ case ARG_RUNTIME:
+ arg_runtime = true;
+ break;
+
+ case ARG_NO_RELOAD:
+ arg_reload = false;
+ break;
+
+ case ARG_CAT:
+ arg_cat = true;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+ }
+
+ return 1;
+}
+
+int main(int argc, char *argv[]) {
+
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, help },
+ { "list", VERB_ANY, 1, VERB_DEFAULT, list_images },
+ { "attach", 2, VERB_ANY, 0, attach_image },
+ { "detach", 2, 2, 0, detach_image },
+ { "inspect", 2, VERB_ANY, 0, inspect_image },
+ { "is-attached", 2, 2, 0, is_image_attached },
+ { "read-only", 2, 3, 0, read_only_image },
+ { "remove", 2, VERB_ANY, 0, remove_image },
+ { "set-limit", 3, 3, 0, set_limit },
+ {}
+ };
+
+ int r;
+
+ log_parse_environment();
+ log_open();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ goto finish;
+
+ r = dispatch_verb(argc, argv, verbs, NULL);
+
+finish:
+ pager_close();
+
+ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+}
diff --git a/src/portable/portabled-bus.c b/src/portable/portabled-bus.c
new file mode 100644
index 0000000000..70fc750fe3
--- /dev/null
+++ b/src/portable/portabled-bus.c
@@ -0,0 +1,402 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "alloc-util.h"
+#include "btrfs-util.h"
+#include "bus-common-errors.h"
+#include "bus-util.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "machine-image.h"
+#include "portable.h"
+#include "portabled-bus.h"
+#include "portabled-image-bus.h"
+#include "portabled-image.h"
+#include "portabled.h"
+#include "strv.h"
+#include "user-util.h"
+
+static int property_get_pool_path(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ assert(bus);
+ assert(reply);
+
+ return sd_bus_message_append(reply, "s", "/var/lib/portables");
+}
+
+static int property_get_pool_usage(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_close_ int fd = -1;
+ uint64_t usage = (uint64_t) -1;
+
+ assert(bus);
+ assert(reply);
+
+ fd = open("/var/lib/portables", O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (fd >= 0) {
+ BtrfsQuotaInfo q;
+
+ if (btrfs_subvol_get_subtree_quota_fd(fd, 0, &q) >= 0)
+ usage = q.referenced;
+ }
+
+ return sd_bus_message_append(reply, "t", usage);
+}
+
+static int property_get_pool_limit(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_close_ int fd = -1;
+ uint64_t size = (uint64_t) -1;
+
+ assert(bus);
+ assert(reply);
+
+ fd = open("/var/lib/portables", O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (fd >= 0) {
+ BtrfsQuotaInfo q;
+
+ if (btrfs_subvol_get_subtree_quota_fd(fd, 0, &q) >= 0)
+ size = q.referenced_max;
+ }
+
+ return sd_bus_message_append(reply, "t", size);
+}
+
+static int property_get_profiles(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ _cleanup_strv_free_ char **l = NULL;
+ int r;
+
+ assert(bus);
+ assert(reply);
+
+ r = portable_get_profiles(&l);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_append_strv(reply, l);
+}
+
+static int method_get_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_free_ char *p = NULL;
+ Manager *m = userdata;
+ const char *name;
+ Image *image;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ r = sd_bus_message_read(message, "s", &name);
+ if (r < 0)
+ return r;
+
+ r = bus_image_acquire(m, message, name, NULL, BUS_IMAGE_REFUSE_BY_PATH, NULL, &image, error);
+ if (r < 0)
+ return r;
+
+ r = bus_image_path(image, &p);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, "o", p);
+}
+
+static int method_list_images(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_(image_hashmap_freep) Hashmap *images = NULL;
+ Manager *m = userdata;
+ Image *image;
+ Iterator i;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ images = hashmap_new(&string_hash_ops);
+ if (!images)
+ return -ENOMEM;
+
+ r = manager_image_cache_discover(m, images, error);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(ssbtttso)");
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(image, images, i) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error_state = SD_BUS_ERROR_NULL;
+ PortableState state = _PORTABLE_STATE_INVALID;
+ _cleanup_free_ char *p = NULL;
+
+ r = bus_image_path(image, &p);
+ if (r < 0)
+ return r;
+
+ r = portable_get_state(
+ sd_bus_message_get_bus(message),
+ image->path,
+ 0,
+ &state,
+ &error_state);
+ if (r < 0)
+ log_debug_errno(r, "Failed to get state of image '%s', ignoring: %s",
+ image->path, bus_error_message(&error_state, r));
+
+ r = sd_bus_message_append(reply, "(ssbtttso)",
+ image->name,
+ image_type_to_string(image->type),
+ image->read_only,
+ image->crtime,
+ image->mtime,
+ image->usage,
+ portable_state_to_string(state),
+ p);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int redirect_method_to_image(
+ Manager *m,
+ sd_bus_message *message,
+ sd_bus_error *error,
+ int (*method)(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error* error)) {
+
+ const char *name_or_path;
+ int r;
+
+ assert(m);
+ assert(message);
+ assert(method);
+
+ r = sd_bus_message_read(message, "s", &name_or_path);
+ if (r < 0)
+ return r;
+
+ return method(m, message, name_or_path, NULL, error);
+}
+
+static int method_get_image_os_release(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_get_os_release);
+}
+
+static int method_get_image_metadata(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_get_metadata);
+}
+
+static int method_get_image_state(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ const char *name_or_path;
+ PortableState state;
+ int r;
+
+ assert(message);
+
+ r = sd_bus_message_read(message, "s", &name_or_path);
+ if (r < 0)
+ return r;
+
+ r = portable_get_state(
+ sd_bus_message_get_bus(message),
+ name_or_path,
+ 0,
+ &state,
+ error);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, "s", portable_state_to_string(state));
+}
+
+static int method_attach_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_attach);
+}
+
+static int method_detach_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ PortableChange *changes = NULL;
+ Manager *m = userdata;
+ size_t n_changes = 0;
+ const char *name_or_path;
+ int r, runtime;
+
+ assert(message);
+ assert(m);
+
+ /* Note that we do not redirect detaching to the image object here, because we want to allow that users can
+ * detach already deleted images too, in case the user already deleted an image before properly detaching
+ * it. */
+
+ r = sd_bus_message_read(message, "sb", &name_or_path, &runtime);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.portable1.attach-images",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = portable_detach(
+ sd_bus_message_get_bus(message),
+ name_or_path,
+ runtime ? PORTABLE_RUNTIME : 0,
+ &changes,
+ &n_changes,
+ error);
+ if (r < 0)
+ goto finish;
+
+ r = reply_portable_changes(message, changes, n_changes);
+
+finish:
+ portable_changes_free(changes, n_changes);
+ return r;
+}
+
+static int method_remove_image(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_remove);
+}
+
+static int method_mark_image_read_only(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_mark_read_only);
+}
+
+static int method_set_image_limit(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return redirect_method_to_image(userdata, message, error, bus_image_common_set_limit);
+}
+
+static int method_set_pool_limit(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ Manager *m = userdata;
+ uint64_t limit;
+ int r;
+
+ assert(message);
+
+ r = sd_bus_message_read(message, "t", &limit);
+ if (r < 0)
+ return r;
+ if (!FILE_SIZE_VALID_OR_INFINITY(limit))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "New limit out of range");
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.portable1.manage-images",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ (void) btrfs_qgroup_set_limit("/var/lib/portables", 0, limit);
+
+ r = btrfs_subvol_set_subtree_quota_limit("/var/lib/portables", 0, limit);
+ if (r == -ENOTTY)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Quota is only supported on btrfs.");
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to adjust quota limit: %m");
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+const sd_bus_vtable manager_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_PROPERTY("PoolPath", "s", property_get_pool_path, 0, 0),
+ SD_BUS_PROPERTY("PoolUsage", "t", property_get_pool_usage, 0, 0),
+ SD_BUS_PROPERTY("PoolLimit", "t", property_get_pool_limit, 0, 0),
+ SD_BUS_PROPERTY("Profiles", "as", property_get_profiles, 0, 0),
+ SD_BUS_METHOD("GetImage", "s", "o", method_get_image, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("ListImages", NULL, "a(ssbtttso)", method_list_images, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetImageOSRelease", "s", "a{ss}", method_get_image_os_release, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetImageState", "s", "s", method_get_image_state, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetImageMetadata", "sas", "saya{say}", method_get_image_metadata, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("AttachImage", "sassbs", "a(sss)", method_attach_image, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("DetachImage", "sb", "a(sss)", method_detach_image, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("RemoveImage", "s", NULL, method_remove_image, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("MarkImageReadOnly", "sb", NULL, method_mark_image_read_only, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetImageLimit", "st", NULL, method_set_image_limit, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetPoolLimit", "t", NULL, method_set_pool_limit, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_VTABLE_END
+};
+
+int reply_portable_changes(sd_bus_message *m, const PortableChange *changes, size_t n_changes) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ size_t i;
+ int r;
+
+ assert(m);
+ assert(changes || n_changes == 0);
+
+ r = sd_bus_message_new_method_return(m, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "(sss)");
+ if (r < 0)
+ return r;
+
+ for (i = 0; i < n_changes; i++) {
+ r = sd_bus_message_append(reply, "(sss)",
+ portable_change_type_to_string(changes[i].type),
+ changes[i].path,
+ changes[i].source);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
diff --git a/src/portable/portabled-bus.h b/src/portable/portabled-bus.h
new file mode 100644
index 0000000000..5810517ca3
--- /dev/null
+++ b/src/portable/portabled-bus.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "portable.h"
+
+extern const sd_bus_vtable manager_vtable[];
+
+int reply_portable_changes(sd_bus_message *m, const PortableChange *changes, size_t n_changes);
diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c
new file mode 100644
index 0000000000..7a13ea235a
--- /dev/null
+++ b/src/portable/portabled-image-bus.c
@@ -0,0 +1,733 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-label.h"
+#include "bus-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "io-util.h"
+#include "machine-image.h"
+#include "portable.h"
+#include "portabled-bus.h"
+#include "portabled-image-bus.h"
+#include "portabled-image.h"
+#include "portabled.h"
+#include "process-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static BUS_DEFINE_PROPERTY_GET_ENUM(property_get_type, image_type, ImageType);
+
+int bus_image_common_get_os_release(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ int r;
+
+ assert(name_or_path || image);
+ assert(message);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_BY_PATH,
+ "org.freedesktop.portable1.inspect-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Will call us back */
+ return 1;
+
+ if (!image->metadata_valid) {
+ r = image_read_metadata(image);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to read image metadata: %m");
+ }
+
+ return bus_reply_pair_array(message, image->os_release);
+}
+
+static int bus_image_method_get_os_release(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_get_os_release(NULL, message, NULL, userdata, error);
+}
+
+static int append_fd(sd_bus_message *m, PortableMetadata *d) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *buf = NULL;
+ size_t n;
+ int r;
+
+ assert(m);
+ assert(d);
+ assert(d->fd >= 0);
+
+ f = fdopen(d->fd, "re");
+ if (!f)
+ return -errno;
+
+ d->fd = -1;
+
+ r = read_full_stream(f, &buf, &n);
+ if (r < 0)
+ return r;
+
+ return sd_bus_message_append_array(m, 'y', buf, n);
+}
+
+int bus_image_common_get_metadata(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
+ _cleanup_(portable_metadata_hashmap_unrefp) Hashmap *unit_files = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _cleanup_free_ PortableMetadata **sorted = NULL;
+ _cleanup_strv_free_ char **matches = NULL;
+ size_t i;
+ int r;
+
+ assert(name_or_path || image);
+ assert(message);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ r = sd_bus_message_read_strv(message, &matches);
+ if (r < 0)
+ return r;
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_BY_PATH,
+ "org.freedesktop.portable1.inspect-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Will call us back */
+ return 1;
+
+ r = portable_extract(
+ image->path,
+ matches,
+ &os_release,
+ &unit_files,
+ error);
+ if (r < 0)
+ return r;
+
+ r = portable_metadata_hashmap_to_sorted_array(unit_files, &sorted);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_new_method_return(message, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "s", image->path);
+ if (r < 0)
+ return r;
+
+ r = append_fd(reply, os_release);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "{say}");
+ if (r < 0)
+ return r;
+
+ for (i = 0; i < hashmap_size(unit_files); i++) {
+
+
+ r = sd_bus_message_open_container(reply, 'e', "say");
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_append(reply, "s", sorted[i]->name);
+ if (r < 0)
+ return r;
+
+ r = append_fd(reply, sorted[i]);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
+
+static int bus_image_method_get_metadata(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_get_metadata(NULL, message, NULL, userdata, error);
+}
+
+static int bus_image_method_get_state(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Image *image = userdata;
+ PortableState state;
+ int r;
+
+ assert(message);
+ assert(image);
+
+ r = portable_get_state(
+ sd_bus_message_get_bus(message),
+ image->path,
+ 0,
+ &state,
+ error);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, "s", portable_state_to_string(state));
+}
+
+int bus_image_common_attach(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char **matches = NULL;
+ PortableChange *changes = NULL;
+ PortableFlags flags = 0;
+ const char *copy_mode;
+ size_t n_changes = 0;
+ const char *profile;
+ int runtime, r;
+
+ assert(message);
+ assert(name_or_path || image);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ r = sd_bus_message_read_strv(message, &matches);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_read(message, "sbs", &profile, &runtime, ©_mode);
+ if (r < 0)
+ return r;
+
+ if (streq(copy_mode, "symlink"))
+ flags |= PORTABLE_PREFER_SYMLINK;
+ else if (streq(copy_mode, "copy"))
+ flags |= PORTABLE_PREFER_COPY;
+ else if (!isempty(copy_mode))
+ return sd_bus_reply_method_errorf(message, SD_BUS_ERROR_INVALID_ARGS, "Unknown copy mode '%s'", copy_mode);
+
+ if (runtime)
+ flags |= PORTABLE_RUNTIME;
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_ALL,
+ "org.freedesktop.portable1.attach-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0) /* Will call us back */
+ return 1;
+
+ r = portable_attach(
+ sd_bus_message_get_bus(message),
+ image->path,
+ matches,
+ profile,
+ flags,
+ &changes,
+ &n_changes,
+ error);
+ if (r < 0)
+ goto finish;
+
+ r = reply_portable_changes(message, changes, n_changes);
+
+finish:
+ portable_changes_free(changes, n_changes);
+ return r;
+}
+
+static int bus_image_method_attach(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_attach(NULL, message, NULL, userdata, error);
+}
+
+static int bus_image_method_detach(
+ sd_bus_message *message,
+ void *userdata,
+ sd_bus_error *error) {
+
+ PortableChange *changes = NULL;
+ Image *image = userdata;
+ Manager *m = image->userdata;
+ size_t n_changes = 0;
+ int r, runtime;
+
+ assert(message);
+ assert(image);
+ assert(m);
+
+ r = sd_bus_message_read(message, "b", &runtime);
+ if (r < 0)
+ return r;
+
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ "org.freedesktop.portable1.attach-images",
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = portable_detach(
+ sd_bus_message_get_bus(message),
+ image->path,
+ runtime ? PORTABLE_RUNTIME : 0,
+ &changes,
+ &n_changes,
+ error);
+ if (r < 0)
+ goto finish;
+
+ r = reply_portable_changes(message, changes, n_changes);
+
+finish:
+ portable_changes_free(changes, n_changes);
+ return r;
+}
+
+int bus_image_common_remove(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ _cleanup_close_pair_ int errno_pipe_fd[2] = { -1, -1 };
+ _cleanup_(sigkill_waitp) pid_t child = 0;
+ PortableState state;
+ int r;
+
+ assert(message);
+ assert(name_or_path || image);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ if (m->n_operations >= OPERATIONS_MAX)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_LIMITS_EXCEEDED, "Too many ongoing operations.");
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_ALL,
+ "org.freedesktop.portable1.manage-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = portable_get_state(
+ sd_bus_message_get_bus(message),
+ image->path,
+ 0,
+ &state,
+ error);
+ if (r < 0)
+ return r;
+
+ if (state != PORTABLE_DETACHED)
+ return sd_bus_error_set_errnof(error, EBUSY, "Image '%s' is not detached, refusing.", image->path);
+
+ if (pipe2(errno_pipe_fd, O_CLOEXEC|O_NONBLOCK) < 0)
+ return sd_bus_error_set_errnof(error, errno, "Failed to create pipe: %m");
+
+ r = safe_fork("(sd-imgrm)", FORK_RESET_SIGNALS, &child);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to fork(): %m");
+ if (r == 0) {
+ errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]);
+
+ r = image_remove(image);
+ if (r < 0) {
+ (void) write(errno_pipe_fd[1], &r, sizeof(r));
+ _exit(EXIT_FAILURE);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ errno_pipe_fd[1] = safe_close(errno_pipe_fd[1]);
+
+ r = operation_new(m, child, message, errno_pipe_fd[0], NULL);
+ if (r < 0)
+ return r;
+
+ child = 0;
+ errno_pipe_fd[0] = -1;
+
+ return 1;
+}
+
+static int bus_image_method_remove(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_remove(NULL, message, NULL, userdata, error);
+}
+
+int bus_image_common_mark_read_only(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ int r, read_only;
+
+ assert(message);
+ assert(name_or_path || image);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ r = sd_bus_message_read(message, "b", &read_only);
+ if (r < 0)
+ return r;
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_ALL,
+ "org.freedesktop.portable1.manage-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = image_read_only(image, read_only);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static int bus_image_method_mark_read_only(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_mark_read_only(NULL, message, NULL, userdata, error);
+}
+
+int bus_image_common_set_limit(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ sd_bus_error *error) {
+
+ uint64_t limit;
+ int r;
+
+ assert(message);
+ assert(name_or_path || image);
+
+ if (!m) {
+ assert(image);
+ m = image->userdata;
+ }
+
+ r = sd_bus_message_read(message, "t", &limit);
+ if (r < 0)
+ return r;
+ if (!FILE_SIZE_VALID_OR_INFINITY(limit))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "New limit out of range");
+
+ r = bus_image_acquire(m,
+ message,
+ name_or_path,
+ image,
+ BUS_IMAGE_AUTHENTICATE_ALL,
+ "org.freedesktop.portable1.manage-images",
+ &image,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* Will call us back */
+
+ r = image_set_limit(image, limit);
+ if (r < 0)
+ return r;
+
+ return sd_bus_reply_method_return(message, NULL);
+}
+
+static int bus_image_method_set_limit(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ return bus_image_common_set_limit(NULL, message, NULL, userdata, error);
+}
+
+const sd_bus_vtable image_vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_PROPERTY("Name", "s", NULL, offsetof(Image, name), 0),
+ SD_BUS_PROPERTY("Path", "s", NULL, offsetof(Image, path), 0),
+ SD_BUS_PROPERTY("Type", "s", property_get_type, offsetof(Image, type), 0),
+ SD_BUS_PROPERTY("ReadOnly", "b", bus_property_get_bool, offsetof(Image, read_only), 0),
+ SD_BUS_PROPERTY("CreationTimestamp", "t", NULL, offsetof(Image, crtime), 0),
+ SD_BUS_PROPERTY("ModificationTimestamp", "t", NULL, offsetof(Image, mtime), 0),
+ SD_BUS_PROPERTY("Usage", "t", NULL, offsetof(Image, usage), 0),
+ SD_BUS_PROPERTY("Limit", "t", NULL, offsetof(Image, limit), 0),
+ SD_BUS_PROPERTY("UsageExclusive", "t", NULL, offsetof(Image, usage_exclusive), 0),
+ SD_BUS_PROPERTY("LimitExclusive", "t", NULL, offsetof(Image, limit_exclusive), 0),
+ SD_BUS_METHOD("GetOSRelease", NULL, "a{ss}", bus_image_method_get_os_release, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetMedatadata", "as", "saya{say}", bus_image_method_get_metadata, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("GetState", NULL, "s", bus_image_method_get_state, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Attach", "assbs", "a(sss)", bus_image_method_attach, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Detach", "b", "a(sss)", bus_image_method_detach, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("Remove", NULL, NULL, bus_image_method_remove, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("MarkReadOnly", "b", NULL, bus_image_method_mark_read_only, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD("SetLimit", "t", NULL, bus_image_method_set_limit, SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_VTABLE_END
+};
+
+int bus_image_path(Image *image, char **ret) {
+ assert(image);
+ assert(ret);
+
+ if (!image->discoverable)
+ return -EINVAL;
+
+ return sd_bus_path_encode("/org/freedesktop/portable1/image", image->name, ret);
+}
+
+int bus_image_acquire(
+ Manager *m,
+ sd_bus_message *message,
+ const char *name_or_path,
+ Image *image,
+ ImageAcquireMode mode,
+ const char *polkit_action,
+ Image **ret,
+ sd_bus_error *error) {
+
+ _cleanup_(image_unrefp) Image *loaded = NULL;
+ Image *cached;
+ int r;
+
+ assert(m);
+ assert(message);
+ assert(name_or_path || image);
+ assert(mode >= 0);
+ assert(mode < _BUS_IMAGE_ACQUIRE_MODE_MAX);
+ assert(polkit_action || mode == BUS_IMAGE_REFUSE_BY_PATH);
+ assert(ret);
+
+ /* Acquires an 'Image' object if not acquired yet, and enforces necessary authentication while doing so. */
+
+ if (mode == BUS_IMAGE_AUTHENTICATE_ALL) {
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ polkit_action,
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0) { /* Will call us back */
+ *ret = NULL;
+ return 0;
+ }
+ }
+
+ /* Already passed in? */
+ if (image) {
+ *ret = image;
+ return 1;
+ }
+
+ /* Let's see if this image is already cached? */
+ cached = manager_image_cache_get(m, name_or_path);
+ if (cached) {
+ *ret = cached;
+ return 1;
+ }
+
+ if (image_name_is_valid(name_or_path)) {
+
+ /* If it's a short name, let's search for it */
+ r = image_find(IMAGE_PORTABLE, name_or_path, &loaded);
+ if (r == -ENOENT)
+ return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_PORTABLE_IMAGE, "No image '%s' found.", name_or_path);
+
+ /* other errors are handled below… */
+ } else {
+ /* Don't accept path if this is always forbidden */
+ if (mode == BUS_IMAGE_REFUSE_BY_PATH)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Expected image name, not path in place of '%s'.", name_or_path);
+
+ if (!path_is_absolute(name_or_path))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image name '%s' is not valid or not a valid path.", name_or_path);
+
+ if (!path_is_normalized(name_or_path))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image path '%s' is not normalized.", name_or_path);
+
+ if (mode == BUS_IMAGE_AUTHENTICATE_BY_PATH) {
+ r = bus_verify_polkit_async(
+ message,
+ CAP_SYS_ADMIN,
+ polkit_action,
+ NULL,
+ false,
+ UID_INVALID,
+ &m->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0) { /* Will call us back */
+ *ret = NULL;
+ return 0;
+ }
+ }
+
+ r = image_from_path(name_or_path, &loaded);
+ }
+ if (r < 0)
+ return r;
+
+ /* Add what we just loaded to the cache. This has as side-effect that the object stays in memory until the
+ * cache is purged again, i.e. at least for the current event loop iteration, which is all we need, and which
+ * means we don't actually need to ref the return object. */
+ r = manager_image_cache_add(m, loaded);
+ if (r < 0)
+ return r;
+
+ *ret = loaded;
+ return 1;
+}
+
+int bus_image_object_find(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ void *userdata,
+ void **found,
+ sd_bus_error *error) {
+
+ _cleanup_free_ char *e = NULL;
+ Manager *m = userdata;
+ Image *image = NULL;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+
+ r = sd_bus_path_decode(path, "/org/freedesktop/portable1/image", &e);
+ if (r < 0)
+ return 0;
+ if (r == 0)
+ goto not_found;
+
+ r = bus_image_acquire(m, sd_bus_get_current_message(bus), e, NULL, BUS_IMAGE_REFUSE_BY_PATH, NULL, &image, error);
+ if (r == -ENOENT)
+ goto not_found;
+ if (r < 0)
+ return r;
+
+ *found = image;
+ return 1;
+
+not_found:
+ *found = NULL;
+ return 0;
+}
+
+int bus_image_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) {
+ _cleanup_(image_hashmap_freep) Hashmap *images = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ size_t n_allocated = 0, n = 0;
+ Manager *m = userdata;
+ Image *image;
+ Iterator i;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(nodes);
+
+ images = hashmap_new(&string_hash_ops);
+ if (!images)
+ return -ENOMEM;
+
+ r = manager_image_cache_discover(m, images, error);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(image, images, i) {
+ char *p;
+
+ r = bus_image_path(image, &p);
+ if (r < 0)
+ return r;
+
+ if (!GREEDY_REALLOC(l, n_allocated, n+2)) {
+ free(p);
+ return -ENOMEM;
+ }
+
+ l[n++] = p;
+ l[n] = NULL;
+ }
+
+ *nodes = TAKE_PTR(l);
+
+ return 1;
+}
diff --git a/src/portable/portabled-image-bus.h b/src/portable/portabled-image-bus.h
new file mode 100644
index 0000000000..81c17f68af
--- /dev/null
+++ b/src/portable/portabled-image-bus.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "sd-bus.h"
+
+#include "machine-image.h"
+#include "portabled.h"
+
+int bus_image_common_get_os_release(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+int bus_image_common_get_metadata(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+int bus_image_common_attach(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+int bus_image_common_remove(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+int bus_image_common_mark_read_only(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+int bus_image_common_set_limit(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, sd_bus_error *error);
+
+extern const sd_bus_vtable image_vtable[];
+
+int bus_image_path(Image *image, char **ret);
+
+/* So here's some complexity: some of operations can either take an image name, or a fully qualified file system path
+ * to an image. We need to authenticate differently when processing these two: images referenced via simple image names
+ * mean the images are located in the image search path and thus safe for limited read access for unprivileged
+ * clients. For operations on images located anywhere else we need explicit authentication however, so that
+ * unprivileged clients can't make us open arbitrary files in the file system.
+ *
+ * The "Image" bus objects directly represent images in the image search path, but do not exist for path-referenced
+ * images. Hence, when requesting a bus object we need to refuse references by file system path, but still allow
+ * references by image name. Depending on the operation to execute potentially we need to authenticate in all cases. */
+
+typedef enum ImageAcquireMode {
+ BUS_IMAGE_REFUSE_BY_PATH, /* allow by name + prohibit by path */
+ BUS_IMAGE_AUTHENTICATE_BY_PATH, /* allow by name + polkit by path */
+ BUS_IMAGE_AUTHENTICATE_ALL, /* polkit by name + polkit by path */
+ _BUS_IMAGE_ACQUIRE_MODE_MAX,
+ _BUS_IMAGE_ACQUIRE_MODE_INVALID = -1
+} ImageAcquireMode;
+
+int bus_image_acquire(Manager *m, sd_bus_message *message, const char *name_or_path, Image *image, ImageAcquireMode mode, const char *polkit_action, Image **ret, sd_bus_error *error);
+
+int bus_image_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error);
+int bus_image_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error);
diff --git a/src/portable/portabled-image.c b/src/portable/portabled-image.c
new file mode 100644
index 0000000000..c58197c9a6
--- /dev/null
+++ b/src/portable/portabled-image.c
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "portable.h"
+#include "portabled-image.h"
+#include "portabled.h"
+
+Image *manager_image_cache_get(Manager *m, const char *name_or_path) {
+ assert(m);
+
+ return hashmap_get(m->image_cache, name_or_path);
+}
+
+static int image_cache_flush(sd_event_source *s, void *userdata) {
+ Manager *m = userdata;
+
+ assert(s);
+ assert(m);
+
+ hashmap_clear_with_destructor(m->image_cache, image_unref);
+ return 0;
+}
+
+static int manager_image_cache_initialize(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = hashmap_ensure_allocated(&m->image_cache, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ /* We flush the cache as soon as we are idle again */
+ if (!m->image_cache_defer_event) {
+ r = sd_event_add_defer(m->event, &m->image_cache_defer_event, image_cache_flush, m);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_priority(m->image_cache_defer_event, SD_EVENT_PRIORITY_IDLE);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_event_source_set_enabled(m->image_cache_defer_event, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+int manager_image_cache_add(Manager *m, Image *image) {
+ int r;
+
+ assert(m);
+
+ /* We add the specified image to the cache under two keys.
+ *
+ * 1. Always under its path
+ *
+ * 2. If the image was discovered in the search path (i.e. its discoverable boolean set) we'll also add it
+ * under its short name.
+ *
+ */
+
+ r = manager_image_cache_initialize(m);
+ if (r < 0)
+ return r;
+
+ image->userdata = m;
+
+ r = hashmap_put(m->image_cache, image->path, image);
+ if (r < 0)
+ return r;
+
+ image_ref(image);
+
+ if (image->discoverable) {
+ r = hashmap_put(m->image_cache, image->name, image);
+ if (r < 0)
+ return r;
+
+ image_ref(image);
+ }
+
+ return 0;
+}
+
+int manager_image_cache_discover(Manager *m, Hashmap *images, sd_bus_error *error) {
+ Image *image;
+ Iterator i;
+ int r;
+
+ assert(m);
+
+ /* A wrapper around image_discover() (for finding images in search path) and portable_discover_attached() (for
+ * finding attached images). */
+
+ r = image_discover(IMAGE_PORTABLE, images);
+ if (r < 0)
+ return r;
+
+ HASHMAP_FOREACH(image, images, i)
+ (void) manager_image_cache_add(m, image);
+
+ return 0;
+}
diff --git a/src/portable/portabled-image.h b/src/portable/portabled-image.h
new file mode 100644
index 0000000000..f29bd89e83
--- /dev/null
+++ b/src/portable/portabled-image.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "hashmap.h"
+#include "machine-image.h"
+#include "portabled.h"
+
+Image *manager_image_cache_get(Manager *m, const char *name_or_path);
+
+int manager_image_cache_add(Manager *m, Image *image);
+
+int manager_image_cache_discover(Manager *m, Hashmap *images, sd_bus_error *error);
diff --git a/src/portable/portabled-operation.c b/src/portable/portabled-operation.c
new file mode 100644
index 0000000000..305c96ca6f
--- /dev/null
+++ b/src/portable/portabled-operation.c
@@ -0,0 +1,128 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "portabled-operation.h"
+#include "process-util.h"
+
+static int operation_done(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ Operation *o = userdata;
+ int r;
+
+ assert(o);
+ assert(si);
+
+ log_debug("Operating " PID_FMT " is now complete with code=%s status=%i",
+ o->pid,
+ sigchld_code_to_string(si->si_code), si->si_status);
+
+ o->pid = 0;
+
+ if (si->si_code != CLD_EXITED) {
+ r = sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, "Child died abnormally.");
+ goto fail;
+ }
+
+ if (si->si_status == EXIT_SUCCESS)
+ r = 0;
+ else if (read(o->errno_fd, &r, sizeof(r)) != sizeof(r)) { /* Try to acquire error code for failed operation */
+ r = sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED, "Child failed.");
+ goto fail;
+ }
+
+ if (o->done) {
+ /* A completion routine is set for this operation, call it. */
+ r = o->done(o, r, &error);
+ if (r < 0) {
+ if (!sd_bus_error_is_set(&error))
+ sd_bus_error_set_errno(&error, r);
+
+ goto fail;
+ }
+
+ } else {
+ /* The default operation when done is to simply return an error on failure or an empty success
+ * message on success. */
+ if (r < 0) {
+ sd_bus_error_set_errno(&error, r);
+ goto fail;
+ }
+
+ r = sd_bus_reply_method_return(o->message, NULL);
+ if (r < 0)
+ log_error_errno(r, "Failed to reply to message: %m");
+ }
+
+ operation_free(o);
+ return 0;
+
+fail:
+ r = sd_bus_reply_method_error(o->message, &error);
+ if (r < 0)
+ log_error_errno(r, "Failed to reply to message: %m");
+
+ operation_free(o);
+ return 0;
+}
+
+int operation_new(Manager *manager, pid_t child, sd_bus_message *message, int errno_fd, Operation **ret) {
+ Operation *o;
+ int r;
+
+ assert(manager);
+ assert(child > 1);
+ assert(message);
+ assert(errno_fd >= 0);
+
+ o = new0(Operation, 1);
+ if (!o)
+ return -ENOMEM;
+
+ o->extra_fd = -1;
+
+ r = sd_event_add_child(manager->event, &o->event_source, child, WEXITED, operation_done, o);
+ if (r < 0) {
+ free(o);
+ return r;
+ }
+
+ o->pid = child;
+ o->message = sd_bus_message_ref(message);
+ o->errno_fd = errno_fd;
+
+ LIST_PREPEND(operations, manager->operations, o);
+ manager->n_operations++;
+ o->manager = manager;
+
+ log_debug("Started new operation " PID_FMT ".", child);
+
+ /* At this point we took ownership of both the child and the errno file descriptor! */
+
+ if (ret)
+ *ret = o;
+
+ return 0;
+}
+
+Operation *operation_free(Operation *o) {
+ if (!o)
+ return NULL;
+
+ sd_event_source_unref(o->event_source);
+
+ safe_close(o->errno_fd);
+ safe_close(o->extra_fd);
+
+ if (o->pid > 1)
+ (void) sigkill_wait(o->pid);
+
+ sd_bus_message_unref(o->message);
+
+ if (o->manager) {
+ LIST_REMOVE(operations, o->manager->operations, o);
+ o->manager->n_operations--;
+ }
+
+ return mfree(o);
+}
diff --git a/src/portable/portabled-operation.h b/src/portable/portabled-operation.h
new file mode 100644
index 0000000000..701f399bbb
--- /dev/null
+++ b/src/portable/portabled-operation.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include
+
+#include "sd-bus.h"
+#include "sd-event.h"
+
+#include "list.h"
+
+typedef struct Operation Operation;
+
+#include "portabled.h"
+
+#define OPERATIONS_MAX 64
+
+struct Operation {
+ Manager *manager;
+ pid_t pid;
+ sd_bus_message *message;
+ int errno_fd;
+ int extra_fd;
+ sd_event_source *event_source;
+ int (*done)(Operation *o, int ret, sd_bus_error *error);
+ LIST_FIELDS(Operation, operations);
+};
+
+int operation_new(Manager *manager, pid_t child, sd_bus_message *message, int errno_fd, Operation **ret);
+Operation *operation_free(Operation *o);
diff --git a/src/portable/portabled.c b/src/portable/portabled.c
new file mode 100644
index 0000000000..53faa09bac
--- /dev/null
+++ b/src/portable/portabled.c
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "sd-bus.h"
+#include "sd-daemon.h"
+
+#include "alloc-util.h"
+#include "bus-util.h"
+#include "def.h"
+#include "portabled-bus.h"
+#include "portabled-image-bus.h"
+#include "portabled.h"
+#include "process-util.h"
+#include "signal-util.h"
+
+static Manager* manager_unref(Manager *m);
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_unref);
+
+static int manager_new(Manager **ret) {
+ _cleanup_(manager_unrefp) Manager *m = NULL;
+ int r;
+
+ assert(ret);
+
+ m = new0(Manager, 1);
+ if (!m)
+ return -ENOMEM;
+
+ r = sd_event_default(&m->event);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_set_watchdog(m->event, true);
+
+ *ret = TAKE_PTR(m);
+ return 0;
+}
+
+static Manager* manager_unref(Manager *m) {
+ assert(m);
+
+ hashmap_free_with_destructor(m->image_cache, image_unref);
+
+ sd_event_source_unref(m->image_cache_defer_event);
+
+ bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+ sd_bus_unref(m->bus);
+ sd_event_unref(m->event);
+
+ return mfree(m);
+}
+
+static int manager_connect_bus(Manager *m) {
+ int r;
+
+ assert(m);
+ assert(!m->bus);
+
+ r = sd_bus_default_system(&m->bus);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to system bus: %m");
+
+ r = sd_bus_add_object_vtable(m->bus, NULL, "/org/freedesktop/portable1", "org.freedesktop.portable1.Manager", manager_vtable, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add manager object vtable: %m");
+
+ r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/portable1/image", "org.freedesktop.portable1.Image", image_vtable, bus_image_object_find, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add image object vtable: %m");
+
+ r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/portable1/image", bus_image_node_enumerator, m);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add image enumerator: %m");
+
+ r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.portable1", 0, NULL, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to request name: %m");
+
+ r = sd_bus_attach_event(m->bus, m->event, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to attach bus to event loop: %m");
+
+ (void) sd_bus_set_exit_on_disconnect(m->bus, true);
+
+ return 0;
+}
+
+static int manager_startup(Manager *m) {
+ int r;
+
+ assert(m);
+
+ r = manager_connect_bus(m);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static bool check_idle(void *userdata) {
+ Manager *m = userdata;
+
+ return !m->operations;
+}
+
+static int manager_run(Manager *m) {
+ assert(m);
+
+ return bus_event_loop_with_idle(
+ m->event,
+ m->bus,
+ "org.freedesktop.portable1",
+ DEFAULT_EXIT_USEC,
+ check_idle, m);
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(manager_unrefp) Manager *m = NULL;
+ int r;
+
+ log_set_target(LOG_TARGET_AUTO);
+ log_parse_environment();
+ log_open();
+
+ umask(0022);
+
+ if (argc != 1) {
+ log_error("This program takes no arguments.");
+ r = -EINVAL;
+ goto finish;
+ }
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, -1) >= 0);
+
+ r = manager_new(&m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to allocate manager object: %m");
+ goto finish;
+ }
+
+ r = manager_startup(m);
+ if (r < 0) {
+ log_error_errno(r, "Failed to fully start up daemon: %m");
+ goto finish;
+ }
+
+ log_debug("systemd-portabled running as pid " PID_FMT, getpid_cached());
+
+ sd_notify(false,
+ "READY=1\n"
+ "STATUS=Processing requests...");
+
+ r = manager_run(m);
+
+ log_debug("systemd-portabled stopped as pid " PID_FMT, getpid_cached());
+
+finish:
+ return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
+
+}
diff --git a/src/portable/portabled.h b/src/portable/portabled.h
new file mode 100644
index 0000000000..00461edfca
--- /dev/null
+++ b/src/portable/portabled.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+#include "sd-bus.h"
+#include "sd-event.h"
+
+#include "hashmap.h"
+#include "list.h"
+
+typedef struct Manager Manager;
+
+#include "portabled-operation.h"
+
+struct Manager {
+ sd_event *event;
+ sd_bus *bus;
+
+ Hashmap *polkit_registry;
+
+ Hashmap *image_cache;
+ sd_event_source *image_cache_defer_event;
+
+ LIST_HEAD(Operation, operations);
+ unsigned n_operations;
+};
diff --git a/src/portable/profile/default/service.conf b/src/portable/profile/default/service.conf
new file mode 100644
index 0000000000..993d351638
--- /dev/null
+++ b/src/portable/profile/default/service.conf
@@ -0,0 +1,30 @@
+# The "default" security profile for services, i.e. a number of useful restrictions
+
+[Service]
+MountAPIVFS=yes
+TemporaryFileSystem=/run
+BindReadOnlyPaths=/run/systemd/notify
+BindReadOnlyPaths=/dev/log /run/systemd/journal/socket /run/systemd/journal/stdout
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=/etc/resolv.conf
+BindReadOnlyPaths=/run/dbus/system_bus_socket
+DynamicUser=yes
+RemoveIPC=yes
+CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER \
+ CAP_FSETID CAP_IPC_LOCK CAP_IPC_OWNER CAP_KILL CAP_MKNOD CAP_NET_ADMIN \
+ CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_SETGID CAP_SETPCAP \
+ CAP_SETUID CAP_SYS_ADMIN CAP_SYS_CHROOT CAP_SYS_NICE CAP_SYS_RESOURCE
+PrivateTmp=yes
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectHome=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
diff --git a/src/portable/profile/nonetwork/service.conf b/src/portable/profile/nonetwork/service.conf
new file mode 100644
index 0000000000..0d9c5a38d8
--- /dev/null
+++ b/src/portable/profile/nonetwork/service.conf
@@ -0,0 +1,30 @@
+# The "nonetwork" security profile for services, i.e. like "default" but without networking
+
+[Service]
+MountAPIVFS=yes
+TemporaryFileSystem=/run
+BindReadOnlyPaths=/run/systemd/notify
+BindReadOnlyPaths=/dev/log /run/systemd/journal/socket /run/systemd/journal/stdout
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=/run/dbus/system_bus_socket
+DynamicUser=yes
+RemoveIPC=yes
+CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER \
+ CAP_FSETID CAP_IPC_LOCK CAP_IPC_OWNER CAP_KILL CAP_MKNOD CAP_SETGID CAP_SETPCAP \
+ CAP_SETUID CAP_SYS_ADMIN CAP_SYS_CHROOT CAP_SYS_NICE CAP_SYS_RESOURCE
+PrivateTmp=yes
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectHome=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+PrivateNetwork=yes
+IPAddressDeny=any
diff --git a/src/portable/profile/strict/service.conf b/src/portable/profile/strict/service.conf
new file mode 100644
index 0000000000..d12620fc99
--- /dev/null
+++ b/src/portable/profile/strict/service.conf
@@ -0,0 +1,29 @@
+# The "strict" security profile for services, all options turned on
+
+[Service]
+MountAPIVFS=yes
+TemporaryFileSystem=/run
+BindReadOnlyPaths=/run/systemd/notify
+BindReadOnlyPaths=/dev/log /run/systemd/journal/socket /run/systemd/journal/stdout
+BindReadOnlyPaths=/etc/machine-id
+DynamicUser=yes
+RemoveIPC=yes
+CapabilityBoundingSet=
+PrivateTmp=yes
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectHome=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX
+LockPersonality=yes
+NoNewPrivileges=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+SystemCallArchitectures=native
+PrivateNetwork=yes
+IPAddressDeny=any
+TasksMax=4
diff --git a/src/portable/profile/trusted/service.conf b/src/portable/profile/trusted/service.conf
new file mode 100644
index 0000000000..9a6af70b93
--- /dev/null
+++ b/src/portable/profile/trusted/service.conf
@@ -0,0 +1,7 @@
+# The "trusted" profile for services, i.e. no restrictions are applied
+
+[Service]
+MountAPIVFS=yes
+BindPaths=/run
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=/etc/resolv.conf
diff --git a/src/resolve/resolved-link.c b/src/resolve/resolved-link.c
index f2552a11de..a0eedd86d3 100644
--- a/src/resolve/resolved-link.c
+++ b/src/resolve/resolved-link.c
@@ -1202,7 +1202,7 @@ int link_load_user(Link *l) {
if (l->is_managed)
return 0; /* if the device is managed, then networkd is our configuration source, not the bus API */
- r = parse_env_file(l->state_file, NEWLINE,
+ r = parse_env_file(NULL, l->state_file, NEWLINE,
"LLMNR", &llmnr,
"MDNS", &mdns,
"DNSSEC", &dnssec,
diff --git a/src/shared/bus-util.c b/src/shared/bus-util.c
index ed30edfb60..998134ec8c 100644
--- a/src/shared/bus-util.c
+++ b/src/shared/bus-util.c
@@ -1847,3 +1847,34 @@ int bus_request_name_async_may_reload_dbus(sd_bus *bus, sd_bus_slot **ret_slot,
return sd_bus_request_name_async(bus, ret_slot, name, flags, request_name_handler_may_reload_dbus, data);
}
+
+int bus_reply_pair_array(sd_bus_message *m, char **l) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ char **k, **v;
+ int r;
+
+ assert(m);
+
+ /* Reply to the specified message with a message containing a dictionary put together from the specified
+ * strv */
+
+ r = sd_bus_message_new_method_return(m, &reply);
+ if (r < 0)
+ return r;
+
+ r = sd_bus_message_open_container(reply, 'a', "{ss}");
+ if (r < 0)
+ return r;
+
+ STRV_FOREACH_PAIR(k, v, l) {
+ r = sd_bus_message_append(reply, "{ss}", *k, *v);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(NULL, reply, NULL);
+}
diff --git a/src/shared/bus-util.h b/src/shared/bus-util.h
index 16aeca74dd..ee9f1ff097 100644
--- a/src/shared/bus-util.h
+++ b/src/shared/bus-util.h
@@ -184,3 +184,5 @@ static inline int bus_open_system_watch_bind(sd_bus **ret) {
}
int bus_request_name_async_may_reload_dbus(sd_bus *bus, sd_bus_slot **ret_slot, const char *name, uint64_t flags, void *userdata);
+
+int bus_reply_pair_array(sd_bus_message *m, char **l);
diff --git a/src/shared/cgroup-show.c b/src/shared/cgroup-show.c
index ea2db32510..4db638a874 100644
--- a/src/shared/cgroup-show.c
+++ b/src/shared/cgroup-show.c
@@ -344,7 +344,7 @@ int show_cgroup_get_path_and_warn(
const char *m;
m = strjoina("/run/systemd/machines/", machine);
- r = parse_env_file(m, NEWLINE, "SCOPE", &unit, NULL);
+ r = parse_env_file(NULL, m, NEWLINE, "SCOPE", &unit, NULL);
if (r < 0)
return log_error_errno(r, "Failed to load machine data: %m");
diff --git a/src/shared/condition.c b/src/shared/condition.c
index 87e8a485ff..bf6476c339 100644
--- a/src/shared/condition.c
+++ b/src/shared/condition.c
@@ -464,7 +464,7 @@ static int condition_test_needs_update(Condition *c) {
uint64_t timestamp;
int r;
- r = parse_env_file(p, NULL, "TIMESTAMP_NSEC", ×tamp_str, NULL);
+ r = parse_env_file(NULL, p, NULL, "TIMESTAMP_NSEC", ×tamp_str, NULL);
if (r < 0) {
log_error_errno(r, "Failed to parse timestamp file '%s', using mtime: %m", p);
return true;
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 14e905b2d9..59ee4fe9e5 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -30,6 +30,7 @@
#include "linux-3.13/dm-ioctl.h"
#include "missing.h"
#include "mount-util.h"
+#include "os-util.h"
#include "path-util.h"
#include "process-util.h"
#include "raw-clone.h"
@@ -779,6 +780,14 @@ int dissected_image_mount(DissectedImage *m, const char *where, uid_t uid_shift,
r = mount_partition(m->partitions + PARTITION_ROOT, where, NULL, uid_shift, flags);
if (r < 0)
return r;
+
+ if (flags & DISSECT_IMAGE_VALIDATE_OS) {
+ r = path_is_os_tree(where);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -EMEDIUMTYPE;
+ }
}
if ((flags & DISSECT_IMAGE_MOUNT_ROOT_ONLY))
@@ -1270,15 +1279,11 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
if (r < 0)
goto finish;
- r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS, &child);
+ r = safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, &child);
if (r < 0)
goto finish;
if (r == 0) {
- /* Make sure we never propagate to the host */
- if (mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL) < 0)
- _exit(EXIT_FAILURE);
-
- r = dissected_image_mount(m, t, UID_INVALID, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_MOUNT_ROOT_ONLY);
+ r = dissected_image_mount(m, t, UID_INVALID, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_MOUNT_ROOT_ONLY|DISSECT_IMAGE_VALIDATE_OS);
if (r < 0) {
log_debug_errno(r, "Failed to mount dissected image: %m");
_exit(EXIT_FAILURE);
diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h
index 16bcb6df8f..037fbab3f7 100644
--- a/src/shared/dissect-image.h
+++ b/src/shared/dissect-image.h
@@ -62,6 +62,7 @@ typedef enum DissectImageFlags {
DISSECT_IMAGE_REQUIRE_ROOT = 1 << 5, /* Don't accept disks without root partition */
DISSECT_IMAGE_MOUNT_ROOT_ONLY = 1 << 6, /* Mount only the root partition */
DISSECT_IMAGE_MOUNT_NON_ROOT_ONLY = 1 << 7, /* Mount only non-root partitions */
+ DISSECT_IMAGE_VALIDATE_OS = 1 << 8, /* Refuse mounting images that aren't identifyable as OS images */
} DissectImageFlags;
struct DissectedImage {
diff --git a/src/shared/install.c b/src/shared/install.c
index 424f4ccdbb..ecda662f28 100644
--- a/src/shared/install.c
+++ b/src/shared/install.c
@@ -99,8 +99,6 @@ static inline void presets_freep(Presets *p) {
p->n_rules = 0;
}
-static int unit_file_lookup_state(UnitFileScope scope, const LookupPaths *paths, const char *name, UnitFileState *ret);
-
bool unit_type_may_alias(UnitType type) {
return IN_SET(type,
UNIT_SERVICE,
@@ -2643,7 +2641,7 @@ int unit_file_get_default(
return 0;
}
-static int unit_file_lookup_state(
+int unit_file_lookup_state(
UnitFileScope scope,
const LookupPaths *paths,
const char *name,
diff --git a/src/shared/install.h b/src/shared/install.h
index 9dc91429da..80f98d722e 100644
--- a/src/shared/install.h
+++ b/src/shared/install.h
@@ -207,6 +207,12 @@ int unit_file_add_dependency(
UnitFileChange **changes,
size_t *n_changes);
+int unit_file_lookup_state(
+ UnitFileScope scope,
+ const LookupPaths *paths,
+ const char *name,
+ UnitFileState *ret);
+
int unit_file_get_state(UnitFileScope scope, const char *root_dir, const char *filename, UnitFileState *ret);
int unit_file_exists(UnitFileScope scope, const LookupPaths *paths, const char *name);
diff --git a/src/shared/machine-image.c b/src/shared/machine-image.c
index 440b1f574f..d97b522bdb 100644
--- a/src/shared/machine-image.c
+++ b/src/shared/machine-image.c
@@ -35,6 +35,7 @@
#include "machine-image.h"
#include "macro.h"
#include "mkdir.h"
+#include "os-util.h"
#include "path-util.h"
#include "rm-rf.h"
#include "string-table.h"
@@ -45,16 +46,31 @@
#include "util.h"
#include "xattr-util.h"
-static const char image_search_path[] =
- "/var/lib/machines\0"
- "/var/lib/container\0" /* legacy */
- "/usr/local/lib/machines\0"
- "/usr/lib/machines\0";
+static const char* const image_search_path[_IMAGE_CLASS_MAX] = {
+ [IMAGE_MACHINE] = "/etc/machines\0" /* only place symlinks here */
+ "/run/machines\0" /* and here too */
+ "/var/lib/machines\0" /* the main place for images */
+ "/var/lib/container\0" /* legacy */
+ "/usr/local/lib/machines\0"
+ "/usr/lib/machines\0",
+
+ [IMAGE_PORTABLE] = "/etc/portables\0" /* only place symlinks here */
+ "/run/portables\0" /* and here too */
+ "/var/lib/portables\0" /* the main place for images */
+ "/usr/local/lib/portables\0"
+ "/usr/lib/portables\0",
+};
Image *image_unref(Image *i) {
if (!i)
return NULL;
+ assert(i->n_ref > 0);
+ i->n_ref--;
+
+ if (i->n_ref > 0)
+ return NULL;
+
free(i->name);
free(i->path);
@@ -65,6 +81,16 @@ Image *image_unref(Image *i) {
return mfree(i);
}
+Image *image_ref(Image *i) {
+ if (!i)
+ return NULL;
+
+ assert(i->n_ref > 0);
+ i->n_ref++;
+
+ return i;
+}
+
static char **image_settings_path(Image *image) {
_cleanup_strv_free_ char **l = NULL;
const char *fn, *s;
@@ -125,6 +151,7 @@ static int image_new(
if (!i)
return -ENOMEM;
+ i->n_ref = 1;
i->type = t;
i->read_only = read_only;
i->crtime = crtime;
@@ -140,7 +167,6 @@ static int image_new(
i->path = strjoin(path, "/", filename);
else
i->path = strdup(filename);
-
if (!i->path)
return -ENOMEM;
@@ -151,45 +177,92 @@ static int image_new(
return 0;
}
+static int extract_pretty(const char *path, const char *suffix, char **ret) {
+ _cleanup_free_ char *name = NULL;
+ const char *p;
+ size_t n;
+
+ assert(path);
+ assert(ret);
+
+ p = last_path_component(path);
+ n = strcspn(p, "/");
+
+ name = strndup(p, n);
+ if (!name)
+ return -ENOMEM;
+
+ if (suffix) {
+ char *e;
+
+ e = endswith(name, suffix);
+ if (!e)
+ return -EINVAL;
+
+ *e = 0;
+ }
+
+ if (!image_name_is_valid(name))
+ return -EINVAL;
+
+ *ret = TAKE_PTR(name);
+ return 0;
+}
+
static int image_make(
const char *pretty,
int dfd,
const char *path,
const char *filename,
+ const struct stat *st,
Image **ret) {
- struct stat st;
+ _cleanup_free_ char *pretty_buffer = NULL;
+ struct stat stbuf;
bool read_only;
int r;
+ assert(dfd >= 0 || dfd == AT_FDCWD);
assert(filename);
/* We explicitly *do* follow symlinks here, since we want to allow symlinking trees, raw files and block
- * devices into /var/lib/machines/, and treat them normally. */
+ * devices into /var/lib/machines/, and treat them normally.
+ *
+ * This function returns -ENOENT if we can't find the image after all, and -EMEDIUMTYPE if it's not a file we
+ * recognize. */
- if (fstatat(dfd, filename, &st, 0) < 0)
- return -errno;
+ if (!st) {
+ if (fstatat(dfd, filename, &stbuf, 0) < 0)
+ return -errno;
+
+ st = &stbuf;
+ }
read_only =
(path && path_startswith(path, "/usr")) ||
(faccessat(dfd, filename, W_OK, AT_EACCESS) < 0 && errno == EROFS);
- if (S_ISDIR(st.st_mode)) {
+ if (S_ISDIR(st->st_mode)) {
_cleanup_close_ int fd = -1;
unsigned file_attr = 0;
if (!ret)
- return 1;
+ return 0;
- if (!pretty)
- pretty = filename;
+ if (!pretty) {
+ r = extract_pretty(filename, NULL, &pretty_buffer);
+ if (r < 0)
+ return r;
+
+ pretty = pretty_buffer;
+ }
fd = openat(dfd, filename, O_CLOEXEC|O_NOCTTY|O_DIRECTORY);
if (fd < 0)
return -errno;
/* btrfs subvolumes have inode 256 */
- if (st.st_ino == 256) {
+ if (st->st_ino == 256) {
r = btrfs_is_filesystem(fd);
if (r < 0)
@@ -227,7 +300,7 @@ static int image_make(
}
}
- return 1;
+ return 0;
}
}
@@ -248,56 +321,69 @@ static int image_make(
if (r < 0)
return r;
- return 1;
+ return 0;
- } else if (S_ISREG(st.st_mode) && endswith(filename, ".raw")) {
+ } else if (S_ISREG(st->st_mode) && endswith(filename, ".raw")) {
usec_t crtime = 0;
/* It's a RAW disk image */
if (!ret)
- return 1;
+ return 0;
- fd_getcrtime_at(dfd, filename, &crtime, 0);
+ (void) fd_getcrtime_at(dfd, filename, &crtime, 0);
- if (!pretty)
- pretty = strndupa(filename, strlen(filename) - 4);
+ if (!pretty) {
+ r = extract_pretty(filename, ".raw", &pretty_buffer);
+ if (r < 0)
+ return r;
+
+ pretty = pretty_buffer;
+ }
r = image_new(IMAGE_RAW,
pretty,
path,
filename,
- !(st.st_mode & 0222) || read_only,
+ !(st->st_mode & 0222) || read_only,
crtime,
- timespec_load(&st.st_mtim),
+ timespec_load(&st->st_mtim),
ret);
if (r < 0)
return r;
- (*ret)->usage = (*ret)->usage_exclusive = st.st_blocks * 512;
- (*ret)->limit = (*ret)->limit_exclusive = st.st_size;
+ (*ret)->usage = (*ret)->usage_exclusive = st->st_blocks * 512;
+ (*ret)->limit = (*ret)->limit_exclusive = st->st_size;
- return 1;
+ return 0;
- } else if (S_ISBLK(st.st_mode)) {
+ } else if (S_ISBLK(st->st_mode)) {
_cleanup_close_ int block_fd = -1;
uint64_t size = UINT64_MAX;
/* A block device */
if (!ret)
- return 1;
+ return 0;
- if (!pretty)
- pretty = filename;
+ if (!pretty) {
+ r = extract_pretty(filename, NULL, &pretty_buffer);
+ if (r < 0)
+ return r;
+
+ pretty = pretty_buffer;
+ }
block_fd = openat(dfd, filename, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY);
if (block_fd < 0)
log_debug_errno(errno, "Failed to open block device %s/%s, ignoring: %m", path, filename);
else {
- if (fstat(block_fd, &st) < 0)
+ /* Refresh stat data after opening the node */
+ if (fstat(block_fd, &stbuf) < 0)
return -errno;
- if (!S_ISBLK(st.st_mode)) /* Verify that what we opened is actually what we think it is */
+ st = &stbuf;
+
+ if (!S_ISBLK(st->st_mode)) /* Verify that what we opened is actually what we think it is */
return -ENOTTY;
if (!read_only) {
@@ -310,7 +396,7 @@ static int image_make(
}
if (ioctl(block_fd, BLKGETSIZE64, &size) < 0)
- log_debug_errno(errno, "Failed to issue BLKFLSBUF on device %s/%s, ignoring: %m", path, filename);
+ log_debug_errno(errno, "Failed to issue BLKGETSIZE64 on device %s/%s, ignoring: %m", path, filename);
block_fd = safe_close(block_fd);
}
@@ -319,7 +405,7 @@ static int image_make(
pretty,
path,
filename,
- !(st.st_mode & 0222) || read_only,
+ !(st->st_mode & 0222) || read_only,
0,
0,
ret);
@@ -329,24 +415,27 @@ static int image_make(
if (size != 0 && size != UINT64_MAX)
(*ret)->usage = (*ret)->usage_exclusive = (*ret)->limit = (*ret)->limit_exclusive = size;
- return 1;
+ return 0;
}
- return 0;
+ return -EMEDIUMTYPE;
}
-int image_find(const char *name, Image **ret) {
+int image_find(ImageClass class, const char *name, Image **ret) {
const char *path;
int r;
+ assert(class >= 0);
+ assert(class < _IMAGE_CLASS_MAX);
assert(name);
/* There are no images with invalid names */
if (!image_name_is_valid(name))
- return 0;
+ return -ENOENT;
- NULSTR_FOREACH(path, image_search_path) {
+ NULSTR_FOREACH(path, image_search_path[class]) {
_cleanup_closedir_ DIR *d = NULL;
+ struct stat st;
d = opendir(path);
if (!d) {
@@ -356,37 +445,90 @@ int image_find(const char *name, Image **ret) {
return -errno;
}
- r = image_make(NULL, dirfd(d), path, name, ret);
- if (IN_SET(r, 0, -ENOENT)) {
+ /* As mentioned above, we follow symlinks on this fstatat(), because we want to permit people to
+ * symlink block devices into the search path */
+ if (fstatat(dirfd(d), name, &st, 0) < 0) {
_cleanup_free_ char *raw = NULL;
+ if (errno != ENOENT)
+ return -errno;
+
raw = strappend(name, ".raw");
if (!raw)
return -ENOMEM;
- r = image_make(NULL, dirfd(d), path, raw, ret);
- if (IN_SET(r, 0, -ENOENT))
+ if (fstatat(dirfd(d), raw, &st, 0) < 0) {
+
+ if (errno == ENOENT)
+ continue;
+
+ return -errno;
+ }
+
+ if (!S_ISREG(st.st_mode))
continue;
+
+ r = image_make(name, dirfd(d), path, raw, &st, ret);
+
+ } else {
+ if (!S_ISDIR(st.st_mode) && !S_ISBLK(st.st_mode))
+ continue;
+
+ r = image_make(name, dirfd(d), path, name, &st, ret);
}
+ if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
+ continue;
if (r < 0)
return r;
+ if (ret)
+ (*ret)->discoverable = true;
+
return 1;
}
- if (streq(name, ".host"))
- return image_make(".host", AT_FDCWD, NULL, "/", ret);
+ if (class == IMAGE_MACHINE && streq(name, ".host")) {
+ r = image_make(".host", AT_FDCWD, NULL, "/", NULL, ret);
+ if (r < 0)
+ return r;
- return 0;
+ if (ret)
+ (*ret)->discoverable = true;
+
+ return r;
+ }
+
+ return -ENOENT;
};
-int image_discover(Hashmap *h) {
+int image_from_path(const char *path, Image **ret) {
+
+ /* Note that we don't set the 'discoverable' field of the returned object, because we don't check here whether
+ * the image is in the image search path. And if it is we don't know if the path we used is actually not
+ * overriden by another, different image earlier in the search path */
+
+ if (path_equal(path, "/"))
+ return image_make(".host", AT_FDCWD, NULL, "/", NULL, ret);
+
+ return image_make(NULL, AT_FDCWD, NULL, path, NULL, ret);
+}
+
+int image_find_harder(ImageClass class, const char *name_or_path, Image **ret) {
+ if (image_name_is_valid(name_or_path))
+ return image_find(class, name_or_path, ret);
+
+ return image_from_path(name_or_path, ret);
+}
+
+int image_discover(ImageClass class, Hashmap *h) {
const char *path;
int r;
+ assert(class >= 0);
+ assert(class < _IMAGE_CLASS_MAX);
assert(h);
- NULSTR_FOREACH(path, image_search_path) {
+ NULSTR_FOREACH(path, image_search_path[class]) {
_cleanup_closedir_ DIR *d = NULL;
struct dirent *de;
@@ -400,19 +542,53 @@ int image_discover(Hashmap *h) {
FOREACH_DIRENT_ALL(de, d, return -errno) {
_cleanup_(image_unrefp) Image *image = NULL;
+ _cleanup_free_ char *truncated = NULL;
+ const char *pretty;
+ struct stat st;
- if (!image_name_is_valid(de->d_name))
+ if (dot_or_dot_dot(de->d_name))
continue;
- if (hashmap_contains(h, de->d_name))
+ /* As mentioned above, we follow symlinks on this fstatat(), because we want to permit people
+ * to symlink block devices into the search path */
+ if (fstatat(dirfd(d), de->d_name, &st, 0) < 0) {
+ if (errno == ENOENT)
+ continue;
+
+ return -errno;
+ }
+
+ if (S_ISREG(st.st_mode)) {
+ const char *e;
+
+ e = endswith(de->d_name, ".raw");
+ if (!e)
+ continue;
+
+ truncated = strndup(de->d_name, e - de->d_name);
+ if (!truncated)
+ return -ENOMEM;
+
+ pretty = truncated;
+ } else if (S_ISDIR(st.st_mode) || S_ISBLK(st.st_mode))
+ pretty = de->d_name;
+ else
continue;
- r = image_make(NULL, dirfd(d), path, de->d_name, &image);
- if (IN_SET(r, 0, -ENOENT))
+ if (!image_name_is_valid(pretty))
+ continue;
+
+ if (hashmap_contains(h, pretty))
+ continue;
+
+ r = image_make(pretty, dirfd(d), path, de->d_name, &st, &image);
+ if (IN_SET(r, -ENOENT, -EMEDIUMTYPE))
continue;
if (r < 0)
return r;
+ image->discoverable = true;
+
r = hashmap_put(h, image->name, image);
if (r < 0)
return r;
@@ -421,19 +597,20 @@ int image_discover(Hashmap *h) {
}
}
- if (!hashmap_contains(h, ".host")) {
+ if (class == IMAGE_MACHINE && !hashmap_contains(h, ".host")) {
_cleanup_(image_unrefp) Image *image = NULL;
- r = image_make(".host", AT_FDCWD, NULL, "/", &image);
+ r = image_make(".host", AT_FDCWD, NULL, "/", NULL, &image);
if (r < 0)
return r;
+ image->discoverable = true;
+
r = hashmap_put(h, image->name, image);
if (r < 0)
return r;
image = NULL;
-
}
return 0;
@@ -566,11 +743,11 @@ int image_rename(Image *i, const char *new_name) {
if (r < 0)
return r;
- r = image_find(new_name, NULL);
- if (r < 0)
- return r;
- if (r > 0)
+ r = image_find(IMAGE_MACHINE, new_name, NULL);
+ if (r >= 0)
return -EEXIST;
+ if (r != -ENOENT)
+ return r;
switch (i->type) {
@@ -679,11 +856,11 @@ int image_clone(Image *i, const char *new_name, bool read_only) {
if (r < 0)
return r;
- r = image_find(new_name, NULL);
- if (r < 0)
- return r;
- if (r > 0)
+ r = image_find(IMAGE_MACHINE, new_name, NULL);
+ if (r >= 0)
return -EEXIST;
+ if (r != -ENOENT)
+ return r;
switch (i->type) {
@@ -923,6 +1100,7 @@ int image_read_metadata(Image *i) {
sd_id128_t machine_id = SD_ID128_NULL;
_cleanup_free_ char *hostname = NULL;
_cleanup_free_ char *path = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
r = chase_symlinks("/etc/hostname", i->path, CHASE_PREFIX_ROOT|CHASE_TRAIL_SLASH, &path);
if (r < 0 && r != -ENOENT)
@@ -962,18 +1140,9 @@ int image_read_metadata(Image *i) {
log_debug_errno(r, "Failed to parse machine-info data of %s: %m", i->name);
}
- path = mfree(path);
-
- r = chase_symlinks("/etc/os-release", i->path, CHASE_PREFIX_ROOT|CHASE_TRAIL_SLASH, &path);
- if (r == -ENOENT)
- r = chase_symlinks("/usr/lib/os-release", i->path, CHASE_PREFIX_ROOT|CHASE_TRAIL_SLASH, &path);
- if (r < 0 && r != -ENOENT)
- log_debug_errno(r, "Failed to chase os-release in image: %m");
- else if (r >= 0) {
- r = load_env_file_pairs(NULL, path, NULL, &os_release);
- if (r < 0)
- log_debug_errno(r, "Failed to parse os-release data of %s: %m", i->name);
- }
+ r = load_os_release_pairs(i->path, &os_release);
+ if (r < 0)
+ log_debug_errno(r, "Failed to read os-release in image, ignoring: %m");
free_and_replace(i->hostname, hostname);
i->machine_id = machine_id;
@@ -1059,6 +1228,35 @@ bool image_name_is_valid(const char *s) {
return true;
}
+bool image_in_search_path(ImageClass class, const char *image) {
+ const char *path;
+
+ assert(image);
+
+ NULSTR_FOREACH(path, image_search_path[class]) {
+ const char *p;
+ size_t k;
+
+ p = path_startswith(image, path);
+ if (!p)
+ continue;
+
+ /* Make sure there's a filename following */
+ k = strcspn(p, "/");
+ if (k == 0)
+ continue;
+
+ p += k;
+
+ /* Accept trailing slashes */
+ if (p[strspn(p, "/")] == 0)
+ return true;
+
+ }
+
+ return false;
+}
+
static const char* const image_type_table[_IMAGE_TYPE_MAX] = {
[IMAGE_DIRECTORY] = "directory",
[IMAGE_SUBVOLUME] = "subvolume",
diff --git a/src/shared/machine-image.h b/src/shared/machine-image.h
index 51cfb412c1..60868fac94 100644
--- a/src/shared/machine-image.h
+++ b/src/shared/machine-image.h
@@ -17,6 +17,13 @@
#include "string-util.h"
#include "time-util.h"
+typedef enum ImageClass {
+ IMAGE_MACHINE,
+ IMAGE_PORTABLE,
+ _IMAGE_CLASS_MAX,
+ _IMAGE_CLASS_INVALID = -1
+} ImageClass;
+
typedef enum ImageType {
IMAGE_DIRECTORY,
IMAGE_SUBVOLUME,
@@ -27,6 +34,8 @@ typedef enum ImageType {
} ImageType;
typedef struct Image {
+ unsigned n_ref;
+
ImageType type;
char *name;
char *path;
@@ -45,12 +54,15 @@ typedef struct Image {
char **machine_info;
char **os_release;
- bool metadata_valid;
+ bool metadata_valid:1;
+ bool discoverable:1; /* true if we know for sure that image_find() would find the image given just the short name */
void *userdata;
} Image;
Image *image_unref(Image *i);
+Image *image_ref(Image *i);
+
static inline Hashmap* image_hashmap_free(Hashmap *map) {
return hashmap_free_with_destructor(map, image_unref);
}
@@ -58,8 +70,10 @@ static inline Hashmap* image_hashmap_free(Hashmap *map) {
DEFINE_TRIVIAL_CLEANUP_FUNC(Image*, image_unref);
DEFINE_TRIVIAL_CLEANUP_FUNC(Hashmap*, image_hashmap_free);
-int image_find(const char *name, Image **ret);
-int image_discover(Hashmap *map);
+int image_find(ImageClass class, const char *name, Image **ret);
+int image_from_path(const char *path, Image **ret);
+int image_find_harder(ImageClass class, const char *name_or_path, Image **ret);
+int image_discover(ImageClass class, Hashmap *map);
int image_remove(Image *i);
int image_rename(Image *i, const char *new_name);
@@ -78,6 +92,8 @@ int image_set_limit(Image *i, uint64_t referenced_max);
int image_read_metadata(Image *i);
+bool image_in_search_path(ImageClass class, const char *image);
+
static inline bool IMAGE_IS_HIDDEN(const struct Image *i) {
assert(i);
diff --git a/src/shared/path-lookup.c b/src/shared/path-lookup.c
index 9919b2f2c2..e71af2f7cb 100644
--- a/src/shared/path-lookup.c
+++ b/src/shared/path-lookup.c
@@ -263,10 +263,8 @@ static int acquire_generator_dirs(
if (tempdir)
prefix = tempdir;
-
else if (scope == UNIT_FILE_SYSTEM)
prefix = "/run/systemd";
-
else if (scope == UNIT_FILE_USER) {
const char *e;
@@ -484,6 +482,10 @@ int lookup_paths_init(
assert(scope >= 0);
assert(scope < _UNIT_FILE_SCOPE_MAX);
+#if HAVE_SPLIT_USR
+ flags |= LOOKUP_PATHS_SPLIT_USR;
+#endif
+
if (!empty_or_root(root_dir)) {
if (scope == UNIT_FILE_USER)
return -EINVAL;
@@ -582,9 +584,7 @@ int lookup_paths_init(
"/usr/local/lib/systemd/system",
SYSTEM_DATA_UNIT_PATH,
"/usr/lib/systemd/system",
-#if HAVE_SPLIT_USR
- "/lib/systemd/system",
-#endif
+ STRV_IFNOTNULL(flags & LOOKUP_PATHS_SPLIT_USR ? "/lib/systemd/system" : NULL),
STRV_IFNOTNULL(generator_late),
NULL);
break;
diff --git a/src/shared/path-lookup.h b/src/shared/path-lookup.h
index f5bba63f5b..357816fd08 100644
--- a/src/shared/path-lookup.h
+++ b/src/shared/path-lookup.h
@@ -15,8 +15,9 @@ typedef struct LookupPaths LookupPaths;
#include "macro.h"
typedef enum LookupPathsFlags {
- LOOKUP_PATHS_EXCLUDE_GENERATED = 1 << 0,
- LOOKUP_PATHS_TEMPORARY_GENERATED = 1 << 1,
+ LOOKUP_PATHS_EXCLUDE_GENERATED = 1U << 0,
+ LOOKUP_PATHS_TEMPORARY_GENERATED = 1U << 1,
+ LOOKUP_PATHS_SPLIT_USR = 1U << 2,
} LookupPathsFlags;
struct LookupPaths {
diff --git a/src/shared/udev-util.c b/src/shared/udev-util.c
index cd2e38533f..29484226c6 100644
--- a/src/shared/udev-util.c
+++ b/src/shared/udev-util.c
@@ -18,7 +18,7 @@ int udev_parse_config(void) {
size_t n;
int r;
- r = parse_env_file("/etc/udev/udev.conf", NEWLINE, "udev_log", &val, NULL);
+ r = parse_env_file(NULL, "/etc/udev/udev.conf", NEWLINE, "udev_log", &val, NULL);
if (r == -ENOENT || !val)
return 0;
if (r < 0)
diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c
index fcc6c08492..60861dd791 100644
--- a/src/systemctl/systemctl.c
+++ b/src/systemctl/systemctl.c
@@ -5664,7 +5664,7 @@ static int switch_root(int argc, char *argv[], void *userdata) {
if (argc >= 3)
init = argv[2];
else {
- r = parse_env_file("/proc/cmdline", WHITESPACE,
+ r = parse_env_file(NULL, "/proc/cmdline", WHITESPACE,
"init", &cmdline_init,
NULL);
if (r < 0)
diff --git a/src/test/meson.build b/src/test/meson.build
index 4a9982240b..619f0cd823 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -242,6 +242,10 @@ tests += [
[],
[]],
+ [['src/test/test-os-util.c'],
+ [],
+ []],
+
[['src/test/test-escape.c'],
[],
[]],
diff --git a/src/test/test-conf-files.c b/src/test/test-conf-files.c
index 4bc92faca0..7ace4828ac 100644
--- a/src/test/test-conf-files.c
+++ b/src/test/test-conf-files.c
@@ -10,8 +10,10 @@
#include "alloc-util.h"
#include "conf-files.h"
+#include "fileio.h"
#include "fs-util.h"
#include "macro.h"
+#include "mkdir.h"
#include "parse-util.h"
#include "rm-rf.h"
#include "string-util.h"
@@ -22,12 +24,16 @@
static void setup_test_dir(char *tmp_dir, const char *files, ...) {
va_list ap;
- assert_se(mkdtemp(tmp_dir) != NULL);
+ assert_se(mkdtemp(tmp_dir));
va_start(ap, files);
- while (files != NULL) {
- _cleanup_free_ char *path = strappend(tmp_dir, files);
- assert_se(touch_file(path, true, USEC_INFINITY, UID_INVALID, GID_INVALID, MODE_INVALID) == 0);
+ while (files) {
+ _cleanup_free_ char *path;
+
+ assert_se(path = strappend(tmp_dir, files));
+ (void) mkdir_parents(path, 0755);
+ assert_se(write_string_file(path, "foobar", WRITE_STRING_FILE_CREATE) >= 0);
+
files = va_arg(ap, const char *);
}
va_end(ap);
@@ -36,7 +42,7 @@ static void setup_test_dir(char *tmp_dir, const char *files, ...) {
static void test_conf_files_list(bool use_root) {
char tmp_dir[] = "/tmp/test-conf-files-XXXXXX";
_cleanup_strv_free_ char **found_files = NULL, **found_files2 = NULL;
- const char *root_dir, *search_1, *search_2, *expect_a, *expect_b, *expect_c;
+ const char *root_dir, *search_1, *search_2, *expect_a, *expect_b, *expect_c, *mask;
log_debug("/* %s */", __func__);
@@ -45,8 +51,12 @@ static void test_conf_files_list(bool use_root) {
"/dir2/a.conf",
"/dir2/b.conf",
"/dir2/c.foo",
+ "/dir2/d.conf",
NULL);
+ mask = strjoina(tmp_dir, "/dir1/d.conf");
+ assert_se(symlink("/dev/null", mask) >= 0);
+
if (use_root) {
root_dir = tmp_dir;
search_1 = "/dir1";
@@ -63,23 +73,23 @@ static void test_conf_files_list(bool use_root) {
log_debug("/* Check when filtered by suffix */");
- assert_se(conf_files_list(&found_files, ".conf", root_dir, 0, search_1, search_2, NULL) == 0);
+ assert_se(conf_files_list(&found_files, ".conf", root_dir, CONF_FILES_FILTER_MASKED, search_1, search_2, NULL) == 0);
strv_print(found_files);
assert_se(found_files);
assert_se(streq_ptr(found_files[0], expect_a));
assert_se(streq_ptr(found_files[1], expect_b));
- assert_se(found_files[2] == NULL);
+ assert_se(!found_files[2]);
log_debug("/* Check when unfiltered */");
- assert_se(conf_files_list(&found_files2, NULL, root_dir, 0, search_1, search_2, NULL) == 0);
+ assert_se(conf_files_list(&found_files2, NULL, root_dir, CONF_FILES_FILTER_MASKED, search_1, search_2, NULL) == 0);
strv_print(found_files2);
assert_se(found_files2);
assert_se(streq_ptr(found_files2[0], expect_a));
assert_se(streq_ptr(found_files2[1], expect_b));
assert_se(streq_ptr(found_files2[2], expect_c));
- assert_se(found_files2[3] == NULL);
+ assert_se(!found_files2[3]);
assert_se(rm_rf(tmp_dir, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
}
diff --git a/src/test/test-fd-util.c b/src/test/test-fd-util.c
index 432fa5d403..fc3c463385 100644
--- a/src/test/test-fd-util.c
+++ b/src/test/test-fd-util.c
@@ -228,6 +228,93 @@ static void test_rearrange_stdio(void) {
}
}
+static void assert_equal_fd(int fd1, int fd2) {
+
+ for (;;) {
+ uint8_t a[4096], b[4096];
+ ssize_t x, y;
+
+ x = read(fd1, a, sizeof(a));
+ assert(x >= 0);
+
+ y = read(fd2, b, sizeof(b));
+ assert(y >= 0);
+
+ assert(x == y);
+
+ if (x == 0)
+ break;
+
+ assert(memcmp(a, b, x) == 0);
+ }
+}
+
+static void test_fd_duplicate_data_fd(void) {
+ _cleanup_close_ int fd1 = -1, fd2 = -1;
+ _cleanup_(close_pairp) int sfd[2] = { -1, -1 };
+ _cleanup_(sigkill_waitp) pid_t pid = -1;
+ uint64_t i, j;
+ int r;
+
+ fd1 = open("/etc/fstab", O_RDONLY|O_CLOEXEC);
+ if (fd1 >= 0) {
+
+ fd2 = fd_duplicate_data_fd(fd1);
+ assert_se(fd2 >= 0);
+
+ assert_se(lseek(fd1, 0, SEEK_SET) == 0);
+ assert_equal_fd(fd1, fd2);
+ }
+
+ fd1 = safe_close(fd1);
+ fd2 = safe_close(fd2);
+
+ fd1 = acquire_data_fd("hallo", 6, 0);
+ assert_se(fd1 >= 0);
+
+ fd2 = fd_duplicate_data_fd(fd1);
+ assert_se(fd2 >= 0);
+
+ safe_close(fd1);
+ fd1 = acquire_data_fd("hallo", 6, 0);
+ assert_se(fd1 >= 0);
+
+ assert_equal_fd(fd1, fd2);
+
+ fd1 = safe_close(fd1);
+ fd2 = safe_close(fd2);
+
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, sfd) >= 0);
+
+ r = safe_fork("(sd-pipe)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* child */
+
+ sfd[0] = safe_close(sfd[0]);
+
+ for (i = 0; i < 1536*1024 / sizeof(uint64_t); i++)
+ assert_se(write(sfd[1], &i, sizeof(i)) == sizeof(i));
+
+ sfd[1] = safe_close(sfd[1]);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ sfd[1] = safe_close(sfd[1]);
+
+ fd2 = fd_duplicate_data_fd(sfd[0]);
+ assert_se(fd2 >= 0);
+
+ for (i = 0; i < 1536*1024 / sizeof(uint64_t); i++) {
+ assert_se(read(fd2, &j, sizeof(j)) == sizeof(j));
+ assert_se(i == j);
+ }
+
+ assert_se(read(fd2, &j, sizeof(j)) == 0);
+}
+
int main(int argc, char *argv[]) {
test_close_many();
test_close_nointr();
@@ -236,6 +323,7 @@ int main(int argc, char *argv[]) {
test_acquire_data_fd();
test_fd_move_above_stdio();
test_rearrange_stdio();
+ test_fd_duplicate_data_fd();
return 0;
}
diff --git a/src/test/test-fileio.c b/src/test/test-fileio.c
index bbddbf3bcd..3476c41291 100644
--- a/src/test/test-fileio.c
+++ b/src/test/test-fileio.c
@@ -95,7 +95,7 @@ static void test_parse_env_file(void) {
}
r = parse_env_file(
- t, NULL,
+ NULL, t, NULL,
"one", &one,
"two", &two,
"three", &three,
diff --git a/src/test/test-os-util.c b/src/test/test-os-util.c
new file mode 100644
index 0000000000..8d8b52d7f6
--- /dev/null
+++ b/src/test/test-os-util.c
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include
+
+#include "log.h"
+#include "os-util.h"
+
+static void test_path_is_os_tree(void) {
+ assert_se(path_is_os_tree("/") > 0);
+ assert_se(path_is_os_tree("/etc") == 0);
+ assert_se(path_is_os_tree("/idontexist") == -ENOENT);
+}
+
+int main(int argc, char *argv[]) {
+ log_set_max_level(LOG_DEBUG);
+ log_parse_environment();
+ log_open();
+
+ test_path_is_os_tree();
+
+ return 0;
+}
diff --git a/src/test/test-stat-util.c b/src/test/test-stat-util.c
index 6c9c132612..1697b2d777 100644
--- a/src/test/test-stat-util.c
+++ b/src/test/test-stat-util.c
@@ -52,12 +52,6 @@ static void test_is_symlink(void) {
unlink(name_link);
}
-static void test_path_is_os_tree(void) {
- assert_se(path_is_os_tree("/") > 0);
- assert_se(path_is_os_tree("/etc") == 0);
- assert_se(path_is_os_tree("/idontexist") == -ENOENT);
-}
-
static void test_path_is_fs_type(void) {
/* run might not be a mount point in build chroots */
if (path_is_mount_point("/run", NULL, AT_SYMLINK_FOLLOW) > 0) {
@@ -81,7 +75,6 @@ static void test_path_is_temporary_fs(void) {
int main(int argc, char *argv[]) {
test_files_same();
test_is_symlink();
- test_path_is_os_tree();
test_path_is_fs_type();
test_path_is_temporary_fs();
diff --git a/src/vconsole/vconsole-setup.c b/src/vconsole/vconsole-setup.c
index 9cda56e916..6bf773a1ed 100644
--- a/src/vconsole/vconsole-setup.c
+++ b/src/vconsole/vconsole-setup.c
@@ -420,7 +420,7 @@ int main(int argc, char **argv) {
utf8 = is_locale_utf8();
- r = parse_env_file("/etc/vconsole.conf", NEWLINE,
+ r = parse_env_file(NULL, "/etc/vconsole.conf", NEWLINE,
"KEYMAP", &vc_keymap,
"KEYMAP_TOGGLE", &vc_keymap_toggle,
"FONT", &vc_font,
@@ -432,7 +432,7 @@ int main(int argc, char **argv) {
/* Let the kernel command line override /etc/vconsole.conf */
if (detect_container() <= 0) {
- r = parse_env_file("/proc/cmdline", WHITESPACE,
+ r = parse_env_file(NULL, "/proc/cmdline", WHITESPACE,
"vconsole.keymap", &vc_keymap,
"vconsole.keymap_toggle", &vc_keymap_toggle,
"vconsole.font", &vc_font,
diff --git a/tmpfiles.d/meson.build b/tmpfiles.d/meson.build
index 20b1e0d0bc..93072a242d 100644
--- a/tmpfiles.d/meson.build
+++ b/tmpfiles.d/meson.build
@@ -8,6 +8,7 @@ tmpfiles = [['home.conf', ''],
['journal-nocow.conf', ''],
['systemd-nologin.conf', ''],
['systemd-nspawn.conf', 'ENABLE_MACHINED'],
+ ['portables.conf', 'ENABLE_PORTABLED'],
['tmp.conf', ''],
['x11.conf', ''],
['legacy.conf', 'HAVE_SYSV_COMPAT'],
diff --git a/tmpfiles.d/portables.conf b/tmpfiles.d/portables.conf
new file mode 100644
index 0000000000..b8ddf7f4b7
--- /dev/null
+++ b/tmpfiles.d/portables.conf
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: LGPL-2.1+
+# See tmpfiles.d(5) for details
+
+Q /var/lib/portables 0700
diff --git a/units/meson.build b/units/meson.build
index 799fc5d567..55cfc15825 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -177,6 +177,8 @@ in_units = [
['systemd-networkd-wait-online.service', 'ENABLE_NETWORKD',
join_paths(pkgsysconfdir, 'system/network-online.target.wants/')],
['systemd-nspawn@.service', ''],
+ ['systemd-portabled.service', 'ENABLE_PORTABLED',
+ 'dbus-org.freedesktop.portable1.service'],
['systemd-poweroff.service', ''],
['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'],
['systemd-random-seed.service', 'ENABLE_RANDOMSEED',
diff --git a/units/systemd-portabled.service.in b/units/systemd-portabled.service.in
new file mode 100644
index 0000000000..a868f61dba
--- /dev/null
+++ b/units/systemd-portabled.service.in
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: LGPL-2.1+
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Portable Service Manager
+Documentation=man:systemd-portabled.service(8)
+RequiresMountsFor=/var/lib/portables
+
+[Service]
+ExecStart=@rootlibexecdir@/systemd-portabled
+BusName=org.freedesktop.portable1
+WatchdogSec=3min
+CapabilityBoundingSet=CAP_KILL CAP_SYS_PTRACE CAP_SYS_ADMIN CAP_SETGID CAP_SYS_CHROOT CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE CAP_CHOWN CAP_FOWNER CAP_FSETID CAP_MKNOD
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+SystemCallFilter=~@clock @cpu-emulation @debug @keyring @module @obsolete @raw-io @reboot @swap
+SystemCallArchitectures=native
+LockPersonality=yes
+IPAddressDeny=any