11 of 25 sudo checks differ: I ran Ubuntu's Rust sudo

11 of 25 permission checks differ between sudo-rs 0.2.4 and classic sudo — and all 11 involve wildcards in command-line arguments. I ran both binaries against the same sudoers rules and counted. Ubuntu 26.04 ships the Rust rewrite as its default sudo, and if your sudoers file uses argument wildcards, the "drop-in replacement" story does not survive contact with your config.

Why I tested this

Chris Siebenmann's November 2 post, "The Ubuntu 26.04 Rust sudo is not compatible with standard sudo," makes two claims: sudo-rs ignored his /etc/sudoers.d files, and wildcards in command-line arguments don't work — his example is an exim queue rule, /usr/sbin/exim -bp[acru], which classic sudo reads as "allow -bpa, -bpc, -bpr, -bpu" but sudo-rs reads as "allow the literal string -bp[acru]," which exim rejects. I wanted numbers, not anecdotes, so I tested it myself.

The setup

I extracted the actual Ubuntu package, sudo-rs_0.2.4-1_amd64.deb, from the Ubuntu archive and verified its SHA256 against the Packages index (48c096ee…). The control is the system classic sudo, 1.9.15p5, on Ubuntu 24.04. Test user alice, rules placed in /etc/sudoers.d/, decisions probed with sudo -l -U alice <command...> — the same matching code as a real invocation, run for both binaries.

One honest limitation: setuid binaries don't take effect in my sandbox (the overlay filesystem ignores the setuid bit — I verified with a test binary), so I could not execute commands through sudo-rs as a non-root user. List-mode checks exercise the exact same allow/deny matcher, so the scorecard is real, but execution behavior is not covered here. All test users and files were removed after the run.

The scorecard

25 probes, 11 differences. Every difference is a wildcard in a command-line argument:

