Skip to content

init/console: replace cooked-mode reader with raw-mode CSI scanner#355

Merged
anatol merged 3 commits into
anatol:masterfrom
pilotstew:pr/raw-mode-reader
May 6, 2026
Merged

init/console: replace cooked-mode reader with raw-mode CSI scanner#355
anatol merged 3 commits into
anatol:masterfrom
pilotstew:pr/raw-mode-reader

Conversation

@pilotstew
Copy link
Copy Markdown
Contributor

What this PR does

Replaces console.go's cooked-termios reader (readPasswordLine + readPasswordLocked + readPassword) with a new console_input.go raw-mode reader that honors ctx cancellation. Builds on #354 which established context.Context as the cancellation idiom in unlock paths.

Three commits: implementation, scanner + pty-harness tests, manpage section. console_input.go is intentionally over-commented (state-machine flow diagram, per-flag termios rationale, acronym key block) to keep the dense FSM code review-tractable.

Headline behaviour fix

When a non-interactive token (TPM2 PCR-only, touchless FIDO2, or clevis) wins the unlock race while the keyboard prompt is showing, the cooked-mode reader has no way to honor the cancellation: it stays blocked in read(2). The prompt dangles, inputMutex / keyboardMu stay held, and any subsequent volumes that need keyboard entry block until the user manually presses Enter on the now-pointless prompt.

After this change the reader checks ctx.Done() between Poll(100ms) cycles, returns ctx.Err() within ~100 ms of cancellation, restores termios via defer, ends the prompt line cleanly, and releases the locks. End-to-end behaviour: when a token wins, the keyboard prompt dismisses on its own.

