$RodHat_
MOTD

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

Published by

`sudo` wrote its last heap overflow in C. The Rust rewrite shipped.
Photo: AI-generated — no human photographer / RodHat AI Cover

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

  1. Prossimo: sudo and su — Internet Security Research Group (ISRG)
  2. sudo-rs — sudo and su rewritten in Rust — Trifecta Tech Foundation