terminal-util: fix boot hang from ANSI terminal size queries

Since v257, terminal_fix_size() is called during early boot via
console_setup() → reset_dev_console_fd() to query terminal dimensions
via ANSI escape sequences. This has caused intermittent boot hangs
where the system gets stuck with a blinking cursor and requires a
keypress to continue (see systemd/systemd#35499).

The function tries CSI 18 first, then falls back to DSR if that fails.
Previously, each method independently opened a non-blocking fd, disabled
echo/icanon, ran its query, restored termios, and closed its fd. This
created two problems:

1. Echo window between CSI 18 and DSR fallback: After CSI 18 times out
   and restores termios (re-enabling ECHO and ICANON), there is a brief
   window before DSR disables them again. If the terminal's CSI 18
   response arrives during this window, it is echoed back to the
   terminal — where the terminal interprets \e[8;rows;cols t as a
   "resize text area" command — and the response bytes land in the
   canonical line buffer as stale input that can confuse the DSR
   response parser.

2. Cursor left at bottom-right on DSR timeout: The DSR method worked by
   sending two DSR queries — one to save the cursor position, then
   moving the cursor to (32766,32766) and sending another to read the
   clamped position. If neither response was received (timeout), the
   cursor restore was skipped (conditional on saved_row > 0), leaving
   the cursor at the bottom-right corner of the terminal. The
   subsequent terminal_reset_ansi_seq() then moved it to the beginning
   of the last line via \e[1G, making boot output appear at the bottom
   of the screen — giving the appearance of a hang even when the system
   was still booting.

This commit fixes both issues:

- terminal_fix_size() now opens the non-blocking fd and configures
  termios once for both query methods, so echo stays disabled for the
  entire CSI 18 → DSR fallback sequence with no gap. tcflush(TCIFLUSH)
  is called before each query to drain any stale input from the tty
  input queue.

- The DSR method now uses DECSC (\e7) / DECRC (\e8) to save and restore
  the cursor position via hardware, instead of querying it with a
  separate DSR round-trip. All four sequences (DECSC, CUP to
  bottom-right, DSR query, DECRC) are sent in a single write, so the
  terminal processes DECRC and restores the cursor regardless of whether
  userspace ever reads the DSR response. This eliminates the
  cursor-at-bottom-right artifact on timeout and simplifies the read
  loop to only need a single DSR response instead of two.

- The repeated setup boilerplate (dumb check, verify_same, fd_reopen,
  termios save/disable) is extracted into terminal_prepare_query(),
  shared by terminal_get_size_by_csi18(), terminal_get_size_by_dsr(),
  and terminal_fix_size().

Fixes: systemd/systemd#35499

Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daan De Meyer
2026-03-31 14:42:02 +02:00
committed by Daan De Meyer
parent 9fc49a4ca7
commit da69848791
4 changed files with 148 additions and 167 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ int exec_context_apply_tty_size(
if (rows == UINT_MAX && cols == UINT_MAX &&
exec_context_shall_ansi_seq_reset(context) &&
isatty_safe(input_fd)) {
r = terminal_get_size_by_dsr(input_fd, output_fd, &rows, &cols);
r = terminal_get_size(input_fd, output_fd, &rows, &cols, /* try_dsr= */ true, /* try_csi18= */ false);
if (r < 0)
log_debug_errno(r, "Failed to get terminal size by DSR, ignoring: %m");
}