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.service8. + + Portable service images contain an OS file system tree along with + systemd1 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: + + + + + + + Suppresses additional informational output while running. + + + + PROFILE + PROFILE + + When attaching an image, select the profile to use. By default the default + profile is used. For details about profiles, see below. + + + + + + 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. + + + + + + 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. + + + + + + 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. + + + + + + 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-release5 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 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.unit5 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 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-release5 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 (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 + switch this will show a short state identifier for the image. Specifically: + + + Image attachment states + + + + + + State + Description + + + + + + The image is currently not attached. + + + + The image is currently attached, i.e. its unit files have been made available to the host system. + + + + Like , but the the unit files have been made available transiently only, i.e. the attach command has been invoked with the option. + + + + The image is currently attached, and at least one unit file associated with it has been enabled. + + + + Like , but the the unit files have been made available transiently only, i.e. the attach command has been invoked with the option. + + + + The image is currently attached, and at least one unit file associated with it is running. + + + + 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 (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 + + systemd1, + systemd-portabled.service8 + + + +
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 + portablectl1 command. + + + See the Portable + Services Documentation for details about the concepts this service implements. + + + + See Also + + systemd1, + portablectl1 + + + + 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