diff --git a/docs/OSC-CONTEXT.md b/docs/OSC-CONTEXT.md new file mode 100644 index 0000000000..52ccb03284 --- /dev/null +++ b/docs/OSC-CONTEXT.md @@ -0,0 +1,339 @@ +--- +title: OSC 3008: Hierarchical Context Signalling +category: Interfaces +layout: default +SPDX-License-Identifier: LGPL-2.1-or-later +--- + +# OSC 3008: Hierarchical Context Signalling + +A terminal connects a user with programs. Control of the program side of +terminals is typically passed around to various different components while the +user is active: a shell might pass control to a process it invokes. If that +process is `run0` then primary control is passed to the privileged session of +the target user. If `systemd-nspawn` is invoked to start a container, primary +control is passed to that container, and so on. + +A terminal emulator might be interested to know which component is currently in +primary control of the program side of a terminal. OSC 3008 is a mechanism to +inform it about such contexts. Each component taking over control can inform +the terminal emulators that a new context begins now, and then use the terminal +or pass control down to further apps, which can introduce contexts. Each +context may carry various descriptive metadata fields. + +## Status + +This OSC sequence has been invented by the systemd project and is generated by +systemd. Currently, no terminal application is known that consumes these +sequences. + +## Use Cases + +Terminal emulators can use hierarchical context information: + +1. To introduce markers/bookmarks in the output that the user can jump between. + +2. To visually identify output from different contexts. For example the + background of the associated output can be tinted in a reddish tone when + privileges are acquired, and similar. + +3. Meta information on specific output can be shown in a tooltip or similar + +4. Programs (and all subcontexts) can be killed via a right-click menu on the + output they generate. + +5. Similar, a right-click menu might offer an item to offer opening a new + interactive shell in the same working directory that was current on the + selected context. + +6. Failed commands or aborted sessions can be marked requesting user attention. + +## Context Types + +There are various types of contexts defined by this specification: + +1. `boot` → a booted system initiates this context early at boot. (systemd's + PID 1 generates this on `/dev/console`.) + +2. `container` → a container manager initialized an interactive connection to a + container. (`systemd-nspawn` generates this when interactively invoking a + container. `machinectl login`, `machinectl shell` do this too.) + +3. `vm` → a VM manager initialized a terminal connection to a + VM. (`systemd-vmspawn` generates this when interactively invoking a VM, as + one example.) + +4. `elevate` → when the user interactively acquired higher privileges. (`run0` + initiates a context of this type whenever the user invokes it to acquire + root privileges.) + +5. `chpriv` → similar, but when the user acquired *different* privileges, not + necessarily higher ones. (`run0` initiates a context of this type whenever + the user invokes it to acquire non-root privileges of another user.) + +5. `subcontext` → similar, but the source and target privileges where + identical. (`run0` initiates a context of this type whenever the user + invokes it to acquire privileges of the user itself.) + +6. `remote` → a user invoked a tool such as `ssh` to connect to a remote + system. + +7. `shell` → an interactive terminal shell initiates this context + +8. `command` → a shell interactively invokes a new program. + +9. `app` → an interactive program may initiate this context. + +10. `service` → the service manager invokes an interactive service on the terminal + +11. `session` → a login session of the user is initialized. + +## Semantics + +Contexts in the sense of OSC 3008 are hierarchical, and describe a tree +structure: whenever a new context is opened it becomes the new active context, +and the previously active context becomes its parent (if there is one). Only +one context is currently active, but previously opened contexts remain valid in +the background. Any other data written or read should be considered associated +with the currently active context. + +Each context carries an identifier, chosen by the component opening the +context. The identifier can chosen freely, but must not be longer than 64 +characters. The characters may be in the 32…126 byte range. Identifiers should +be universally unique, for example randomly generated. A freshly generated UUID +would work well for this, but this could also be something like the Linux boot +ID combined with the 64bit inode number of Linux pidfds, or something hashed +from it. + +Fundamentally, there are two OSC 3008 commands defined: + +1. OSC "`3008;start=`" … (the *start sequence*) → this initiates, updates or + indicates a return to a context. It carries a context identifier, and + typically some metadata. This may be sent to first initiate a context. If + sent again for the same context ID that was initiated already this indicates + an update of the existing context. In this case, *any* previously set + metadata fields for the context are flushed out, reset to their defaults, + and then reinitialized from the newly supplied data. Also, in this case any + subcontexts of the contexts are implicitly terminated. + +2. OSC "`3008;end=`" … (the *end sequence*) → this terminates a context. It + carries a context identifier to close, initiated before with OSC + "`3008;start=`". It may also carry additional metadata. + +## General Syntax + +This builds on ECMA-48, and reuses the OSC and ST concepts introduced there. + +For sequences following this specification it is recommended to encode OSC as +0x1B 0x5D, and ST as 0x1B 0x5C. + +ECMA-48 only allows characters from the range 0x20…0x7e (i.e. 32…126) inside +OSC sequences. However, most terminal emulators nowadays allow the ASCII byte +range > 0x7f in the OSC sequences they process, and so does this +specification. Control characters (< 0x20 and 0x7f) are not allowed. The +semicolon character ("`;`") – which is used as field separator by this +specification – shall be replaced by "`\x3b`" and the backslash character +("`\`") shall be replaced by "`\x5c`". All textual fields must be encoded in +UTF-8, and then escaped with these two replacements. + +The start sequence begins with OSC, followed by the string `3008;start=`, +followed by the context ID. This is then followed by any number of metadata +fields, including none. Metadata fields begin with a semicolon (`;`) followed +by in a string identifying the type of field, followed by an equal sign (`=`), +and the field value. The sequence ends in ST. + +The end sequence begins with OSC, followed by the string `3008;end=`, followed +by the context ID, and a series of metadata fields in the same syntax as for +the start sequence. The sequence ends in ST. + +## Metadata Fields + +The following fields are currently defined for the start sequence: + +| Field | Context Types | Description | +|---------------|---------------|-------------------------------------------------------------------------------------------------------------| +| `type=` | *all* | Declares the context type, one of the types described above | +| `user=` | *all* | UNIX user name the process issuing the sequence runs as | +| `hostname=` | *all* | UNIX host name of the system the process issuing the sequence runs on | +| `machineid=` | *all* | The machine ID (i.e. `/etc/machine-id`) of the system the process issuing the sequence runs on | +| `bootid=` | *all* | The boot ID (i.e. `/proc/sys/kernel/random/boot_id`) of the system the process issuing the sequence runs on | +| `pid=` | *all* | The numeric PID of the process issuing the sequence, in decimal notation | +| `pidfdid=` | *all* | The 64bit inode number of the pidfd of the process issuing the sequence, in decimal notation | +| `comm=` | *all* | The process name (i.e. `/proc/$PID/comm`, `PR_GET_NAME`) of the process issuing the sequence | +| `cwd=` | `shell`, `command` | The current working directory | +| `cmdline=` | `command` | The full command line of the invoked command | +| `vm=` | `vm` | The name of the VM being invoked | +| `container=` | `container` | The name of the container being invoked | +| `targetuser=` | `elevate`, `chpriv`, `vm`, `container`, `remote`, `session` | Target UNIX user name | +| `targethost=` | `remote` | Target UNIX, DNS host name, or IP address | +| `sessionid=` | `session` | New allocated session ID | + +The following fields are currently defined for the end sequence: + +| Field | Context Types | Description | +|---------------|---------------|-------------------------------------------------------------------------------------------------------------| +| `exit=` | `command` | One of `success`, `failure`, `crash`, `interrupt`, indicating how the program terminated | +| `status=` | `command` | The command's numeric exit status, i.e. the 0…255 value a program returns | +| `signal=` | `command` | The termination signal of the command, if it died abnormally. A symbolic signal name. (`SIGKILL`, …) | + +All fields are optional, including the context type. However, it is generally +recommended to always include the first 7 fields listed above, to make it easy +to pinpoint the origin of a context in a race-free fashion, without any +ambiguities. + +The order of the metadata fields is undefined, they may appear in any order +(including that `type=` is specified at the very end or in the middle!). Note +that `start=` and `end=` are not considered metadata fields but part of the +start sequence, and hence must always appear right after OSC. + +## Processing, Limits, Security + +All context information provided like this should be considered auxiliary and – +to some degree – redundant information. Hence, it would be wise for a terminal +to enforce limits on various resources, dropping additional data once these +limits are hit. Most importantly, a maximum stacking depth should probably +enforced: any attempts to initiate further contexts should be ignored once the +stack limit is hit (i.e. the earlier contexts should be kept, the later +contexts be discarded, not the opposite). Overly long fields should be +discarded (or potentially truncated, depending on the field type). This +specification does not recommend any specific stack or string limits for now. + +The usual terminal reset sequences should *not* effect the stack of contexts +(this is a safety feature: a program down the stack should not be able to +affect the stack further up, possibly hiding relevant information). A temporary +TTY hangup (`vhangup()`) should result in a full reset of the stack. + +All provided data should be processed in a lenient, graceful fashion: if a +sequence contains invalid fields, those fields should be ignored, but the rest +of the fields should still be used. In particular, unknown fields should be +ignored. + +The fields provided in these sequences should not contain sensitive +information. Context IDs should not be considered confidential, but it is +strongly recommended to generate them in a fashion that guarantees their +sufficient uniqueness and avoids accidental or intended clashes with other +contents. + +## Examples + +1. A new container `foobar` has been invoked by user `lennart` on host `zeta`: + `OSC "3008;start=bed86fab93af4328bbed0a1224af6d40;type=container;user=lennart;hostname=zeta;machineid=3deb5353d3ba43d08201c136a47ead7b;bootid=d4a3d0fdf2e24fdea6d971ce73f4fbf2;pid=1062862;pidfdid=1063162;comm=systemd-nspawn;container=foobar" ST` + +2. This context ends: `OSC "3008;end=bed86fab93af4328bbed0a1224af6d40" ST` + +## Syntax in ABNF + +```abnf +OSC = %x1B %x5D +ST = %x1B %x5C + +DECIMAL = "0"-"9" +HEX = "0"-"9" / "A"-"F" / "a-f" +ID128 = 32*36(HEX / "-") +UINT64 = 1*20DECIMAL +ESCSEMICOLON = "\x3b" +ESCBACKSLASH = "\x5c" +SAFE = %x20-3a / %x3c-5b / %x5d-7e / ESCSEMICOLON / ESCBACKSLASH + +CTXID = 1*64SAFE +TYPEENUM = "service" / "session" / "shell" / "command" / "vm" / "container" / "elevate" / "chpriv" / "subcontext" / "remote" / "boot" / "app" + +TYPE = "type=" TYPEENUM +USER = "user=" 1*255SAFE +HOSTNAME = "hostname=" 1*255SAFE +MACHINEID = "machineid=" 1D128 +BOOTID = "bootid=" ID128 +PID = "pid=" UINT64 +PIDFDID = "pidfdid=" UINT64 +COMM = "comm=" 1*255SAFE +CWD = "cwd=" 1*255SAFE +CMDLINE = "cmdline=" *255SAFE +VM = "vm=" 1*255SAFE +CONTAINER = "container=" 1*255SAFE +TARGETUSER = "targetuser=" 1*255SAFE +TARGETHOST = "targethost=" 1*255SAFE +SESSIONID = "sessionid=" 1*255SAFE + +STARTFIELD = TYPE / USER / HOSTNAME / MACHINEID / BOOTID / PID / PIDFDID / COMM / CWD / CMDLINE / VM / CONTAINER / TARGETUSER / TARGETHOST / SESSIONID +STARTSEQ = OSC "3008;start=" CTXID *(";" STARTFIELD) ST + +EXITENUM = "success" / "failure" / "crash" / "interrupt" +SIGNALENUM = "SIGBUS" / "SIGTRAP" / "SIGABRT" / "SIGSEGV" / … + +EXIT = "exit=" EXITENUM +STATUS = "status=" UINT64 +SIGNAL = "signal=" SIGNALENUM + +ENDFIELD = EXIT / STATUS / SIGNAL +ENDSEQ = OSC "3008;end=" CTXID *(";" ENDFIELD) ST +``` + +## Known OSC Prefixes + +Here's a list of OSC prefixes used by the various sequences currently in public +use in various terminal emulators. It's not going to be complete, but I tried +to do some reasonably thorough research to avoid conflicts with the new OSC +sequence defined above. + +| OSC Prefix | Purpose | +|----------------:|------------------------------------------------------------| +| `OSC "0;…"` | Icon name + window title | +| `OSC "1;…"` | Icon name | +| `OSC "2;…"` | Window title | +| `OSC "3;…"` | X11 property | +| `OSC "4;…"` | Palette | +| `OSC "5;…"` | Special palette | +| `OSC "6;…"` | Disable special color | +| `OSC "7;…"` | Report cwd | +| `OSC "8;…"` | Hyperlink | +| `OSC "9;…"` | Progress bar (conemu) [conflict: also growl notifications] | +| `OSC "10;…"` | Change colors | +| `OSC "11;…"` | " | +| `OSC "12;…"` | " | +| `OSC "13;…"` | " | +| `OSC "14;…"` | " | +| `OSC "15;…"` | " | +| `OSC "16;…"` | " | +| `OSC "17;…"` | " | +| `OSC "18;…"` | " | +| `OSC "19;…"` | " | +| `OSC "21;…"` | Query colors (kitty) | +| `OSC "22;…"` | Cursor shape | +| `OSC "46;…"` | Log file | +| `OSC "50;…"` | Set font | +| `OSC "51;…"` | Emacs shell | +| `OSC "52;…"` | Manipulate selection data (aka clipboard) | +| `OSC "60;…"` | Query allowed | +| `OSC "61;…"` | Query disallowed | +| `OSC "99;…"` | Notifications (kitty) | +| `OSC "104;…"` | Reset color | +| `OSC "105;…"` | Enable/disable special color | +| `OSC "110;…"` | Reset colors | +| `OSC "111;…"` | " | +| `OSC "112;…"` | " | +| `OSC "113;…"` | " | +| `OSC "114;…"` | " | +| `OSC "115;…"` | " | +| `OSC "116;…"` | " | +| `OSC "117;…"` | " | +| `OSC "118;…"` | " | +| `OSC "119;…"` | " | +| `OSC "133;…"` | Prompt/command begin/command end (finalterm/iterm2) | +| `OSC "440;…"` | Audio (mintty) | +| `OSC "633;…"` | vscode action (Windows Terminal) | +| `OSC "666;…"` | "termprop" (vte) | +| `OSC "701;…"` | Locale (mintty) | +| `OSC "777;…"` | Notification (rxvt) | +| `OSC "3008;…"` | This specification | +| `OSC "7704;…"` | ANSI colors (mintty) | +| `OSC "7750;…"` | Emoji style (mintty) | +| `OSC "7770;…"` | Font size (mintty) | +| `OSC "7771;…"` | Glyph coverage (mintty) | +| `OSC "7721:…"` | Copy window title (mintty) | +| `OSC "7777;…"` | Window size (mintty) | +| `OSC "9001;…"` | Action (Windows Terminal) | +| `OSC "1337;…"` | iterm2 multiplex seeuqnece | +| `OSC "5522;…"` | Clipboard (kitty) | +| `OSC "30001;…"` | Push color onto stack (kitty) | +| `OSC "30101;…"` | Pop color from stack (kitty) | +| `OSC "77119;…"` | Wide chars (mintty) | diff --git a/meson.build b/meson.build index e87c8ea2ec..0865c861ec 100644 --- a/meson.build +++ b/meson.build @@ -233,6 +233,7 @@ if shellprofiledir == '' shellprofiledir = sysconfdir / 'profile.d' endif conf.set10('LINK_SHELL_EXTRA_DROPIN', shellprofiledir != 'no' and not shellprofiledir.startswith('/usr/')) +conf.set10('LINK_OSC_CONTEXT_DROPIN', shellprofiledir != 'no' and not shellprofiledir.startswith('/usr/')) conf.set('SHELLPROFILEDIR', shellprofiledir, description : 'shell profile directory') memory_accounting_default = get_option('memory-accounting-default') diff --git a/profile.d/80-systemd-osc-context.sh b/profile.d/80-systemd-osc-context.sh new file mode 100644 index 0000000000..59af2e1cde --- /dev/null +++ b/profile.d/80-systemd-osc-context.sh @@ -0,0 +1,59 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck shell=bash +# shellcheck disable=SC2016 +# shellcheck disable=SC1003 + +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +__systemd_osc_context_escape() { + # Escape according to the OSC 8003 spec. Since this requires shelling out + # to 'sed' we'll only do it where it's strictly necessary, and skip it when + # processing strings we are pretty sure we won't need it for, such as + # uuids, id128, hostnames, usernames, since they all come with syntax + # requirements that exclude \ and ; anyway. This hence primarily is about + # escaping the current working directory. + echo "$1" | sed -e 's/\\/\\x5x/g' -e 's/;/\\x3b/g' +} + +__systemd_osc_context_common() { + printf ";user=%s;hostname=%s;machineid=%s;bootid=%s;pid=%s" "$USER" "$HOSTNAME" "$(= 0); + assert(!FLAGS_SET(flags, TERMINAL_RESET_AVOID_ANSI_SEQ|TERMINAL_RESET_FORCE_ANSI_SEQ)); - /* Resets the terminal comprehensively, but defensively. i.e. both resets the tty via ioctl()s and - * via ANSI sequences, but avoids the latter in case we are talking to a pty. That's a safety measure - * because ptys might be connected to shell pipelines where we cannot expect such ansi sequences to - * work. Given that ptys are generally short-lived (and not recycled) this restriction shouldn't hurt - * much. - * - * The specified fd should be open for *writing*! */ + /* Resets the terminal comprehensively, i.e. via both ioctl()s and via ANSI sequences, but do so only + * if $TERM is unset or set to "dumb" */ if (!isatty_safe(fd)) return -ENOTTY; - RET_GATHER(r, terminal_reset_ioctl(fd, switch_to_text)); + RET_GATHER(r, terminal_reset_ioctl(fd, FLAGS_SET(flags, TERMINAL_RESET_SWITCH_TO_TEXT))); - if (terminal_is_pty_fd(fd) == 0) + if (!FLAGS_SET(flags, TERMINAL_RESET_AVOID_ANSI_SEQ) && + (FLAGS_SET(flags, TERMINAL_RESET_FORCE_ANSI_SEQ) || !getenv_terminal_is_dumb())) RET_GATHER(r, terminal_reset_ansi_seq(fd)); return r; } -int terminal_reset_defensive_locked(int fd, bool switch_to_text) { +int terminal_reset_defensive_locked(int fd, TerminalResetFlags flags) { assert(fd >= 0); _cleanup_close_ int lock_fd = lock_dev_console(); if (lock_fd < 0) log_debug_errno(lock_fd, "Failed to acquire lock for /dev/console, ignoring: %m"); - return terminal_reset_defensive(fd, switch_to_text); + return terminal_reset_defensive(fd, flags); } void termios_disable_echo(struct termios *termios) { diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index 698838e63f..9decf42f22 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -41,8 +41,14 @@ bool isatty_safe(int fd); -int terminal_reset_defensive(int fd, bool switch_to_text); -int terminal_reset_defensive_locked(int fd, bool switch_to_text); +typedef enum TerminalResetFlags { + TERMINAL_RESET_SWITCH_TO_TEXT = 1 << 0, + TERMINAL_RESET_AVOID_ANSI_SEQ = 1 << 1, + TERMINAL_RESET_FORCE_ANSI_SEQ = 1 << 2, +} TerminalResetFlags; + +int terminal_reset_defensive(int fd, TerminalResetFlags flags); +int terminal_reset_defensive_locked(int fd, TerminalResetFlags flags); int terminal_set_cursor_position(int fd, unsigned row, unsigned column); diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 10f425139b..94157a9e99 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -46,6 +46,7 @@ #include "missing_securebits.h" #include "missing_syscall.h" #include "mkdir-label.h" +#include "osc-context.h" #include "proc-cmdline.h" #include "process-util.h" #include "psi-util.h" @@ -668,7 +669,7 @@ static int setup_confirm_stdio( if (r < 0) return r; - r = terminal_reset_defensive(fd, /* switch_to_text= */ true); + r = terminal_reset_defensive(fd, TERMINAL_RESET_SWITCH_TO_TEXT); if (r < 0) return r; @@ -4367,6 +4368,10 @@ static void prepare_terminal( p->stdout_fd >= 0)) return; + /* Let's explicitly determine whether to reset via ANSI sequences or not, taking our ExecContext + * information into account */ + bool use_ansi = exec_context_shall_ansi_seq_reset(context); + if (context->tty_reset) { /* When we are resetting the TTY, then let's create a lock first, to synchronize access. This * in particular matters as concurrent resets and the TTY size ANSI DSR logic done by the @@ -4375,10 +4380,16 @@ static void prepare_terminal( if (lock_fd < 0) log_exec_debug_errno(context, p, lock_fd, "Failed to lock /dev/console, ignoring: %m"); - (void) terminal_reset_defensive(STDOUT_FILENO, /* switch_to_text= */ false); + /* We explicitly control whether to send ansi sequences or not here, since we want to consult + * the env vars explicitly configured in the ExecContext, rather than our own environment + * block. */ + (void) terminal_reset_defensive(STDOUT_FILENO, use_ansi ? TERMINAL_RESET_FORCE_ANSI_SEQ : TERMINAL_RESET_AVOID_ANSI_SEQ); } (void) exec_context_apply_tty_size(context, STDIN_FILENO, STDOUT_FILENO, /* tty_path= */ NULL); + + if (use_ansi) + (void) osc_context_open_service(p->unit_id, p->invocation_id, /* ret_seq= */ NULL); } int exec_invoke( @@ -4555,8 +4566,10 @@ int exec_invoke( * disallocate the VT), to get rid of any prior uses of the device. Note that we do not keep any fd * open here, hence some of the settings made here might vanish again, depending on the TTY driver * used. A 2nd ("constructive") initialization after we opened the input/output fds we actually want - * will fix this. */ - exec_context_tty_reset(context, params); + * will fix this. Note that we pass a NULL invocation ID here – as exec_context_tty_reset() expects + * the invocation ID associated with the OSC 3008 context ID to close. But we don't want to close any + * OSC 3008 context here, and opening a fresh OSC 3008 context happens a bit further down. */ + exec_context_tty_reset(context, params, /* invocation_id= */ SD_ID128_NULL); if (params->shall_confirm_spawn && exec_context_shall_confirm_spawn(context)) { _cleanup_free_ char *cmdline = NULL; diff --git a/src/core/execute.c b/src/core/execute.c index 6811bf301c..a7e8b3340c 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -39,6 +39,7 @@ #include "format-util.h" #include "glob-util.h" #include "hexdecoct.h" +#include "io-util.h" #include "ioprio-util.h" #include "lock-util.h" #include "log.h" @@ -50,6 +51,7 @@ #include "missing_prctl.h" #include "mkdir-label.h" #include "namespace.h" +#include "osc-context.h" #include "parse-util.h" #include "path-util.h" #include "process-util.h" @@ -129,11 +131,12 @@ int exec_context_apply_tty_size( rows == UINT_MAX ? &rows : NULL, cols == UINT_MAX ? &cols : NULL); - /* If we got nothing so far and we are talking to a physical device, and the TTY reset logic is on, - * then let's query dimensions from the ANSI driver. */ + /* If we got nothing so far and we are talking to a physical device, then let's query dimensions from + * the ANSI terminal driver. Note that we will not bother with this in case terminal reset via ansi + * sequences is not enabled, as the DSR logic relies on ANSI sequences after all, and if we shall not + * use those during initialization we need to skip it. */ if (rows == UINT_MAX && cols == UINT_MAX && - context->tty_reset && - terminal_is_pty_fd(output_fd) == 0 && + exec_context_shall_ansi_seq_reset(context) && isatty_safe(input_fd)) { r = terminal_get_size_by_dsr(input_fd, output_fd, &rows, &cols); if (r < 0) @@ -143,7 +146,7 @@ int exec_context_apply_tty_size( return terminal_set_size_fd(output_fd, tty_path, rows, cols); } -void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p) { +void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p, sd_id128_t invocation_id) { _cleanup_close_ int _fd = -EBADF, lock_fd = -EBADF; int fd, r; @@ -178,12 +181,32 @@ void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p) log_warning_errno(lock_fd, "Failed to lock /dev/console, proceeding without lock: %m"); if (context->tty_reset) - (void) terminal_reset_defensive(fd, /* switch_to_text= */ true); + (void) terminal_reset_defensive( + fd, + TERMINAL_RESET_SWITCH_TO_TEXT | + (exec_context_shall_ansi_seq_reset(context) ? TERMINAL_RESET_FORCE_ANSI_SEQ : TERMINAL_RESET_AVOID_ANSI_SEQ)); r = exec_context_apply_tty_size(context, fd, fd, path); if (r < 0) log_debug_errno(r, "Failed to configure TTY dimensions, ignoring: %m"); + if (!sd_id128_is_null(invocation_id)) { + sd_id128_t context_id; + + r = osc_context_id_from_invocation_id(invocation_id, &context_id); + if (r < 0) + log_debug_errno(r, "Failed to derive context ID from invocation ID, ignoring: %m"); + else { + _cleanup_free_ char *seq = NULL; + + r = osc_context_close(context_id, &seq); + if (r < 0) + log_debug_errno(r, "Failed to acquire OSC close sequence, ignoring: %m"); + else + (void) loop_write(fd, seq, SIZE_MAX); + } + } + if (context->tty_vhangup) (void) terminal_vhangup_fd(fd); @@ -974,6 +997,22 @@ bool exec_context_may_touch_console(const ExecContext *ec) { tty_may_match_dev_console(exec_context_tty_path(ec)); } +bool exec_context_shall_ansi_seq_reset(const ExecContext *c) { + assert(c); + + /* Determines whether ANSI sequences shall be used during any terminal initialisation: + * + * 1. If the reset logic is enabled at all, this is an immediate no. + * + * 2. If $TERM is set to anything other than "dumb", it's a yes. + */ + + if (!c->tty_reset) + return false; + + return !streq_ptr(strv_env_get(c->environment, "TERM"), "dumb"); +} + static void strv_fprintf(FILE *f, char **l) { assert(f); @@ -1610,7 +1649,7 @@ void exec_context_free_log_extra_fields(ExecContext *c) { c->n_log_extra_fields = 0; } -void exec_context_revert_tty(ExecContext *c) { +void exec_context_revert_tty(ExecContext *c, sd_id128_t invocation_id) { _cleanup_close_ int fd = -EBADF; const char *path; struct stat st; @@ -1619,7 +1658,7 @@ void exec_context_revert_tty(ExecContext *c) { assert(c); /* First, reset the TTY (possibly kicking everybody else from the TTY) */ - exec_context_tty_reset(c, /* parameters= */ NULL); + exec_context_tty_reset(c, /* parameters= */ NULL, invocation_id); /* And then undo what chown_terminal() did earlier. Note that we only do this if we have a path * configured. If the TTY was passed to us as file descriptor we assume the TTY is opened and managed diff --git a/src/core/execute.h b/src/core/execute.h index 6421f19cc4..8128c4c928 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -530,6 +530,7 @@ const char* exec_context_fdname(const ExecContext *c, int fd_index); bool exec_context_may_touch_console(const ExecContext *c); bool exec_context_maintains_privileges(const ExecContext *c); +bool exec_context_shall_ansi_seq_reset(const ExecContext *c); int exec_context_get_effective_ioprio(const ExecContext *c); bool exec_context_get_effective_mount_apivfs(const ExecContext *c); @@ -537,14 +538,14 @@ bool exec_context_get_effective_bind_log_sockets(const ExecContext *c); void exec_context_free_log_extra_fields(ExecContext *c); -void exec_context_revert_tty(ExecContext *c); +void exec_context_revert_tty(ExecContext *c, sd_id128_t invocation_id); int exec_context_get_clean_directories(ExecContext *c, char **prefix, ExecCleanMask mask, char ***ret); int exec_context_get_clean_mask(ExecContext *c, ExecCleanMask *ret); const char* exec_context_tty_path(const ExecContext *context); int exec_context_apply_tty_size(const ExecContext *context, int input_fd, int output_fd, const char *tty_path); -void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p); +void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p, sd_id128_t invocation_id); uint64_t exec_context_get_rlimit(const ExecContext *c, const char *name); int exec_context_get_oom_score_adjust(const ExecContext *c); diff --git a/src/core/main.c b/src/core/main.c index a73bbb83dd..bee1d356a4 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -45,6 +45,7 @@ #include "efivars.h" #include "emergency-action.h" #include "env-util.h" +#include "escape.h" #include "exit-status.h" #include "fd-util.h" #include "fdset.h" @@ -57,6 +58,7 @@ #include "ima-setup.h" #include "import-creds.h" #include "initrd-util.h" +#include "io-util.h" #include "ipe-setup.h" #include "killall.h" #include "kmod-setup.h" @@ -73,6 +75,7 @@ #include "mount-setup.h" #include "mount-util.h" #include "os-util.h" +#include "osc-context.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -2382,6 +2385,38 @@ static void log_execution_mode(bool *ret_first_boot) { *ret_first_boot = first_boot; } +static int write_boot_or_shutdown_osc(const char *type) { + int r; + + assert(STRPTR_IN_SET(type, "boot", "shutdown")); + + if (getenv_terminal_is_dumb()) + return 0; + + _cleanup_close_ int fd = open_terminal("/dev/console", O_WRONLY|O_NOCTTY|O_CLOEXEC); + if (fd < 0) + return log_debug_errno(fd, "Failed to open /dev/console to print %s OSC, ignoring: %m", type); + + _cleanup_free_ char *seq = NULL; + if (streq(type, "boot")) + r = osc_context_open_boot(&seq); + else + r = osc_context_close(SD_ID128_ALLF, &seq); + if (r < 0) + return log_debug_errno(r, "Failed to acquire %s OSC sequence, ignoring: %m", type); + + r = loop_write(fd, seq, SIZE_MAX); + if (r < 0) + return log_debug_errno(r, "Failed to write %s OSC sequence, ignoring: %m", type); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = cescape(seq); + log_debug("OSC sequence for %s successfully written: %s", type, strna(h)); + } + + return 0; +} + static int initialize_runtime( bool skip_setup, bool first_boot, @@ -2440,6 +2475,8 @@ static int initialize_runtime( write_container_id(); + (void) write_boot_or_shutdown_osc("boot"); + /* Copy os-release to the propagate directory, so that we update it for services running * under RootDirectory=/RootImage= when we do a soft reboot. */ r = setup_os_release(RUNTIME_SCOPE_SYSTEM); @@ -3422,6 +3459,8 @@ finish: } #endif + (void) write_boot_or_shutdown_osc("shutdown"); + if (r < 0) (void) sd_notifyf(/* unset_environment= */ false, "ERRNO=%i", -r); diff --git a/src/core/service.c b/src/core/service.c index 112e4370a5..12b367ccc8 100644 --- a/src/core/service.c +++ b/src/core/service.c @@ -2178,7 +2178,7 @@ static void service_enter_dead(Service *s, ServiceResult f, bool allow_restart) (void) unlink(s->pid_file); /* Reset TTY ownership if necessary */ - exec_context_revert_tty(&s->exec_context); + exec_context_revert_tty(&s->exec_context, UNIT(s)->invocation_id); } static void service_enter_stop_post(Service *s, ServiceResult f) { diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 99e75b3422..9ef8d89560 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -121,7 +121,7 @@ static void print_welcome(int rfd) { pn = os_release_pretty_name(pretty_name, os_name); ac = isempty(ansi_color) ? "0" : ansi_color; - (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* switch_to_text= */ false); + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); if (colors_enabled()) printf("\nWelcome to your new installation of \x1B[%sm%s\x1B[0m!\n", ac, pn); diff --git a/src/home/homectl.c b/src/home/homectl.c index c398b324fd..a7754c2299 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -2515,7 +2515,7 @@ static int create_interactively(void) { return 0; } - (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* switch_to_text= */ false); + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); for (;;) { username = mfree(username); diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index 6dbf22d7b3..4c0f1a58b8 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -39,11 +39,13 @@ #include "format-util.h" #include "fs-util.h" #include "hostname-util.h" +#include "io-util.h" #include "json-util.h" #include "locale-util.h" #include "login-util.h" #include "macro.h" #include "missing_syscall.h" +#include "osc-context.h" #include "pam-util.h" #include "parse-util.h" #include "path-util.h" @@ -1081,6 +1083,7 @@ static int register_session( UserRecord *ur, bool debug, char **ret_seat, + char **ret_type, char **ret_runtime_dir) { int r; @@ -1089,19 +1092,20 @@ static int register_session( assert(c); assert(ur); assert(ret_seat); + assert(ret_type); assert(ret_runtime_dir); /* We don't register session class none with logind */ if (streq(c->class, "none")) { pam_debug_syslog(handle, debug, "Skipping logind registration for session class none."); - *ret_seat = *ret_runtime_dir = NULL; + *ret_seat = *ret_type = *ret_runtime_dir = NULL; return PAM_SUCCESS; } /* Make most of this a NOP on non-logind systems */ if (!logind_running()) { pam_debug_syslog(handle, debug, "Skipping logind registration as logind is not running."); - *ret_seat = *ret_runtime_dir = NULL; + *ret_seat = *ret_type = *ret_runtime_dir = NULL; return PAM_SUCCESS; } @@ -1174,7 +1178,7 @@ static int register_session( if (streq_ptr(error_id, "io.systemd.Login.AlreadySessionMember")) { /* We are already in a session, don't do anything */ pam_debug_syslog(handle, debug, "Not creating session: %s", error_id); - *ret_seat = *ret_runtime_dir = NULL; + *ret_seat = *ret_type= *ret_runtime_dir = NULL; return PAM_SUCCESS; } if (error_id) @@ -1271,7 +1275,7 @@ static int register_session( /* We are already in a session, don't do anything */ pam_debug_syslog(handle, debug, "Not creating session: %s", bus_error_message(&error, r)); - *ret_seat = *ret_runtime_dir = NULL; + *ret_seat = *ret_type = *ret_runtime_dir = NULL; return PAM_SUCCESS; } @@ -1311,6 +1315,10 @@ static int register_session( * somewhere else (for example PAM module parameters). Let's now update the environment variables, so that this * data is inherited into the session processes, and programs can rely on them to be initialized. */ + _cleanup_free_ char *real_type = strdup(c->type); /* make copy because this might point to env block, which we are going to update shortly */ + if (!real_type) + return pam_log_oom(handle); + r = update_environment(handle, "XDG_SESSION_TYPE", c->type); if (r != PAM_SUCCESS) return r; @@ -1367,6 +1375,7 @@ static int register_session( c->vtnr = real_vtnr; c->seat = *ret_seat = TAKE_PTR(rs); + c->type = *ret_type = TAKE_PTR(real_type); *ret_runtime_dir = TAKE_PTR(rt); return PAM_SUCCESS; @@ -1574,6 +1583,126 @@ static int setup_environment( return export_legacy_dbus_address(handle, runtime_directory); } +static int open_osc_context(pam_handle_t *handle, const char *session_type, UserRecord *ur) { + int r; + + assert(handle); + assert(ur); + + /* If this is a TTY session, then output the session start OSC sequence */ + + if (!streq_ptr(session_type, "tty")) + return PAM_SUCCESS; + + const char *e = pam_getenv(handle, "TERM"); + if (!e) + e = getenv("TERM"); + if (streq_ptr(e, "dumb")) + return PAM_SUCCESS; + + /* NB: we output directly to stdout, instead of going via pam_info() or so, because that's too + * high-level for us, as it suffixes the output with a newline, expecting a full blown text message + * as prompt string, not just an ANSI sequence. Note that PAM's conv_misc() actually goes to stdout + * anyway, hence let's do so here too, but only after careful validation. */ + if (!isatty(STDOUT_FILENO)) + return PAM_SUCCESS; + + /* Keep a reference to the TTY we are operating on, so that we can issue the OSC close sequence also + * if the TTY is already closed. We use an O_PATH reference here, rather than a properly opened fd, + * so that we don't delay tty hang-up. */ + _cleanup_close_ int tty_opath_fd = fd_reopen(STDOUT_FILENO, O_PATH|O_CLOEXEC); + if (tty_opath_fd < 0) + pam_syslog_errno(handle, LOG_DEBUG, tty_opath_fd, "Failed to pin TTY, ignoring: %m"); + else + tty_opath_fd = fd_move_above_stdio(tty_opath_fd); + + _cleanup_free_ char *osc = NULL; + sd_id128_t osc_id; + r = osc_context_open_session( + ur->user_name, + pam_getenv(handle, "XDG_SESSION_ID"), + &osc, + &osc_id); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to prepare OSC sequence: %m"); + + r = loop_write(STDOUT_FILENO, osc, SIZE_MAX); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to write OSC sequence to TTY: %m"); + + /* Remember the OSC context id, so that we can close it cleanly later */ + _cleanup_free_ sd_id128_t *osc_id_copy = newdup(sd_id128_t, &osc_id, 1); + if (!osc_id_copy) + return pam_log_oom(handle); + + r = pam_set_data(handle, "systemd.osc-context-id", osc_id_copy, pam_cleanup_free); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, + "Failed to set PAM OSC sequence ID data: @PAMERR@"); + + TAKE_PTR(osc_id_copy); + + if (tty_opath_fd >= 0) { + r = pam_set_data(handle, "systemd.osc-context-fd", FD_TO_PTR(tty_opath_fd), pam_cleanup_close); + if (r != PAM_SUCCESS) + return pam_syslog_pam_error(handle, LOG_ERR, r, + "Failed to set PAM OSC sequence fd data: @PAMERR@"); + + TAKE_FD(tty_opath_fd); + } + + return PAM_SUCCESS; +} + +static int close_osc_context(pam_handle_t *handle) { + int r; + + assert(handle); + + const void *p; + int tty_opath_fd = -EBADF; + r = pam_get_data(handle, "systemd.osc-context-fd", &p); + if (r == PAM_SUCCESS) + tty_opath_fd = PTR_TO_FD(p); + else if (r != PAM_NO_MODULE_DATA) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM OSC context fd: @PAMERR@"); + if (tty_opath_fd < 0) + return PAM_SUCCESS; + + const sd_id128_t *osc_id = NULL; + r = pam_get_data(handle, "systemd.osc-context-id", (const void**) &osc_id); + if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) + return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM OSC context id data: @PAMERR@"); + if (!osc_id) + return PAM_SUCCESS; + + /* Now open the original TTY again, so that we can write on it */ + _cleanup_close_ int fd = fd_reopen(tty_opath_fd, O_WRONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (fd < 0) { + pam_syslog_errno(handle, LOG_DEBUG, fd, "Failed to reopen TTY, ignoring: %m"); + return PAM_SUCCESS; + } + + /* /bin/login calls us with fds 0, 1, 2 closed, which is just weird. Let's step outside of that + * range, just in case pam_syslog() or so logs to stderr */ + fd = fd_move_above_stdio(fd); + + /* Safety check, let's verify this is a valid TTY we just opened */ + if (!isatty(fd)) + return PAM_SUCCESS; + + _cleanup_free_ char *osc = NULL; + r = osc_context_close(*osc_id, &osc); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to prepare OSC sequence: %m"); + + r = loop_write(fd, osc, SIZE_MAX); + if (r < 0) + return pam_syslog_errno(handle, LOG_ERR, r, "Failed to write OSC sequence to TTY: %m"); + + return PAM_SUCCESS; +} + _public_ PAM_EXTERN int pam_sm_open_session( pam_handle_t *handle, int flags, @@ -1637,8 +1766,8 @@ _public_ PAM_EXTERN int pam_sm_open_session( session_context_mangle(handle, &c, ur, debug); - _cleanup_free_ char *seat_buffer = NULL, *runtime_dir = NULL; - r = register_session(handle, &c, ur, debug, &seat_buffer, &runtime_dir); + _cleanup_free_ char *seat_buffer = NULL, *type_buffer = NULL, *runtime_dir = NULL; + r = register_session(handle, &c, ur, debug, &seat_buffer, &type_buffer, &runtime_dir); if (r != PAM_SUCCESS) return r; @@ -1653,7 +1782,11 @@ _public_ PAM_EXTERN int pam_sm_open_session( if (default_capability_ambient_set == UINT64_MAX) default_capability_ambient_set = pick_default_capability_ambient_set(ur, c.service, c.seat); - return apply_user_record_settings(handle, ur, debug, default_capability_bounding_set, default_capability_ambient_set); + r = apply_user_record_settings(handle, ur, debug, default_capability_bounding_set, default_capability_ambient_set); + if (r != PAM_SUCCESS) + return r; + + return open_osc_context(handle, c.type, ur); } _public_ PAM_EXTERN int pam_sm_close_session( @@ -1690,6 +1823,8 @@ _public_ PAM_EXTERN int pam_sm_close_session( return pam_syslog_pam_error(handle, LOG_ERR, r, "Failed to get PAM systemd.existing data: @PAMERR@"); + (void) close_osc_context(handle); + id = pam_getenv(handle, "XDG_SESSION_ID"); if (id && !existing) { _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL; diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c index 91bef1a5e9..b7b7130fdb 100644 --- a/src/machine/machinectl.c +++ b/src/machine/machinectl.c @@ -46,6 +46,7 @@ #include "main-func.h" #include "mkdir.h" #include "nulstr-util.h" +#include "osc-context.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -1222,6 +1223,13 @@ static int process_forward(sd_event *event, sd_bus_slot *machine_removed_slot, i log_info("Connected to machine %s. Press ^] three times within 1s to exit session.", name); } + _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; + if (!terminal_is_dumb()) { + r = osc_context_open_container(name, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } + r = sd_event_set_signal_exit(event, true); if (r < 0) return log_error_errno(r, "Failed to enable SIGINT/SITERM handling: %m"); diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 066156a5ca..91bc8e8b0e 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -85,6 +85,7 @@ #include "nsresource.h" #include "nulstr-util.h" #include "os-util.h" +#include "osc-context.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -3581,9 +3582,9 @@ static int inner_child( /* LXC sets container=lxc, so follow the scheme here */ envp[n_env++] = strjoina("container=", arg_container_service_name); - envp[n_env] = strv_find_prefix(environ, "TERM="); - if (envp[n_env]) - n_env++; + /* Propagate $TERM unless we are invoked in pipe mode and stdin/stdout/stderr don't refer to a TTY */ + const char *term = (arg_console_mode != CONSOLE_PIPE || on_tty()) ? strv_find_prefix(environ, "TERM=") : NULL; + envp[n_env++] = (char*) (term ?: "TERM=dumb"); if (home || !uid_is_valid(arg_uid) || arg_uid == 0) if (asprintf(envp + n_env++, "HOME=%s", home ?: "/root") < 0) @@ -5645,6 +5646,13 @@ static int run_container( (void) expose_port_execute(rtnl, &expose_args->fw_ctx, arg_expose_ports, AF_INET6, &expose_args->address6); } + _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; + if (IN_SET(arg_console_mode, CONSOLE_INTERACTIVE, CONSOLE_READ_ONLY) && !terminal_is_dumb()) { + r = osc_context_open_container(arg_machine, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } + if (arg_console_mode != CONSOLE_PIPE) { _cleanup_close_ int fd = -EBADF; PTYForwardFlags flags = 0; diff --git a/src/run/run.c b/src/run/run.c index 8d1606af07..816fd8dd9a 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -31,6 +31,7 @@ #include "fs-util.h" #include "hostname-util.h" #include "main-func.h" +#include "osc-context.h" #include "parse-argument.h" #include "parse-util.h" #include "path-util.h" @@ -1232,7 +1233,7 @@ static int transient_kill_set_properties(sd_bus_message *m) { } static int transient_service_set_properties(sd_bus_message *m, const char *pty_path, int pty_fd) { - bool send_term = false; + int send_term = false; /* tri-state */ int r; /* We disable environment expansion on the server side via ExecStartEx=:. @@ -1311,14 +1312,22 @@ static int transient_service_set_properties(sd_bus_message *m, const char *pty_p if (r < 0) return bus_log_create_error(r); - send_term = isatty_safe(STDIN_FILENO) || isatty_safe(STDOUT_FILENO) || isatty_safe(STDERR_FILENO); + send_term = -1; } - if (send_term) { + if (send_term != 0) { const char *e; - e = getenv("TERM"); - if (e) { + /* Propagate $TERM only if we are actually connected to a TTY */ + if (isatty_safe(STDIN_FILENO) || isatty_safe(STDOUT_FILENO) || isatty_safe(STDERR_FILENO)) { + e = getenv("TERM"); + send_term = true; + } else + /* If we are not connected to any TTY ourselves, then send TERM=dumb, but only if we + * really need to (because we actually allocated a TTY for the service) */ + e = "dumb"; + + if (send_term > 0) { _cleanup_free_ char *n = NULL; n = strjoin("TERM=", e); @@ -2194,7 +2203,14 @@ static int start_transient_service(sd_bus *bus) { if (!c.bus_path) return log_oom(); + _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; if (pty_fd >= 0) { + if (!terminal_is_dumb() && arg_exec_user) { + r = osc_context_open_chpriv(arg_exec_user, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } + (void) sd_event_set_signal_exit(c.event, true); if (!arg_quiet) diff --git a/src/shared/meson.build b/src/shared/meson.build index 0459975005..62c6bcf511 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -132,6 +132,7 @@ shared_sources = files( 'numa-util.c', 'open-file.c', 'openssl-util.c', + 'osc-context.c', 'output-mode.c', 'pager.c', 'parse-argument.c', diff --git a/src/shared/osc-context.c b/src/shared/osc-context.c new file mode 100644 index 0000000000..d1121ba0dd --- /dev/null +++ b/src/shared/osc-context.c @@ -0,0 +1,342 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "escape.h" +#include "hostname-util.h" +#include "id128-util.h" +#include "osc-context.h" +#include "pidfd-util.h" +#include "process-util.h" +#include "string-util.h" +#include "terminal-util.h" +#include "user-util.h" + +/* This currently generates open sequences for OSC 3008 types "boot", "container", "vm", "elevate", "chpriv", + * "subcontext", "session", "sevice". + * + * NB: Not generated by systemd: "remote" (would have to be generated from the SSH client), "app". */ + +static int strextend_escaped(char **s, const char *prefix, const char *value) { + assert(s); + assert(value); + + if (!strextend(s, prefix)) + return -ENOMEM; + + _cleanup_free_ char *e = xescape(value, ";\\"); + if (!e) + return -ENOMEM; + + if (!strextend(s, e)) + return -ENOMEM; + + return 0; +} + +static int osc_append_identity(char **s) { + int r; + + assert(s); + + _cleanup_free_ char *u = getusername_malloc(); + if (u) { + r = strextend_escaped(s, ";user=", u); + if (r < 0) + return r; + } + + _cleanup_free_ char *h = gethostname_malloc(); + if (h) { + r = strextend_escaped(s, ";hostname=", h); + if (r < 0) + return r; + } + + sd_id128_t id; + if (sd_id128_get_machine(&id) >= 0) { + r = strextendf(s, ";machineid=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(id)); + if (r < 0) + return r; + } + + if (sd_id128_get_boot(&id) >= 0) { + r = strextendf(s, ";bootid=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(id)); + if (r < 0) + return r; + } + + r = strextendf(s, ";pid=" PID_FMT, getpid_cached()); + if (r < 0) + return r; + + uint64_t pidfdid; + if (pidfd_get_inode_id_self_cached(&pidfdid) >= 0) { + r = strextendf(s, ";pidfdid=%" PRIu64, pidfdid); + if (r < 0) + return r; + } + + r = strextend_escaped(s, ";comm=", program_invocation_short_name); + if (r < 0) + return r; + + return 0; +} + +static void osc_context_default_id(sd_id128_t *ret_id) { + + /* Usually we only want one context ID per tool. Since we don't want to store the ID let's just hash + * one from process credentials */ + + struct { + const char label[32]; + uint64_t pidfdid; + uint8_t auxval[16]; + pid_t pid; + } data = { + .label = "systemd context hash bytes v1", + .pid = getpid_cached(), + }; + + assert(ret_id); + + memcpy(data.auxval, ULONG_TO_PTR(getauxval(AT_RANDOM)), sizeof(data.auxval)); + (void) pidfd_get_inode_id_self_cached(&data.pidfdid); + + *ret_id = id128_digest(&data, sizeof(data)); +} + +static int osc_context_intro_raw(sd_id128_t id, char **ret_seq) { + int r; + + assert(ret_seq); + + _cleanup_free_ char *seq = NULL; + if (asprintf(&seq, ANSI_OSC "3008;start=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(id)) < 0) + return -ENOMEM; + + r = osc_append_identity(&seq); + if (r < 0) + return r; + + *ret_seq = TAKE_PTR(seq); + return 0; +} + +static int osc_context_intro(char **ret_seq, sd_id128_t *ret_context_id) { + int r; + + assert(ret_seq); + + /* If the user passed us a buffer for the context ID generate a randomized one, since we have a place + * to store it. The user should pass the ID back to osc_context_close() later on. If the user did not + * pass us a buffer, we'll use a session ID hashed from process properties that remain stable as long + * our process exists. It hence also remains stable across reexec and similar. */ + sd_id128_t id; + if (ret_context_id) { + r = sd_id128_randomize(&id); + if (r < 0) + return r; + } else + osc_context_default_id(&id); + + _cleanup_free_ char *seq = NULL; + r = osc_context_intro_raw(id, &seq); + if (r < 0) + return r; + + if (ret_context_id) + *ret_context_id = id; + + *ret_seq = TAKE_PTR(seq); + return 0; +} + +static int osc_context_outro(char *_seq, sd_id128_t id, char **ret_seq, sd_id128_t *ret_context_id) { + _cleanup_free_ char *seq = TAKE_PTR(_seq); /* We take possession of the string no matter what */ + + if (ret_seq) + *ret_seq = TAKE_PTR(seq); + else { + fputs(seq, stdout); + fflush(stdout); + } + + if (ret_context_id) + *ret_context_id = id; + + return 0; +} + +int osc_context_open_boot(char **ret_seq) { + int r; + + _cleanup_free_ char *seq = NULL; + r = osc_context_intro(&seq, /* ret_context_id= */ NULL); + if (r < 0) + return r; + + if (!strextend(&seq, ";type=boot" ANSI_ST)) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), /* context_id= */ SD_ID128_NULL, ret_seq, /* ret_context_id= */ NULL); +} + +int osc_context_open_container(const char *name, char **ret_seq, sd_id128_t *ret_context_id) { + int r; + + _cleanup_free_ char *seq = NULL; + sd_id128_t id = SD_ID128_NULL; + r = osc_context_intro(&seq, ret_context_id ? &id : NULL); + if (r < 0) + return r; + + if (name) { + r = strextend_escaped(&seq, ";container=", name); + if (r < 0) + return r; + } + + if (!strextend(&seq, ";type=container" ANSI_ST)) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), id, ret_seq, ret_context_id); +} + +int osc_context_open_vm(const char *name, char **ret_seq, sd_id128_t *ret_context_id) { + int r; + + assert(name); + + _cleanup_free_ char *seq = NULL; + sd_id128_t id = SD_ID128_NULL; + r = osc_context_intro(&seq, ret_context_id ? &id : NULL); + if (r < 0) + return r; + + r = strextend_escaped(&seq, ";vm=", name); + if (r < 0) + return r; + + if (!strextend(&seq, ";type=vm" ANSI_ST)) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), id, ret_seq, ret_context_id); +} + +int osc_context_open_chpriv(const char *target_user, char **ret_seq, sd_id128_t *ret_context_id) { + int r; + + assert(target_user); + + _cleanup_free_ char *seq = NULL; + sd_id128_t id = SD_ID128_NULL; + r = osc_context_intro(&seq, ret_context_id ? &id : NULL); + if (r < 0) + return r; + + if (is_this_me(target_user) > 0) { + if (!strextend(&seq, ";type=subcontext" ANSI_ST)) + return -ENOMEM; + } else if (STR_IN_SET(target_user, "root", "0")) { + if (!strextend(&seq, ";type=elevate" ANSI_ST)) + return -ENOMEM; + } else { + r = strextend_escaped(&seq, ";targetuser=", target_user); + if (r < 0) + return r; + + if (!strextend(&seq, ";type=chpriv" ANSI_ST)) + return -ENOMEM; + } + + return osc_context_outro(TAKE_PTR(seq), id, ret_seq, ret_context_id); +} + +int osc_context_open_session(const char *user, const char *session_id, char **ret_seq, sd_id128_t *ret_context_id) { + int r; + + _cleanup_free_ char *seq = NULL; + sd_id128_t id = SD_ID128_NULL; + r = osc_context_intro(&seq, ret_context_id ? &id : NULL); + if (r < 0) + return r; + + if (user) { + r = strextend_escaped(&seq, ";targetuser=", user); + if (r < 0) + return r; + } + + if (session_id) { + r = strextend_escaped(&seq, ";sessionid=", session_id); + if (r < 0) + return r; + } + + if (!strextend(&seq, ";type=session" ANSI_ST)) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), id, ret_seq, ret_context_id); +} + +int osc_context_open_service(const char *unit, sd_id128_t invocation_id, char **ret_seq) { + int r; + + sd_id128_t id = SD_ID128_NULL; + r = osc_context_id_from_invocation_id(invocation_id, &id); + if (r < 0) + return r; + + _cleanup_free_ char *seq = NULL; + r = osc_context_intro_raw(id, &seq); + if (r < 0) + return r; + + if (unit) { + r = strextend_escaped(&seq, ";servicename=", unit); + if (r < 0) + return r; + } + + r = strextendf(&seq, ";invocationid=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(invocation_id)); + if (r < 0) + return r; + + if (!strextend(&seq, ";type=service" ANSI_ST)) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), id, ret_seq, /* ret_context_id= */ NULL); +} + +int osc_context_close(sd_id128_t id, char **ret_seq) { + + if (sd_id128_is_null(id)) { + /* nil uuid: no session opened */ + if (ret_seq) + *ret_seq = NULL; + return 0; + } + + if (sd_id128_is_allf(id)) /* max uuid: default session opened */ + osc_context_default_id(&id); + + _cleanup_free_ char *seq = NULL; + if (asprintf(&seq, ANSI_OSC "3008;end=" SD_ID128_FORMAT_STR ANSI_ST, SD_ID128_FORMAT_VAL(id)) < 0) + return -ENOMEM; + + return osc_context_outro(TAKE_PTR(seq), SD_ID128_NULL, ret_seq, /* ret_context_id= */ NULL); +} + +int osc_context_id_from_invocation_id(sd_id128_t id, sd_id128_t *ret) { + static const sd_id128_t app_id = SD_ID128_MAKE(5d,63,a5,8d,96,fd,45,d0,a0,f0,63,50,fc,d8,9a,cd); + + assert(ret); + + /* Hash the context id from the invocation id, so that we don't have to store the context id (as long + * as we have the invocation ID). We don't use the invocation ID literally in order not to make it too + * easy to cancel our service context because the invocation ID is reported literally via the + * $INVOCATION_ID env var. */ + return sd_id128_get_app_specific(id, app_id, ret); +} diff --git a/src/shared/osc-context.h b/src/shared/osc-context.h new file mode 100644 index 0000000000..20c70b7183 --- /dev/null +++ b/src/shared/osc-context.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-id128.h" + +#include "macro.h" + +int osc_context_open_boot(char **ret_seq); +int osc_context_open_container(const char *name, char **ret_seq, sd_id128_t *ret_context_id); +int osc_context_open_vm(const char *name, char **ret_seq, sd_id128_t *ret_context_id); +int osc_context_open_chpriv(const char *target_user, char **ret_seq, sd_id128_t *ret_context_id); +int osc_context_open_session(const char *user, const char *session_id, char **ret_seq, sd_id128_t *ret_context_id); +int osc_context_open_service(const char *unit, sd_id128_t invocation_id, char **ret_seq); +int osc_context_close(sd_id128_t id, char **ret_seq); + +static inline void osc_context_closep(sd_id128_t *context_id) { + (void) osc_context_close(*ASSERT_PTR(context_id), NULL); +} + +int osc_context_id_from_invocation_id(sd_id128_t id, sd_id128_t *ret); diff --git a/src/shared/pam-util.c b/src/shared/pam-util.c index 0b97e81ab8..95e646b460 100644 --- a/src/shared/pam-util.c +++ b/src/shared/pam-util.c @@ -7,6 +7,7 @@ #include "alloc-util.h" #include "bus-internal.h" #include "errno-util.h" +#include "fd-util.h" #include "format-util.h" #include "macro.h" #include "pam-util.h" @@ -249,6 +250,20 @@ void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) { free(data); } +void pam_cleanup_close(pam_handle_t *handle, void *data, int error_status) { + + /* A generic destructor for pam_set_data() that just closes the specified fd. + * + * As per pam_set_data() docs: the PAM_DATA_SILENT indicates whether we are called in the forked off + * payload child of the new session. However, all file descriptors are most likely already closed + * there (that's what /bin/login does after all), hence let's simply turn this into a NOP in the + * child, and only close the fd in the parent. */ + if (FLAGS_SET(error_status, PAM_DATA_SILENT)) + return; + + safe_close(PTR_TO_FD(data)); +} + int pam_get_item_many_internal(pam_handle_t *handle, ...) { va_list ap; int r; diff --git a/src/shared/pam-util.h b/src/shared/pam-util.h index 2e19516042..7dae9dd31d 100644 --- a/src/shared/pam-util.h +++ b/src/shared/pam-util.h @@ -42,6 +42,7 @@ int pam_release_bus_connection(pam_handle_t *handle, const char *module_name); int pam_get_bus_data(pam_handle_t *handle, const char *module_name, PamBusData **ret); void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status); +void pam_cleanup_close(pam_handle_t *handle, void *data, int error_status); int pam_get_item_many_internal(pam_handle_t *handle, ...); #define pam_get_item_many(handle, ...) pam_get_item_many_internal(handle, __VA_ARGS__, -1) diff --git a/src/test/meson.build b/src/test/meson.build index 1e8bded9d4..ec03ad2b7d 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -137,6 +137,7 @@ simple_tests += files( 'test-open-file.c', 'test-ordered-set.c', 'test-os-util.c', + 'test-osc-context.c', 'test-parse-argument.c', 'test-parse-helpers.c', 'test-path-lookup.c', diff --git a/src/test/test-osc-context.c b/src/test/test-osc-context.c new file mode 100644 index 0000000000..030cb91084 --- /dev/null +++ b/src/test/test-osc-context.c @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "hexdecoct.h" +#include "osc-context.h" +#include "tests.h" +#include "user-util.h" + +TEST(osc) { + _cleanup_free_ char *seq = NULL; + + log_info("boot"); + ASSERT_OK(osc_context_open_boot(&seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(SD_ID128_ALLF, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("container"); + sd_id128_t id; + ASSERT_OK(osc_context_open_container("foobar", &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("vm"); + ASSERT_OK(osc_context_open_vm("foobar", &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("chpriv → root"); + ASSERT_OK(osc_context_open_chpriv("root", &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("chpriv → userxyz"); + ASSERT_OK(osc_context_open_chpriv("userxyz", &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("chpriv → self"); + _cleanup_free_ char *self = ASSERT_PTR(getusername_malloc()); + ASSERT_OK(osc_context_open_chpriv(self, &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("session"); + ASSERT_OK(osc_context_open_session("foobaruser", "session1", &seq, &id)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + log_info("service"); + sd_id128_t invocation_id; + ASSERT_OK(sd_id128_randomize(&invocation_id)); + ASSERT_OK(osc_context_open_service("getty@tty1.service", invocation_id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); + + ASSERT_OK(osc_context_id_from_invocation_id(invocation_id, &id)); + ASSERT_OK(osc_context_close(id, &seq)); + hexdump(/* f = */ NULL, seq, SIZE_MAX); + seq = mfree(seq); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index 87304346e9..a5616c8121 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -290,7 +290,7 @@ TEST(get_color_mode) { TEST(terminal_reset_defensive) { int r; - r = terminal_reset_defensive(STDOUT_FILENO, /* switch_to_text= */ false); + r = terminal_reset_defensive(STDOUT_FILENO, /* flags= */ 0); if (r < 0) log_notice_errno(r, "Failed to reset terminal: %m"); } diff --git a/src/tty-ask-password-agent/tty-ask-password-agent.c b/src/tty-ask-password-agent/tty-ask-password-agent.c index 6950a8697e..b3471e272f 100644 --- a/src/tty-ask-password-agent/tty-ask-password-agent.c +++ b/src/tty-ask-password-agent/tty-ask-password-agent.c @@ -145,7 +145,7 @@ static int agent_ask_password_tty( if (tty_fd < 0) return log_error_errno(tty_fd, "Failed to acquire %s: %m", con); - (void) terminal_reset_defensive_locked(tty_fd, /* switch_to_text= */ true); + (void) terminal_reset_defensive_locked(tty_fd, TERMINAL_RESET_SWITCH_TO_TEXT); log_info("Starting password query on %s.", con); } diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 8776614001..e0f49bf092 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -48,6 +48,7 @@ #include "mkdir.h" #include "namespace-util.h" #include "netif-util.h" +#include "osc-context.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" @@ -2249,8 +2250,15 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { /* Exit when the child exits */ (void) event_add_child_pidref(event, NULL, &child_pidref, WEXITED, on_child_exit, NULL); + _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; _cleanup_(pty_forward_freep) PTYForward *forward = NULL; if (master >= 0) { + if (!terminal_is_dumb()) { + r = osc_context_open_vm(arg_machine, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } + r = pty_forward_new(event, master, ptyfwd_flags, &forward); if (r < 0) return log_error_errno(r, "Failed to create PTY forwarder: %m"); diff --git a/test/units/TEST-13-NSPAWN.machined.sh b/test/units/TEST-13-NSPAWN.machined.sh index 4875e48f80..d8c15dfcfb 100755 --- a/test/units/TEST-13-NSPAWN.machined.sh +++ b/test/units/TEST-13-NSPAWN.machined.sh @@ -399,7 +399,7 @@ grep -q "BAR" /tmp/none-existent-file kill "$PID" # Test varlinkctl's --exec fd passing logic properly -assert_eq "$(varlinkctl --exec call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Open '{"name": ".host", "mode": "shell", "user": "root", "path": "/bin/bash", "args": ["/bin/bash", "-c", "echo $((7 + 8))"]}' -- bash -c 'read -r -N 2 x <&3 ; echo "$x"')" 15 +assert_eq "$(varlinkctl --exec call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.Open '{"name": ".host", "mode": "shell", "user": "root", "path": "/bin/bash", "args": ["/bin/bash", "-c", "echo $((7 + 8))"], "environment": ["TERM=dumb"]}' -- bash -c 'read -r -N 2 x <&3 ; echo "$x"')" 15 # test io.systemd.Machine.MapFrom varlinkctl call /run/systemd/machine/io.systemd.Machine io.systemd.Machine.MapFrom '{"name": "long-running", "uid":0, "gid": 0}' diff --git a/tmpfiles.d/20-systemd-osc-context.conf.in b/tmpfiles.d/20-systemd-osc-context.conf.in new file mode 100644 index 0000000000..dc39f9a13a --- /dev/null +++ b/tmpfiles.d/20-systemd-osc-context.conf.in @@ -0,0 +1,12 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +# See tmpfiles.d(5) for details. + +{% if LINK_OSC_CONTEXT_DROPIN %} +L$ {{SHELLPROFILEDIR}}/80-systemd-osc-context.sh - - - - {{LIBEXECDIR}}/profile.d/80-systemd-osc-context.sh +{% endif %} diff --git a/tmpfiles.d/meson.build b/tmpfiles.d/meson.build index 19eb6d9c99..db58a9817a 100644 --- a/tmpfiles.d/meson.build +++ b/tmpfiles.d/meson.build @@ -31,6 +31,7 @@ endforeach in_files = [ ['20-systemd-shell-extra.conf', 'LINK_SHELL_EXTRA_DROPIN'], + ['20-systemd-osc-context.conf', 'LINK_OSC_CONTEXT_DROPIN'], ['20-systemd-ssh-generator.conf', 'ENABLE_SSH_PROXY_CONFIG'], ['20-systemd-stub.conf', 'ENABLE_EFI'], ['20-systemd-userdb.conf', 'ENABLE_SSH_USERDB_CONFIG'],