DEV Community

Sedat Ali Zevit
Sedat Ali Zevit

Posted on AI-assisted

My Rust TUI Let Any Local User Write to My Terminal

tuxctl is a lightweight, keyboard-first Linux TUI for monitoring and managing processes, systemd services, journal logs, hardware and network activity. It is written in Rust with Ratatui and Crossterm.

While preparing v0.3.0, I found a bug that had been in every earlier release: any local user could make tuxctl write arbitrary bytes to my terminal. No memory corruption, no exploit chain. Just a name in a process list.

This post is about how that happened, how I found it, and why I think it is an easy mistake to make when you build a terminal UI.

The assumption I made

A system monitor reads text from the system and prints it. Process names, command lines and log messages all come from /proc or journalctl, so I treated them as data about the machine: something to measure, sort and lay out in columns.

But much of that text is not written by the system. It is written by whoever can run a process on it. Any local user can set a process name with prctl, choose their own command line, and write a journal message with logger. I was rendering input from the least trusted people on the box, and I never thought of it as input.

Why it happened

When Ratatui renders a span, it removes newline characters and nothing else. The backend then prints each cell's symbol verbatim. So if a process name contains an ESC byte, that byte reaches the terminal as the start of an escape sequence, exactly as if a program had printed it.

Two sequences worried me most:

  • OSC 52, which asks the terminal to write to the clipboard (in terminals that allow it).
  • ESC c, which resets the terminal. Rust did not save me here. ESC is a perfectly valid UTF-8 character, so the strings were "safe" as far as the type system was concerned. The problem was never memory safety. It was trusting what the text would do once it was printed.

A harmless demo

You do not need anything dangerous to see the problem. This script gives its own process a name that is an escape sequence setting the terminal window title:

import ctypes, time

libc = ctypes.CDLL(None)
PR_SET_NAME = 15

# ESC ] 0 ; hello BEL  -> "set the window title to hello"
libc.prctl(PR_SET_NAME, b"\x1b]0;hello\x07", 0, 0, 0)
time.sleep(600)
Enter fullscreen mode Exit fullscreen mode

Run it as any user and check /proc/<pid>/comm: the name is stored with the raw escape bytes in it. A monitor that prints that name without cleaning it hands those bytes to your terminal, and the title changes. A title is harmless. The same channel carries the clipboard and reset sequences above, and the person who controls the bytes is not the person looking at the screen.

The same trick works with logger for journal messages and with the command line of any process.

How I found it

I found it in the security review for v0.3.0, not by using the tool. By then the project had 278 automated tests, clean clippy runs, resize sweeps and a performance baseline in CI. None of that caught it, because tests check what you thought to test, and I had never thought of a process name as hostile input.

A note on process: I build tuxctl with Claude as a coding partner, and the pull requests carry Co-Authored-By trailers that say so. The review was part of that workflow, and it was worth it. The question that finds this class of bug is short: which strings on my screen can someone other than me control?

The second bug had the same root cause

The review turned up a second problem. A process whose name is not valid UTF-8 was dropped from the process list, because its /proc/<pid>/stat could not be read as a string. Any user could hide a process from tuxctl by choosing such a name.

It is the same mistake from the other side. In the first bug I trusted the bytes too much when printing them. In the second I assumed the bytes would always be well-formed when reading them. In both cases the data came from someone I do not control.

The fix in v0.3.0

v0.3.0 now removes control characters and bidirectional overrides from everything tuxctl draws. Affected characters are shown as �, so you can still see that something odd was there. The filter covers all drawn text, not only the three sources I knew about when I started looking. Bidirectional overrides are in the list because they can reorder text on screen and make a name look like something it is not.

For the second bug, /proc/<pid>/stat is now read lossily. Processes with non-UTF-8 names appear in the list and can be signaled like any other.

The release added no new dependencies and no async runtime.

What else I checked

Once I was asking "what can a hostile user influence?", I went through the other places where tuxctl touches the system:

  • Spawned commands. systemctl and journalctl run with fixed arguments. The one sh -c in service.rs is test-only.
  • Signals. Before sending a signal, tuxctl revalidates the process by (pid, start_time) and sends it through a pidfd, so a recycled PID cannot receive it. Both held up. It was still worth writing them down, because "I checked and it was fine" is a result too.

A checklist if you build a TUI or CLI

  • List every string you draw that came from outside your program: process names, file names, log lines, hostnames, command output.
  • Strip control characters and bidirectional overrides before they reach the terminal, at the point where you draw, not in each data source.
  • Read the rendering library's docs for what it does not sanitize. In my case it removed newlines and nothing more.
  • Never drop an entry because its text is malformed. Decode lossily and show it, or an attacker can hide things from you.
  • Test with hostile input on purpose. A passing test suite only covers the cases you imagined. ## If you use tuxctl

Upgrade to v0.3.0 or later, especially if you run it as root or on a shared machine. Prebuilt x86_64 and aarch64 binaries are on the releases page, or build from source:

cargo install --git https://github.com/Seqat/tuxctl --tag v0.3.3 --locked
Enter fullscreen mode Exit fullscreen mode

The code is at github.com/Seqat/tuxctl. If you find another way to make it print something it should not, please open an issue. I would rather hear about it from you than find it in the next review.

Top comments (0)