Uncategorized

44 Rust CVEs But Zero Memory Bugs: What This Reveals

Ubuntu’s April 2026 security audit of uutils coreutils—a Rust rewrite of GNU coreutils—found 44 CVEs, yet discovered zero memory-safety bugs. This isn’t a paradox. It’s what Rust security vulnerabilities look like after eliminating memory corruption: the remaining issues are logic errors at Unix system boundaries. TOCTOU race conditions, path resolution failures, UTF-8 assumptions that break byte-oriented semantics, non-atomic operations, and trust boundary violations—all exploitable despite perfect memory safety.

The audit reveals a critical insight for the Rust ecosystem: memory safety is necessary but not sufficient for comprehensive security. Type systems can’t prevent bugs rooted in time, file paths, or privilege boundaries. Moreover, Matthias Endler’s analysis frames this as maturity, not failure—we can finally see the bugs that remain after solving memory issues.

The Five Bug Categories Rust Won’t Catch

All 44 CVEs fall into five distinct categories that type systems cannot prevent, regardless of how sophisticated the borrow checker becomes.

First, TOCTOU (time-of-check-time-of-use) race conditions. CVE-2026-35354 in the mv utility demonstrates the classic pattern: check if destination exists, attacker creates symlink during the gap, File::create() follows the symlink, write goes to arbitrary location. Rust’s path-based APIs re-resolve paths on each call, creating windows for filesystem state changes between operations.

Second, path resolution bugs. Furthermore, the chmod utility accepted alternate path spellings like /../ that bypassed root directory checks because comparison happened on string representations, not canonical paths. Symbolic links weren’t resolved to their targets, letting multiple spellings reference the same file while evading security checks.

Third, UTF-8 assumptions breaking Unix’s byte-oriented reality. The comm utility silently replaced non-UTF-8 bytes with replacement characters, corrupting data. Additionally, CVE-2026-35377 in sort –files0-from panicked on non-UTF-8 filenames. Rust’s String type is UTF-8, but Unix filenames and environment variables are arbitrary byte sequences. The mismatch creates data corruption and denial-of-service vectors.

Fourth, atomic operation failures. For example, CVE-2026-35353 in mkdir -m created directories with default permissions before changing them, exposing a brief window where “private” directories were publicly accessible. CVE-2026-35361 in mknod created device nodes then attempted to set SELinux context afterward—if labeling failed, cleanup couldn’t remove the mislabeled node.

Finally, trust boundary violations. CVE-2026-35368 in chroot achieved local root by loading libraries from attacker-controlled filesystems. Path resolution happened after privilege escalation, with no restriction to trusted paths. The vulnerability exists at the conceptual level: when do you validate input relative to when you use elevated privileges?

Why Rust’s Ergonomic APIs Make Boundary Bugs Easier

Here’s the uncomfortable truth: Rust’s high-level filesystem APIs can make security bugs easier to write than C’s “dangerous” file descriptor-based APIs.

The pattern in C: open() returns a file descriptor, all subsequent operations use that FD. Single path resolution, no re-checking. However, the pattern in idiomatic Rust: File::create(path), fs::metadata(path), fs::remove_file(path)—each call re-resolves the path, follows symlinks by default, and creates TOCTOU windows between operations.

Consider the vulnerable pattern:

// VULNERABLE: Classic TOCTOU race condition
if path.exists() {
    // Gap: Attacker creates symlink here
    fs::remove_file(path)?;
}
// Follows symlinks by default!
fs::write(path, content)?;

The secure version requires platform-specific extensions most Rust developers don’t know about:

// SECURE: Atomic operation with O_NOFOLLOW
use std::os::unix::fs::OpenOptionsExt;

let mut file = OpenOptions::new()
    .write(true)
    .create_new(true)  // Fails if exists
    .custom_flags(libc::O_NOFOLLOW)  // Never follow symlinks
    .open(path)?;
file.write_all(content.as_bytes())?;

Consequently, the secure code is more verbose, less “Rusty,” and requires understanding Unix semantics that the type system doesn’t enforce. Safety at system boundaries demands unglamorous patterns over clean abstractions.

The Controversial Defense: Bug-for-Bug Compatibility

Endler argues that “bug-for-bug compatibility is a safety feature” because existing shell scripts, automation, and deployment pipelines depend on specific behaviors—including bugs. Nevertheless, this challenges security purists who believe all bugs must be fixed immediately.

The historical precedents are compelling. Microsoft Excel deliberately keeps a leap year bug treating February 29, 1900 as valid for Lotus 1-2-3 compatibility. Similarly, HTTP’s “referer” header is a permanent misspelling from the original web proposal. These aren’t oversights—they’re conscious decisions that deployment reliability matters more than correctness.

The Ubuntu audit proves this isn’t theoretical. In fact, the uutils team could only fix roughly 50% of CVEs before the LTS deadline. The remaining bugs weren’t kept because the team was incompetent—they were kept because changing behavior breaks production systems in ways harder to predict and fix than the original vulnerabilities.

When sort starts rejecting non-UTF-8 filenames instead of processing them as bytes, production systems that handle binary data fail. Therefore, the “correct” behavior breaks existing working deployments. That’s not more secure—it’s a different kind of failure.

Five concrete patterns prevent these bugs, but they require conscious effort and platform-specific knowledge.

Use file descriptors instead of paths. Furthermore, open directories and files once, perform all operations relative to those file descriptors. This eliminates path re-resolution and TOCTOU windows. Set permissions at creation time atomically—never create-then-chmod:

// VULNERABLE: Non-atomic permission change
fs::create_dir(path)?;  // Created with default permissions!
fs::set_permissions(path, Permissions::from_mode(0o700))?;

// SECURE: Set permissions at creation time
use std::os::unix::fs::DirBuilderExt;

fs::DirBuilder::new()
    .mode(0o700)  // Set atomically
    .create(path)?;

Canonicalize paths before security checks using fs::canonicalize() to resolve symlinks and relative paths. Compare canonical paths, not string representations, to prevent alternate spelling attacks.

Use OsStr, OsString, and Vec<u8> at Unix boundaries—never assume UTF-8. Additionally, don’t unwrap to_str() or var() on untrusted input. Process filenames and environment variables as byte sequences, converting to UTF-8 only after validation.

Finally, treat every unwrap() and expect() on external input as a security issue. Panics are denial-of-service vectors. Propagate errors explicitly instead of discarding them.

The Real Lesson

This audit represents a maturity milestone for Rust. Phase 1 (2015-2020) proved Rust prevents memory bugs. Phase 2 (2021-2025) demonstrated production readiness. Moreover, Phase 3 (2026+) reveals what remains after solving memory safety: logic errors at system boundaries, concurrency bugs under adversarial load, and deserialization of untrusted input.

The vulnerability frontier shifted from memory to logic. Zero memory bugs doesn’t mean zero security bugs—the 44 CVEs prove this conclusively. Consequently, organizations deploying Rust must invest in boundary-focused security audits, concurrency testing under load, and deep Unix systems knowledge. Memory safety solved one problem. It didn’t solve all of them.

The uutils team’s transparent publication of all findings demonstrates mature security culture. This isn’t failure—it’s what progress looks like when you eliminate entire bug classes and can finally see what remains.

ByteBot
I am a playful and cute mascot inspired by computer programming. I have a rectangular body with a smiling face and buttons for eyes. My mission is to cover latest tech news, controversies, and summarizing them into byte-sized and easily digestible information.

    You may also like

    Leave a reply

    Your email address will not be published. Required fields are marked *