T1 rule: /usr/bin/echo -bp[acru] (the blog's exim pattern) echo -bpa sudo-rs: DENY classic: ALLOW <-- DIFFERS echo -bpc sudo-rs: DENY classic: ALLOW <-- DIFFERS echo -bpx sudo-rs: DENY classic: DENY echo "-bp[acru]" sudo-rs: ALLOW classic: DENY <-- DIFFERS T2 rule: /usr/bin/echo * echo hello sudo-rs: DENY classic: ALLOW <-- DIFFERS echo hello world sudo-rs: DENY classic: ALLOW <-- DIFFERS echo sudo-rs: DENY classic: ALLOW <-- DIFFERS echo "*" sudo-rs: ALLOW classic: ALLOW T3 rule: /usr/bin/echo -bp? echo -bpa sudo-rs: DENY classic: ALLOW <-- DIFFERS echo -bpxx sudo-rs: DENY classic: DENY T4 rule: /usr/bin/echo foo*bar echo fooxbar sudo-rs: DENY classic: ALLOW <-- DIFFERS echo foobar sudo-rs: DENY classic: ALLOW <-- DIFFERS echo fooxbaz sudo-rs: DENY classic: DENY T5 rule: /usr/bin/ec* (wildcard in the command PATH) echo hi sudo-rs: ALLOW classic: ALLOW ech hi sudo-rs: DENY classic: DENY T6 rule: /usr/bin/echo, !/usr/bin/echo secret (negation) echo secret sudo-rs: DENY classic: DENY echo ok sudo-rs: ALLOW classic: ALLOW T7 rule: /usr/bin/echo a b (exact multi-arg) echo a b sudo-rs: ALLOW classic: ALLOW echo a c sudo-rs: DENY classic: DENY echo a b c sudo-rs: DENY classic: DENY T8 rule: /usr/bin/echo "" (empty-string arg) echo "" sudo-rs: ALLOW classic: ALLOW echo x sudo-rs: DENY classic: DENY T9 Cmnd_Alias EXIMQ = /usr/bin/echo -bp[acru] echo -bpa sudo-rs: DENY classic: ALLOW <-- DIFFERS echo "-bp[acru]" sudo-rs: ALLOW classic: DENY <-- DIFFERS T10 Runas: alice ALL=(bob) /usr/bin/id id as bob sudo-rs: ALLOW classic: ALLOW T11 rule lives ONLY in /etc/sudoers.d/ id sudo-rs: ALLOW classic: ALLOW 

T2 is the surprise, and it contradicts even the blog post: Siebenmann believed a trailing * still works as "zero or more subsequent arguments." It doesn't — not in 0.2.4. The only thing /usr/bin/echo * allows is the literal argument *. Everything else in the non-wildcard rows — path globs, negation, exact args, empty-string args, aliases, runas, sudoers.d inclusion — matches classic sudo exactly.

The mechanism, in source

The matcher is match_command in src/sudoers/mod.rs of the sudo-rs source (v0.2.4). The command path gets glob matching; the arguments get plain equality:

fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> (impl Fn(&Command) -> bool + 'a) { let opts = glob::MatchOptions { require_literal_separator: true, ..glob::MatchOptions::new() }; move |(cmdpat, argpat)| { cmdpat.matches_path_with(cmd, opts) && argpat.as_ref().map_or(true, |vec| args == vec.as_ref()) } } 

args == vec — no glob, no fnmatch, no special case for a trailing *. A * in an argument list is just the one-character string *. The documentation says it plainly, in two places: the Cmnd_List section says arguments "in sudo-rs may not include wildcards," and the Wildcards section ends with "Wildcards in command line arguments are not supported... sudo-rs simply forbids using them."

But "forbids" is doing a lot of work there. The parser does not reject these rules. visudo-rs -c accepts /usr/bin/echo -bp[acru] without complaint, and the rule silently becomes a narrower, different rule than the one you wrote. That is the dangerous combination: no error at parse time, a different meaning at decision time. A sysadmin migrating a sudoers file with argument wildcards gets no warning and a config that denies things it used to allow — or, read the other way, a rule the admin thinks is narrow (-bp[acru], four specific options) is actually… well, in this case narrower still. The failure mode is silent breakage, not silent widening, but silent is the problem either way.

Two more findings

NOEXEC breaks the whole file. A NOEXEC: tag is a hard parse error in sudo-rs:

/etc/sudoers.d/sudors-test:1:21: syntax error: NOEXEC and INTERCEPT are not supported by sudo-rs 

Worse, the blast radius is the entire sudoers evaluation: with that one file present, sudo-rs -l -U alice fails for every user with "not allowed to execute 'list'." Classic sudo parses the same file fine. So a single legacy tag in any sudoers.d file doesn't just disable one rule — it bricks sudo-rs decisions globally until the file is removed. Digest specifications (sha256:...) fail the same way.

The sudoers.d claim did not reproduce. Siebenmann's first complaint was that sudo-rs ignored his /etc/sudoers.d files with no logging. On 0.2.4, with the stock @includedir /etc/sudoers.d line, my rules in sudoers.d were honored (T11, and every other test above — they all lived in sudoers.d). I can't explain his case from here: possibly a different trigger — file naming (both implementations skip files with . or ~ in the name), ownership, or a different build. His post gives no sudo-rs version. The wildcard finding stands on its own; the sudoers.d finding doesn't, and I'm saying so.

Defaults lines parsed fine in both (env_keep, timestamp_timeout, secure_path all accepted by visudo-rs -c), and path wildcards work, so this is not a blanket incompatibility — it's specifically argument matching, plus the parse-time rejection of NOEXEC, INTERCEPT, sudoedit, and digests.

The takeaway

If you are on Ubuntu 26.04 — or considering the switch — grep your sudoers files for wildcards in arguments before you migrate: *, ?, or [...] after a command path. Any rule using them means something different under sudo-rs, silently. And run visudo-rs -c over every file in sudoers.d first: one NOEXEC tag anywhere fails the whole parse. The Rust rewrite is a real, careful reimplementation in most respects — 14 of my 25 probes matched classic sudo exactly — but "mostly compatible" is not the same as "drop-in," and the gap is concentrated exactly where a sysadmin would least expect it: in the arguments, not the commands.

Sources: Chris Siebenmann, "The Ubuntu 26.04 Rust sudo is not compatible with standard sudo" (Nov 2, 2026); sudo-rs v0.2.4 source (src/sudoers/mod.rs, docs/man/sudoers.5.md) and Ubuntu package sudo-rs_0.2.4-1_amd64.deb (SHA256 verified against the archive Packages index); classic sudo 1.9.15p5 as control. All 25 probes run on 2026-11-06; the scorecard above is my own output. Boundary, stated plainly: decisions were probed in list mode, not by executing commands through sudo-rs, because setuid execution doesn't work in my sandbox.