]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
portable: Enable unpriv operation 40091/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 2 Dec 2025 10:17:13 +0000 (11:17 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 21 Jan 2026 14:09:46 +0000 (15:09 +0100)
This does not yet support directory images properly
as systemd itself does not support unpriv directory
images properly yet.

The user profiles are a copy of the system profiles but without
DynamicUser=yes (can't be used by user managers) and without
ProtectHome=yes (this masks /home which breaks StateDirectory= which
is lcoated inside /home)

34 files changed:
docs/PORTABLE_SERVICES.md
man/os-release.xml
meson.build
mkosi/mkosi.images/minimal-0/mkosi.extra/usr/lib/systemd/user/minimal-app0.service [new file with mode: 0644]
mkosi/mkosi.images/minimal-0/mkosi.postinst
mkosi/mkosi.images/minimal-1/mkosi.extra/usr/lib/systemd/user/minimal-app0.service [new file with mode: 0644]
mkosi/mkosi.images/minimal-1/mkosi.postinst
src/analyze/analyze-security.c
src/libsystemd/sd-path/path-lookup.c
src/portable/meson.build
src/portable/org.freedesktop.portable1.service-for-session [new file with mode: 0644]
src/portable/portable.c
src/portable/portable.h
src/portable/portablectl.c
src/portable/portabled-bus.c
src/portable/portabled-image-bus.c
src/portable/portabled.c
src/portable/portabled.h
src/portable/profile/system/default/service.conf [moved from src/portable/profile/default/service.conf with 100% similarity]
src/portable/profile/system/nonetwork/service.conf [moved from src/portable/profile/nonetwork/service.conf with 100% similarity]
src/portable/profile/system/strict/service.conf [moved from src/portable/profile/strict/service.conf with 100% similarity]
src/portable/profile/system/trusted/service.conf [moved from src/portable/profile/trusted/service.conf with 100% similarity]
src/portable/profile/user/default/service.conf [new file with mode: 0644]
src/portable/profile/user/nonetwork/service.conf [new file with mode: 0644]
src/portable/profile/user/strict/service.conf [new file with mode: 0644]
src/portable/profile/user/trusted/service.conf [new file with mode: 0644]
src/shared/discover-image.c
src/shared/dissect-image.c
src/shared/portable-util.c
src/shared/portable-util.h
test/units/TEST-29-PORTABLE.user.sh [new file with mode: 0755]
test/units/util.sh
units/user/meson.build
units/user/systemd-portabled.service.in [new file with mode: 0644]

index ec198b43041726a67e2b3c488f1816260b0c99f0..832c9566f9a64ae17327535133b5b440dca94113 100644 (file)
@@ -17,7 +17,8 @@ two specific features of container management:
 2. Stricter default security policies, i.e. sand-boxing of applications.
 
 The primary tool for interacting with Portable Services is `portablectl`,
-and they are managed by the `systemd-portabled` service.
+and they are managed by the `systemd-portabled` service. `systemd-portabled` can
+run as a system or a user service.
 
 Portable services don't bring anything inherently new to the table.
 All they do is put together known concepts to cover a specific set of use-cases in a
@@ -250,6 +251,14 @@ validated against the (authenticated) image contents.
 If the field is not specified the image will work fine, but is not necessarily recognizable as
 portable service image, and any set of units included in the image may be attached, there are no restrictions enforced.
 
+The [os-release(5)](https://www.freedesktop.org/software/systemd/man/os-release.html) may
+optionally be extended with a `PORTABLE_SCOPE=` field listing the scope in which the portable
+service may be used. This field may be set to either `system`, in which case the portable service
+can only be attached to the system instance of `systemd-portabled`, `user` in which case the portable
+can only be attached to a user instance of `systemd-portabled`, or `any` in which case it can be
+attached to either the system instance or user instances of `systemd-portabled`. If not specified, the
+`system` scope is implied.
+
 ## Extension Images
 
 Portable services can be delivered as one or multiple images that extend the base
index 0c9b3de493b9208330b514f3643130932ebb2bc4..1ab7e652b8923dc0dc16cd67504e7c12c2e0852e 100644 (file)
 
           <xi:include href="version-info.xml" xpointer="v250"/></listitem>
         </varlistentry>
+
+        <varlistentry>
+          <term><varname>PORTABLE_SCOPE=</varname></term>
+          <listitem><para>Specifies the scope of the portable service. Takes one of <literal>system</literal>,
+          <literal>user</literal>, or <literal>any</literal>. When set to <literal>system</literal>, the
+          portable service can only be attached to the system instance of <command>systemd-portabled</command>.
+          When set to <literal>user</literal>, the portable service can only be attached to the user instance
+          of <command>systemd-portabled</command>. When set to <literal>any</literal>, the portable service
+          can be attached to both the system and user instances of <command>systemd-portabled</command>.
+          If not set, <literal>PORTABLE_SCOPE=system</literal> is implied.</para>
+
+          <xi:include href="version-info.xml" xpointer="v259"/></listitem>
+        </varlistentry>
       </variablelist>
     </refsect2>
 
index da7b01f0a790e6ca5be662b44c9e4f2df19bd2f5..6a4deab9703424f5752c3c79b5385c7f5d48c2dd 100644 (file)
@@ -144,41 +144,42 @@ modprobedir = prefixdir / 'lib/modprobe.d'
 pkgdatadir = datadir / 'systemd'
 environmentdir = prefixdir / 'lib/environment.d'
 pkgsysconfdir = sysconfdir / 'systemd'
-userunitdir = prefixdir / 'lib/systemd/user'
-userpresetdir = prefixdir / 'lib/systemd/user-preset'
+userunitdir = libexecdir / 'user'
+userpresetdir = libexecdir / 'user-preset'
 tmpfilesdir = prefixdir / 'lib/tmpfiles.d'
 usertmpfilesdir = prefixdir / 'share/user-tmpfiles.d'
 sysusersdir = prefixdir / 'lib/sysusers.d'
 sysctldir = prefixdir / 'lib/sysctl.d'
 binfmtdir = prefixdir / 'lib/binfmt.d'
 modulesloaddir = prefixdir / 'lib/modules-load.d'
-networkdir = prefixdir / 'lib/systemd/network'
+networkdir = libexecdir / 'network'
 systemgeneratordir = libexecdir / 'system-generators'
-usergeneratordir = prefixdir / 'lib/systemd/user-generators'
-systemenvgeneratordir = prefixdir / 'lib/systemd/system-environment-generators'
-userenvgeneratordir = prefixdir / 'lib/systemd/user-environment-generators'
+usergeneratordir = libexecdir / 'user-generators'
+systemenvgeneratordir = libexecdir / 'system-environment-generators'
+userenvgeneratordir = libexecdir / 'user-environment-generators'
 systemshutdowndir = libexecdir / 'system-shutdown'
 systemsleepdir = libexecdir / 'system-sleep'
-systemunitdir = prefixdir / 'lib/systemd/system'
-systempresetdir = prefixdir / 'lib/systemd/system-preset'
-initrdpresetdir = prefixdir / 'lib/systemd/initrd-preset'
+systemunitdir = libexecdir / 'system'
+systempresetdir = libexecdir / 'system-preset'
+initrdpresetdir = libexecdir / 'initrd-preset'
 udevlibexecdir = prefixdir / 'lib/udev'
 udevrulesdir = udevlibexecdir / 'rules.d'
 udevhwdbdir = udevlibexecdir / 'hwdb.d'
-catalogdir = prefixdir / 'lib/systemd/catalog'
+catalogdir = libexecdir / 'catalog'
 kerneldir = prefixdir / 'lib/kernel'
 kernelinstalldir = kerneldir / 'install.d'
 factorydir = datadir / 'factory'
-bootlibdir = prefixdir / 'lib/systemd/boot/efi'
-testsdir = prefixdir / 'lib/systemd/tests'
+bootlibdir = libexecdir / 'boot/efi'
+testsdir = libexecdir / 'tests'
 unittestsdir = testsdir / 'unit-tests'
 testdata_dir = testsdir / 'testdata'
 systemdstatedir = localstatedir / 'lib/systemd'
 catalogstatedir = systemdstatedir / 'catalog'
 randomseeddir = localstatedir / 'lib/systemd'
-profiledir = libexecdir / 'portable' / 'profile'
+systemprofiledir = libexecdir / 'portable' / 'profile'
+userprofiledir = libexecdir / 'user' / 'portable' / 'profile'
 repartdefinitionsdir = libexecdir / 'repart/definitions'
-ntpservicelistdir = prefixdir / 'lib/systemd/ntp-units.d'
+ntpservicelistdir = libexecdir / 'ntp-units.d'
 credstoredir = prefixdir / 'lib/credstore'
 pcrlockdir = prefixdir / 'lib/pcrlock.d'
 mimepackagesdir = prefixdir / 'share/mime/packages'
diff --git a/mkosi/mkosi.images/minimal-0/mkosi.extra/usr/lib/systemd/user/minimal-app0.service b/mkosi/mkosi.images/minimal-0/mkosi.extra/usr/lib/systemd/user/minimal-app0.service
new file mode 100644 (file)
index 0000000..0532112
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+[Service]
+ExecStartPre=cat /usr/lib/os-release
+ExecStart=sleep 120
index 8b009f5012f588f0cd34f73d895e9842d76f0287..5de521b58edd74e2fc6d6bd67a2fe72a745ee085 100755 (executable)
@@ -7,8 +7,10 @@ mkdir -p "$BUILDROOT/var/lib/app1"
 cat >>"$BUILDROOT/usr/lib/os-release" <<EOF
 MARKER=1
 PORTABLE_PREFIXES=app0 minimal minimal-app0
+PORTABLE_SCOPE=any
 EOF
 if [ ! -L "$BUILDROOT/etc/os-release" ]; then
     cp "$BUILDROOT/usr/lib/os-release" "$BUILDROOT/etc/os-release"
 fi
 cp "$BUILDROOT/usr/lib/systemd/system/minimal-app0.service" "$BUILDROOT/usr/lib/systemd/system/minimal-app0-foo.service"
+cp "$BUILDROOT/usr/lib/systemd/user/minimal-app0.service" "$BUILDROOT/usr/lib/systemd/user/minimal-app0-foo.service"
diff --git a/mkosi/mkosi.images/minimal-1/mkosi.extra/usr/lib/systemd/user/minimal-app0.service b/mkosi/mkosi.images/minimal-1/mkosi.extra/usr/lib/systemd/user/minimal-app0.service
new file mode 100644 (file)
index 0000000..0532112
--- /dev/null
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+[Service]
+ExecStartPre=cat /usr/lib/os-release
+ExecStart=sleep 120
index a971748ad9657d051ccd6efc5cf1d6426b542485..c4835dd73e3d2c5c30d7ab98b7b033af223ad133 100755 (executable)
@@ -7,8 +7,10 @@ mkdir -p "$BUILDROOT/var/lib/app1"
 cat >>"$BUILDROOT/usr/lib/os-release" <<EOF
 MARKER=2
 PORTABLE_PREFIXES=app0 minimal minimal-app0
+PORTABLE_SCOPE=any
 EOF
 if [ ! -L "$BUILDROOT/etc/os-release" ]; then
     cp "$BUILDROOT/usr/lib/os-release" "$BUILDROOT/etc/os-release"
 fi
 cp "$BUILDROOT/usr/lib/systemd/system/minimal-app0.service" "$BUILDROOT/usr/lib/systemd/system/minimal-app0-bar.service"
+cp "$BUILDROOT/usr/lib/systemd/user/minimal-app0.service" "$BUILDROOT/usr/lib/systemd/user/minimal-app0-bar.service"
index 142262cb554e7298db510183606ce59bac51215d..3e086a1775d6b1afb4cadb60eda3678f007e5375 100644 (file)
@@ -2752,7 +2752,7 @@ static int offline_security_checks(
                         (void) mkdir_parents(dropin, 0755);
 
                         if (!is_path(profile)) {
-                                r = find_portable_profile(profile, unit_name, &profile_path);
+                                r = find_portable_profile(scope, profile, unit_name, &profile_path);
                                 if (r < 0)
                                         return log_error_errno(r, "Failed to find portable profile %s: %m", profile);
                                 profile = profile_path;
index 53439a99f3cfa2dae801ffa2cd475163c717e343..32c14fb14a7d5d8d509261bb785ba6b7c8e2ccfe 100644 (file)
@@ -231,7 +231,7 @@ static int acquire_lookup_dirs(
                 },
                 [LOOKUP_DIR_ATTACHED] = {
                         [RUNTIME_SCOPE_SYSTEM] = { "/etc/systemd/system.attached", "/run/systemd/system.attached" },
-                        /* Portable services are not available to regular users for now. */
+                        [RUNTIME_SCOPE_USER]   = { "systemd/user.attached",        "systemd/user.attached"        },
                 },
         };
 
@@ -349,7 +349,9 @@ static int get_paths_from_environ(const char *var, char ***ret) {
 
 static char** user_unit_search_dirs(
                 const char *persistent_config,
+                const char *persistent_attached,
                 const char *runtime_config,
+                const char *runtime_attached,
                 const char *global_persistent_config,
                 const char *global_runtime_config,
                 const char *generator,
@@ -364,6 +366,7 @@ static char** user_unit_search_dirs(
         /* The returned strv might contain duplicates, and we expect caller to filter them. */
 
         assert(persistent_config);
+        assert(persistent_attached);
         assert(global_persistent_config);
         assert(global_runtime_config);
         assert(persistent_control);
@@ -375,7 +378,8 @@ static char** user_unit_search_dirs(
                          STRV_IFNOTNULL(runtime_control),
                          STRV_IFNOTNULL(transient),
                          STRV_IFNOTNULL(generator_early),
-                         persistent_config);
+                         persistent_config,
+                         persistent_attached);
         if (!paths)
                 return NULL;
 
@@ -392,6 +396,7 @@ static char** user_unit_search_dirs(
         /* strv_extend_many() can deal with NULL-s in arguments */
         if (strv_extend_many(&paths,
                              runtime_config,
+                             runtime_attached,
                              global_runtime_config,
                              generator) < 0)
                 return NULL;
@@ -551,7 +556,8 @@ int lookup_paths_init(
                         break;
 
                 case RUNTIME_SCOPE_USER:
-                        add = user_unit_search_dirs(persistent_config, runtime_config,
+                        add = user_unit_search_dirs(persistent_config, persistent_attached,
+                                                    runtime_config, runtime_attached,
                                                     global_persistent_config, global_runtime_config,
                                                     generator, generator_early, generator_late,
                                                     transient,
index d9c266ff9e1a9596f8e85ef984901c9c08e84bb3..3029ad417778402d6d3c0c695725f23a5abf98b5 100644 (file)
@@ -48,10 +48,18 @@ install_data('org.freedesktop.portable1.conf',
              install_dir : dbuspolicydir)
 install_data('org.freedesktop.portable1.service',
              install_dir : dbussystemservicedir)
+install_data('org.freedesktop.portable1.service-for-session',
+             install_dir : dbussessionservicedir,
+             rename : 'org.freedesktop.portable1.service')
 install_data('org.freedesktop.portable1.policy',
              install_dir : polkitpolicydir)
 
-install_data('profile/default/service.conf', install_dir : profiledir / 'default')
-install_data('profile/nonetwork/service.conf', install_dir : profiledir / 'nonetwork')
-install_data('profile/strict/service.conf', install_dir : profiledir / 'strict')
-install_data('profile/trusted/service.conf', install_dir : profiledir / 'trusted')
+install_data('profile/system/default/service.conf', install_dir : systemprofiledir / 'default')
+install_data('profile/system/nonetwork/service.conf', install_dir : systemprofiledir / 'nonetwork')
+install_data('profile/system/strict/service.conf', install_dir : systemprofiledir / 'strict')
+install_data('profile/system/trusted/service.conf', install_dir : systemprofiledir / 'trusted')
+
+install_data('profile/user/default/service.conf', install_dir : userprofiledir / 'default')
+install_data('profile/user/nonetwork/service.conf', install_dir : userprofiledir / 'nonetwork')
+install_data('profile/user/strict/service.conf', install_dir : userprofiledir / 'strict')
+install_data('profile/user/trusted/service.conf', install_dir : userprofiledir / 'trusted')
diff --git a/src/portable/org.freedesktop.portable1.service-for-session b/src/portable/org.freedesktop.portable1.service-for-session
new file mode 100644 (file)
index 0000000..5e6ef19
--- /dev/null
@@ -0,0 +1,13 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.portable1
+Exec=/bin/false
+SystemdService=dbus-org.freedesktop.portable1.service
index a9aa5ae8f307da7cd9f587330b1c50d63b169574..b65fa7467eeeb68c016798552d485bd109be388c 100644 (file)
@@ -1,6 +1,7 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include <linux/loop.h>
+#include <sched.h>
 #include <unistd.h>
 
 #include "sd-bus.h"
@@ -35,6 +36,8 @@
 #include "log.h"
 #include "loop-util.h"
 #include "mkdir.h"
+#include "namespace-util.h"
+#include "nsresource.h"
 #include "os-util.h"
 #include "path-lookup.h"
 #include "pidref.h"
@@ -49,6 +52,7 @@
 #include "string-table.h"
 #include "strv.h"
 #include "tmpfile-util.h"
+#include "uid-classification.h"
 #include "unit-name.h"
 #include "vpick.h"
 
@@ -334,10 +338,14 @@ static int extract_now(
                 }
         }
 
-        /* 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 the
-         * image might have a legacy split-usr layout. */
-        r = lookup_paths_init(&paths, scope, LOOKUP_PATHS_SPLIT_USR, /* root_dir= */ NULL);
+        /* Then, send unit file data to the parent (or/and add it to the hashmap). For that we use our usual
+         * unit discovery logic. If we're running in a user session, we look for units in
+         * /usr/lib/systemd/user/ and corresponding directories. */
+        r = lookup_paths_init(
+                        &paths,
+                        scope == RUNTIME_SCOPE_USER ? RUNTIME_SCOPE_GLOBAL : RUNTIME_SCOPE_SYSTEM,
+                        LOOKUP_PATHS_SPLIT_USR,
+                        /* root_dir= */ NULL);
         if (r < 0)
                 return log_debug_errno(r, "Failed to acquire lookup paths: %m");
 
@@ -456,52 +464,106 @@ static int portable_extract_by_path(
         _cleanup_hashmap_free_ Hashmap *unit_files = NULL;
         _cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL;
         _cleanup_(image_policy_freep) ImagePolicy *pinned_image_policy = NULL;
-        _cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
         int r;
 
         assert(path);
 
-        r = loop_device_make_by_path(
-                        path,
-                        O_RDONLY,
-                        /* sector_size= */ UINT32_MAX,
-                        LO_FLAGS_PARTSCAN,
-                        LOCK_SH,
-                        &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. */
-
-                 _cleanup_free_ char *image_name = NULL;
+        _cleanup_close_ int rfd = open(path, O_PATH|O_CLOEXEC);
+        if (rfd < 0)
+                return log_error_errno(errno, "Failed to open '%s': %m", path);
+
+        struct stat st;
+        if (fstat(rfd, &st) < 0)
+                return log_debug_errno(errno, "Failed to stat '%s': %m", path);
+
+        if (S_ISDIR(st.st_mode)) {
+                _cleanup_free_ char *image_name = NULL;
                 r = path_extract_filename(path, &image_name);
                 if (r < 0)
                         return log_error_errno(r, "Failed to extract image name from path '%s': %m", path);
 
-                _cleanup_close_ int rfd = open(path, O_DIRECTORY|O_CLOEXEC);
-                if (rfd < 0)
-                        return log_error_errno(errno, "Failed to open '%s': %m", path);
+                if (scope == RUNTIME_SCOPE_USER && uid_is_foreign(st.st_uid)) {
+                        _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K);
+                        if (userns_fd < 0)
+                                return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m");
 
-                r = extract_now(scope,
-                                rfd,
-                                path,
-                                matches,
-                                image_name,
-                                path_is_extension,
-                                /* relax_extension_release_check= */ false,
-                                /* socket_fd= */ -EBADF,
-                                &os_release,
-                                &unit_files);
-                if (r < 0)
-                        return r;
+                        _cleanup_close_ int mfd = -EBADF;
+                        r = mountfsd_mount_directory_fd(rfd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &mfd);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to open '%s' via mountfsd: %m", path);
 
-        } else if (r < 0)
-                return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path);
-        else {
-                _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT;
+                        _cleanup_close_pair_ int seq[2] = EBADF_PAIR;
+                        if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0)
+                                return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m");
+
+                        _cleanup_close_pair_ int errno_pipe_fd[2] = EBADF_PAIR;
+                        if (pipe2(errno_pipe_fd, O_CLOEXEC|O_NONBLOCK) < 0)
+                                return log_debug_errno(errno, "Failed to create pipe: %m");
+
+                        _cleanup_(pidref_done_sigkill_wait) PidRef child = PIDREF_NULL;
+                        r = pidref_safe_fork("(sd-extract)",
+                                             FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_REOPEN_LOG,
+                                             &child);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                seq[0] = safe_close(seq[0]);
+                                errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]);
+
+                                if (setns(CLONE_NEWUSER, userns_fd) < 0) {
+                                        r = log_debug_errno(errno, "Failed to join userns: %m");
+                                        report_errno_and_exit(errno_pipe_fd[1], r);
+                                }
+
+                                r = extract_now(scope,
+                                                mfd,
+                                                path,
+                                                matches,
+                                                image_name,
+                                                path_is_extension,
+                                                /* relax_extension_release_check= */ false,
+                                                seq[1],
+                                                /* ret_os_release= */ NULL,
+                                                /* ret_unit_files= */ NULL);
+                                report_errno_and_exit(errno_pipe_fd[1], r);
+                        }
+
+                        seq[1] = safe_close(seq[1]);
+                        errno_pipe_fd[1] = safe_close(errno_pipe_fd[1]);
+
+                        r = receive_portable_metadata(seq[0], path, &os_release, &unit_files);
+                        if (r < 0)
+                                return r;
+
+                        r = pidref_wait_for_terminate_and_check("(sd-extract)", &child, 0);
+                        if (r < 0)
+                                return r;
+                        if (r != EXIT_SUCCESS) {
+                                if (read(errno_pipe_fd[0], &r, sizeof(r)) == sizeof(r))
+                                        return log_debug_errno(r, "Failed to extract portable metadata from '%s': %m", path);
+
+                                return log_debug_errno(SYNTHETIC_ERRNO(EPROTO), "Child failed.");
+                        }
+                } else {
+                        r = extract_now(scope,
+                                        rfd,
+                                        path,
+                                        matches,
+                                        image_name,
+                                        path_is_extension,
+                                        /* relax_extension_release_check= */ false,
+                                        /* socket_fd= */ -EBADF,
+                                        &os_release,
+                                        &unit_files);
+                        if (r < 0)
+                                return r;
+                }
+        } else {
                 _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
                 _cleanup_(rmdir_and_freep) char *tmpdir = NULL;
                 _cleanup_close_pair_ int seq[2] = EBADF_PAIR, errno_pipe_fd[2] = EBADF_PAIR;
                 _cleanup_(pidref_done_sigkill_wait) PidRef child = PIDREF_NULL;
+                _cleanup_close_ int userns_fd = -EBADF;
                 DissectImageFlags flags =
                         DISSECT_IMAGE_READ_ONLY |
                         DISSECT_IMAGE_GENERIC_ROOT |
@@ -518,6 +580,18 @@ static int portable_extract_by_path(
                 else
                         flags |= DISSECT_IMAGE_VALIDATE_OS;
 
+                _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT;
+                r = verity_settings_load(
+                                &verity,
+                                path,
+                                /* root_hash_path= */ NULL,
+                                /* root_hash_sig_path= */ NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read verity artifacts for %s: %m", path);
+
+                if (verity.data_path)
+                        flags |= DISSECT_IMAGE_NO_PARTITION_TABLE;
+
                 /* 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. */
 
@@ -529,38 +603,70 @@ static int portable_extract_by_path(
                 if (r < 0)
                         return log_debug_errno(r, "Failed to create temporary directory: %m");
 
-                r = dissect_loop_device(
-                                d,
-                                /* verity= */ NULL,
-                                /* mount_options= */ NULL,
-                                image_policy,
-                                /* image_filter= */ NULL,
-                                flags,
-                                &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 (scope == RUNTIME_SCOPE_USER) {
+                        userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K);
+                        if (userns_fd < 0)
+                                return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m");
 
-                r = verity_settings_load(&verity, path, NULL, NULL);
-                if (r < 0)
-                        return log_debug_errno(r, "Failed to load root hash: %m");
+                        r = mountfsd_mount_image_fd(
+                                        rfd,
+                                        userns_fd,
+                                        /* options= */ NULL,
+                                        image_policy,
+                                        &verity,
+                                        flags,
+                                        &m);
+                        if (r < 0)
+                                return r;
+                } else {
+                        _cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
 
-                r = dissected_image_load_verity_sig_partition(m, d->fd, &verity);
-                if (r < 0)
-                        return r;
+                        r = loop_device_make_by_path_at(
+                                        rfd,
+                                        /* path= */ NULL,
+                                        O_RDONLY,
+                                        /* sector_size= */ UINT32_MAX,
+                                        LO_FLAGS_PARTSCAN,
+                                        LOCK_SH,
+                                        &d);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path);
+
+                        r = dissect_loop_device(
+                                        d,
+                                        &verity,
+                                        /* mount_options= */ NULL,
+                                        image_policy,
+                                        /* image_filter= */ NULL,
+                                        flags,
+                                        &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;
 
-                r = dissected_image_guess_verity_roothash(m, &verity);
-                if (r < 0)
-                        return r;
+                        r = dissected_image_load_verity_sig_partition(m, d->fd, &verity);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to load verity sig partition for '%s': %m", path);
+
+                        r = dissected_image_guess_verity_roothash(m, &verity);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to guess verity roothash for '%s': %m", path);
+                }
+
+                if (!m->image_name) {
+                        r = dissected_image_name_from_path(path, &m->image_name);
+                        if (r < 0)
+                                return r;
+                }
 
                 if (ret_pinned_image_policy) {
                         pinned_image_policy = image_policy_new_from_dissected(m, &verity);
@@ -574,33 +680,44 @@ static int portable_extract_by_path(
                 if (pipe2(errno_pipe_fd, O_CLOEXEC|O_NONBLOCK) < 0)
                         return log_debug_errno(errno, "Failed to create pipe: %m");
 
-                r = pidref_safe_fork("(sd-dissect)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE|FORK_LOG, &child);
+                r = pidref_safe_fork(
+                                "(sd-dissect)",
+                                FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|(scope == RUNTIME_SCOPE_SYSTEM ? FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE : 0),
+                                &child);
                 if (r < 0)
                         return r;
                 if (r == 0) {
                         seq[0] = safe_close(seq[0]);
                         errno_pipe_fd[0] = safe_close(errno_pipe_fd[0]);
 
+                        if (scope == RUNTIME_SCOPE_USER) {
+                                r = detach_mount_namespace_userns(userns_fd);
+                                if (r < 0) {
+                                        log_debug_errno(r, "Failed to detach mount namespace: %m");
+                                        report_errno_and_exit(errno_pipe_fd[1], r);
+                                }
+                        }
+
                         r = dissected_image_mount(
                                         m,
                                         tmpdir,
                                         /* uid_shift= */ UID_INVALID,
                                         /* uid_range= */ UID_INVALID,
-                                        /* userns_fd= */ -EBADF,
+                                        userns_fd,
                                         flags);
                         if (r < 0) {
-                                log_debug_errno(r, "Failed to mount dissected image: %m");
+                                log_debug_errno(r, "Failed to mount dissected image '%s': %m", path);
                                 report_errno_and_exit(errno_pipe_fd[1], r);
                         }
 
-                        _cleanup_close_ int rfd = open(tmpdir, O_DIRECTORY|O_CLOEXEC);
-                        if (rfd < 0) {
+                        _cleanup_close_ int mfd = open(tmpdir, O_DIRECTORY|O_CLOEXEC);
+                        if (mfd < 0) {
                                 r = log_debug_errno(errno, "Failed to open '%s': %m", tmpdir);
                                 report_errno_and_exit(errno_pipe_fd[1], r);
                         }
 
                         r = extract_now(scope,
-                                        rfd,
+                                        mfd,
                                         path,
                                         matches,
                                         m->image_name,
@@ -772,15 +889,17 @@ static int extract_image_and_extensions(
          * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when
          * the units are started. Also, collect valid portable prefixes if caller requested that. */
         if (validate_extension || ret_valid_prefixes) {
-                _cleanup_free_ char *prefixes = NULL;
-
-                r = parse_env_file_fd(os_release->fd, os_release->name,
-                                     "ID", &id,
-                                     "ID_LIKE", &id_like,
-                                     "VERSION_ID", &version_id,
-                                     "SYSEXT_LEVEL", &sysext_level,
-                                     "CONFEXT_LEVEL", &confext_level,
-                                     "PORTABLE_PREFIXES", &prefixes);
+                _cleanup_free_ char *prefixes = NULL, *portable_scope_str = NULL;
+
+                r = parse_env_file_fd(
+                                os_release->fd, os_release->name,
+                                "ID", &id,
+                                "ID_LIKE", &id_like,
+                                "VERSION_ID", &version_id,
+                                "SYSEXT_LEVEL", &sysext_level,
+                                "CONFEXT_LEVEL", &confext_level,
+                                "PORTABLE_PREFIXES", &prefixes,
+                                "PORTABLE_SCOPE", &portable_scope_str);
                 if (r < 0)
                         return r;
                 if (isempty(id))
@@ -791,6 +910,31 @@ static int extract_image_and_extensions(
                         if (!valid_prefixes)
                                 return -ENOMEM;
                 }
+
+                RuntimeScope portable_scope = RUNTIME_SCOPE_SYSTEM;
+                if (portable_scope_str) {
+                        if (streq(portable_scope_str, "any"))
+                                portable_scope = _RUNTIME_SCOPE_INVALID;
+                        else {
+                                portable_scope = runtime_scope_from_string(portable_scope_str);
+                                if (portable_scope < 0)
+                                        return sd_bus_error_setf(
+                                                        error,
+                                                        SD_BUS_ERROR_INVALID_ARGS,
+                                                        "Invalid PORTABLE_SCOPE value '%s' in image %s.",
+                                                        portable_scope_str,
+                                                        name_or_path);
+                        }
+                }
+
+                if (portable_scope != _RUNTIME_SCOPE_INVALID && portable_scope != scope)
+                        return sd_bus_error_setf(
+                                        error,
+                                        SD_BUS_ERROR_INVALID_ARGS,
+                                        "Image %s portable scope '%s' incompatible with portabled runtime scope '%s'.",
+                                        name_or_path,
+                                        runtime_scope_to_string(portable_scope),
+                                        runtime_scope_to_string(scope));
         }
 
         ORDERED_HASHMAP_FOREACH(ext, extension_images) {
@@ -1377,6 +1521,7 @@ static int install_chroot_dropin(
 }
 
 static int install_profile_dropin(
+                RuntimeScope scope,
                 const char *image_path,
                 const PortableMetadata *m,
                 const char *dropin_dir,
@@ -1396,7 +1541,7 @@ static int install_profile_dropin(
         if (!profile)
                 return 0;
 
-        r = find_portable_profile(profile, m->name, &from);
+        r = find_portable_profile(scope, profile, m->name, &from);
         if (r < 0) {
                 if (r != -ENOENT)
                         return log_debug_errno(errno, "Profile '%s' is not accessible: %m", profile);
@@ -1454,6 +1599,7 @@ static const char *attached_path(const LookupPaths *paths, PortableFlags flags)
 }
 
 static int attach_unit_file(
+                RuntimeScope scope,
                 const LookupPaths *paths,
                 const char *image_path,
                 ImageType type,
@@ -1523,7 +1669,7 @@ static int attach_unit_file(
         if (r < 0)
                 return r;
 
-        r = install_profile_dropin(image_path, m, dropin_dir, profile, flags, &profile_dropin, changes, n_changes);
+        r = install_profile_dropin(scope, image_path, m, dropin_dir, profile, flags, &profile_dropin, changes, n_changes);
         if (r < 0)
                 return r;
 
@@ -1577,13 +1723,11 @@ static int attach_unit_file(
         return 0;
 }
 
-static int image_target_path(
-                const char *image_path,
-                PortableFlags flags,
-                char **ret) {
-
-        const char *fn, *where;
+static int image_target_path(RuntimeScope scope, const char *image_path, PortableFlags flags, char **ret) {
+        _cleanup_free_ char *where = NULL;
+        const char *fn;
         char *joined = NULL;
+        int r;
 
         assert(image_path);
         assert(ret);
@@ -1591,11 +1735,13 @@ static int image_target_path(
         fn = last_path_component(image_path);
 
         if (flags & PORTABLE_RUNTIME)
-                where = "/run/portables/";
+                r = runtime_directory_generic(scope, "portables", &where);
         else
-                where = "/etc/portables/";
+                r = config_directory_generic(scope, "portables", &where);
+        if (r < 0)
+                return r;
 
-        joined = strjoin(where, fn);
+        joined = path_join(where, fn);
         if (!joined)
                 return -ENOMEM;
 
@@ -1623,28 +1769,63 @@ static int install_image(
         if (image_in_search_path(scope, IMAGE_PORTABLE, NULL, image_path))
                 return 0;
 
-        r = image_target_path(image_path, flags, &target);
+        r = image_target_path(scope, image_path, flags, &target);
         if (r < 0)
                 return log_debug_errno(r, "Failed to generate image symlink path: %m");
 
         (void) mkdir_parents(target, 0755);
 
         if (flags & PORTABLE_MIXED_COPY_LINK) {
-                r = copy_tree(image_path,
-                              target,
-                              UID_INVALID,
-                              GID_INVALID,
-                              COPY_REFLINK | COPY_FSYNC | COPY_FSYNC_FULL | COPY_SYNCFS,
-                              /* denylist= */ NULL,
-                              /* subvolumes= */ NULL);
-                if (r < 0)
-                        return log_debug_errno(
-                                        r,
-                                        "Failed to copy %s %s %s: %m",
-                                        image_path,
-                                        glyph(GLYPH_ARROW_RIGHT),
-                                        target);
+                if (scope == RUNTIME_SCOPE_USER) {
+                        _cleanup_close_ int userns_fd = nsresource_allocate_userns(/* name= */ NULL, NSRESOURCE_UIDS_64K);
+                        if (userns_fd < 0)
+                                return log_debug_errno(userns_fd, "Failed to allocate user namespace: %m");
+
+                        _cleanup_close_ int fd = open(image_path, O_DIRECTORY|O_CLOEXEC);
+                        if (fd < 0)
+                                return log_error_errno(errno, "Failed to open '%s': %m", image_path);
+
+                        struct stat st;
+                        if (fstat(fd, &st) < 0)
+                                return log_error_errno(errno, "Failed to stat '%s': %m", image_path);
+
+                        _cleanup_close_ int tree_fd = -EBADF;
+                        if (uid_is_foreign(st.st_uid)) {
+                                r = mountfsd_mount_directory_fd(fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &tree_fd);
+                                if (r < 0)
+                                        return r;
+                        } else
+                                tree_fd = TAKE_FD(fd);
 
+                        _cleanup_close_ int directory_fd = -EBADF;
+                        r = mountfsd_make_directory(target, MODE_INVALID, /* flags= */ 0, &directory_fd);
+                        if (r < 0)
+                                return r;
+
+                        _cleanup_close_ int copy_fd = -EBADF;
+                        r = mountfsd_mount_directory_fd(directory_fd, userns_fd, DISSECT_IMAGE_FOREIGN_UID, &copy_fd);
+                        if (r < 0)
+                                return r;
+
+                        r = copy_tree_at_foreign(tree_fd, copy_fd, userns_fd);
+                        if (r < 0)
+                                return r;
+                } else {
+                        r = copy_tree(image_path,
+                                      target,
+                                      UID_INVALID,
+                                      GID_INVALID,
+                                      COPY_REFLINK | COPY_FSYNC | COPY_FSYNC_FULL | COPY_SYNCFS,
+                                      /* denylist= */ NULL,
+                                      /* subvolumes= */ NULL);
+                        if (r < 0)
+                                return log_debug_errno(
+                                                r,
+                                                "Failed to copy %s %s %s: %m",
+                                                image_path,
+                                                glyph(GLYPH_ARROW_RIGHT),
+                                                target);
+                }
         } else {
                 if (symlink(image_path, target) < 0)
                         return log_debug_errno(
@@ -1881,6 +2062,7 @@ int portable_attach(
 
         HASHMAP_FOREACH(item, unit_files) {
                 r = attach_unit_file(
+                                scope,
                                 &paths,
                                 image->path,
                                 image->type,
@@ -2187,7 +2369,7 @@ int portable_detach(
         SET_FOREACH(item, markers) {
                 _cleanup_free_ char *target = NULL;
 
-                r = image_target_path(item, flags, &target);
+                r = image_target_path(scope, item, flags, &target);
                 if (r < 0) {
                         log_debug_errno(r, "Failed to determine image path for '%s', ignoring: %m", item);
                         continue;
@@ -2351,10 +2533,17 @@ int portable_get_state(
         return 0;
 }
 
-int portable_get_profiles(char ***ret) {
+int portable_get_profiles(RuntimeScope scope, char ***ret) {
+        _cleanup_strv_free_ char **dirs = NULL;
+        int r;
+
         assert(ret);
 
-        return conf_files_list_nulstr(ret, NULL, NULL, CONF_FILES_DIRECTORY|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED, PORTABLE_PROFILE_DIRS);
+        r = portable_profile_dirs(scope, &dirs);
+        if (r < 0)
+                return r;
+
+        return conf_files_list_strv(ret, NULL, NULL, CONF_FILES_DIRECTORY|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED, (const char* const*) dirs);
 }
 
 static const char* const portable_change_type_table[_PORTABLE_CHANGE_TYPE_MAX] = {
index e42cf52c54cd2e9f9ab5eb5de98c754881d68fbb..5065babaab24c949c191c211f5c6585649e53a6e 100644 (file)
@@ -110,7 +110,7 @@ int portable_get_state(
                 PortableState *ret,
                 sd_bus_error *error);
 
-int portable_get_profiles(char ***ret);
+int portable_get_profiles(RuntimeScope scope, char ***ret);
 
 void portable_changes_free(PortableChange *changes, size_t n_changes);
 
index f259769bfd361a8152b5a94ea42246b4a4b6b8fd..b55eda795531a1a9f6ade8f335a54cd504e1a407 100644 (file)
@@ -48,6 +48,7 @@ static bool arg_no_block = false;
 static char **arg_extension_images = NULL;
 static bool arg_force = false;
 static bool arg_clean = false;
+static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
 
 STATIC_DESTRUCTOR_REGISTER(arg_extension_images, strv_freep);
 
@@ -224,9 +225,9 @@ static int acquire_bus(sd_bus **bus) {
         if (*bus)
                 return 0;
 
-        r = bus_connect_transport(arg_transport, arg_host, RUNTIME_SCOPE_SYSTEM, bus);
+        r = bus_connect_transport(arg_transport, arg_host, arg_runtime_scope, bus);
         if (r < 0)
-                return bus_log_connect_error(r, arg_transport, RUNTIME_SCOPE_SYSTEM);
+                return bus_log_connect_error(r, arg_transport, arg_runtime_scope);
 
         (void) sd_bus_set_allow_interactive_authorization(*bus, arg_ask_password);
 
@@ -1335,6 +1336,8 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_EXTENSION,
                 ARG_FORCE,
                 ARG_CLEAN,
+                ARG_USER,
+                ARG_SYSTEM,
         };
 
         static const struct option options[] = {
@@ -1357,6 +1360,8 @@ static int parse_argv(int argc, char *argv[]) {
                 { "extension",       required_argument, NULL, ARG_EXTENSION       },
                 { "force",           no_argument,       NULL, ARG_FORCE           },
                 { "clean",           no_argument,       NULL, ARG_CLEAN           },
+                { "user",            no_argument,       NULL, ARG_USER            },
+                { "system",          no_argument,       NULL, ARG_SYSTEM          },
                 {}
         };
 
@@ -1468,6 +1473,14 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_clean = true;
                         break;
 
+                case ARG_USER:
+                        arg_runtime_scope = RUNTIME_SCOPE_USER;
+                        break;
+
+                case ARG_SYSTEM:
+                        arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
index 7f3d496a6eb4b01c51a203044cc49d430c3ccc4e..aec0793fcdf93a2c7a8fe5b6f650757196f4a3fb 100644 (file)
@@ -3,12 +3,10 @@
 #include "sd-bus.h"
 
 #include "alloc-util.h"
-#include "btrfs-util.h"
 #include "bus-error.h"
 #include "bus-object.h"
 #include "bus-polkit.h"
 #include "discover-image.h"
-#include "fd-util.h"
 #include "hashmap.h"
 #include "io-util.h"
 #include "log.h"
@@ -28,10 +26,15 @@ static int property_get_pool_path(
                 void *userdata,
                 sd_bus_error *error) {
 
+        _cleanup_free_ char *dir = NULL;
+        Manager *m = ASSERT_PTR(userdata);
+
         assert(bus);
         assert(reply);
 
-        return sd_bus_message_append(reply, "s", "/var/lib/portables");
+        (void) image_get_pool_path(m->runtime_scope, IMAGE_PORTABLE, &dir);
+
+        return sd_bus_message_append(reply, "s", strempty(dir));
 }
 
 static int property_get_pool_usage(
@@ -43,19 +46,13 @@ static int property_get_pool_usage(
                 void *userdata,
                 sd_bus_error *error) {
 
-        _cleanup_close_ int fd = -EBADF;
         uint64_t usage = UINT64_MAX;
+        Manager *m = ASSERT_PTR(userdata);
 
         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;
-        }
+        (void) image_get_pool_usage(m->runtime_scope, IMAGE_PORTABLE, &usage);
 
         return sd_bus_message_append(reply, "t", usage);
 }
@@ -69,19 +66,13 @@ static int property_get_pool_limit(
                 void *userdata,
                 sd_bus_error *error) {
 
-        _cleanup_close_ int fd = -EBADF;
+        Manager *m = ASSERT_PTR(userdata);
         uint64_t size = UINT64_MAX;
 
         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;
-        }
+        (void) image_get_pool_limit(m->runtime_scope, IMAGE_PORTABLE, &size);
 
         return sd_bus_message_append(reply, "t", size);
 }
@@ -96,12 +87,13 @@ static int property_get_profiles(
                 sd_bus_error *error) {
 
         _cleanup_strv_free_ char **l = NULL;
+        Manager *m = ASSERT_PTR(userdata);
         int r;
 
         assert(bus);
         assert(reply);
 
-        r = portable_get_profiles(&l);
+        r = portable_get_profiles(m->runtime_scope, &l);
         if (r < 0)
                 return r;
 
@@ -319,16 +311,18 @@ static int method_detach_image(sd_bus_message *message, void *userdata, sd_bus_e
                         flags |= PORTABLE_RUNTIME;
         }
 
-        r = bus_verify_polkit_async(
-                        message,
-                        "org.freedesktop.portable1.attach-images",
-                        /* details= */ NULL,
-                        &m->polkit_registry,
-                        error);
-        if (r < 0)
-                return r;
-        if (r == 0)
-                return 1; /* Will call us back */
+        if (m->runtime_scope != RUNTIME_SCOPE_USER) {
+                r = bus_verify_polkit_async(
+                                message,
+                                "org.freedesktop.portable1.attach-images",
+                                /* details= */ NULL,
+                                &m->polkit_registry,
+                                error);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return 1; /* Will call us back */
+        }
 
         r = portable_detach(
                         m->runtime_scope,
@@ -374,21 +368,21 @@ static int method_set_pool_limit(sd_bus_message *message, void *userdata, sd_bus
         if (!FILE_SIZE_VALID_OR_INFINITY(limit))
                 return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "New limit out of range");
 
-        r = bus_verify_polkit_async(
-                        message,
-                        "org.freedesktop.portable1.manage-images",
-                        /* details= */ NULL,
-                        &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);
+        if (m->runtime_scope != RUNTIME_SCOPE_USER) {
+                r = bus_verify_polkit_async(
+                                message,
+                                "org.freedesktop.portable1.manage-images",
+                                /* details= */ NULL,
+                                &m->polkit_registry,
+                                error);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return 1; /* Will call us back */
+        }
 
-        r = btrfs_subvol_set_subtree_quota_limit("/var/lib/portables", 0, limit);
-        if (r == -ENOTTY)
+        r = image_set_pool_limit(m->runtime_scope, IMAGE_MACHINE, limit);
+        if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
                 return sd_bus_error_set(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");
index a9cd48f1fc0189d649179368bd12903eef1e991c..48f0274a54cd60910e79fc4b053fbf87c5bcf6e4 100644 (file)
@@ -454,16 +454,18 @@ static int bus_image_method_detach(
                         flags |= PORTABLE_RUNTIME;
         }
 
-        r = bus_verify_polkit_async(
-                        message,
-                        "org.freedesktop.portable1.attach-images",
-                        /* details= */ NULL,
-                        &m->polkit_registry,
-                        error);
-        if (r < 0)
-                return r;
-        if (r == 0)
-                return 1; /* Will call us back */
+        if (m->runtime_scope != RUNTIME_SCOPE_USER) {
+                r = bus_verify_polkit_async(
+                                message,
+                                "org.freedesktop.portable1.attach-images",
+                                /* details= */ NULL,
+                                &m->polkit_registry,
+                                error);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return 1; /* Will call us back */
+        }
 
         r = portable_detach(
                         m->runtime_scope,
@@ -1015,7 +1017,7 @@ int bus_image_acquire(
 
         /* Acquires an 'Image' object if not acquired yet, and enforces necessary authentication while doing so. */
 
-        if (mode == BUS_IMAGE_AUTHENTICATE_ALL) {
+        if (mode == BUS_IMAGE_AUTHENTICATE_ALL && m->runtime_scope != RUNTIME_SCOPE_USER) {
                 r = bus_verify_polkit_async(
                                 message,
                                 polkit_action,
@@ -1066,7 +1068,7 @@ int bus_image_acquire(
                         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) {
+                if (mode == BUS_IMAGE_AUTHENTICATE_BY_PATH && m->runtime_scope != RUNTIME_SCOPE_USER) {
                         r = bus_verify_polkit_async(
                                         message,
                                         polkit_action,
index e5e6eb9810acfcffa117376151a077a110d7fd99..15e948220554cd7cf700f52155545bb4a5998b61 100644 (file)
@@ -15,6 +15,7 @@
 #include "hashmap.h"
 #include "log.h"
 #include "main-func.h"
+#include "path-lookup.h"
 #include "portabled.h"
 #include "service-util.h"
 #include "signal-util.h"
@@ -22,7 +23,7 @@
 static Manager* manager_unref(Manager *m);
 DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_unref);
 
-static int manager_new(Manager **ret) {
+static int manager_new(RuntimeScope scope, Manager **ret) {
         _cleanup_(manager_unrefp) Manager *m = NULL;
         int r;
 
@@ -33,9 +34,13 @@ static int manager_new(Manager **ret) {
                 return -ENOMEM;
 
         *m = (Manager) {
-                .runtime_scope = RUNTIME_SCOPE_SYSTEM,
+                .runtime_scope = scope,
         };
 
+        r = runtime_directory_generic(scope, "systemd/portables", &m->state_dir);
+        if (r < 0)
+                return r;
+
         r = sd_event_default(&m->event);
         if (r < 0)
                 return r;
@@ -70,6 +75,8 @@ static Manager* manager_unref(Manager *m) {
         sd_bus_flush_close_unref(m->bus);
         sd_event_unref(m->event);
 
+        free(m->state_dir);
+
         return mfree(m);
 }
 
@@ -79,9 +86,21 @@ static int manager_connect_bus(Manager *m) {
         assert(m);
         assert(!m->bus);
 
-        r = sd_bus_default_system(&m->bus);
+        if (m->runtime_scope == RUNTIME_SCOPE_SYSTEM) {
+                r = sd_bus_default_system(&m->bus);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to connect to system bus: %m");
+        } else {
+                assert(m->runtime_scope == RUNTIME_SCOPE_USER);
+
+                r = sd_bus_default_user(&m->bus);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to connect to user bus: %m");
+        }
+
+        r = sd_bus_attach_event(m->bus, m->event, 0);
         if (r < 0)
-                return log_error_errno(r, "Failed to connect to system bus: %m");
+                return log_error_errno(r, "Failed to attach user bus to event loop: %m");
 
         r = bus_add_implementation(m->bus, &manager_object, m);
         if (r < 0)
@@ -95,10 +114,6 @@ static int manager_connect_bus(Manager *m) {
         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;
@@ -125,6 +140,7 @@ static bool check_idle(void *userdata) {
 
 static int run(int argc, char *argv[]) {
         _cleanup_(manager_unrefp) Manager *m = NULL;
+        RuntimeScope scope = RUNTIME_SCOPE_SYSTEM;
         int r;
 
         log_setup();
@@ -133,17 +149,14 @@ static int run(int argc, char *argv[]) {
                                "Manage registrations of portable images.",
                                BUS_IMPLEMENTATIONS(&manager_object,
                                                    &log_control_object),
-                               /* runtime_scope= */ NULL,
+                               &scope,
                                argc, argv);
         if (r <= 0)
                 return r;
 
         umask(0022);
 
-        if (argc != 1)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
-
-        r = manager_new(&m);
+        r = manager_new(scope, &m);
         if (r < 0)
                 return log_error_errno(r, "Failed to allocate manager object: %m");
 
index 1fe41cdd1a5f7c3664d52cd08c8ead0d826d09b4..50e65ef81a888e7315a91add73d7101dcce04a2a 100644 (file)
@@ -17,7 +17,8 @@ typedef struct Manager {
         LIST_HEAD(Operation, operations);
         unsigned n_operations;
 
-        RuntimeScope runtime_scope; /* for now always RUNTIME_SCOPE_SYSTEM */
+        RuntimeScope runtime_scope;
+        char *state_dir;
 } Manager;
 
 extern const BusObjectImplementation manager_object;
diff --git a/src/portable/profile/user/default/service.conf b/src/portable/profile/user/default/service.conf
new file mode 100644 (file)
index 0000000..7d75c04
--- /dev/null
@@ -0,0 +1,28 @@
+# The "default" security profile for services, i.e. a number of useful restrictions
+
+[Service]
+MountAPIVFS=yes
+BindLogSockets=yes
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=-/etc/resolv.conf
+BindReadOnlyPaths=/run/dbus/system_bus_socket
+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
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+DelegateNamespaces=no
+SystemCallFilter=@system-service
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
diff --git a/src/portable/profile/user/nonetwork/service.conf b/src/portable/profile/user/nonetwork/service.conf
new file mode 100644 (file)
index 0000000..2a73e6c
--- /dev/null
@@ -0,0 +1,28 @@
+# The "nonetwork" security profile for services, i.e. like "default" but without networking
+
+[Service]
+MountAPIVFS=yes
+BindLogSockets=yes
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=/run/dbus/system_bus_socket
+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
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+DelegateNamespaces=no
+SystemCallFilter=@system-service
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+PrivateNetwork=yes
+IPAddressDeny=any
diff --git a/src/portable/profile/user/strict/service.conf b/src/portable/profile/user/strict/service.conf
new file mode 100644 (file)
index 0000000..a16a28b
--- /dev/null
@@ -0,0 +1,27 @@
+# The "strict" security profile for services, all options turned on
+
+[Service]
+MountAPIVFS=yes
+BindLogSockets=yes
+BindReadOnlyPaths=/etc/machine-id
+RemoveIPC=yes
+CapabilityBoundingSet=
+PrivateDevices=yes
+PrivateUsers=yes
+ProtectSystem=strict
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictAddressFamilies=AF_UNIX
+LockPersonality=yes
+NoNewPrivileges=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictNamespaces=yes
+DelegateNamespaces=no
+SystemCallFilter=@system-service
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+PrivateNetwork=yes
+IPAddressDeny=any
+TasksMax=4
diff --git a/src/portable/profile/user/trusted/service.conf b/src/portable/profile/user/trusted/service.conf
new file mode 100644 (file)
index 0000000..144d4f6
--- /dev/null
@@ -0,0 +1,8 @@
+# The "trusted" profile for services, i.e. no restrictions are applied apart from a private /tmp
+
+[Service]
+MountAPIVFS=yes
+PrivateTmp=yes
+BindPaths=/run
+BindReadOnlyPaths=/etc/machine-id
+BindReadOnlyPaths=-/etc/resolv.conf
index 2935cbd97e736dc7f5012b9b5a5a01bb3525775a..54c86887fc527e921c124c7d30098d04491f3081 100644 (file)
@@ -663,7 +663,7 @@ static int pick_image_search_path(
         }
 
         case RUNTIME_SCOPE_USER: {
-                if (class != IMAGE_MACHINE)
+                if (!IN_SET(class, IMAGE_MACHINE, IMAGE_PORTABLE))
                         break;
 
                 static const uint64_t dirs[] = {
@@ -676,7 +676,7 @@ static int pick_image_search_path(
                 FOREACH_ELEMENT(d, dirs) {
                         _cleanup_free_ char *p = NULL;
 
-                        r = sd_path_lookup(*d, "machines", &p);
+                        r = sd_path_lookup(*d, image_dirname_to_string(class), &p);
                         if (r == -ENXIO) /* No XDG_RUNTIME_DIR set */
                                 continue;
                         if (r < 0)
index 5bc764cefb8f596af927d025cfb65cd03fcf41f4..986c74e840e733a64cdfdefd20389cba8259301d 100644 (file)
@@ -4512,7 +4512,7 @@ Architecture dissected_image_architecture(DissectedImage *m) {
 }
 
 bool dissected_image_is_portable(DissectedImage *m) {
-        return m && strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES");
+        return m && (strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES") || strv_env_pairs_get(m->os_release, "PORTABLE_SCOPE"));
 }
 
 bool dissected_image_is_initrd(DissectedImage *m) {
index d474db2495d25a8ed6c515558dc50500f6b0bb48..078d311dda64dd9650a07a788c543bc211429208 100644 (file)
@@ -5,10 +5,70 @@
 #include "alloc-util.h"
 #include "fs-util.h"
 #include "nulstr-util.h"
+#include "path-lookup.h"
 #include "portable-util.h"
 #include "string-util.h"
+#include "strv.h"
 
-int find_portable_profile(const char *name, const char *unit, char **ret_path) {
+int portable_profile_dirs(RuntimeScope scope, char ***ret) {
+        _cleanup_strv_free_ char **dirs = NULL;
+        int r;
+
+        assert(ret);
+
+        switch (scope) {
+
+        case RUNTIME_SCOPE_SYSTEM:
+                r = strv_from_nulstr(&dirs, PORTABLE_PROFILE_DIRS);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        case RUNTIME_SCOPE_USER: {
+                _cleanup_free_ char *d = NULL;
+
+                r = xdg_user_config_dir("systemd/portable/profile", &d);
+                if (r < 0 && r != -ENXIO)
+                        return r;
+                if (r >= 0) {
+                        r = strv_consume(&dirs, TAKE_PTR(d));
+                        if (r < 0)
+                                return r;
+                }
+
+                r = xdg_user_runtime_dir("systemd/portable/profile", &d);
+                if (r < 0 && r != -ENXIO)
+                        return r;
+                if (r >= 0) {
+                        r = strv_consume(&dirs, TAKE_PTR(d));
+                        if (r < 0)
+                                return r;
+                }
+
+                _fallthrough_;
+        }
+
+        case RUNTIME_SCOPE_GLOBAL:
+                r = strv_extend_strv(
+                                &dirs,
+                                CONF_PATHS_STRV("systemd/user/portable/profile"),
+                                /* filter_duplicates= */ false);
+                if (r < 0)
+                        return r;
+
+                break;
+
+        default:
+                return -EINVAL;
+        }
+
+        *ret = TAKE_PTR(dirs);
+        return 0;
+}
+
+int find_portable_profile(RuntimeScope scope, const char *name, const char *unit, char **ret_path) {
+        _cleanup_strv_free_ char **dirs = NULL;
         const char *dot;
         int r;
 
@@ -17,10 +77,14 @@ int find_portable_profile(const char *name, const char *unit, char **ret_path) {
 
         assert_se(dot = strrchr(unit, '.'));
 
-        NULSTR_FOREACH(p, PORTABLE_PROFILE_DIRS) {
+        r = portable_profile_dirs(scope, &dirs);
+        if (r < 0)
+                return r;
+
+        STRV_FOREACH(p, dirs) {
                 _cleanup_free_ char *joined = NULL;
 
-                joined = strjoin(p, "/", name, "/", dot + 1, ".conf");
+                joined = strjoin(*p, "/", name, "/", dot + 1, ".conf");
                 if (!joined)
                         return -ENOMEM;
 
index 1d9f6d20a4a060f8622d7f12f98643fab2c5cdd6..fd78f7070010212567bca26f585cbffcc65aeead 100644 (file)
@@ -6,4 +6,5 @@
 
 #define PORTABLE_PROFILE_DIRS CONF_PATHS_NULSTR("systemd/portable/profile")
 
-int find_portable_profile(const char *name, const char *unit, char **ret_path);
+int portable_profile_dirs(RuntimeScope scope, char ***ret);
+int find_portable_profile(RuntimeScope scope, const char *name, const char *unit, char **ret_path);
diff --git a/test/units/TEST-29-PORTABLE.user.sh b/test/units/TEST-29-PORTABLE.user.sh
new file mode 100755 (executable)
index 0000000..87488dd
--- /dev/null
@@ -0,0 +1,324 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# shellcheck disable=SC2233,SC2235
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+if [[ ! -f /usr/lib/systemd/system/systemd-mountfsd.socket ]] ||
+   [[ ! -f /usr/lib/systemd/system/systemd-nsresourced.socket ]] ||
+   ! command -v mksquashfs ||
+   ! grep -q bpf /sys/kernel/security/lsm ||
+   ! find /usr/lib* -name libbpf.so.1 2>/dev/null | grep . ||
+   systemd-analyze compare-versions "$(uname -r)" lt 6.5 ||
+   systemd-analyze compare-versions "$(pkcheck --version | awk '{print $3}')" lt 124 ||
+   systemctl --version | grep -- "-BTF" >/dev/null; then
+    echo "Skipping mountfsd/nsresourced tests"
+    exit 0
+fi
+
+systemctl start systemd-mountfsd.socket systemd-nsresourced.socket
+
+# Arrays cannot be exported, so redefine in each test script
+ARGS=()
+if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
+    # If we're running under sanitizers, we need to use a less restrictive
+    # profile, otherwise LSan syscall would get blocked by seccomp
+    ARGS+=(--profile=trusted)
+fi
+
+# To be able to mount images as an unprivileged user we need verity sidecars so generate them for app1 which
+# doesn't have them by default.
+veritysetup format /tmp/app1.raw /tmp/app1.verity --root-hash-file /tmp/app1.roothash
+openssl smime -sign -nocerts -noattr -binary \
+    -in /tmp/app1.roothash \
+    -inkey /usr/share/mkosi.key \
+    -signer /usr/share/mkosi.crt \
+    -outform der \
+    -out /tmp/app1.roothash.p7s
+chmod go+r /tmp/app1*
+
+at_exit() {
+    set +e
+
+    rm -f /tmp/app1.verity /tmp/app1.roothash /tmp/app1.roothash.p7s
+    loginctl disable-linger testuser
+}
+
+trap at_exit EXIT
+
+# For unprivileged user manager
+loginctl enable-linger testuser
+
+systemctl start user@4711.service
+
+portablectl_user() {
+    runas testuser env XDG_RUNTIME_DIR=/run/user/4711 portablectl --user "$@"
+}
+
+busctl_user() {
+    runas testuser env XDG_RUNTIME_DIR=/run/user/4711 busctl --user "$@"
+}
+
+systemctl_user() {
+    runas testuser env XDG_RUNTIME_DIR=/run/user/4711 systemctl --user "$@"
+}
+
+runas_user() {
+    runas testuser env XDG_RUNTIME_DIR=/run/user/4711 "$@"
+}
+
+# Start the user portable daemon
+systemctl_user start dbus-org.freedesktop.portable1.service
+
+: "Test basic attach, reattach and detach for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --now --runtime /usr/share/minimal_0.raw minimal-app0
+
+portablectl_user is-attached minimal-app0
+portablectl_user inspect /usr/share/minimal_0.raw minimal-app0.service
+systemctl_user is-active minimal-app0.service
+systemctl_user is-active minimal-app0-foo.service
+systemctl_user is-active minimal-app0-bar.service && exit 1
+
+# Ensure pinning by policy works
+cat /run/user/4711/systemd/user.attached/minimal-app0-foo.service.d/20-portable.conf
+grep -q -F 'root=signed+squashfs:' /run/user/4711/systemd/user.attached/minimal-app0-foo.service.d/20-portable.conf
+
+portablectl_user "${ARGS[@]}" reattach --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl_user is-attached minimal-app0
+portablectl_user inspect /usr/share/minimal_0.raw minimal-app0.service
+systemctl_user is-active minimal-app0.service
+systemctl_user is-active minimal-app0-bar.service
+systemctl_user is-active minimal-app0-foo.service && exit 1
+
+portablectl_user list | grep -F "minimal_1" >/dev/null
+busctl_user tree org.freedesktop.portable1 --no-pager | grep -F '/org/freedesktop/portable1/image/minimal_5f1' >/dev/null
+
+portablectl_user detach --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl_user list | grep -F "No images." >/dev/null
+busctl_user tree org.freedesktop.portable1 --no-pager | grep -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1 >/dev/null
+
+: "Test --force for user portable services"
+
+runas_user mkdir -p /run/user/4711/systemd/user.attached/minimal-app0.service.d/
+runas_user tee /run/user/4711/systemd/user.attached/minimal-app0.service >/dev/null <<EOF
+[Unit]
+Description=Minimal App 0
+EOF
+runas_user tee /run/user/4711/systemd/user.attached/minimal-app0.service.d/10-profile.conf >/dev/null <<EOF
+[Unit]
+Description=Minimal App 0
+EOF
+runas_user tee /run/user/4711/systemd/user.attached/minimal-app0.service.d/20-portable.conf >/dev/null <<EOF
+[Unit]
+Description=Minimal App 0
+EOF
+systemctl_user daemon-reload
+
+portablectl_user "${ARGS[@]}" attach --force --now --runtime /usr/share/minimal_0.raw minimal-app0
+
+portablectl_user is-attached --force minimal-app0
+portablectl_user inspect --force /usr/share/minimal_0.raw minimal-app0.service
+systemctl_user is-active minimal-app0.service
+systemctl_user is-active minimal-app0-foo.service
+systemctl_user is-active minimal-app0-bar.service && exit 1
+
+portablectl_user "${ARGS[@]}" reattach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl_user is-attached --force minimal-app0
+portablectl_user inspect --force /usr/share/minimal_0.raw minimal-app0.service
+systemctl_user is-active minimal-app0.service
+systemctl_user is-active minimal-app0-bar.service
+systemctl_user is-active minimal-app0-foo.service && exit 1
+
+portablectl_user list | grep -F "minimal_1" >/dev/null
+busctl_user tree org.freedesktop.portable1 --no-pager | grep -F '/org/freedesktop/portable1/image/minimal_5f1' >/dev/null
+
+portablectl_user detach --force --now --runtime /usr/share/minimal_1.raw minimal-app0
+
+portablectl_user list | grep -F "No images." >/dev/null
+busctl_user tree org.freedesktop.portable1 --no-pager | grep -F '/org/freedesktop/portable1/image/minimal_5f1' && exit 1 >/dev/null
+
+: "Test extension images for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+
+systemctl_user is-active app0.service
+status="$(portablectl_user is-attached --extension app0 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+# Ensure pinning by policy works
+grep -q -F 'RootImagePolicy=root=signed+squashfs:' /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf >/dev/null
+grep -q -F 'ExtensionImagePolicy=root=signed+squashfs:' /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf >/dev/null
+
+portablectl_user "${ARGS[@]}" reattach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
+
+systemctl_user is-active app0.service
+status="$(portablectl_user is-attached --extension app0 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_1.raw" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/user/4711/systemd/user.attached/app0.service.d/20-portable.conf
+
+portablectl_user detach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0
+
+: "Test versioned extension images for user portable services"
+
+cp /tmp/app0.raw /tmp/app0_1.0.raw
+cp /tmp/app0.verity /tmp/app0_1.0.verity
+cp /tmp/app0.roothash /tmp/app0_1.0.roothash
+cp /tmp/app0.roothash.p7s /tmp/app0_1.0.roothash.p7s
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_0.raw app0
+
+systemctl_user is-active app0.service
+status="$(portablectl_user is-attached --extension app0_1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user detach --now --runtime --extension /tmp/app0_1.0.raw /usr/share/minimal_1.raw app0
+rm -f /tmp/app0_1.0*
+
+: "Test reattach with version changes for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+# Ensure that adding or removing a version to the image doesn't break reattaching
+cp /tmp/app1.raw /tmp/app1_2.raw
+cp /tmp/app1.verity /tmp/app1_2.verity
+cp /tmp/app1.roothash /tmp/app1_2.roothash
+cp /tmp/app1.roothash.p7s /tmp/app1_2.roothash.p7s
+portablectl_user "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1_2.raw /usr/share/minimal_1.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1_2 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1 minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user detach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_1.raw app1
+
+: "Test --no-reload for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user detach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+portablectl_user "${ARGS[@]}" attach --force --no-reload --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+systemctl_user daemon-reload
+systemctl_user restart app1.service
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user detach --now --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+# : "Test vpick for user portable services"
+
+mkdir -p /tmp/app1.v/
+cp /tmp/app1.raw /tmp/app1.v/app1_1.0.raw
+cp /tmp/app1.verity /tmp/app1.v/app1_1.0.verity
+cp /tmp/app1.roothash /tmp/app1.v/app1_1.0.roothash
+cp /tmp/app1.roothash.p7s /tmp/app1.v/app1_1.0.roothash.p7s
+cp /tmp/app1_2.raw /tmp/app1.v/app1_2.0.raw
+cp /tmp/app1_2.verity /tmp/app1.v/app1_2.0.verity
+cp /tmp/app1_2.roothash /tmp/app1.v/app1_2.0.roothash
+cp /tmp/app1_2.roothash.p7s /tmp/app1.v/app1_2.0.roothash.p7s
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1_2.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+rm -f /tmp/app1.v/app1_2.0*
+portablectl_user "${ARGS[@]}" reattach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_1.raw app1
+
+systemctl_user is-active app1.service
+status="$(portablectl_user is-attached --extension app1_1.0.raw minimal_1)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user detach --now --runtime --extension /tmp/app1.v/ /usr/share/minimal_0.raw app1
+rm -f /tmp/app1.v/app1_1.0*
+
+: "Test extension-release.NAME override for user portable services"
+
+cp /tmp/app0.raw /tmp/app10.raw
+cp /tmp/app0.verity /tmp/app10.verity
+cp /tmp/app0.roothash /tmp/app10.roothash
+cp /tmp/app0.roothash.p7s /tmp/app10.roothash.p7s
+portablectl_user "${ARGS[@]}" attach --force --now --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
+
+systemctl_user is-active app0.service
+status="$(portablectl_user is-attached --extension /tmp/app10.raw /usr/share/minimal_0.raw)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user inspect --force --cat --extension /tmp/app10.raw /usr/share/minimal_0.raw app0 | grep -F "Extension Release: /tmp/app10.raw" >/dev/null
+
+# Ensure that we can detach even when an image has been deleted already (stop the unit manually as
+# portablectl won't find it)
+rm -f /tmp/app10*
+systemctl_user stop app0.service
+portablectl_user detach --force --runtime --extension /tmp/app10.raw /usr/share/minimal_0.raw app0
+
+: "Test confext images for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
+
+systemctl_user is-active app0.service
+status="$(portablectl_user is-attached --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw)"
+[[ "${status}" == "running-runtime" ]]
+
+portablectl_user inspect --force --cat --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0 | grep -F "Extension Release: /tmp/conf0.raw" >/dev/null
+
+portablectl_user detach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0
+
+: "Test multiple portables sharing the same base image for user portable services"
+
+portablectl_user "${ARGS[@]}" attach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0
+portablectl_user "${ARGS[@]}" attach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app1
+
+status="$(portablectl_user is-attached --extension app0 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+(! portablectl_user detach --runtime /usr/share/minimal_0.raw app)
+
+status="$(portablectl_user is-attached --extension app0 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+# Ensure 'portablectl list' shows the correct status for both images
+portablectl_user list
+portablectl_user list | grep -F "minimal_0" | grep -F "attached-runtime" >/dev/null
+portablectl_user list | grep -F "app0" | grep -F "attached-runtime" >/dev/null
+portablectl_user list | grep -F "app1" | grep -F "attached-runtime" >/dev/null
+
+portablectl_user detach --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app
+
+status="$(portablectl_user is-attached --extension app1 minimal_0)"
+[[ "${status}" == "attached-runtime" ]]
+
+portablectl_user detach --runtime --extension /tmp/app1.raw /usr/share/minimal_0.raw app
index 1f334945116d4761567a3c81e625dc94e8920e45..372ce1c58d21eef76cd6b38849364621cb9cba9e 100755 (executable)
@@ -296,22 +296,25 @@ install_extension_images() {
         fi
 
         local initdir="/var/tmp/app0"
-        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
+        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/opt"
         grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app0"
         echo "$version_id" >>"$initdir/usr/lib/extension-release.d/extension-release.app0"
         (
             echo "$version_id"
             echo "SYSEXT_IMAGE_ID=app"
         ) >>"$initdir/usr/lib/extension-release.d/extension-release.app0"
-        cat >"$initdir/usr/lib/systemd/system/app0.service" <<EOF
+        for scope in system user; do
+            mkdir -p "$initdir/usr/lib/systemd/$scope"
+            cat >"$initdir/usr/lib/systemd/$scope/app0.service" <<EOF
 [Service]
 Type=oneshot
 RemainAfterExit=yes
 ExecStart=/opt/script0.sh
-TemporaryFileSystem=/var/lib
+TemporaryFileSystem=/var/lib /home
 StateDirectory=app0
 RuntimeDirectory=app0
 EOF
+        done
         cat >"$initdir/opt/script0.sh" <<EOF
 #!/usr/bin/env bash
 set -e
@@ -351,7 +354,7 @@ EOF
         chmod go+r /tmp/conf0*
 
         initdir="/var/tmp/app1"
-        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
+        mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/opt"
         grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
         (
             echo "$version_id"
@@ -361,14 +364,18 @@ EOF
             echo "PORTABLE_PREFIXES=app1"
         ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
         setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
-        cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
+        for scope in system user; do
+            mkdir -p "$initdir/usr/lib/systemd/$scope"
+            cat >"$initdir/usr/lib/systemd/$scope/app1.service" <<EOF
 [Service]
 Type=oneshot
 RemainAfterExit=yes
 ExecStart=/opt/script1.sh
+TemporaryFileSystem=/home
 StateDirectory=app1
 RuntimeDirectory=app1
 EOF
+        done
         cat >"$initdir/opt/script1.sh" <<EOF
 #!/usr/bin/env bash
 set -e
index 71d242503802046fad6fb305df33c8b975a29db3..baa30c500fe143030e1f258f20e3f8f79dc9daed 100644 (file)
@@ -38,6 +38,11 @@ units = [
           'conditions' : ['ENABLE_IMPORTD'],
           'symlinks' : ['sockets.target.wants/'],
         },
+        {
+          'file' : 'systemd-portabled.service.in',
+          'conditions' : ['ENABLE_PORTABLED'],
+          'symlinks' : ['dbus-org.freedesktop.portable1.service'],
+        },
         { 'file' : 'paths.target' },
         { 'file' : 'printer.target' },
         { 'file' : 'session.slice' },
diff --git a/units/user/systemd-portabled.service.in b/units/user/systemd-portabled.service.in
new file mode 100644 (file)
index 0000000..61aa85e
--- /dev/null
@@ -0,0 +1,26 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[Unit]
+Description=Portable Service Manager
+Documentation=man:systemd-portabled.service(8)
+Documentation=man:org.freedesktop.portable1(5)
+
+[Service]
+ExecStart={{LIBEXECDIR}}/systemd-portabled --user
+BusName=org.freedesktop.portable1
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
+SystemCallFilter=@system-service @mount
+SystemCallErrorNumber=EPERM
+SystemCallArchitectures=native
+LockPersonality=yes
+IPAddressDeny=any
+{{SERVICE_WATCHDOG}}