From 6810b646d0f5b21eb405483a34ead2e2b4d1a225 Mon Sep 17 00:00:00 2001 From: Elena FRISON Date: Sat, 17 Oct 2026 15:11:38 +0200 Subject: [PATCH] exec_pty: keep user tty output flags in sync with the command When use_pty is enabled (the default) and the command's stderr is redirected to a pipe, socket or FIFO while its stdout is still the user's terminal, a third party reading that stderr (e.g. the tee in "sudo strace cmd 2> >(tee log)") writes back to the terminal itself. Since the terminal was in raw mode, the newline-only output was rendered staircased. Statically preserving the terminal's output flags, as is done for a piped stdout (commit 78b7121), would fix this but regress full-screen commands, which run their pty raw and emit bare newlines that ONLCR would mangle. Instead, track the command's OPOST state via its pty and toggle the user terminal's output flags to match as output is relayed: ONLCR while the command is cooked, raw while it is not. This is gated on stderr being a pipe/socket/FIFO, reuses sudo_term_raw() (no ABI change) and caches the applied state. While a full-screen command holds its pty raw, stderr echoed to the terminal by an external reader is still staircased; a single line discipline cannot serve both sudo's relayed output and a raw stream sudo does not control. This is no worse than before. Fixes GitHub issue #349 --- src/exec_pty.c | 83 ++++++++++++++++++++++++++++++++++++++++++++++++- src/sudo_exec.h | 10 ++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/src/exec_pty.c b/src/exec_pty.c index b8898d8a8a..ab7b0026be 100644 --- a/src/exec_pty.c +++ b/src/exec_pty.c @@ -185,6 +185,8 @@ resume_terminal(struct exec_closure *ec) if (ec->foreground) { /* Foreground process, set tty to raw mode. */ ec->term_raw = sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags); + /* Re-sync the user tty output flags with the command on next output. */ + ec->synced_opost = -1; } else { /* Background process, no access to tty. */ ec->term_raw = false; @@ -360,6 +362,56 @@ revoke_pty(struct exec_closure *ec) } } +/* + * Keep the user's terminal output flags in sync with the command's pty. + * + * This is only used when the command's stderr is redirected to a pipe, + * socket or FIFO (see exec_pty()). In that case a third party reading the + * redirected stderr (e.g. "sudo strace cmd 2> >(tee log)") writes back to + * the user's terminal directly, bypassing sudo. For that output to be + * formatted correctly the terminal must retain its output post-processing + * (OPOST/ONLCR) whenever the command itself runs in cooked mode. A full + * screen command, however, puts its pty into raw mode (OPOST cleared) and + * emits bare newlines; mirroring that onto the user's terminal avoids + * corrupting its display. We therefore track the command's OPOST state + * and toggle the user terminal's output flags to match. + */ +static void +sync_user_oflag(struct exec_closure *ec) +{ + struct termios term; + int opost; + debug_decl(sync_user_oflag, SUDO_DEBUG_EXEC); + + if (!ec->sync_oflag || !ec->term_raw) + debug_return; + if (io_fds[SFD_LEADER] == -1 || io_fds[SFD_USERTTY] == -1) + debug_return; + + if (tcgetattr(io_fds[SFD_LEADER], &term) == -1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_ERRNO, + "%s: unable to get pty terminal settings", __func__); + debug_return; + } + + opost = ISSET(term.c_oflag, OPOST) ? 1 : 0; + if (opost == ec->synced_opost) + debug_return; + + /* + * If the command's pty performs output post-processing, keep the + * user terminal's output flags too; otherwise match its raw mode. + */ + if (sudo_term_raw(io_fds[SFD_USERTTY], opost ? SUDO_TERM_OFLAG : 0)) { + ec->synced_opost = opost; + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: user tty output flags %s", __func__, + opost ? "preserved" : "set raw"); + } + + debug_return; +} + /* * Read an iobuf that is ready. */ @@ -429,6 +481,13 @@ read_callback(int fd, int what, void *v) default: sudo_debug_printf(SUDO_DEBUG_INFO, "read %zd bytes from fd %d", n, fd); + /* + * When relaying the command's output, keep the user terminal's + * output flags in sync with the command's pty (no-op unless the + * command's stderr was redirected to a pipe, socket or FIFO). + */ + if (fd == io_fds[SFD_LEADER]) + sync_user_oflag(iob->ec); if (!iob->action(iob->buf + iob->len, (unsigned int)n, iob)) { terminate_command(iob->ec->cmnd_pid, true); iob->ec->cmnd_pid = -1; @@ -975,6 +1034,7 @@ init_exec_closure(struct exec_closure *ec, struct command_status *cstat, ec->cstat = cstat; ec->details = details; ec->intercept_fd = -1; + ec->synced_opost = -1; ec->rows = user_details->ts_rows; ec->cols = user_details->ts_cols; @@ -1293,6 +1353,17 @@ exec_pty(struct command_details *details, if (interpose[STDERR_FILENO]) { sudo_debug_printf(SUDO_DEBUG_INFO, "stderr not user's tty, creating a pipe"); + /* + * Preserve the terminal's output flags (e.g. ONLCR) so that + * stderr written back to the user's terminal is not malformed + * when the terminal is in raw mode. Unlike stdout, we do not + * set CD_EXEC_BG: stdout may still be the user's tty, in which + * case the command must keep running in the foreground. The + * output flags are kept in sync with the command's pty while it + * runs (see sync_user_oflag()). + */ + term_raw_flags = SUDO_TERM_OFLAG; + ec->sync_oflag = true; if (pipe2(io_pipe[STDERR_FILENO], O_CLOEXEC) != 0) sudo_fatal("%s", U_("unable to create pipe")); io_buf_new(io_pipe[STDERR_FILENO][0], STDERR_FILENO, @@ -1302,6 +1373,16 @@ exec_pty(struct command_details *details, /* Not logging stderr, do not interpose. */ sudo_debug_printf(SUDO_DEBUG_INFO, "stderr not user's tty, not logging"); + /* + * A third party reading the redirected stderr (pipe, socket or + * FIFO) may write back to the user's terminal, so preserve the + * terminal's output flags and keep them in sync with the + * command's pty while it runs (see sync_user_oflag()). + */ + if (S_ISFIFO(sb.st_mode) || S_ISSOCK(sb.st_mode)) { + term_raw_flags = SUDO_TERM_OFLAG; + ec->sync_oflag = true; + } io_fds[SFD_STDERR] = dup(STDERR_FILENO); if (io_fds[SFD_STDERR] == -1) sudo_fatal("dup"); @@ -1320,7 +1401,7 @@ exec_pty(struct command_details *details, /* Start in raw mode unless the command will run in the background. */ cmnd_foreground = ec->foreground && !ISSET(details->flags, CD_EXEC_BG); if (cmnd_foreground) { - if (sudo_term_raw(io_fds[SFD_USERTTY], 0)) + if (sudo_term_raw(io_fds[SFD_USERTTY], term_raw_flags)) ec->term_raw = true; } diff --git a/src/sudo_exec.h b/src/sudo_exec.h index 7db0d5bb9d..f885171b2c 100644 --- a/src/sudo_exec.h +++ b/src/sudo_exec.h @@ -81,6 +81,16 @@ struct exec_closure { int cols; bool foreground; bool term_raw; + /* + * Set when the command's stderr is redirected to a pipe, socket or FIFO + * while its output still reaches the user's terminal. In that case we + * keep the user's terminal output flags (e.g. ONLCR) in sync with the + * command's pty so that data written to the terminal by a third party + * reading the redirected stderr is not malformed. synced_opost caches + * the last OPOST state applied to the user's tty (-1 when unknown). + */ + bool sync_oflag; + int synced_opost; }; /*