DEV Community

Cover image for 11 of 25 sudo checks differ: I ran Ubuntu's Rust sudo
frank chu
frank chu

Posted on

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

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
Enter fullscreen mode Exit fullscreen mode

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())
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)