From 641c19e21e21dcacef73df38573d64f8683c4ddb Mon Sep 17 00:00:00 2001 From: tunaichao Date: Fri, 21 Aug 2026 05:30:08 +0000 Subject: [PATCH] core: apply UnsetEnvironment= after command line expansion UnsetEnvironment= was applied to the environment block before the command line was expanded. Since the expansion looks the variables up in that same block, any variable listed in UnsetEnvironment= was already gone when $VAR in ExecStart= and friends was expanded, and a whole-word $VAR reference was dropped from the command line. Let the expansion see the full environment block, and apply the unsetting only to the environment block passed to the executed process. This way UnsetEnvironment= can hide variables from the executed process without breaking command lines that reference them. Extend the exec-unsetenvironment test accordingly: the whole-word $VAR6 argument must be expanded into a positional parameter for the shell, while ${VAR6-X} must evaluate to X in the executed process. Also document in both the UnsetEnvironment= description and the "Environment Variables in Spawned Processes" section that the unsetting happens after command line variable expansion. Fixes: #43343 --- man/systemd.exec.xml | 8 ++++-- src/core/exec-invoke.c | 27 ++++++++++--------- .../exec-unsetenvironment.service | 8 +++--- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/man/systemd.exec.xml b/man/systemd.exec.xml index 5775e6c641..cbb2983d02 100644 --- a/man/systemd.exec.xml +++ b/man/systemd.exec.xml @@ -3366,7 +3366,9 @@ SystemCallErrorNumber=EPERM assignments made through Environment= or EnvironmentFile=, inherited from the system manager's global set of environment variables, inherited via PassEnvironment=, set by the service manager itself (such as $NOTIFY_SOCKET and such), or set by a PAM module - (in case PAMName= is used). + (in case PAMName= is used). Note that variable expansion in the command lines of + ExecStart= and friends is not affected by this option, as it happens before the + unsetting is applied. See "Environment Variables in Spawned Processes" below for a description of how those settings combine to form the inherited environment. See If the same environment variable is set by multiple of these sources, the later source — according to the order of the list above — wins. Note that as the final step all variables listed in UnsetEnvironment= are removed from the compiled environment variable list, immediately - before it is passed to the executed process. + before it is passed to the executed process. This unsetting happens after variable expansion in + the command lines of ExecStart= and friends is performed, hence command line + expansion is not affected by UnsetEnvironment=. The general philosophy is to expose a small curated list of environment variables to processes. Services started by the system manager (PID 1) will be started, without additional service-specific diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 74a556fa3b..2fabfe3410 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -6665,18 +6665,6 @@ int exec_invoke( #endif } - if (!strv_isempty(context->unset_environment)) { - char **ee = NULL; - - ee = strv_env_delete(accum_env, 1, context->unset_environment); - if (!ee) { - *exit_status = EXIT_MEMORY; - return log_oom(); - } - - strv_free_and_replace(accum_env, ee); - } - _cleanup_strv_free_ char **replaced_argv = NULL, **argv_via_shell = NULL; char **final_argv = FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL) ? strv_skip(command->argv, 1) : command->argv; @@ -6701,6 +6689,21 @@ int exec_invoke( } } + /* Apply UnsetEnvironment= after the command line has been expanded, so that the variables it + * removes are still available for expansion. Only the environment block passed to the executed + * process is affected, not the expansion. */ + if (!strv_isempty(context->unset_environment)) { + char **ee = NULL; + + ee = strv_env_delete(accum_env, 1, context->unset_environment); + if (!ee) { + *exit_status = EXIT_MEMORY; + return log_oom(); + } + + strv_free_and_replace(accum_env, ee); + } + if (FLAGS_SET(command->flags, EXEC_COMMAND_VIA_SHELL)) { r = strv_extendf(&argv_via_shell, "%s%s", command->argv[0][0] == '-' ? "-" : "", path); if (r < 0) { diff --git a/test/test-execute/exec-unsetenvironment.service b/test/test-execute/exec-unsetenvironment.service index e64e261072..16a5559bfb 100644 --- a/test/test-execute/exec-unsetenvironment.service +++ b/test/test-execute/exec-unsetenvironment.service @@ -3,7 +3,9 @@ Description=Test for UnsetEnvironment [Service] -ExecStart=bash -x -c 'test "$$FOO" = "bar" && test "$${QUUX-X}" = "X" && test "$$VAR3" = "value3" && test "$${VAR4-X}" = "X" && test "$$VAR5" = "value5" && test "$${X%b-X}" = "X"' +# Variable expansion in the command line is done by the service manager before UnsetEnvironment= is +# applied, so $VAR6 is expanded, but the variable is not part of the environment of the executed bash. +ExecStart=bash -x -c 'test "$$FOO" = "bar" && test "$${QUUX-X}" = "X" && test "$$VAR3" = "value3" && test "$${VAR4-X}" = "X" && test "$$VAR5" = "value5" && test "$${X%b-X}" = "X" && test "$$1" = "value6" && test "$${VAR6-X}" = "X"' x $VAR6 Type=oneshot -Environment=FOO=bar QUUX=waldo VAR3=value3 VAR4=value4 VAR5=value5 X%b=%U -UnsetEnvironment=QUUX=waldo VAR3=somethingelse VAR4 X%b=%U +Environment=FOO=bar QUUX=waldo VAR3=value3 VAR4=value4 VAR5=value5 X%b=%U VAR6=value6 +UnsetEnvironment=QUUX=waldo VAR3=somethingelse VAR4 X%b=%U VAR6