]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
nspawn: Add --bind-user-shell= to control shells for --bind-user
authorNick Labich <nick@labich.org>
Fri, 27 Jun 2025 15:39:46 +0000 (11:39 -0400)
committerLennart Poettering <lennart@poettering.net>
Mon, 30 Jun 2025 08:45:10 +0000 (10:45 +0200)
Prior to this change, no user shell can be specified in the user
records passed into a container via --bind-user=. This new option
allows users to:

1. When false (the default), continue to specify no user shell for
   each bound user record, resulting in the use of the container's
   default shell for bound users.

2. When true, include each host user's shell in the corresponding
   user record passed into a container (via --bind-user=).

3. When an absolute path, set that path as the user shell for each
   user record passed into a container (via --bind-user=).

This does not change the existing behavior, but allows users to
opt-in to either copy the shells specified by the host user records
or override the shell explicitly by path.

man/systemd-nspawn.xml
man/systemd.nspawn.xml
src/nspawn/nspawn-bind-user.c
src/nspawn/nspawn-bind-user.h
src/nspawn/nspawn-gperf.gperf
src/nspawn/nspawn-settings.c
src/nspawn/nspawn-settings.h
src/nspawn/nspawn.c
test/units/TEST-13-NSPAWN.nspawn.sh

index 794d6e8f29a27d3356017da4bfbbe3a14fe3d931..b05744dc08b8f24a3f534621a5c79bd11eacf317 100644 (file)
@@ -1648,6 +1648,28 @@ After=sys-subsystem-net-devices-ens1.device</programlisting>
         <xi:include href="version-info.xml" xpointer="v249"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--bind-user-shell=</option></term>
+
+        <listitem><para>When used with <option>--bind-user=</option>, includes the specified shell in the
+        user records of users bound into the container. Takes either a boolean or an absolute path.</para>
+
+        <itemizedlist>
+          <listitem><para>If false (the default), no shell is passed in the user records for users bound into
+          the container. This causes bound users to the use the container's default shell.</para></listitem>
+
+          <listitem><para>If true, the shells specified by the host user records are included in the user records of all users bound into the container.</para></listitem>
+
+          <listitem><para>If passed an absolute path, sets that path as the shell for user records of all users bound into the container.</para></listitem>
+        </itemizedlist>
+
+        <para>Note: This will not check whether the specified shells exist in the container.</para>
+
+        <para>This operation is only supported in combination with <option>--bind-user=</option>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><option>--inaccessible=</option></term>
 
index 4870d6b7abf1f7777fddc3dd3fa7a8cf5136c3aa..1db41a1b72c6af8f0dc5a42786bc29583b5c17e3 100644 (file)
         <xi:include href="version-info.xml" xpointer="v249"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>BindUserShell=</varname></term>
+
+        <listitem><para>When used with <varname>BindUser</varname>, specifies the shell that is included in
+        the user record of users bound from the host into the container. This option is equivalent to the
+        command line switch <option>--bind-user-shell=</option>, see
+        <citerefentry><refentrytitle>systemd-nspawn</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+        for details about the specific options supported. This setting is privileged (see above).</para>
+
+        <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>TemporaryFileSystem=</varname></term>
 
