]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
nspawn: split boot parameters into env vars and argv
authorDaan De Meyer <daan@amutable.com>
Tue, 12 May 2026 13:03:49 +0000 (13:03 +0000)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 14 May 2026 14:37:34 +0000 (23:37 +0900)
When the kernel hands the command line to PID 1, any KEY=VALUE assignment
whose KEY does not contain a '.' is exported as an environment variable
(with '-' replaced by '_') rather than passed as an argument. Mimic the
same split in --boot mode so kernel-cmdline-style arguments passed after
the container path behave as they would on a real boot.

man/systemd-nspawn.xml
src/nspawn/nspawn.c
test/units/TEST-13-NSPAWN.nspawn.sh

index 54d6f83915c7afaf59f3ed409ade742cea56271a..c93fad377627e406bad04c96376f3799a0a060ae 100644 (file)
 
         <para>Note that <option>--boot</option> is the default mode of operation if the
         <filename>systemd-nspawn@.service</filename> template unit file is used.</para>
+
+        <para>When <option>--boot</option> is used, the passed parameters are processed the same way the
+        kernel processes its command line before handing it to PID 1: any <literal>KEY=VALUE</literal>
+        assignment whose <literal>KEY</literal> does not contain a <literal>.</literal> is exported as
+        an environment variable for PID 1 (with <literal>-</literal> in the key replaced by
+        <literal>_</literal>, as if specified via <option>--setenv=</option>), while all other
+        parameters (including <literal>systemd.*=</literal> assignments) are passed as arguments to the
+        init program.</para>
         </listitem>
       </varlistentry>
 
index efe927f36e9b61c4cb5bd8141e8e342848c0ba5d..0e532cf7b069910f571d0f473ed8d5f4b4775a4e 100644 (file)
@@ -1639,6 +1639,43 @@ static int verify_arguments(void) {
         return 0;
 }
 
+static int split_boot_parameters(void) {
+        _cleanup_strv_free_ char **kept = NULL;
+        int r;
+
+        /* When the kernel hands the command line to PID 1, any KEY=VALUE assignment whose KEY does not
+         * contain a '.' is exported as an environment variable (with '-' replaced by '_'), rather than
+         * passed as an argument. Mimic the same split here so users can pass kernel-cmdline-style
+         * arguments after the container path and get the behavior they'd get on a real boot. */
+
+        if (arg_start_mode != START_BOOT)
+                return 0;
+
+        STRV_FOREACH(p, arg_parameters) {
+                _cleanup_free_ char *key = NULL, *value = NULL;
+
+                if (split_pair(*p, "=", &key, &value) >= 0 && !strchr(key, '.')) {
+                        string_replace_char(key, '-', '_');
+
+                        if (env_name_is_valid(key) && env_value_is_valid(value)) {
+                                r = strv_env_assign(&arg_setenv, key, value);
+                                if (r < 0)
+                                        return log_error_errno(r, "Cannot assign environment variable: %m");
+
+                                arg_settings_mask |= SETTING_ENVIRONMENT;
+                                continue;
+                        }
+                }
+
+                r = strv_extend(&kept, *p);
+                if (r < 0)
+                        return log_oom();
+        }
+
+        strv_free_and_replace(arg_parameters, kept);
+        return 0;
+}
+
 static int verify_network_interfaces_initialized(void) {
         int r;
         r = test_network_interfaces_initialized(arg_network_interfaces);
@@ -6076,6 +6113,10 @@ static int run(int argc, char *argv[]) {
         if (r < 0)
                 goto finish;
 
+        r = split_boot_parameters();
+        if (r < 0)
+                goto finish;
+
         r = resolve_network_interface_names(arg_network_interfaces);
         if (r < 0)
                 goto finish;
index 0332a12f64665af0030f036949e7d6d09650548e..822a0087329905553f0286f8212beb6716da9b65 100755 (executable)
@@ -1561,6 +1561,46 @@ testcase_volatile_link_journal_no_userns() {
     rm -fr "$root" "$journal_dir"
 }
 
+testcase_boot_param_split() {
+    local root outdir
+
+    root="$(mktemp -d /var/lib/machines/TEST-13-NSPAWN.boot-param-split.XXX)"
+    outdir="$(mktemp -d)"
+    create_dummy_container "$root"
+
+    # Replace the init binary with a stub that records the argv and environment nspawn passes to it,
+    # so we can verify that kernel-cmdline-style KEY=VALUE arguments are split between PID 1's
+    # environment and argv the same way the kernel splits them.
+    cat >"$root/usr/lib/systemd/systemd" <<'EOF'
+#!/bin/bash
+set -e
+printf '%s\n' "$@" >/output/argv
+env >/output/env
+EOF
+    chmod +x "$root/usr/lib/systemd/systemd"
+
+    # Cover the assignments that should land in env (FOO=bar, baz-qux=hello → baz_qux), the
+    # dotted assignments that should stay as argv (systemd.unit=…, some.thing=…), and the malformed
+    # entries that look env-like but must also stay as argv: empty key (=value), key starting with
+    # a digit (123=foo), key with characters that aren't valid in an env var name (foo!=bar).
+    systemd-nspawn --register=no \
+                   --directory="$root" \
+                   --bind="$outdir:/output" \
+                   --boot \
+                   FOO=bar baz-qux=hello systemd.unit=foo.target some.thing=yes plain-arg \
+                   =empty-key 123=leading-digit 'foo!=bad-char'
+
+    diff <(printf 'systemd.unit=foo.target\nsome.thing=yes\nplain-arg\n=empty-key\n123=leading-digit\nfoo!=bad-char\n') "$outdir/argv"
+    grep '^FOO=bar$' >/dev/null "$outdir/env"
+    grep '^baz_qux=hello$' >/dev/null "$outdir/env"
+    (! grep -E '^(systemd\.unit|some\.thing)=' >/dev/null "$outdir/env")
+    (! grep -E '^(FOO|baz_qux)=' >/dev/null "$outdir/argv")
+    (! grep -E '^(123|foo!?)=' >/dev/null "$outdir/env")
+    (! grep -E '^=' >/dev/null "$outdir/env")
+
+    rm -fr "$root" "$outdir"
+}
+
 testcase_cap_net_bind_service() {
     local root