Skip to content

Commit

Permalink
Issue OSC ANSI sequence whenever we change "context" of a TTY, i.e. a…
Browse files Browse the repository at this point in the history
…cquire privs, enter container or VM or similar (systemd#35224)

This is mostly a strawman to get a discussion going regarding how to
communicate to terminal emulators such as ptyxis about run0 (and nspawn,
and vmspawn, and moe) and what it does.

It's hierarchical and I think still relatively simple.

/cc @chergert
  • Loading branch information
poettering authored Feb 27, 2025
2 parents 78f2c17 + bbdad5c commit 46bd501
Show file tree
Hide file tree
Showing 30 changed files with 1,208 additions and 49 deletions.
339 changes: 339 additions & 0 deletions docs/OSC-CONTEXT.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
59 changes: 59 additions & 0 deletions profile.d/80-systemd-osc-context.sh
Original file line number Diff line number Diff line change
@@ -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" "$(</etc/machine-id)" "$(</proc/sys/kernel/random/boot_id)" "$$"
}

__systemd_osc_context_precmdline() {
local systemd_exitstatus="$?"

# Close previous command
if [ -n "${systemd_osc_context_cmd_id:-}" ]; then
if [ "$systemd_exitstatus" -ge 127 ]; then
printf "\033]8003;end=%s;exit=interrupt;signal=%s\033\\" "$systemd_osc_context_cmd_id" $((systemd_exitstatus-127))
elif [ "$systemd_exitstatus" -ne 0 ]; then
printf "\033]8003;end=%s;exit=failure;status=%s\033\\" "$systemd_osc_context_cmd_id" $((systemd_exitstatus))
else
printf "\033]8003;end=%s;exit=success\033\\" "$systemd_osc_context_cmd_id"
fi
fi

# Prepare a context ID for this shell if we have none
if [ -z "${systemd_osc_context_shell_id:-}" ]; then
read -r systemd_osc_context_shell_id </proc/sys/kernel/random/uuid
fi

# Create or update the shell session
printf "\033]8003;start=%s%s;type=shell;cwd=%s\033\\" "$systemd_osc_context_shell_id" "$(__systemd_osc_context_common)" "$(__systemd_osc_context_escape "$PWD")"

# Prepare cmd id for next command
read -r systemd_osc_context_cmd_id </proc/sys/kernel/random/uuid
}

if [[ -n "${BASH_VERSION:-}" ]]; then
# Whenever a new prompt is shown close the previous command, and prepare new command
PROMPT_COMMAND+=(__systemd_osc_context_precmdline)

# PS0 is shown right after a prompt completed, but before the command is executed
PS0='\033]8003;start=$systemd_osc_context_cmd_id$(__systemd_osc_context_common);type=command;cwd=$(__systemd_osc_context_escape "$PWD")\033\\'"${PS0:-}"
fi
8 changes: 8 additions & 0 deletions profile.d/meson.build
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# SPDX-License-Identifier: LGPL-2.1-or-later

install_data('70-systemd-shell-extra.sh', install_dir : shellprofiledir.startswith('/usr/') ? shellprofiledir : libexecdir / 'profile.d')
install_data('80-systemd-osc-context.sh', install_dir : shellprofiledir.startswith('/usr/') ? shellprofiledir : libexecdir / 'profile.d')

if conf.get('LINK_SHELL_EXTRA_DROPIN') == 1
install_emptydir(shellprofiledir)

meson.add_install_script(sh, '-c',
ln_s.format(libexecdir / 'profile.d' / '70-systemd-shell-extra.sh', shellprofiledir / '70-systemd-shell-extra.sh'))
endif

if conf.get('LINK_OSC_CONTEXT_DROPIN') == 1
install_emptydir(shellprofiledir)

meson.add_install_script(sh, '-c',
ln_s.format(libexecdir / 'profile.d' / '80-systemd-osc-context.sh', shellprofiledir / '80-systemd-osc-context.sh'))
endif
21 changes: 9 additions & 12 deletions src/basic/terminal-util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1930,38 +1930,35 @@ 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);
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) {
Expand Down
10 changes: 8 additions & 2 deletions src/basic/terminal-util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
21 changes: 17 additions & 4 deletions src/core/exec-invoke.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
55 changes: 47 additions & 8 deletions src/core/execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/core/execute.h
Original file line number Diff line number Diff line change
Expand Up @@ -530,21 +530,22 @@ 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);
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);
Expand Down
Loading

0 comments on commit 46bd501

Please sign in to comment.