From 92a0761d553c207012ad17c9d55ec58ddabd9bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 5 Feb 2026 11:09:59 +0100 Subject: [PATCH 01/10] core/exec-invoke: add missing char in comment --- src/core/exec-invoke.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 315ef41031..b588eb232e 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -2089,7 +2089,7 @@ static int build_environment( /* If this is D-Bus, tell the nss-systemd module, since it relies on being able to use blocking * Varlink calls back to us for look up dynamic users in PID 1. Break the deadlock between D-Bus and - * PID 1 by disabling use of PID1' NSS interface for looking up dynamic users. */ + * PID 1 by disabling use of PID1's NSS interface for looking up dynamic users. */ if (p->flags & EXEC_NSS_DYNAMIC_BYPASS) { r = strv_extend_with_size(&e, &n, "SYSTEMD_NSS_DYNAMIC_BYPASS=1"); if (r < 0) @@ -2097,8 +2097,8 @@ static int build_environment( } /* We query "root" if this is a system unit and User= is not specified. $USER is always set. $HOME - * could cause problem for e.g. getty, since login doesn't override $HOME, and $LOGNAME and $SHELL don't - * really make much sense since we're not logged in. Hence we conditionalize the three based on + * could cause problem for e.g. getty, since login doesn't override $HOME, and $LOGNAME and $SHELL + * don't really make much sense since we're not logged in. Hence we conditionalize the three based on * SetLoginEnvironment= switch. */ if (!username && !c->dynamic_user && p->runtime_scope == RUNTIME_SCOPE_SYSTEM) { assert(!c->user); From 457bd2345c35627b1eca036b95029f50b2042142 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 24 Feb 2026 23:01:12 +0100 Subject: [PATCH 02/10] core/execute: do not suppress ExecSharedRuntime if userns path is specified Follow-up for 79dd24cf14adc809620479d45a7b469cf3e82892 --- src/core/execute.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/execute.c b/src/core/execute.c index 6a7fc65f20..50865a2a06 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -2437,7 +2437,8 @@ static int exec_shared_runtime_make( assert(id); /* It is not necessary to create ExecSharedRuntime object. */ - if (!exec_needs_network_namespace(c) && !exec_needs_ipc_namespace(c) && c->private_tmp != PRIVATE_TMP_CONNECTED) { + if (!c->user_namespace_path && !exec_needs_network_namespace(c) && !exec_needs_ipc_namespace(c) && + c->private_tmp != PRIVATE_TMP_CONNECTED) { *ret = NULL; return 0; } From 2e71d0a2bdcae7b7767041c1de4d5672fb347813 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 24 Feb 2026 23:33:24 +0100 Subject: [PATCH 03/10] core/unit: refuse UserNamespacePath= + PrivateUsers= Such setup makes zero sense, as if we unshare userns ourselves we'd immediately lose CAP_SYS_ADMIN over the userns specified. --- src/core/unit.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/unit.c b/src/core/unit.c index 1995bb7f8e..5d3282e121 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -4329,14 +4329,17 @@ static int unit_verify_contexts(const Unit *u) { if (exec_needs_pid_namespace(ec, /* params= */ NULL) && !UNIT_VTABLE(u)->notify_pidref) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivatePIDs= setting is only supported for service units. Refusing."); + if ((ec->user || ec->dynamic_user || ec->group || ec->pam_name) && ec->private_users == PRIVATE_USERS_MANAGED) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=/Group=/PAMName=, refusing."); + + if (ec->user_namespace_path && ec->private_users != PRIVATE_USERS_NO) + return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers= may not be used with custom UserNamespacePath=, refusing."); + const KillContext *kc = unit_get_kill_context(u); if (ec->pam_name && kc && !IN_SET(kc->kill_mode, KILL_CONTROL_GROUP, KILL_MIXED)) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "Unit has PAM enabled. Kill mode must be set to 'control-group' or 'mixed'. Refusing."); - if ((ec->user || ec->dynamic_user || ec->group || ec->pam_name) && ec->private_users == PRIVATE_USERS_MANAGED) - return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "PrivateUsers=managed may not be used in combination with User=/DynamicUser=/Group=/PAMName=, refusing."); - return 0; } From a45fb074fedb488f5d16a0a3a50bd0e866453756 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 25 Feb 2026 09:26:44 +0100 Subject: [PATCH 04/10] core/namespace: rename() might return ENOTEMPTY if target dir exists --- src/core/namespace.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index d363ac0973..aa80570885 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -3338,6 +3338,8 @@ static int make_tmp_prefix(const char *prefix) { _cleanup_close_ int fd = -EBADF; int r; + assert(prefix); + /* Don't do anything unless we know the dir is actually missing */ r = access(prefix, F_OK); if (r >= 0) @@ -3369,7 +3371,7 @@ static int make_tmp_prefix(const char *prefix) { r = RET_NERRNO(rename(t, prefix)); if (r < 0) { (void) rmdir(t); - return r == -EEXIST ? 0 : r; /* it's fine if someone else created the dir by now */ + return IN_SET(r, -EEXIST, -ENOTEMPTY) ? 0 : r; /* it's fine if someone else created the dir by now */ } return 0; From e438e5923d60440b643560799e41ea3092ae5cbe Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 25 Feb 2026 09:23:50 +0100 Subject: [PATCH 05/10] core/namespace: two fixes for namespace_cleanup_tmpdir() * Make sure we're not passing NULL to rmdir() * Remove the inner "tmp" subdir as well, so that callers can handle this transparently (e.g. exec_shared_runtime_make() is not aware of the nested rmdir() requirements) While at it, remove unneeded PROTECT_ERRNO. --- src/core/namespace.c | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index aa80570885..b97eabd51b 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -3333,6 +3333,23 @@ int temporary_filesystem_add( return 0; } +char* namespace_cleanup_tmpdir(char *p) { + if (!p) + return NULL; + + if (!streq(p, RUN_SYSTEMD_EMPTY)) { + _cleanup_free_ char *child = path_join(p, "tmp"); + if (!child) + log_oom_debug(); + else + (void) rmdir(child); + + (void) rmdir(p); + } + + return mfree(p); +} + static int make_tmp_prefix(const char *prefix) { _cleanup_free_ char *t = NULL; _cleanup_close_ int fd = -EBADF; @@ -3444,13 +3461,6 @@ static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, ch return 0; } -char* namespace_cleanup_tmpdir(char *p) { - PROTECT_ERRNO; - if (!streq_ptr(p, RUN_SYSTEMD_EMPTY)) - (void) rmdir(p); - return mfree(p); -} - int setup_tmp_dirs(const char *id, char **tmp_dir, char **var_tmp_dir) { _cleanup_(namespace_cleanup_tmpdirp) char *a = NULL; _cleanup_(rmdir_and_freep) char *a_tmp = NULL; From 0d10a125f6047016a63003a9607243da81838c74 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 24 Feb 2026 23:08:40 +0100 Subject: [PATCH 06/10] core/execute: make exec_shared_runtime_make() handle distinct PrivateTmp= modes properly --- src/core/execute.c | 19 ++++++++---- src/core/namespace.c | 65 +++++++++++++-------------------------- src/core/namespace.h | 5 +-- src/test/test-namespace.c | 3 +- 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/src/core/execute.c b/src/core/execute.c index 50865a2a06..439f4b7b41 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -2394,7 +2394,6 @@ static int exec_shared_runtime_add( if (r < 0) return r; - assert(!!rt->tmp_dir == !!rt->var_tmp_dir); /* We require both to be set together */ rt->tmp_dir = TAKE_PTR(*tmp_dir); rt->var_tmp_dir = TAKE_PTR(*var_tmp_dir); @@ -2438,16 +2437,24 @@ static int exec_shared_runtime_make( /* It is not necessary to create ExecSharedRuntime object. */ if (!c->user_namespace_path && !exec_needs_network_namespace(c) && !exec_needs_ipc_namespace(c) && - c->private_tmp != PRIVATE_TMP_CONNECTED) { + c->private_tmp != PRIVATE_TMP_CONNECTED && c->private_var_tmp != PRIVATE_TMP_CONNECTED) { *ret = NULL; return 0; } if (c->private_tmp == PRIVATE_TMP_CONNECTED && - !(prefixed_path_strv_contains(c->inaccessible_paths, "/tmp") && - (prefixed_path_strv_contains(c->inaccessible_paths, "/var/tmp") || - prefixed_path_strv_contains(c->inaccessible_paths, "/var")))) { - r = setup_tmp_dirs(id, &tmp_dir, &var_tmp_dir); + !prefixed_path_strv_contains(c->inaccessible_paths, "/tmp")) { + + r = setup_tmp_dir_one(id, "/tmp", &tmp_dir); + if (r < 0) + return r; + } + + if (c->private_var_tmp == PRIVATE_TMP_CONNECTED && + !prefixed_path_strv_contains(c->inaccessible_paths, "/var/tmp") && + !prefixed_path_strv_contains(c->inaccessible_paths, "/var")) { + + r = setup_tmp_dir_one(id, "/var/tmp", &var_tmp_dir); if (r < 0) return r; } diff --git a/src/core/namespace.c b/src/core/namespace.c index b97eabd51b..f899fa241c 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -3394,16 +3394,15 @@ static int make_tmp_prefix(const char *prefix) { return 0; } -static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, char **tmp_path) { - _cleanup_free_ char *x = NULL; - _cleanup_free_ char *y = NULL; +int setup_tmp_dir_one(const char *id, const char *prefix, char **ret_path) { + _cleanup_free_ char *d = NULL; sd_id128_t boot_id; bool rw = true; int r; assert(id); assert(prefix); - assert(path); + assert(ret_path); /* We include the boot id in the directory so that after a * reboot we can easily identify obsolete directories. */ @@ -3412,8 +3411,8 @@ static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, ch if (r < 0) return r; - x = strjoin(prefix, "/systemd-private-", SD_ID128_TO_STRING(boot_id), "-", id, "-XXXXXX"); - if (!x) + d = strjoin(prefix, "/systemd-private-", SD_ID128_TO_STRING(boot_id), "-", id, "-XXXXXX"); + if (!d) return -ENOMEM; r = make_tmp_prefix(prefix); @@ -3421,7 +3420,7 @@ static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, ch return r; WITH_UMASK(0077) - if (!mkdtemp(x)) { + if (!mkdtemp(d)) { if (errno == EROFS || ERRNO_IS_DISK_SPACE(errno)) rw = false; else @@ -3429,20 +3428,25 @@ static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, ch } if (rw) { - y = strjoin(x, "/tmp"); - if (!y) + _cleanup_free_ char *inner_dir = path_join(d, "tmp"); + if (!inner_dir) { + (void) rmdir(d); return -ENOMEM; + } WITH_UMASK(0000) - if (mkdir(y, 0777 | S_ISVTX) < 0) - return -errno; - - r = label_fix_full(AT_FDCWD, y, prefix, 0); - if (r < 0) + r = RET_NERRNO(mkdir(inner_dir, 0777 | S_ISVTX)); + if (r < 0) { + (void) rmdir(d); return r; + } - if (tmp_path) - *tmp_path = TAKE_PTR(y); + r = label_fix_full(AT_FDCWD, inner_dir, prefix, 0); + if (r < 0) { + (void) rmdir(inner_dir); + (void) rmdir(d); + return r; + } } else { /* Trouble: we failed to create the directory. Instead of failing, let's simulate /tmp being * read-only. This way the service will get the EROFS result as if it was writing to the real @@ -3452,37 +3456,12 @@ static int setup_one_tmp_dir(const char *id, const char *prefix, char **path, ch if (r < 0) return r; - r = free_and_strdup(&x, RUN_SYSTEMD_EMPTY); + r = free_and_strdup(&d, RUN_SYSTEMD_EMPTY); if (r < 0) return r; } - *path = TAKE_PTR(x); - return 0; -} - -int setup_tmp_dirs(const char *id, char **tmp_dir, char **var_tmp_dir) { - _cleanup_(namespace_cleanup_tmpdirp) char *a = NULL; - _cleanup_(rmdir_and_freep) char *a_tmp = NULL; - char *b; - int r; - - assert(id); - assert(tmp_dir); - assert(var_tmp_dir); - - r = setup_one_tmp_dir(id, "/tmp", &a, &a_tmp); - if (r < 0) - return r; - - r = setup_one_tmp_dir(id, "/var/tmp", &b, NULL); - if (r < 0) - return r; - - a_tmp = mfree(a_tmp); /* avoid rmdir */ - *tmp_dir = TAKE_PTR(a); - *var_tmp_dir = TAKE_PTR(b); - + *ret_path = TAKE_PTR(d); return 0; } diff --git a/src/core/namespace.h b/src/core/namespace.h index 318836651c..f5d792dd77 100644 --- a/src/core/namespace.h +++ b/src/core/namespace.h @@ -232,10 +232,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path); char* namespace_cleanup_tmpdir(char *p); DEFINE_TRIVIAL_CLEANUP_FUNC(char*, namespace_cleanup_tmpdir); -int setup_tmp_dirs( - const char *id, - char **tmp_dir, - char **var_tmp_dir); +int setup_tmp_dir_one(const char *id, const char *prefix, char **ret_path); int setup_shareable_ns(int ns_storage_socket[static 2], unsigned long nsflag); int open_shareable_ns_path(int netns_storage_socket[static 2], const char *path, unsigned long nsflag); diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c index 5b67cd9f96..899f8f6351 100644 --- a/src/test/test-namespace.c +++ b/src/test/test-namespace.c @@ -43,7 +43,8 @@ static void test_tmpdir_one(const char *id, const char *A, const char *B) { struct stat x, y; char *c, *d; - ASSERT_OK_ZERO(setup_tmp_dirs(id, &a, &b)); + ASSERT_OK(setup_tmp_dir_one(id, "/tmp", &a)); + ASSERT_OK(setup_tmp_dir_one(id, "/var/tmp", &b)); ASSERT_OK_ERRNO(stat(a, &x)); ASSERT_OK_ERRNO(stat(b, &y)); From 9984a35d651f5b9851ad17c3a726dd2dc7eae2b5 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 24 Feb 2026 23:11:55 +0100 Subject: [PATCH 07/10] core/execute: stop looking at ExecSharedRuntime to determine PrivateTmp= state The only reason why ExecSharedRuntime.(var_)tmp_dir might be suppressed is when (/var)/tmp/ is listed in InaccessiblePaths=, in which case mount namespace is needed anyways. Moreover, the current logic imposes a chicken-and-egg problem: at various places in pid1 we'd need to check whether the unit runs in a mountns, but the ExecSharedRuntime may not be allocated for it just yet. --- src/core/exec-invoke.c | 4 ++-- src/core/execute.c | 13 ++----------- src/core/execute.h | 2 +- src/core/service.c | 2 +- src/core/unit.c | 2 +- 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index b588eb232e..10a7b55006 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4833,7 +4833,7 @@ static int setup_delegated_namespaces( /* If PrivatePIDs= yes is configured, we're now running as pid 1 in a pid namespace! */ - if (exec_needs_mount_namespace(context, params, runtime) && + if (exec_needs_mount_namespace(context, params) && exec_namespace_is_delegated(context, params, have_cap_sys_admin, CLONE_NEWNS) == delegate) { _cleanup_free_ char *error_path = NULL; @@ -5829,7 +5829,7 @@ int exec_invoke( return log_error_errno(r, "Failed to connect to nsresourced: %m"); } - needs_mount_namespace = exec_needs_mount_namespace(context, params, runtime); + needs_mount_namespace = exec_needs_mount_namespace(context, params); for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) { r = setup_exec_directory(context, params, uid, gid, dt, needs_mount_namespace, exit_status); diff --git a/src/core/execute.c b/src/core/execute.c index 439f4b7b41..a2d148fde5 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -256,11 +256,7 @@ bool exec_needs_pid_namespace(const ExecContext *context, const ExecParameters * return context->private_pids != PRIVATE_PIDS_NO && namespace_type_supported(NAMESPACE_PID); } -bool exec_needs_mount_namespace( - const ExecContext *context, - const ExecParameters *params, - const ExecRuntime *runtime) { - +bool exec_needs_mount_namespace(const ExecContext *context, const ExecParameters *params) { assert(context); if (context->root_image || @@ -295,13 +291,8 @@ bool exec_needs_mount_namespace( if (!IN_SET(context->mount_propagation_flag, 0, MS_SHARED)) return true; - if (context->private_tmp == PRIVATE_TMP_DISCONNECTED) - return true; - - if (context->private_tmp == PRIVATE_TMP_CONNECTED && runtime && runtime->shared && (runtime->shared->tmp_dir || runtime->shared->var_tmp_dir)) - return true; - if (context->private_devices || + context->private_tmp != PRIVATE_TMP_NO || /* no need to check for private_var_tmp here, private_tmp is never demoted to "no" */ context->private_mounts > 0 || (context->private_mounts < 0 && exec_needs_network_namespace(context)) || context->protect_system != PROTECT_SYSTEM_NO || diff --git a/src/core/execute.h b/src/core/execute.h index c5f3988371..094b0a3a44 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -638,7 +638,7 @@ DECLARE_STRING_TABLE_LOOKUP(exec_resource_type, ExecDirectoryType); DECLARE_STRING_TABLE_LOOKUP(memory_thp, MemoryTHP); -bool exec_needs_mount_namespace(const ExecContext *context, const ExecParameters *params, const ExecRuntime *runtime); +bool exec_needs_mount_namespace(const ExecContext *context, const ExecParameters *params); bool exec_needs_network_namespace(const ExecContext *context); bool exec_needs_ipc_namespace(const ExecContext *context); bool exec_needs_pid_namespace(const ExecContext *context, const ExecParameters *params); diff --git a/src/core/service.c b/src/core/service.c index b9efd9bdb9..51bba291e8 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -5800,7 +5800,7 @@ static int service_can_live_mount(Unit *u, sd_bus_error *reterr_error) { Service *s = ASSERT_PTR(SERVICE(u)); /* Ensure that the unit runs in a private mount namespace */ - if (!exec_needs_mount_namespace(&s->exec_context, /* params= */ NULL, s->exec_runtime)) + if (!exec_needs_mount_namespace(&s->exec_context, /* params= */ NULL)) return sd_bus_error_setf( reterr_error, SD_BUS_ERROR_INVALID_ARGS, diff --git a/src/core/unit.c b/src/core/unit.c index 5d3282e121..bf4542c080 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -4323,7 +4323,7 @@ static int unit_verify_contexts(const Unit *u) { return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "WorkingDirectory=~ is not allowed under DynamicUser=yes. Refusing."); if (ec->working_directory && path_below_api_vfs(ec->working_directory) && - exec_needs_mount_namespace(ec, /* params= */ NULL, /* runtime= */ NULL)) + exec_needs_mount_namespace(ec, /* params= */ NULL)) return log_unit_error_errno(u, SYNTHETIC_ERRNO(ENOEXEC), "WorkingDirectory= may not be below /proc/, /sys/ or /dev/ when using mount namespacing. Refusing."); if (exec_needs_pid_namespace(ec, /* params= */ NULL) && !UNIT_VTABLE(u)->notify_pidref) From fa33eef344373ca5c8acd6c24270454f05672534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 18 Nov 2025 13:51:41 +0100 Subject: [PATCH 08/10] core: upgrade /tmp when PrivateTmp=yes/DefaultDeps=no to disconnected In https://github.com/systemd/systemd/issues/28515, multiple people report that services that have PrivateTmp=yes and DefaultDependencies=no fail to create the temporary directories under /tmp, when /tmp is e.g. a bind mount or some other kind of mount that takes more time. Before PrivateTmp=disconnected was added, we didn't have a nice solution: DefaultDependencies=no is used to start services very early, so we wouldn't want to add a dependency on /tmp automatically. With PrivateTmp=disconnected we have a fairly nice solution. Let's "upgrade" to this mode automatically. Strictly speaking, it is a small compat break, but in practice it's unlikely to matter for early-boot services whether their /tmp is private or disconnected. The dependency on /tmp that is checked is After. I think this is enough, since any tmp.mount would be pulled in by local-fs.target and the rest of the transaction anyway, so we don't need to check more than After. The asserts are relaxed, because now the two settings can now diverge in either way. Resolves https://github.com/systemd/systemd/issues/28515. [yhndnzj: fix unit_add_exec_dependencies() to handle the new combination; add a comment in exec_needs_sys_admin()] --- man/systemd.exec.xml | 11 ++++++-- src/core/exec-invoke.c | 12 ++++---- src/core/execute.h | 6 ++-- src/core/namespace.c | 4 +-- src/core/unit.c | 62 ++++++++++++++++++++++++++++++++++++------ 5 files changed, 72 insertions(+), 23 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index c89adad66a..3f06f564b3 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -63,10 +63,15 @@ Units with PrivateTmp=yes automatically gain dependencies of type Wants= and After= on all mounts required to access - /tmp/ and /var/tmp/. They will also gain an automatic + /tmp/ and /var/tmp/ and an automatic After= dependency on - systemd-tmpfiles-setup.service8. - + systemd-tmpfiles-setup.service8, + unless DefaultDependencies=no is specified. If + DefaultDependencies=no is specified, and a + RequiresMountsFor=/tmp/, WantsMountsFor=/tmp/, + After=tmp.mount, or RootDirectory=/RootImage= + are not specified, PrivateTmp=yes is converted to + PrivateTmp=disconnected. Units with PrivateTmp=disconnected automatically gain dependencies of type Wants= and After= on the mount required to access diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 10a7b55006..25c12ece25 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -2248,10 +2248,8 @@ static int build_environment( } assert(c->private_var_tmp >= 0 && c->private_var_tmp < _PRIVATE_TMP_MAX); - if (needs_sandboxing && c->private_tmp != c->private_var_tmp) { - assert(c->private_tmp == PRIVATE_TMP_DISCONNECTED); - assert(c->private_var_tmp == PRIVATE_TMP_NO); - + if (needs_sandboxing && + c->private_var_tmp == PRIVATE_TMP_NO && c->private_tmp != PRIVATE_TMP_NO) { /* When private tmpfs is enabled only on /tmp/, then explicitly set $TMPDIR to suggest the * service to use /tmp/. */ @@ -3907,14 +3905,16 @@ static int apply_mount_namespace( if (needs_sandboxing) { /* The runtime struct only contains the parent of the private /tmp, which is non-accessible * to world users. Inside of it there's a /tmp that is sticky, and that's the one we want to - * use here. This does not apply when we are using /run/systemd/empty as fallback. */ + * use here. This does not apply when we are using /run/systemd/empty as fallback. */ if (context->private_tmp == PRIVATE_TMP_CONNECTED && runtime->shared) { if (streq_ptr(runtime->shared->tmp_dir, RUN_SYSTEMD_EMPTY)) tmp_dir = runtime->shared->tmp_dir; else if (runtime->shared->tmp_dir) tmp_dir = strjoina(runtime->shared->tmp_dir, "/tmp"); + } + if (context->private_var_tmp == PRIVATE_TMP_CONNECTED && runtime->shared) { if (streq_ptr(runtime->shared->var_tmp_dir, RUN_SYSTEMD_EMPTY)) var_tmp_dir = runtime->shared->var_tmp_dir; else if (runtime->shared->var_tmp_dir) @@ -4624,7 +4624,7 @@ static bool exec_needs_cap_sys_admin(const ExecContext *context, const ExecParam return false; return context->private_users != PRIVATE_USERS_NO || - context->private_tmp != PRIVATE_TMP_NO || + context->private_tmp != PRIVATE_TMP_NO || /* no need to check for private_var_tmp here, private_tmp is never demoted to "no" */ context->private_devices || context->private_network || context->user_namespace_path || diff --git a/src/core/execute.h b/src/core/execute.h index 094b0a3a44..3d5e92cccf 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -333,9 +333,9 @@ typedef struct ExecContext { int bind_log_sockets; int memory_ksm; MemoryTHP memory_thp; - PrivateTmp private_tmp; - PrivateTmp private_var_tmp; /* This is not an independent parameter, but calculated from other - * parameters in unit_patch_contexts(). */ + PrivateTmp private_tmp; /* Those are not independent parameters, but are calculated from */ + PrivateTmp private_var_tmp; /* other parameters in unit_patch_contexts(). */ + bool private_network; bool private_devices; PrivateUsers private_users; diff --git a/src/core/namespace.c b/src/core/namespace.c index f899fa241c..54504564ab 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -756,8 +756,8 @@ static int append_private_tmp(MountList *ml, const NamespaceParameters *p) { assert(ml); assert(p); - assert(p->private_tmp == p->private_var_tmp || - (p->private_tmp == PRIVATE_TMP_DISCONNECTED && p->private_var_tmp == PRIVATE_TMP_NO)); + assert(p->private_tmp >= 0 && p->private_tmp < _PRIVATE_TMP_MAX); + assert(p->private_var_tmp >= 0 && p->private_var_tmp < _PRIVATE_TMP_MAX); if (p->tmp_dir) { assert(p->private_tmp == PRIVATE_TMP_CONNECTED); diff --git a/src/core/unit.c b/src/core/unit.c index bf4542c080..dc158fb335 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -1310,20 +1310,15 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { assert(c->private_var_tmp >= 0 && c->private_var_tmp < _PRIVATE_TMP_MAX); if (c->private_tmp == PRIVATE_TMP_CONNECTED) { - assert(c->private_var_tmp == PRIVATE_TMP_CONNECTED); - r = unit_add_mounts_for(u, "/tmp/", UNIT_DEPENDENCY_FILE, UNIT_MOUNT_WANTS); if (r < 0) return r; + } + if (c->private_var_tmp == PRIVATE_TMP_CONNECTED) { r = unit_add_mounts_for(u, "/var/tmp/", UNIT_DEPENDENCY_FILE, UNIT_MOUNT_WANTS); if (r < 0) return r; - - r = unit_add_dependency_by_name(u, UNIT_AFTER, SPECIAL_TMPFILES_SETUP_SERVICE, true, UNIT_DEPENDENCY_FILE); - if (r < 0) - return r; - } else if (c->private_var_tmp == PRIVATE_TMP_DISCONNECTED && !exec_context_with_rootfs(c)) { /* Even if PrivateTmp=disconnected, we still require /var/tmp/ mountpoint to be present, * i.e. /var/ needs to be mounted. See comments in unit_patch_contexts(). */ @@ -1332,6 +1327,12 @@ int unit_add_exec_dependencies(Unit *u, ExecContext *c) { return r; } + if (c->private_tmp == PRIVATE_TMP_CONNECTED || c->private_var_tmp == PRIVATE_TMP_CONNECTED) { + r = unit_add_dependency_by_name(u, UNIT_AFTER, SPECIAL_TMPFILES_SETUP_SERVICE, true, UNIT_DEPENDENCY_FILE); + if (r < 0) + return r; + } + if (c->root_image || c->root_mstack) { /* We need to wait for /dev/loopX to appear when doing RootImage=, hence let's add an * implicit dependency on udev. (And for RootMStack= we might need it) */ @@ -4349,8 +4350,8 @@ static PrivateTmp unit_get_private_var_tmp(const Unit *u, const ExecContext *c) assert(c->private_tmp >= 0 && c->private_tmp < _PRIVATE_TMP_MAX); /* Disable disconnected private tmpfs on /var/tmp/ when DefaultDependencies=no and - * RootImage=/RootDirectory= are not set, as /var/ may be a separated partition. - * See issue #37258. */ + * RootImage=/RootDirectory= are not set, as /var/ may be a separate partition. + * See https://github.com/systemd/systemd/issues/37258. */ /* PrivateTmp=yes/no also enables/disables private tmpfs on /var/tmp/. */ if (c->private_tmp != PRIVATE_TMP_DISCONNECTED) @@ -4388,6 +4389,42 @@ static PrivateTmp unit_get_private_var_tmp(const Unit *u, const ExecContext *c) return PRIVATE_TMP_NO; } +static PrivateTmp unit_get_private_tmp(const Unit *u, const ExecContext *c) { + assert(u); + assert(c); + assert(c->private_tmp >= 0 && c->private_tmp < _PRIVATE_TMP_MAX); + + /* Upgrade "PrivateTmp=yes" (a.k.a. 'connected') to 'disconnected' when + * DefaultDependencies=no and RootImage=/RootDirectory= are not set, as /tmp/ may be a + * separate partition. See https://github.com/systemd/systemd/issues/28515. + * + * Note that the change goes in the opposite direction than unit_get_private_var_tmp() + * above. For /var/tmp/, we need to disable the setting, because we don't want to create + * the /var/tmp/ directory if /var/ is a mount point. We don't have this problem with + * /tmp/ because there is no nesting. */ + + if (c->private_tmp != PRIVATE_TMP_CONNECTED || + u->default_dependencies || + exec_context_with_rootfs(c)) + return c->private_tmp; + + /* Even if DefaultDependencies=no, honour tmpfs setting when + * RequiresMountsFor=/WantsMountsFor=/tmp/ is explicitly set. */ + for (UnitMountDependencyType t = 0; t < _UNIT_MOUNT_DEPENDENCY_TYPE_MAX; t++) + if (hashmap_contains(u->mounts_for[t], "/tmp/")) + return c->private_tmp; + + /* Check the same but for After=. */ + Unit *m = manager_get_unit(u->manager, "tmp.mount"); + if (!m) + return c->private_tmp; + + if (unit_has_dependency(u, UNIT_ATOM_AFTER, m)) + return c->private_tmp; + + return PRIVATE_TMP_DISCONNECTED; +} + int unit_patch_contexts(Unit *u) { CGroupContext *cc; ExecContext *ec; @@ -4464,7 +4501,14 @@ int unit_patch_contexts(Unit *u) { ec->restrict_suid_sgid = true; } + /* Table of possible combinations: + * /var/tmp /tmp + * PrivateTmp=no no no + * PrivateTmp=connected connected connected,disconnected + * PrivateTmp=disconnected disconnected,no disconnected + */ ec->private_var_tmp = unit_get_private_var_tmp(u, ec); + ec->private_tmp = unit_get_private_tmp(u, ec); FOREACH_ARRAY(d, ec->directories, _EXEC_DIRECTORY_TYPE_MAX) exec_directory_sort(d); From 0a17bb5c343acd3d75df22499d0cf2fce458d140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Tue, 18 Nov 2025 15:12:55 +0100 Subject: [PATCH 09/10] core: simplify requirements in unit_get_private_var_tmp() to just After= As in the previous commit, checking for both requirements and ordering seems unnecessary. In practical cases, the mount will be pulled in by the rest of the transaction, so ordering is the part that matters. (The setup is racy without the ordering.) If we drop the second check, the admin can just use After=tmp.mount to achieve the desired behaviour, without needing to explicitly pull in the unit. This is easier to configure and more robust. This changes the implementation introduced in 6156bec7a464815084fa5218fe782ea6cb20ad52. Also actually describe the implemented behaviour in the man page. --- man/systemd.exec.xml | 8 +++++++- src/core/unit.c | 12 ++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 3f06f564b3..e7d5e63c96 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -76,7 +76,13 @@ Units with PrivateTmp=disconnected automatically gain dependencies of type Wants= and After= on the mount required to access /var/, unless DefaultDependencies=no and/or - RootDirectory=/RootImage= are specified. + RootDirectory=/RootImage= are specified. If + DefaultDependencies=no is specified, and a + RequiresMountsFor=/var/, WantsMountsFor=/var/, + After=var.mount, RootDirectory=/RootImage= are + not specified, the private mount on /tmp/ is reused for + /var/tmp/ by setting $TMPDIR appropriately. + Units whose standard output or error output is connected to or (or their combinations with console output, see below) automatically acquire diff --git a/src/core/unit.c b/src/core/unit.c index dc158fb335..bb3430186c 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -4373,17 +4373,9 @@ static PrivateTmp unit_get_private_var_tmp(const Unit *u, const ExecContext *c) if (hashmap_contains(u->mounts_for[t], "/var/")) return PRIVATE_TMP_DISCONNECTED; - /* Check the same but for After= with Requires=/Requisite=/Wants= or friends. */ + /* Check the same but for After=. */ Unit *m = manager_get_unit(u->manager, "var.mount"); - if (!m) - return PRIVATE_TMP_NO; - - if (!unit_has_dependency(u, UNIT_ATOM_AFTER, m)) - return PRIVATE_TMP_NO; - - if (unit_has_dependency(u, UNIT_ATOM_PULL_IN_START, m) || - unit_has_dependency(u, UNIT_ATOM_PULL_IN_VERIFY, m) || - unit_has_dependency(u, UNIT_ATOM_PULL_IN_START_IGNORED, m)) + if (m && unit_has_dependency(u, UNIT_ATOM_AFTER, m)) return PRIVATE_TMP_DISCONNECTED; return PRIVATE_TMP_NO; From 2b67122cdf931bb91d7bf27f6f71e7173d1c5288 Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Tue, 24 Feb 2026 19:40:14 +0100 Subject: [PATCH 10/10] core/namespace: clean up append_private_tmp() a bit With the previous changes the modes for /tmp/ and /var/tmp/ may come in arbitrary directions, i.e. we may downgrade one and upgrade another. Hence let's refactor the setup logic a bit so they're truly independent. --- src/core/namespace.c | 94 +++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 54504564ab..9727d725eb 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -751,59 +751,71 @@ static int append_tmpfs_mounts(MountList *ml, const TemporaryFileSystem *tmpfs, return 0; } +static int append_private_tmp_one( + MountList *ml, + PrivateTmp mode, + const char *path, + const char *connected_source) { + + assert(ml); + assert(mode >= 0 && mode < _PRIVATE_TMP_MAX); + assert(path); + + if (mode == PRIVATE_TMP_NO) + return 0; + + if (mode == PRIVATE_TMP_CONNECTED && !connected_source) + /* Do nothing if the private tmp dir was suppressed as it would be made inaccessible anyways + * (see exec_shared_runtime_make()). */ + return 0; + + MountEntry *me = mount_list_extend(ml); + if (!me) + return log_oom_debug(); + + if (mode == PRIVATE_TMP_CONNECTED) + *me = (MountEntry) { + .path_const = path, + .mode = MOUNT_PRIVATE_TMP, + .read_only = streq(connected_source, RUN_SYSTEMD_EMPTY), + .source_const = connected_source, + }; + else + *me = (MountEntry) { + .path_const = path, + .mode = MOUNT_PRIVATE_TMPFS, + .options_const = "mode=0700" NESTED_TMPFS_LIMITS, + .flags = MS_NODEV|MS_STRICTATIME, + }; + + return 0; +} + static int append_private_tmp(MountList *ml, const NamespaceParameters *p) { - MountEntry *me; + int r; assert(ml); assert(p); assert(p->private_tmp >= 0 && p->private_tmp < _PRIVATE_TMP_MAX); assert(p->private_var_tmp >= 0 && p->private_var_tmp < _PRIVATE_TMP_MAX); - if (p->tmp_dir) { - assert(p->private_tmp == PRIVATE_TMP_CONNECTED); + if (p->private_tmp != PRIVATE_TMP_DISCONNECTED || p->private_var_tmp != PRIVATE_TMP_DISCONNECTED) { + r = append_private_tmp_one(ml, p->private_tmp, "/tmp/", p->tmp_dir); + if (r < 0) + return r; - me = mount_list_extend(ml); - if (!me) - return log_oom_debug(); - *me = (MountEntry) { - .path_const = "/tmp/", - .mode = MOUNT_PRIVATE_TMP, - .read_only = streq(p->tmp_dir, RUN_SYSTEMD_EMPTY), - .source_const = p->tmp_dir, - }; - } - - if (p->var_tmp_dir) { - assert(p->private_var_tmp == PRIVATE_TMP_CONNECTED); - - me = mount_list_extend(ml); - if (!me) - return log_oom_debug(); - *me = (MountEntry) { - .path_const = "/var/tmp/", - .mode = MOUNT_PRIVATE_TMP, - .read_only = streq(p->var_tmp_dir, RUN_SYSTEMD_EMPTY), - .source_const = p->var_tmp_dir, - }; - } - - if (p->private_tmp != PRIVATE_TMP_DISCONNECTED) - return 0; - - if (p->private_var_tmp == PRIVATE_TMP_NO) { - me = mount_list_extend(ml); - if (!me) - return log_oom_debug(); - *me = (MountEntry) { - .path_const = "/tmp/", - .mode = MOUNT_PRIVATE_TMPFS, - .options_const = "mode=0700" NESTED_TMPFS_LIMITS, - .flags = MS_NODEV|MS_STRICTATIME, - }; + r = append_private_tmp_one(ml, p->private_var_tmp, "/var/tmp/", p->var_tmp_dir); + if (r < 0) + return r; return 0; } + /* Fully disconnected private tmp: we mount a single tmpfs instance with two subdirs which are + * bind mounted to /tmp/ and /var/tmp/. */ + + MountEntry *me; + _cleanup_free_ char *tmpfs_dir = NULL, *tmp_dir = NULL, *var_tmp_dir = NULL; tmpfs_dir = path_join(p->private_namespace_dir, "unit-private-tmp"); tmp_dir = path_join(tmpfs_dir, "tmp");