From 09b130c9cd6cbef6dfc17951a5ef5ca9a82a168f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 19 Nov 2024 00:05:42 +0100 Subject: [PATCH 01/14] doc: add OSC 3008 spec --- docs/OSC-CONTEXT.md | 339 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 339 insertions(+) create mode 100644 docs/OSC-CONTEXT.md 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) | From 91aa93ec8d213340b7dea8dcd2e3fe5f90265517 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 11:45:59 +0100 Subject: [PATCH 02/14] osc-util: add helpers for writing OSC context events --- src/shared/meson.build | 1 + src/shared/osc-context.c | 342 ++++++++++++++++++++++++++++++++++++ src/shared/osc-context.h | 20 +++ src/test/meson.build | 1 + src/test/test-osc-context.c | 89 ++++++++++ 5 files changed, 453 insertions(+) create mode 100644 src/shared/osc-context.c create mode 100644 src/shared/osc-context.h create mode 100644 src/test/test-osc-context.c 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/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); From 98c283131cda67c98946ef373e3bb33aa52de59a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 11:46:42 +0100 Subject: [PATCH 03/14] pid1: issue boot context issue at boot --- src/core/main.c | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) 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); From 466b285bdbb185fa1e1404b2eb3b08d6419a8bf2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 12:33:13 +0100 Subject: [PATCH 04/14] nspawn: output context OSC --- src/nspawn/nspawn.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 066156a5ca..a504279fa5 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" @@ -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; From 5eb39a385a4b216ef2a3a596da42cbb20fdc7fa6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 12:35:00 +0100 Subject: [PATCH 05/14] vmspawn: generate vm context OSC --- src/vmspawn/vmspawn.c | 8 ++++++++ 1 file changed, 8 insertions(+) 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"); From ff03ac873093db2bdb165c1216ab196322502b4f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 12:46:08 +0100 Subject: [PATCH 06/14] machinectl: add OSC context support to login/shell commands --- src/machine/machinectl.c | 8 ++++++++ 1 file changed, 8 insertions(+) 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"); From 575922c914c732bb77c99aee54c84dc365d60631 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 18 Nov 2024 12:46:50 +0100 Subject: [PATCH 07/14] run: generate OSC context sequence in run0/system-run --- src/run/run.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/run/run.c b/src/run/run.c index 8d1606af07..b2d74f6088 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" @@ -2194,7 +2195,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) From d8069b8add9a2290d7ed85012f8459fccfc632ed Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 30 Jan 2025 13:45:55 +0100 Subject: [PATCH 08/14] pam_systemd: issue context OSC sequences when allocating new TTY session note: this also adds making a copy of the session type string after registering the session. That's because we need to check the session type we settled on later to condition out the OSC sequence (because it should only be issued on TTY sessions). However, the session type string originally quite likely points into the PAM environment block, which we update in the meantime, invalidating that pointer. hence, make an explicit copy first, and use that. --- src/login/pam_systemd.c | 149 ++++++++++++++++++++++++++++++++++++++-- src/shared/pam-util.c | 15 ++++ src/shared/pam-util.h | 1 + 3 files changed, 158 insertions(+), 7 deletions(-) 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/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) From dadbb34919abd3fefeb5b8ccc9794da9398a2503 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 30 Jan 2025 15:09:21 +0100 Subject: [PATCH 09/14] profile: generate shell + command OSC events --- meson.build | 1 + profile.d/80-systemd-osc-context.sh | 59 +++++++++++++++++++++++ profile.d/meson.build | 8 +++ tmpfiles.d/20-systemd-osc-context.conf.in | 12 +++++ tmpfiles.d/meson.build | 1 + 5 files changed, 81 insertions(+) create mode 100644 profile.d/80-systemd-osc-context.sh create mode 100644 tmpfiles.d/20-systemd-osc-context.conf.in 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" "$( Date: Fri, 31 Jan 2025 15:06:34 +0100 Subject: [PATCH 10/14] run: tweak logic how we set $TERM for activated services There are two cases when we invoke a service on a TTY: 1. We ourselves are connected to a TTY and would intend to enable PTY forwarding. 2. We are allocating a TTY but are not ourselves connected to a TTY and just want to input/output to pipe or other non-TTY fd. Let's propagate $TERM only as-is in the first case. In the 2nd case, let's explicitly set $TERM to "dumb", so that invoked progams do not issue needless ansi sequences, since we are not propagating them to a terminal either. This should be a much safer result, for cases where people include invocations of systemd-nspawn with full TTY allocation in a shell pipeline or so. (of course, the user can always explicitly override this) --- src/run/run.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/run/run.c b/src/run/run.c index b2d74f6088..816fd8dd9a 100644 --- a/src/run/run.c +++ b/src/run/run.c @@ -1233,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=:. @@ -1312,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); From ddb503b7f601f7874d68bc4959c2a426f9740fc9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 31 Jan 2025 20:23:39 +0100 Subject: [PATCH 11/14] nspawn: set TERM to "dumb" if we are invoked outside of TTY context --- src/nspawn/nspawn.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index a504279fa5..91bc8e8b0e 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -3582,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) From 9ab703d8e13da560bd04c2692c0faa7a76cebf33 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 31 Jan 2025 15:30:07 +0100 Subject: [PATCH 12/14] terminal-util: change 2nd parameter of terminal_reset_defensive() to flags let's convert the 2nd argumeng form a boolean to a proper flags parameter. Doesn't change behaviour in anyway, but is more readable, and prepares ground for adding more flags soon. --- src/basic/terminal-util.c | 8 ++++---- src/basic/terminal-util.h | 8 ++++++-- src/core/exec-invoke.c | 4 ++-- src/core/execute.c | 2 +- src/firstboot/firstboot.c | 2 +- src/home/homectl.c | 2 +- src/test/test-terminal-util.c | 2 +- src/tty-ask-password-agent/tty-ask-password-agent.c | 2 +- 8 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index fce3bb95a3..1ff0dbcedc 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1930,7 +1930,7 @@ int terminal_set_cursor_position(int fd, unsigned row, unsigned column) { return loop_write(fd, cursor_position, SIZE_MAX); } -int terminal_reset_defensive(int fd, bool switch_to_text) { +int terminal_reset_defensive(int fd, TerminalResetFlags flags) { int r = 0; assert(fd >= 0); @@ -1946,7 +1946,7 @@ int terminal_reset_defensive(int fd, bool switch_to_text) { 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) RET_GATHER(r, terminal_reset_ansi_seq(fd)); @@ -1954,14 +1954,14 @@ int terminal_reset_defensive(int fd, bool switch_to_text) { 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..e2c4a83230 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -41,8 +41,12 @@ 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, +} 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..8d48d1f75a 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -668,7 +668,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; @@ -4375,7 +4375,7 @@ 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); + (void) terminal_reset_defensive(STDOUT_FILENO, /* flags= */ 0); } (void) exec_context_apply_tty_size(context, STDIN_FILENO, STDOUT_FILENO, /* tty_path= */ NULL); diff --git a/src/core/execute.c b/src/core/execute.c index 6811bf301c..62b9e6e221 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -178,7 +178,7 @@ 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); r = exec_context_apply_tty_size(context, fd, fd, path); if (r < 0) 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/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); } From 5b3eaf9e680d98e5247bd0af16afc004cca1d754 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 31 Jan 2025 15:32:01 +0100 Subject: [PATCH 13/14] terminal-util: change conditioning in terminal_reset_defensive() So far we conditioned the logic that issues ansi sequences for resetting the TTY based on whether something is a pty is not (under the assumption we need no reset on ptys, since they are shortlived). This is simply wrong though. The pty that a container getty is invoked on is generally long-lived: as long as the container is up, and it will be reused between getty instances/sessions all the time. In such a case we really should reset properly. Let's instead make the logic dependent on whether TERM is set to anything other than "dumb". The previous commit made sure we always set TERM in a sensible way in systemd-run, hence this *explicit* logic sounds like a much better choice now, as it mea --- src/basic/terminal-util.c | 13 +++++-------- src/basic/terminal-util.h | 2 ++ src/core/exec-invoke.c | 9 ++++++++- src/core/execute.c | 30 +++++++++++++++++++++++++----- src/core/execute.h | 1 + 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index 1ff0dbcedc..787e698738 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1934,21 +1934,18 @@ int terminal_reset_defensive(int fd, TerminalResetFlags flags) { int r = 0; assert(fd >= 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, 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; diff --git a/src/basic/terminal-util.h b/src/basic/terminal-util.h index e2c4a83230..9decf42f22 100644 --- a/src/basic/terminal-util.h +++ b/src/basic/terminal-util.h @@ -43,6 +43,8 @@ bool isatty_safe(int fd); 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); diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 8d48d1f75a..490b3a522a 100644 --- a/src/core/exec-invoke.c +++ b/src/core/exec-invoke.c @@ -4367,6 +4367,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,7 +4379,10 @@ 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, /* flags= */ 0); + /* 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); diff --git a/src/core/execute.c b/src/core/execute.c index 62b9e6e221..7d99e72328 100644 --- a/src/core/execute.c +++ b/src/core/execute.c @@ -129,11 +129,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) @@ -178,7 +179,10 @@ 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, TERMINAL_RESET_SWITCH_TO_TEXT); + (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) @@ -974,6 +978,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); diff --git a/src/core/execute.h b/src/core/execute.h index 6421f19cc4..abfa31afcb 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); From bbdad5c025fbc037e357723e2c33ce8339f95287 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 31 Jan 2025 17:40:46 +0100 Subject: [PATCH 14/14] core: also issue OSC 3008 from service context (Note: we also change TEST-13-NSPAWN.machined.sh minimally here, because it checks for byte precise output of a pty allocated for a service invocation - which it's not going to get if it claims that the pty is an all-powerful one. After all this PR ensures that we'll generate the new OSC sequence on non-dumb terminals associated with services. Hence, set TERM=dumb explicitly to ensure no ANSI sequences are generated, ever. Which is a nice test btw that TERM=dumb really does its thing here.) --- src/core/exec-invoke.c | 10 ++++++++-- src/core/execute.c | 25 ++++++++++++++++++++++--- src/core/execute.h | 4 ++-- src/core/service.c | 2 +- test/units/TEST-13-NSPAWN.machined.sh | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c index 490b3a522a..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" @@ -4386,6 +4387,9 @@ static void prepare_terminal( } (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( @@ -4562,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 7d99e72328..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" @@ -144,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; @@ -188,6 +190,23 @@ void exec_context_tty_reset(const ExecContext *context, const ExecParameters *p) 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); @@ -1630,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; @@ -1639,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 abfa31afcb..8128c4c928 100644 --- a/src/core/execute.h +++ b/src/core/execute.h @@ -538,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/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/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}'