]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
vmspawn: propagate $TERM from host into VM via kernel command line
authorDaan De Meyer <daan@amutable.com>
Sat, 28 Mar 2026 13:12:25 +0000 (13:12 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 2 Apr 2026 16:06:10 +0000 (18:06 +0200)
When running in a console mode (interactive, native, or read-only),
propagate the host's $TERM into the VM by adding TERM= and
systemd.tty.term.hvc0= to the kernel command line.

TERM= is picked up by PID 1 and inherited by services on /dev/console
(such as emergency.service). systemd.tty.term.hvc0= is used by services
directly attached to /dev/hvc0 (such as serial-getty@hvc0.service) which
look up $TERM via the systemd.tty.term.<tty> kernel command line
parameter.

While systemd can auto-detect the terminal type via DCS XTGETTCAP, not
all terminal emulators implement this, so explicitly propagating $TERM
provides a more reliable experience. We skip propagation when $TERM is
unset or set to "unknown" (as is the case in GitHub Actions and some
other CI environments).

Previously this was handled by mkosi synthesizing the corresponding
kernel command line parameters externally.

Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
src/basic/terminal-util.c
src/basic/terminal-util.h
src/vmspawn/vmspawn.c

index 2da4f1c2fb86c7580bc8cb2fe3313c35b977f102..ecdc2412472860372c3efbdf9654973b5542e12b 100644 (file)
@@ -1713,6 +1713,16 @@ static bool on_dev_null(void) {
         return cached_on_dev_null;
 }
 
+bool term_env_valid(const char *term) {
+        /* Checks if the specified $TERM value is suitable for propagation, i.e. is not empty, not set to
+         * "unknown" (as is common in CI), and only contains characters valid in terminal type names.
+         * Valid $TERM values are things like "xterm-256color", "linux", "screen.xterm-256color", i.e.
+         * alphanumeric characters, hyphens, underscores, dots, and plus signs. */
+        return !isempty(term) &&
+                !streq(term, "unknown") &&
+                in_charset(term, ALPHANUMERICAL "-_+.");
+}
+
 bool getenv_terminal_is_dumb(void) {
         const char *e;
 
index dde1430243cf817ec5adabfed282d576d3e22214..7ac5661104159316af469146f902738f179eb0af 100644 (file)
@@ -118,6 +118,7 @@ void columns_lines_cache_reset(int _unused_ signum);
 void reset_terminal_feature_caches(void);
 
 bool on_tty(void);
+bool term_env_valid(const char *term);
 bool getenv_terminal_is_dumb(void);
 bool terminal_is_dumb(void);
 
index 73df1cbadd966e66c0f20f2ed4d51d76bebecfcf..1891ac8bad742aadf0b3e00081ee8b7f54e27fa8 100644 (file)
@@ -3031,6 +3031,24 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) {
                 r = strv_prepend(&arg_kernel_cmdline_extra, "console=hvc0");
                 if (r < 0)
                         return log_oom();
+
+                /* Propagate the host's $TERM into the VM via the kernel command line. TERM= is
+                 * picked up by PID 1 and inherited by services on /dev/console, and
+                 * systemd.tty.term.hvc0= is used by services directly attached to /dev/hvc0 (such
+                 * as serial-getty). While systemd can auto-detect the terminal type via DCS
+                 * XTGETTCAP, not all terminal emulators implement this, so let's always propagate
+                 * $TERM if we have it. */
+                const char *term = getenv("TERM");
+                if (term_env_valid(term)) {
+                        FOREACH_STRING(tty_key, "systemd.tty.term.hvc0", "TERM") {
+                                _cleanup_free_ char *p = strjoin(tty_key, "=", term);
+                                if (!p)
+                                        return log_oom();
+
+                                if (strv_consume_prepend(&arg_kernel_cmdline_extra, TAKE_PTR(p)) < 0)
+                                        return log_oom();
+                        }
+                }
         }
 
         _cleanup_free_ char *fstab_extra = NULL;