Bonuses that fall out of raw mode

  • Asterisks echo as the user types (closes Ability to echo asterisks/dots when typing in decryption passphrase? #305).
  • Editing keys: Backspace / Delete, Ctrl+W (word kill), Ctrl+U (line kill), Tab (mask toggle), Enter, Ctrl+C, Ctrl+D (EOF on empty buffer; no-op otherwise).
  • Bracketed-paste support: terminal emulators bracket pasted text with \x1b[200~ ... \x1b[201~ markers. The cooked-mode reader treated those as literal password bytes and corrupted entries from password managers. The new scanner consumes the markers and preserves the pasted content verbatim. Particularly relevant to the in-flight remote SSH unlock work in Add remote ssh LUKS device unlocking #320.

Scope and acknowledged gap

Keyboard-passphrase path only. askPasswordWithFallback / readPassword / readPasswordLocked take ctx context.Context and the call site in requestKeyboardPassword threads it through (already ctx-aware after #354).

FIDO2-PIN and TPM2-PIN call sites pass context.Background() and continue to dangle on autounlock — closing this gap belongs to an upcoming PR that threads ctx into recoverFido2Password / recoverSystemdFido2Password / recoverSystemdTPM2Password. Honest about the gap so the staged review works.

Scanner design

Per-byte FSM consuming ANSI CSI / SS3 / OSC / DCS sequences and bracketed-paste markers so they never enter the password buffer. UTF-8 multi-byte reassembly across read boundaries with overlong / surrogate / >U+10FFFF rejection (via utf8.Valid). Bounded CSI parameter accumulation (16 bytes) — garbage input can't lock up the scanner.

Inspired by Plymouth's on_key_event in src/libply-splash-core/ply-keyboard.c, with intentional divergences:

  • Kept ISIG so Ctrl+C still cancels (Plymouth's cfmakeraw clears it).
  • Accept both BS (\x08) and DEL (\x7f) as backspace; Plymouth only DEL.
  • Proper Ctrl+W word-kill; Plymouth collapses Ctrl+W to line-kill.
  • OSC, DCS, and bracketed-paste handling; Plymouth doesn't have them.
  • UTF-8 validation; Plymouth doesn't validate.
  • Bounded CSI parameter accumulation; Plymouth unbounded.

UTF-8 helpers (trimLastCodepoint, byte-pattern table) are direct ports of Plymouth's ply_utf8_* functions; the CSI scanner itself is reimplemented as a per-byte FSM rather than Plymouth's slice-scan.

No new dependency

The scanner is stdlib Go only. Surveyed alternatives (golang.org/x/term, charmbracelet/x/input, peterh/liner, eiannone/keyboard, mattn/go-tty) either don't support cancellable raw reads (the bug we're fixing) or pull substantial transitive deps inappropriate for an initramfs.

Termios contract

Raw mode clears: ECHO, ICANON, IEXTEN, IXON, IXOFF, BRKINT, INPCK, ISTRIP, INLCR, IGNCR. Keeps ISIG on. VMIN=1, VTIME=0. Every return path restores the original termios via defer.

No signal-based safety net (sigaction). A process killed mid-read could leave the terminal in raw mode until systemd takes over — acceptable for booster's PID-1 use case (rarely takes signals during boot).

Testing

Two test files. Scanner unit tests in console_input_test.go exercise the state machine directly via Feed() (CSI / SS3 / OSC / DCS / bracketed-paste / UTF-8 split across reads / editing keys / killWord boundary semantics).

Pty-harness end-to-end tests in console_input_pty_test.go exercise ctx cancellation against a real Linux pty pair (/dev/ptmx):

  • Headline test: ctx cancellation returns within ~100 ms with termios restored.
  • Reads typed bytes; partial-input cancel discards buffer; Backspace deletes one full UTF-8 codepoint not one byte; empty password (Enter on empty); EOF mid-read returns no error.

Live-tested on local tty1: passphrase entry, FIDO2-touchless-wins-while-prompting (the headline fix verified), wrong passphrase retry, Tab masking toggle. Multi-volume and serial console behavior matches in design but not separately tested.

Risks

  • Critical boot path: bug here can prevent disk unlock.
  • Dense state-machine code in a critical path — review burden is real. The file leans on extensive comments (state-machine flow diagram, per-flag termios rationale, acronym key block) to make it tractable.
  • Termios restoration relies on Go defer; no sigaction safety net (noted above).
  • Keyboard path only; PIN-prompt cancellation lands in an upcoming PR.

Manpage

Adds a "Password entry" section under NOTES documenting editing keys, asterisk echo, auto-dismiss on autounlock-win, and language support.

pilotstew added 3 commits May 5, 2026 20:01
Replaces console.go's readPasswordLine / readPasswordLocked /
readPassword (cooked-termios bufio over stdin) with a new
console_input.go raw-mode reader that honors ctx cancellation.

Headline behaviour fix: when a non-interactive token (TPM2 PCR-only,
touchless FIDO2, or clevis) wins the unlock race while the keyboard
prompt is showing, the cooked reader stays blocked in read(2) — the
prompt dangles, inputMutex/keyboardMu stay held, and any subsequent
volumes that need keyboard entry block indefinitely until the user
manually presses Enter on the now-pointless prompt. After this
change the reader checks ctx.Done() between Poll(100ms) cycles,
returns ctx.Err() within ~100ms of cancellation, restores termios
via defer, ends the prompt line cleanly, and releases the locks.

Scope is the keyboard-passphrase path only:
 - askPasswordWithFallback / readPassword / readPasswordLocked
   take ctx context.Context (threaded from requestKeyboardPassword,
   which is already ctx-aware after the prior PR in this series).
 - FIDO2-PIN and TPM2-PIN call sites pass context.Background() and
   continue to dangle on autounlock; a follow-up PR threads ctx
   into recoverFido2Password / recoverSystemdFido2Password /
   recoverSystemdTPM2Password to close that gap.

Scanner design:
 - Per-byte FSM consuming ANSI CSI / SS3 / OSC / DCS sequences and
   bracketed-paste markers so they never enter the password buffer.
 - UTF-8 multi-byte reassembly across read boundaries with overlong /
   surrogate / >U+10FFFF rejection.
 - Editing keys: Backspace, Delete, Ctrl+W (word kill), Ctrl+U
   (line kill), Tab (mask toggle), Enter (CR/LF/Ctrl+D).
 - Inspired by Plymouth's on_key_event in
   src/libply-splash-core/ply-keyboard.c with intentional
   divergences (kept ISIG so Ctrl+C still cancels; accept BS and
   DEL; proper word-kill; OSC/DCS/bracketed-paste handling;
   bounded CSI parameter accumulation; UTF-8 validation). UTF-8
   helpers are direct ports of ply_utf8_*; the scanner itself is
   reimplemented as a per-byte FSM rather than a slice-scan.

Bracketed-paste support has a specific forward-compat value for
the in-flight remote SSH unlock work (PR anatol#320): SSH users typically
paste passphrases from password managers, and modern terminal
emulators bracket the pasted content with \x1b[200~ ... \x1b[201~
markers. A cooked-mode reader would treat those markers as literal
password bytes and corrupt the entry. K's scanner consumes the
markers and preserves the pasted content verbatim.

Termios: ECHO/ICANON/IEXTEN/IXON/IXOFF/BRKINT/INPCK/ISTRIP/INLCR/
IGNCR cleared in raw mode; ISIG kept on. VMIN=1, VTIME=0. Every
return path restores the original termios via defer. Notably IXON
off means Ctrl+S no longer freezes the prompt.

No new dependency: the scanner is ~440 LOC of stdlib Go. Surveyed
alternatives (golang.org/x/term, charmbracelet/x/input,
peterh/liner, eiannone/keyboard, mattn/go-tty) either don't
support cancellable raw reads or pull substantial transitive
deps inappropriate for an initramfs.
Two test files covering the new raw-mode reader.

console_input_test.go (~400 LOC, 22+ tests) exercises the scanner
state machine directly via Feed(), with no terminal involved:

 - UTF-8 split across Feed boundaries; 2/3/4-byte codepoints; mixed
   ASCII; overlong / surrogate / out-of-range rejection
 - CSI / SS3 / function-key / arrow-key sequences silently consumed
 - OSC body terminated by BEL or ST does not leak into the password
 - DCS body terminated by ST does not leak
 - Bracketed-paste markers consumed; embedded control bytes
   (newline, Ctrl+U, etc.) pass through as literal bytes
 - CSI parameter accumulator is bounded (16 bytes) — overflow
   aborts the sequence and recovers without lock-up
 - Editing keys: Backspace, Delete, Tab, Ctrl+U, Ctrl+W, Enter
   (CR / LF / Ctrl+D)

console_input_pty_test.go (~190 LOC, 3 tests) exercises the
end-to-end ctx-cancel behaviour via a real Linux pty pair
(/dev/ptmx + TIOCSPTLCK + TIOCGPTN), so termios setup, the
Poll(100ms) loop, and the cancellation path are all verified
with a real terminal:

 - TestReadPasswordOnCtxCancelReturnsPromptly: the headline test.
   ctx.Done() fires while the reader is blocked in Poll; the
   reader returns ctx.Err() within ~100ms; termios is restored
   to its pre-call state (Lflag and Iflag both checked).
 - TestReadPasswordOnReadsTypedBytes: bytes typed via the pty
   master end are received and assembled into the password
   returned on Enter.
 - TestReadPasswordOnCancelAfterPartialInput: cancellation
   after partial input discards the partial buffer and returns
   ctx.Err() — the partial bytes are NOT returned as a password.
New "Password entry" section under NOTES covers the user-visible
behaviour this PR introduces — editing keys (Ctrl+W word-kill, Ctrl+U
line-kill, Tab mask-toggle) and auto-dismiss when any unlock method
wins the race — and backfills documentation for the previously-
shipped PIN-prompt behaviours from anatol#347 and anatol#351 (3 attempts; empty
PIN to skip the token to the next unlock prompt).
@anatol
Copy link
Copy Markdown
Owner

anatol commented May 6, 2026

Fantastic! Thank you very much!

@anatol anatol merged commit f1fc0ac into anatol:master May 6, 2026
anatol pushed a commit that referenced this pull request May 6, 2026
Threads ctx context.Context into recoverFido2Password,
recoverSystemdFido2Password, and recoverSystemdTPM2Password. The
PIN prompts in those functions previously called askPasswordWithFallback
with context.Background() and could not be cancelled when a sibling
unlock succeeded. With ctx propagated, every console password prompt
— keyboard-passphrase, FIDO2-PIN, TPM2-PIN — now dismisses cleanly.

recoverTokenPassword (already ctx-aware after #354) updates its two
systemd-token call sites to pass ctx through. Pure additive — no
behavioural change beyond extending the cancellation reach.

Also drops three stale comments left in #355 that referenced this
upcoming work in internal-planning vocabulary; reworded to factually
describe the current state.
anatol pushed a commit that referenced this pull request May 6, 2026
Two changes that together close out the console UX for booster's
concurrent unlock pipeline:

1. Prompt-aware statusMessage redraw. statusMessage now consults the
   active prompt before printing to console. If a passphrase prompt
   is on screen and its volume hasn't been unlocked yet, the current
   line is erased, the message prints, and the prompt is reprinted
   below — cursor stays at the bottom, asterisk count preserved.
   The new promptVolumeUnlocked helper lets statusMessage skip the
   redraw when the prompt's volume is already unlocked, avoiding
   reprinting a stale prompt that ctx-cancel hasn't yet torn down.
   readPasswordOn now sets consolePrompt.{active,text,done} during
   each prompt — fields declared in #355 that become load-bearing
   only now that statusMessage consumes them.

2. Token-unlock confirmation. recoverTokenPassword fires
   statusMessageTimed("X unlocked via Y", 3s) on success. After
   #355 and #356 cleanly dismiss prompts when a sibling token wins
   the race, the user previously saw nothing telling them what
   happened — boot just continued. This adds the missing
   confirmation. tokenFriendlyName provides the short label per
   token type; statusMessageTimed clears the message after 3s so
   it doesn't linger.
pilotstew added a commit to pilotstew/booster that referenced this pull request May 14, 2026
Adds a new NOTES subsection covering the concurrent-unlock model that
landed across PRs anatol#350, anatol#353, anatol#355, anatol#356, anatol#357, anatol#358, and anatol#362:
PIN-token serialization in ascending LUKS2 token-ID order, cancel-on-win
semantics for keyboard/FIDO2-PIN/TPM2-PIN prompts on both the console
and the Plymouth splash (with the MR !393 caveat for older Plymouth
builds), and the per-token 3-attempt PIN cap with empty-PIN skip.

Trims two paragraphs from the existing 'Password entry' subsection
(auto-dismiss and PIN attempts) now that the new section covers them
in fuller context. 'Password entry' keeps the Ctrl+W / Ctrl+U / Tab
edit-key reference.
anatol pushed a commit that referenced this pull request May 14, 2026
Adds a new NOTES subsection covering the concurrent-unlock model that
landed across PRs #350, #353, #355, #356, #357, #358, and #362:
PIN-token serialization in ascending LUKS2 token-ID order, cancel-on-win
semantics for keyboard/FIDO2-PIN/TPM2-PIN prompts on both the console
and the Plymouth splash (with the MR !393 caveat for older Plymouth
builds), and the per-token 3-attempt PIN cap with empty-PIN skip.

Trims two paragraphs from the existing 'Password entry' subsection
(auto-dismiss and PIN attempts) now that the new section covers them
in fuller context. 'Password entry' keeps the Ctrl+W / Ctrl+U / Tab
edit-key reference.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ability to echo asterisks/dots when typing in decryption passphrase?

2 participants