Build1 publisher2 min readPublished
Any local user could write escape sequences to a tuxctl operator's terminal through a process name
tuxctl, a Rust system monitor built on Ratatui, let any local user write arbitrary bytes to its operator's terminal in every release before v0.3.0. Tools on the same stack that draw process names or journal text carry the same exposure unless they filter control bytes themselves.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- Any local user can set a process name with prctl, pick their own command line, or write a journal message with logger, and tuxctl displayed all three.
- Ratatui strips only newline characters when it renders a span, and the backend prints each cell verbatim, so an ESC byte arrives at the terminal as a live escape sequence.
- The bug survived 278 automated tests, clean clippy runs, resize sweeps and a CI performance baseline, and turned up only in the security review for v0.3.0.
- The same review found that processes with non-UTF-8 names were dropped from the list, letting any user hide a process from the monitor.
- Version 0.3.0 replaces control characters and bidirectional overrides in all drawn text with a visible replacement character.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure On a shared host, any account that can start a process or call logger can push a terminal reset, or a clipboard write where OSC 52 is allowed, onto whoever is watching the monitor.
- constraint Rust's safety guarantees do not cover this class of bug: ESC passes every UTF-8 check, so filtering has to be written by hand at the point text is printed.
- decision Maintainers of TUIs that draw system text have to choose where to filter; tuxctl filters all drawn output, so columns added later are covered without a fresh audit.
- cost A larger test suite will not find this; catching it takes a review that asks, for each string on screen, who besides the operator controls it.
The proof of concept is a short Python script. It calls `prctl` with `PR_SET_NAME` (15) and the bytes `\x1b]0;hello\x07`, the sequence for setting the terminal window title to "hello" [7]. Afterwards `/proc/<pid>/comm` holds those raw escape bytes, and a monitor that prints the name uncleaned changes the operator's title bar [7]. A title is harmless. The payloads that worried the developer use the same channel: OSC 52 asks the terminal to write to the clipboard, in terminals that allow it, and ESC c resets the terminal [5].
Rust's guarantees did not reach this. ESC is a valid UTF-8 character, so every hostile name was a well-formed string as far as the compiler cared [6]. A window-title command makes a perfectly legal process name. "The problem was never memory safety. It was trusting what the text would do once it was printed," the developer wrote [19].
Ratatui hands those bytes through to the backend, so in any application on that stack that draws `/proc` or journal text, sanitizing is the application's job [4][3]. The write-up covers only tuxctl, so how many other tools carry the same gap is not established.
Nothing in the project's automated checks was looking for it. The developer wrote that the suite missed the bug "because tests check what you thought to test, and I had never thought of a process name as hostile input" [16]. The developer builds tuxctl with Claude as a coding partner, the pull requests carry `Co-Authored-By` trailers, and the review that caught the bug was part of that workflow [9]. The question that finds this class of bug is short, the developer wrote: "which strings on my screen can someone other than me control?" [10]
The fix is careful work. The filter covers every string tuxctl draws, including text beyond the three sources the developer knew about when the search started [12]. Bidirectional overrides are filtered too, because they can reorder text on screen and make a name look like something it is not [13]. I think the draw layer is the right place for it. A filter attached to particular fields has to be extended whenever someone adds a column, and a filter on all output already covers columns nobody has reviewed.
The second bug came from the reading side. "It is the same mistake from the other side," the developer wrote [18]. `/proc/<pid>/stat` is now read lossily, so processes with non-UTF-8 names appear in the list and can be signaled like any other [14]. The release did this without new dependencies or an async runtime [15].
What to watch
- Whether Ratatui changes its span rendering to filter control characters by default, or documents that applications must do it themselves.
- What the rest of the developer's audit turns up in the places tuxctl touches the system, starting with its spawned systemctl and journalctl commands.
- Whether maintainers of other Ratatui-based process and log viewers report the same raw-ESC path after this write-up.