Bugs Rust Won't Catch
A 2026 audit of uutils found 44 CVEs in production Rust code missed entirely by the borrow checker, Clippy, and cargo-audit, revealing where Rust's safety boundary actually falls.

Rust eliminates the bugs that made C catastrophically dangerous — and the bugs that replaced them are subtler, more architectural, and the type system has no mechanism to see them.
Source
By: Matthias Endler
Published: Apr 29 2026
Type: Technical post
Saved because it grounds the limits of Rust’s safety in a real production audit — 44 CVEs in uutils, Canonical’s Rust reimplementation of GNU coreutils — rather than a constructed example.
The Security Boundary Shifts, It Doesn’t Disappear
In April 2026, Canonical disclosed 44 CVEs in uutils, the Rust reimplementation of GNU coreutils that ships by default since Ubuntu 25.10. The audit was commissioned ahead of the 26.04 LTS release. None of the 44 bugs were buffer overflows, use-after-free, data races, or uninitialized memory reads. Every one landed past the borrow checker, past Clippy, and past cargo-audit without triggering a single safety error.
The largest cluster is TOCTOU — time-of-check-to-time-of-use. The pattern is consistent: one syscall to inspect a path, a second to act on it. Between those two calls, an attacker with write access to the parent directory can swap the path component for a symlink. The kernel re-resolves the full path from scratch on the second call, and the privileged operation lands on whatever target the attacker chose.
Rust’s standard library makes this easy to get wrong. The ergonomic path APIs you reach for first — fs::metadata, File::create, fs::remove_file, fs::set_permissions — all take a &Path and re-resolve it on every call. CVE-2026-35355, simplified from the install utility, is remove_file(to) followed by File::create(to). Any attacker who can write to the parent directory in that window wins. The fix is OpenOptions::new().create_new(true), which atomically guarantees no file or symlink exists at the target at the moment of creation. The underlying model: a &Path is a name to the kernel, not a value — it can point to a different inode between syscalls. Anchor on a file descriptor instead: open the parent directory once, hold the handle, and work relative to it.
Permission timing is a close relative. Creating a directory and then calling set_permissions on it is two operations with a window between them. Any other user on the system can open the directory during that window; the later chmod cannot revoke a file descriptor they already hold. The correct approach is DirBuilderExt::mode() so the directory is born with the right permissions from the start, with no window in between.
Path identity is a separate trap. The original --preserve-root check in chmod compared the argument literally against "/". Any path that resolves to / but is not spelled that way — /../, /./, or a symlink pointing to the root — bypassed the guard entirely. Running chmod -R 000 /../ on the original code would strip permissions system-wide. The fix is fs::canonicalize before the comparison, which resolves .., ., and symlinks into a real absolute path. String equality is not filesystem identity, and the borrow checker has no way to know the difference.
The encoding boundary is another category. Unix paths and byte streams are not UTF-8. Rust’s String is. CVE-2026-35346 in the comm utility used from_utf8_lossy on raw binary input, silently replacing invalid bytes with U+FFFD. GNU comm simply moves bytes around — it does not care about encoding. The fix was writing raw bytes directly to stdout with Write::write_all, bypassing any UTF-8 round-trip entirely. For systems code at Unix boundaries, use OsStr and &[u8] wherever data arrives from the operating system.
Panics are a denial-of-service vector in any privileged tool. CVE-2026-35348 in sort --files0-from called expect() on a UTF-8 conversion of NUL-separated filenames. A single non-UTF-8 byte anywhere in the input file aborts the process. In a cron job or CI pipeline that is a repeatable crash loop. Treat every unwrap, expect, and unchecked slice index in untrusted input paths as a potential CVE; replace them with ? and real errors.
What the borrow checker did prevent is worth stating clearly. The uutils codebase has shipped zero buffer overflows, zero use-after-free, zero double-free, and zero data races — categories where GNU coreutils has shipped real CVEs multiple times in the past two years alone. The borrow checker genuinely moved the security boundary. It just relocated it to the interface between controlled Rust code and the messy, byte-level, time-sensitive Unix world underneath. That interface is now entirely the programmer’s responsibility.
What I want to remember
Remember: In privileged systems code, anchor operations on file descriptors not paths, set permissions at creation not after, stay in bytes at OS boundaries with
OsStrand&[u8], and treat everyunwrapon untrusted input as a crash waiting to be filed.