`sudo` wrote its last heap overflow in C. The Rust rewrite shipped.
Published by RodHat

I have a rule about “rewrite it in Rust” discourse: I don’t engage with it. The hype cycle is predictable, the rewrites usually miss the point, and the resulting argument burns more time than the security benefit justifies for most software. I’ve watched teams spend two years rewriting a five-hundred-line C daemon in Rust to produce a five-hundred-line Rust daemon with different bugs and new FFI hazards at the C boundary. Progress.
sudo-rs is different. I don’t like admitting that, but it’s true.
What sudo actually is
Todd C. Miller has maintained sudo since 1996. The original code dates to 1980 —
Bob Coggeshall and Cliff Spencer at SUNY Buffalo, later touched by Dave Hieb and
Jeff Fesler. It is a setuid-root binary. When your user executes sudo, the
kernel elevates the process to uid 0 before main() even runs. sudo’s job is then
to parse a sudoers policy file, consult PAM, verify the caller’s identity, and
either exec the requested command as root or deny and log the attempt.
There is no privilege separation here. There is no sandboxing. The argument parsing,
the file reading, the string manipulation — all of it happens at full root privilege,
in a process whose credentials the kernel handed over the moment you typed sudo.
The attacker’s input is the argument vector and environment. The trust boundary is
the memory safety of a C program written across four decades by a rotating cast
of contributors, accumulating complexity as it went.
If that sounds like a problem, you have good instincts.
The CVE history is not bad luck
CVE-2021-3156, “Baron Samedit,” was a heap-based buffer overflow in sudo’s argument parsing. Qualys found it in January 2021. It affected every sudo version from 1.8.2 to 1.9.5p1 — a decade of releases. Exploitation was straightforward: a local user could escalate to root on any vulnerable system with sudo installed. On most Linux installs at the time, that was every system. Qualys reported exploiting it successfully on Ubuntu 20.04, Debian 10, and Fedora 33 in default configurations.
This was not an exotic bug in an obscure code path. It was in the argument vector
parsing that runs every time someone types sudo. The root cause was a sudoedit
flag-handling quirk that set a global that caused the argument count to be inflated,
which then drove a loop that wrote past the end of a heap buffer. Four decades of
code, and a heap overflow in the front door.
Before Baron Samedit there was CVE-2019-18634, a stack overflow in the -g flag
handling when pwfeedback was enabled. Before that, CVE-2017-1000367, a privilege
escalation via /proc/self/status parsing on Linux. The list continues back through
multiple kernel versions and sudo versions in a pattern that is not random. The
pattern is: C, setuid root, untrusted input, memory unsafety.
What sudo-rs does
Trifecta Tech Foundation (the renamed Tweede Golf, a Dutch software consultancy) built sudo-rs with funding from ISRG’s Prossimo project — the same organization funding Rustls as an OpenSSL alternative and sudo-rs as part of a deliberate effort to move memory-unsafe code out of the network and privilege boundary of internet infrastructure.
sudo-rs implements sudo and su in Rust. It’s not a full feature-for-feature
clone — some of sudo’s more baroque options (--askpass, the Python plugin API,
some LDAP sudoers backends) are out of scope or still in progress. But the core
policy engine is there: /etc/sudoers parsing, visudo compatibility, PAM
authentication, the logging infrastructure, sudo -l for privilege inspection,
sudo -e (sudoedit) with correct temporary file handling.
The Rust implementation does not eliminate the need to be root before dropping privileges — that constraint is structural, not a language choice. What it eliminates is the class of bugs where an attacker can subvert the privilege-check path through memory corruption before the check completes. A heap overflow in Rust’s argument parser doesn’t write past the buffer; it panics and exits with a non-zero status. That’s the correct outcome.
The sudoers file parser is the part I was most skeptical about. Sudoers syntax
is decades of accumulated edge cases — host aliases, Cmnd_Alias nesting, NOPASSWD
and NOEXEC flags, #include directives, Default settings that interact across
scopes in ways that are poorly documented. Getting this wrong doesn’t just mean a
failed sudo call; it means a security policy isn’t enforced the way the
administrator expected. The project has invested significant test coverage on
the parser, including a compatibility test suite run against Todd Miller’s
reference implementation. It’s not perfect — no complex parser is — but it’s
auditable in a way the C version isn’t.
The limits
sudo-rs is production-ready for the common case: a Linux box with standard
/etc/sudoers, PAM-based authentication, a handful of Cmnd_Alias entries,
and no LDAP sudoers backend. That covers most servers I’ve ever touched.
It is not production-ready for environments that rely on the Python plugin API,
advanced LDAP sudoers integration, or some of sudo’s more unusual authentication
backends. If you’re running sudo_logsrvd for centralized log aggregation, check
the feature matrix carefully before switching.
FreeBSD and other BSDs aren’t the primary target — the project ships a working
binary for Linux. If you’re on FreeBSD where doas is already in base, this is
less relevant. doas from OpenBSD is cleaner than sudo on everything except
policy expressiveness, and that’s been true for years. If you’re running it, keep
running it. See pledge and unveil after ten years
for what the OpenBSD security model gets you above and beyond what any sudo
replacement provides.
Why I’m not arguing with this one
I have argued with Rust hype a lot. I maintain that position for most cases: rewriting a mature C codebase in Rust is a large engineering bet for uncertain safety gain when the codebase doesn’t have an unsafe boundary problem.
sudo has an unsafe boundary problem. It’s structurally unavoidable. Every byte of attacker-influenced input — arguments, environment variables, the terminal type string — is processed by code running at uid 0 before the privilege check completes. The only way to eliminate the class of exploits Baron Samedit represents is to ensure the code processing that input cannot have memory corruption bugs.
In C, you enforce that through code review, sanitizers, and discipline, and you
still get a decade-old heap overflow in the argument parser. In Rust, the language
enforces it at compile time for safe code and requires explicit unsafe blocks for
anything that bypasses those checks — blocks that can be isolated, reviewed, and
minimized. sudo-rs has a small unsafe surface. The argument parsing is entirely
safe Rust.
This is the correct application of the Rust-for-safety argument. Not “Rust is faster” (it isn’t here), not “Rust is more ergonomic” (debatable), not “memory safety is good everywhere therefore Rust everywhere.” The argument is: here is a specific security boundary, here is a specific class of vulnerability that has demonstrated itself repeatedly in the existing implementation, here is a language property that makes that class of vulnerability structurally impossible for the code that handles attacker input. That argument, applied to sudo, is sound.
The rewrite shipped. It works on the common case. The CVE history gives you the base rate. The threat model makes the target obvious. I’d rather be wrong about this one and right about the other fourteen Rust rewrites I’ve watched fail.
Check whether your distro has a package. If you’re starting from a fresh install,
deploying sudo-rs for the core sudo and su functionality is the correct call
and I will not walk that back.
Sources
- Prossimo: sudo and su — Internet Security Research Group (ISRG)
- sudo-rs — sudo and su rewritten in Rust — Trifecta Tech Foundation