index da927347c6a0f1ac6d0d58be7898c2001dcbb0ec..e7d976d1991da9de70ac179b83bab5b3cbffaf84 100644 (file)
@@ -91,6 +91,8 @@ static int convert_user(
                 UserRecord *u,
                 GroupRecord *g,
                 uid_t allocate_uid,
+                const char *shell,
+                bool shell_copy,
                 UserRecord **ret_converted_user,
                 GroupRecord **ret_converted_group) {
 
@@ -104,6 +106,9 @@ static int convert_user(
         assert(g);
         assert(user_record_gid(u) == g->gid);
 
+        if (shell_copy)
+                shell = u->shell;
+
         r = check_etc_passwd_collisions(directory, u->user_name, UID_INVALID);
         if (r < 0)
                 return r;
@@ -138,6 +143,7 @@ static int convert_user(
                                         SD_JSON_BUILD_PAIR_CONDITION(u->disposition >= 0, "disposition", SD_JSON_BUILD_STRING(user_disposition_to_string(u->disposition))),
                                         SD_JSON_BUILD_PAIR("homeDirectory", SD_JSON_BUILD_STRING(h)),
                                         SD_JSON_BUILD_PAIR("service", JSON_BUILD_CONST_STRING("io.systemd.NSpawn")),
+                                        JSON_BUILD_PAIR_STRING_NON_EMPTY("shell", shell),
                                         SD_JSON_BUILD_PAIR("privileged", SD_JSON_BUILD_OBJECT(
                                                                            SD_JSON_BUILD_PAIR_CONDITION(!strv_isempty(u->hashed_password), "hashedPassword", SD_JSON_BUILD_VARIANT(hp)),
                                                                            SD_JSON_BUILD_PAIR_CONDITION(!!ssh, "sshAuthorizedKeys", SD_JSON_BUILD_VARIANT(ssh))))));
@@ -203,6 +209,8 @@ BindUserContext* bind_user_context_free(BindUserContext *c) {
 int bind_user_prepare(
                 const char *directory,
                 char **bind_user,
+                const char *bind_user_shell,
+                bool bind_user_shell_copy,
                 uid_t uid_shift,
                 uid_t uid_range,
                 CustomMount **custom_mounts,
@@ -285,7 +293,7 @@ int bind_user_prepare(
                 if (r < 0)
                         return r;
 
-                r = convert_user(directory, u, g, current_uid, &cu, &cg);
+                r = convert_user(directory, u, g, current_uid, bind_user_shell, bind_user_shell_copy, &cu, &cg);
                 if (r < 0)
                         return r;
 
index b6d4e1e4ad055a1bec39f772f74a389a9c3cada6..cb4d246bece3f0908b610f70df8f060ede6f521b 100644 (file)
@@ -24,6 +24,6 @@ BindUserContext* bind_user_context_free(BindUserContext *c);
 
 DEFINE_TRIVIAL_CLEANUP_FUNC(BindUserContext*, bind_user_context_free);
 
-int bind_user_prepare(const char *directory, char **bind_user, uid_t uid_shift, uid_t uid_range, CustomMount **custom_mounts, size_t *n_custom_mounts, BindUserContext **ret);
+int bind_user_prepare(const char *directory, char **bind_user, const char *bind_user_shell, bool bind_user_shell_copy, uid_t uid_shift, uid_t uid_range, CustomMount **custom_mounts, size_t *n_custom_mounts, BindUserContext **ret);
 
 int bind_user_setup(const BindUserContext *c, const char *root);
index b735093691cd0bc3eda0bc5f352782468d730282..7a8a8d8b3d0aaddd2e901f487a140b721097aadf 100644 (file)
@@ -71,6 +71,7 @@ Files.OverlayReadOnly,        config_parse_overlay,        1,
 Files.PrivateUsersChown,      config_parse_userns_chown,   0,                        offsetof(Settings, userns_ownership)
 Files.PrivateUsersOwnership,  config_parse_userns_ownership, 0,                      offsetof(Settings, userns_ownership)
 Files.BindUser,               config_parse_bind_user,      0,                        offsetof(Settings, bind_user)
+Files.BindUserShell,          config_parse_bind_user_shell, 0,                       0
 Network.Private,              config_parse_tristate,       0,                        offsetof(Settings, private_network)
 Network.Interface,            config_parse_network_iface_pair, 0,                    offsetof(Settings, network_interfaces)
 Network.MACVLAN,              config_parse_macvlan_iface_pair, 0,                    offsetof(Settings, network_macvlan)
index 035796994c2a1ce007ab92c0809731b9307b47d6..f16a3b61b327fdbfeb0ddb0fb93ecb5c20c92b02 100644 (file)
@@ -13,6 +13,7 @@
 #include "nspawn-network.h"
 #include "nspawn-settings.h"
 #include "parse-util.h"
+#include "path-util.h"
 #include "process-util.h"
 #include "rlimit-util.h"
 #include "socket-util.h"
@@ -138,6 +139,7 @@ Settings* settings_free(Settings *s) {
         free(s->hostname);
         cpu_set_done(&s->cpu_set);
         strv_free(s->bind_user);
+        free(s->bind_user_shell);
 
         strv_free(s->network_interfaces);
         strv_free(s->network_macvlan);
@@ -1000,3 +1002,68 @@ int config_parse_bind_user(
 
         return 0;
 }
+
+int parse_bind_user_shell(const char *s, char **ret_sh, bool *ret_copy) {
+        char *sh;
+        int r;
+
+        if (path_is_absolute(s) && path_is_normalized(s)) {
+                sh = strdup(s);
+                if (!sh)
+                        return -ENOMEM;
+
+                *ret_sh = sh;
+                *ret_copy = false;
+        } else {
+                r = parse_boolean(s);
+                if (r < 0)
+                        return r;
+
+                *ret_sh = NULL;
+                *ret_copy = r;
+        }
+
+        return 0;
+}
+
+int config_parse_bind_user_shell(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        Settings *settings = ASSERT_PTR(data);
+        char *sh = NULL;
+        bool copy = false;
+        int r;
+
+        assert(rvalue);
+
+        if (isempty(rvalue)) {
+                settings->bind_user_shell = mfree(settings->bind_user_shell);
+                settings->bind_user_shell_copy = false;
+                settings->bind_user_shell_set = false;
+
+                return 0;
+        }
+
+        r = parse_bind_user_shell(rvalue, &sh, &copy);
+        if (r == -ENOMEM)
+                return log_oom();
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r, "Failed to parse BindUserShell= value, ignoring: %s", rvalue);
+                return 0;
+        }
+
+        free_and_replace(settings->bind_user_shell, sh);
+        settings->bind_user_shell_copy = copy;
+        settings->bind_user_shell_set = true;
+
+        return 0;
+}
index 607db87feb382859a4408791de99c0a83e1c9acc..56a65d5a21a48e8df1cfde577b4d4fe3f1b42e8d 100644 (file)
@@ -123,10 +123,11 @@ typedef enum SettingsMask {
         SETTING_CONSOLE_MODE      = UINT64_C(1) << 29,
         SETTING_CREDENTIALS       = UINT64_C(1) << 30,
         SETTING_BIND_USER         = UINT64_C(1) << 31,
-        SETTING_SUPPRESS_SYNC     = UINT64_C(1) << 32,
-        SETTING_RLIMIT_FIRST      = UINT64_C(1) << 33, /* we define one bit per resource limit here */
-        SETTING_RLIMIT_LAST       = UINT64_C(1) << (33 + _RLIMIT_MAX - 1),
-        _SETTINGS_MASK_ALL        = (UINT64_C(1) << (33 + _RLIMIT_MAX)) -1,
+        SETTING_BIND_USER_SHELL   = UINT64_C(1) << 32,
+        SETTING_SUPPRESS_SYNC     = UINT64_C(1) << 33,
+        SETTING_RLIMIT_FIRST      = UINT64_C(1) << 34, /* we define one bit per resource limit here */
+        SETTING_RLIMIT_LAST       = UINT64_C(1) << (34 + _RLIMIT_MAX - 1),
+        _SETTINGS_MASK_ALL        = (UINT64_C(1) << (34 + _RLIMIT_MAX)) -1,
         _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX
 } SettingsMask;
 
@@ -195,6 +196,9 @@ typedef struct Settings {
         size_t n_custom_mounts;
         UserNamespaceOwnership userns_ownership;
         char **bind_user;
+        char *bind_user_shell;
+        bool bind_user_shell_copy;
+        bool bind_user_shell_set;
 
         /* [Network] */
         int private_network;
@@ -270,6 +274,9 @@ CONFIG_PARSER_PROTOTYPE(config_parse_timezone_mode);
 CONFIG_PARSER_PROTOTYPE(config_parse_userns_chown);
 CONFIG_PARSER_PROTOTYPE(config_parse_userns_ownership);
 CONFIG_PARSER_PROTOTYPE(config_parse_bind_user);
+CONFIG_PARSER_PROTOTYPE(config_parse_bind_user_shell);
+
+int parse_bind_user_shell(const char *s, char **ret_sh, bool *ret_copy);
 
 const char* resolv_conf_mode_to_string(ResolvConfMode a) _const_;
 ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_;
index 92e2c5ec20c7ca22280365033ce104fbe90fc6cb..27d26f980953da4f05101e709d9543eaa51d487e 100644 (file)
@@ -240,6 +240,8 @@ static char **arg_sysctl = NULL;
 static ConsoleMode arg_console_mode = _CONSOLE_MODE_INVALID;
 static MachineCredentialContext arg_credentials = {};
 static char **arg_bind_user = NULL;
+static char *arg_bind_user_shell = NULL;
+static bool arg_bind_user_shell_copy = false;
 static bool arg_suppress_sync = false;
 static char *arg_settings_filename = NULL;
 static Architecture arg_architecture = _ARCHITECTURE_INVALID;
@@ -282,6 +284,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done);
 STATIC_DESTRUCTOR_REGISTER(arg_cpu_set, cpu_set_done);
 STATIC_DESTRUCTOR_REGISTER(arg_sysctl, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_bind_user, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_bind_user_shell, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_settings_filename, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
@@ -692,6 +695,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_SET_CREDENTIAL,
                 ARG_LOAD_CREDENTIAL,
                 ARG_BIND_USER,
+                ARG_BIND_USER_SHELL,
                 ARG_SUPPRESS_SYNC,
                 ARG_IMAGE_POLICY,
                 ARG_BACKGROUND,
@@ -769,6 +773,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "set-credential",         required_argument, NULL, ARG_SET_CREDENTIAL         },
                 { "load-credential",        required_argument, NULL, ARG_LOAD_CREDENTIAL        },
                 { "bind-user",              required_argument, NULL, ARG_BIND_USER              },
+                { "bind-user-shell",        required_argument, NULL, ARG_BIND_USER_SHELL        },
                 { "suppress-sync",          required_argument, NULL, ARG_SUPPRESS_SYNC          },
                 { "image-policy",           required_argument, NULL, ARG_IMAGE_POLICY           },
                 { "background",             required_argument, NULL, ARG_BACKGROUND             },
@@ -1536,6 +1541,22 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_settings_mask |= SETTING_BIND_USER;
                         break;
 
+                case ARG_BIND_USER_SHELL: {
+                        bool copy = false;
+                        char *sh = NULL;
+                        r = parse_bind_user_shell(optarg, &sh, &copy);
+                        if (r == -ENOMEM)
+                                return log_oom();
+                        if (r < 0)
+                                return log_error_errno(r, "Invalid user shell to bind: %s", optarg);
+
+                        free_and_replace(arg_bind_user_shell, sh);
+                        arg_bind_user_shell_copy = copy;
+
+                        arg_settings_mask |= SETTING_BIND_USER_SHELL;
+                        break;
+                }
+
                 case ARG_SUPPRESS_SYNC:
                         r = parse_boolean_argument("--suppress-sync=", optarg, &arg_suppress_sync);
                         if (r < 0)
@@ -1722,6 +1743,9 @@ static int verify_arguments(void) {
         /* Drop duplicate --bind-user= entries */
         strv_uniq(arg_bind_user);
 
+        if (arg_bind_user_shell && strv_isempty(arg_bind_user))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use --bind-user-shell= without --bind-user=");
+
         r = custom_mount_check_all();
         if (r < 0)
                 return r;
@@ -4022,6 +4046,8 @@ static int outer_child(
         r = bind_user_prepare(
                         directory,
                         arg_bind_user,
+                        arg_bind_user_shell,
+                        arg_bind_user_shell_copy,
                         chown_uid,
                         chown_range,
                         &arg_custom_mounts, &arg_n_custom_mounts,
@@ -4841,6 +4867,12 @@ static int merge_settings(Settings *settings, const char *path) {
             !strv_isempty(settings->bind_user))
                 strv_free_and_replace(arg_bind_user, settings->bind_user);
 
+        if (!FLAGS_SET(arg_settings_mask, SETTING_BIND_USER_SHELL) &&
+            settings->bind_user_shell_set) {
+                free_and_replace(arg_bind_user_shell, settings->bind_user_shell);
+                arg_bind_user_shell_copy = settings->bind_user_shell_copy;
+        }
+
         if ((arg_settings_mask & SETTING_NOTIFY_READY) == 0 &&
             settings->notify_ready >= 0)
                 arg_notify_ready = settings->notify_ready;
index ffe5e8507dd6d049fc17eda5007dc1f13b1ad0b5..a0fbb3637d6973e54595af2ce437eb949b596ea0 100755 (executable)
@@ -580,6 +580,151 @@ testcase_bind_user() {
     rm -fr "$root"
 }
 
+testcase_bind_user_shell() {
+    local root
+
+    root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.bind-user.XXX)"
+    create_dummy_container "$root"
+    useradd --create-home --user-group --shell=/usr/bin/bash nspawn-bind-user-1
+    useradd --create-home --user-group --shell=/usr/bin/sh   nspawn-bind-user-2
+    trap bind_user_cleanup RETURN
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user'
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user-shell=no \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user'
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user-shell=yes \
+                   bash -xec 'grep -q "\"shell\":\"/usr/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user'
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user-shell=/bin/bash \
+                   bash -xec 'grep -q "\"shell\":\"/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user'
+
+    (! systemd-nspawn --directory="$root" \
+                      --private-users=pick \
+                      --bind-user=nspawn-bind-user-1 \
+                      --bind-user-shell=bad-argument \
+                      bash -xec 'true')
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user=nspawn-bind-user-2 \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-1.user && grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=yes \
+                   bash -xec 'grep -q "\"shell\":\"/usr/bin/bash\"" /run/host/userdb/nspawn-bind-user-1.user && grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-1 \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=/bin/sh \
+                   bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-1.user && grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    mach=$(basename "$root")
+    mkdir -p /run/systemd/nspawn
+    conf=/run/systemd/nspawn/"$mach".nspawn
+
+    cat <<'EOF' >"$conf"
+# [Files]
+# BindUserShell=no by default
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-2 \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=no
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-2 \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=yes
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-2 \
+                   bash -xec 'grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=/bin/sh
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --bind-user=nspawn-bind-user-2 \
+                   bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+# [Files]
+# BindUserShell=no default doesn't override
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --settings=override \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=yes \
+                   bash -xec 'grep -q "\"shell\":\"/usr/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=no
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --settings=override \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=yes \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=no
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --settings=override \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=/foo \
+                   bash -xec 'grep -qv "\"shell\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    cat <<'EOF' >"$conf"
+[Files]
+BindUserShell=/bin/sh
+EOF
+    systemd-nspawn --directory="$root" \
+                   --private-users=pick \
+                   --settings=override \
+                   --bind-user=nspawn-bind-user-2 \
+                   --bind-user-shell=yes \
+                   bash -xec 'grep -q "\"shell\":\"/bin/sh\"" /run/host/userdb/nspawn-bind-user-2.user'
+
+    rm -fr "$root"
+}
+
 testcase_bind_tmp_path() {
     # https://github.com/systemd/systemd/issues/4789
     local root