run0 vs sudo - Escalating Privilege Without a setuid Binary
run0 vs sudo
The Thing sudo Never Fixed
sudo has been the answer to "run this one command as root" since the
late 1980s. It works, everyone knows it, and it is on every box. It
also rests on a mechanism that has been a quiet liability the entire
time: the setuid bit.
setuid is a file permission that says "when anyone runs this program,
it runs as the file's owner, not as the person who ran it." sudo is
owned by root and setuid, so a normal user running sudo is suddenly
executing a root process. That is the whole trick, and it is the
problem. A setuid binary starts as a full-privilege process that
inherited its entire environment, its variables, its file descriptors,
its execution context, from the unprivileged user who launched it.
Everything after that, all of sudo's careful sanitizing of that
inherited context, is damage control on a design that hands root a
process built by an untrusted caller.
sudo is also large. It has plugins, it parses its own configuration
language, it has had network-facing features, and every one of those
is attack surface running in a process that is root from the first
instruction. The setuid model made a big, complex program the front
door to root, and the size of that program is the size of the risk.
run0, which shipped in systemd 256, is a different answer to the same
question, and the difference is architectural rather than cosmetic.
What run0 Does Instead
run0 does not use setuid at all. There is no setuid binary in the
picture. When you run a command with run0, it does not become root
itself. It asks the systemd service manager to start your command as a
fresh process, and systemd, which is already running as root as PID 1,
is the one that launches it.
That inversion is the entire point. With sudo, an unprivileged process
elevates itself and drags its whole context up with it. With run0,
nothing elevates. A request goes to a privileged manager that already
exists, and that manager starts a clean process from scratch. The
command you run does not inherit your environment variables, your file
descriptors, or your execution context, because it was not forked from
your shell. It was forked from PID 1, in a freshly created context, with
its own pseudo-terminal isolating it from your terminal. The ambient
attack surface that setuid creates simply is not there, because nothing
was elevated in place.
Authentication moves too. sudo checks its own /etc/sudoers file. run0
authenticates through polkit, the system's policy framework, which
means the password prompt is handled outside your terminal session
rather than read by the privileged program itself. Authorization
policy lives in polkit's rules rather than in a sudo-specific config
language. One less bespoke parser, one less thing to get subtly wrong.
There is even a visible tell: by default run0 tints the terminal
background red while you hold elevated privilege, a small honest
reminder that you are in a root context right now.
Why the Difference Is Real, Not Cosmetic
It would be easy to file this as "systemd reinvented sudo." That
misses what changed. The two tools answer the privilege question with
opposite structures.
sudo's model: make a big setuid program the gateway, and spend enormous
care sanitizing everything it inherits, because it starts life as root
holding an untrusted process's baggage. Every sudo vulnerability of the
"environment variable leads to escalation" class comes from this shape,
the program is root and has to defend against the context it was handed.
run0's model: never make a setuid program at all, never inherit the
untrusted context, and let the already-privileged service manager
create a clean process on request. There is no baggage to sanitize
because the process was not built by the caller. An entire category of
setuid-inheritance bugs cannot occur, not because run0 defends against
them, but because the structure that produces them is gone.
This matters most on hardened systems. If you set NoNewPrivileges
across a system, which is exactly the kind of hardening worth doing,
setuid stops working, and sudo along with it. run0 keeps working,
because it never needed setuid in the first place. A privilege tool
that survives the hardening you actually want is a better fit for a
locked-down box than one the hardening breaks.
The Honest Catches
run0 is not a drop-in replacement you should rush everywhere, and the
friction is real.
It depends on systemd and polkit. This is fine on a modern systemd
distro and a non-starter anywhere else. On the BSDs, on
non-systemd Linux, on a minimal container without polkit, run0 is not
an option and sudo or doas remains the answer. run0 is a systemd tool,
with everything that implies about which systems it fits.
It asks for a password on every invocation by default. sudo caches your
authentication for a few minutes, so a burst of admin commands prompts
once. run0 does not cache the same way, so out of the box every command
prompts, which is more secure and more annoying. You can widen this with
polkit rules, but it is friction the first time it surprises you.
It is awkward for automation. sudo's non-interactive behavior is deeply
baked into scripts, cron jobs, CI, and Ansible everywhere. polkit's
interactivity assumption does not map cleanly onto "run this as root
with no human present," so for scripting, sudo, or a dedicated service,
remains the better tool. run0 is aimed at interactive administration,
not unattended automation.
And it is one of several answers, not the anointed winner. sudo-rs, a
memory-safe Rust rewrite of sudo, keeps the exact sudo workflow and is
the default in current Ubuntu LTS, which gives it the fastest real-world
adoption of any of these. opendoas stays tiny and auditable. run0 is
the most architecturally different, but "different" and "what your
distro ships by default" are not the same thing, and for many people
sudo-rs will be the practical path to a safer sudo without changing how
they work.
When To Reach For Which
For interactive administration on a modern systemd box, especially one
you have hardened with NoNewPrivileges or where you want a clean,
auditable, isolated root context every time, run0 is genuinely the
better design and worth adopting. It removes the setuid attack surface
instead of guarding it.
For scripting, automation, cron, CI, and anything unattended, keep
sudo, or move to sudo-rs for the same workflow with the memory-safety
bugs removed. The interactivity model of polkit is the wrong fit for a
human-absent context.
For non-systemd systems, the BSDs, minimal environments, run0 is not
available and the choice is between sudo, sudo-rs, and doas.
They are not really competing for the same slot. run0 asks how to
escalate without a setuid binary at all. sudo-rs asks how to keep sudo
and remove its memory bugs. Both are improvements on the same weak
point from different directions.
The Point
sudo's oldest weakness is structural: it is a large setuid program that
starts as root holding a process the untrusted user built, and most of
its complexity is spent defending that position. run0 escalates
differently, by asking the already-privileged systemd to spawn a clean
process with none of the caller's inherited context and no setuid
anywhere, authenticated through polkit and isolated in its own pty. The
class of setuid-inheritance bugs does not get defended against; it gets
designed out.
Use run0 for interactive admin on systemd systems, and especially where
you have hardened setuid away. Keep sudo, or sudo-rs, for automation and
for everything non-systemd. The mechanism you escalate through is worth
thinking about, because the front door to root should be the smallest,
cleanest thing you can make it, and for a long time setuid meant it was
neither.