Back to Blogs
Web Dev
9 min read
Apr 29, 2026

Bugs Rust Cannot Catch in Production: A 2026 Field Guide

Bugs Rust cannot catch in production include TOCTOU races, logic errors, and panics. Fajarix breaks down what the borrow checker misses and how to fix it.

Bugs Rust cannot catch in production are defects that survive the borrow checker, the type system, clippy, and cargo audit because they live outside Rust's safety model. They include TOCTOU race conditions, logic errors, panics on untrusted input, encoding mismatches at OS boundaries, and integer overflows in release builds. Rust prevents memory unsafety, not every class of bug.

In April 2026, Canonical disclosed 44 CVEs in uutils, the Rust reimplementation of GNU coreutils shipping by default in Ubuntu since 25.10. Every single bug landed in production code written by experienced Rust engineers. None were caught by the compiler. At Fajarix, we've been auditing Rust systems for clients across fintech and infrastructure, and the patterns we see match the uutils audit almost exactly. This guide breaks down the categories of bugs Rust cannot catch in production so your engineering team adopts Rust with realistic expectations.

Why Rust's Safety Guarantees Stop Where the Kernel Begins

Rust's borrow checker is a static analysis tool focused on memory safety: no use-after-free, no data races, no null pointer dereferences in safe code. That's a remarkable guarantee. But the moment your program calls into the kernel, talks to the filesystem, parses untrusted input, or does arithmetic on user-supplied numbers, you're operating in territory the compiler can't reason about.

The borrow checker proves your program won't corrupt its own memory. It does not prove your program is correct, secure, or safe to run as root.

Engineering teams migrating from C or Go to Rust often assume the compiler eliminates entire bug classes. It does, just not the ones that show up most often in security audits. Below, we walk through the six categories of bugs Rust cannot catch in production, with concrete examples and remediation patterns.

Category 1: TOCTOU Race Conditions Across Syscalls

Time-of-check to time-of-use bugs are the largest cluster in the uutils audit. They're also why cp, mv, and rm are still GNU binaries in Ubuntu 26.04 LTS. The pattern: one syscall checks a path, a second syscall acts on it, and an attacker swaps a symlink in between.

The Anatomy of CVE-2026-35355

Here's the simplified vulnerable pattern from install.rs:

fs::remove_file(to)?;
// attacker plants symlink here
let mut dest = File::create(to)?; // follows symlink
copy(from, &mut dest)?;

Between remove_file and File::create, an attacker with write access to the parent directory can plant to as a symlink to /etc/shadow. The privileged process then overwrites /etc/shadow. The fix is OpenOptions::new().write(true).create_new(true).open(to), which refuses to follow dangling symlinks.

The General Rule: Anchor on File Descriptors

A &Path in Rust looks like a value, but to the kernel it's just a name that re-resolves on every syscall. For privileged code, follow these rules:

  1. Use create_new(true) when creating a new file.
  2. Open the parent directory once and use openat-style operations relative to that file descriptor.
  3. Treat any path used in two syscalls as a TOCTOU bug until proven otherwise.
  4. Never assume &Path identity equals filesystem identity.

What Bugs Does Rust Actually Catch?

Rust catches use-after-free, double-free, data races between threads, null pointer dereferences in safe code, buffer overflows on stack and heap allocations, and iterator invalidation. It does not catch logic errors, race conditions across syscalls, panics, integer overflows in release mode, encoding mismatches, deadlocks, or resource exhaustion.

This distinction matters because security teams sometimes treat Rust as a silver bullet for memory-safety CVEs. According to Microsoft's MSRC analysis, roughly 70% of historical CVEs are memory-safety issues, but that means 30% are not, and Rust does nothing for those. The uutils audit is a perfect demonstration: zero memory-safety bugs, 44 CVEs.

Category 2: Permissions Set After Creation

This is a TOCTOU cousin. Teams write code like this:

fs::create_dir(&path)?;
fs::set_permissions(&path, Permissions::from_mode(0o700))?;

For a small window, the directory exists with default permissions. Any local user can open() it during that window, and the later chmod doesn't revoke their file descriptor. The correct pattern is to set permissions atomically at creation time using OpenOptions::mode() or DirBuilderExt::mode().

Category 3: String Equality Mistaken for Filesystem Identity

The original chmod --preserve-root check in uutils was a literal string comparison:

if recursive && preserve_root && file == Path::new("/") {
    return Err(PreserveRoot);
}

Anything that resolved to / but wasn't spelled / bypassed the check: /../, /./, /usr/.., or a symlink to /. The fix uses fs::canonicalize to resolve the path before comparing. For general filesystem identity, you should compare (dev, inode) pairs, the way GNU coreutils does.

The Funniest Bug in the Audit

CVE-2026-35363: rm . and rm .. were correctly refused, but rm ./ and rm .//// deleted the current directory while printing Invalid input. A trailing slash bypassed the safety check entirely. No type system catches that.

Category 4: UTF-8 Assumptions at Unix Boundaries

Rust's String and &str are always UTF-8. That's correct for application logic but wrong for Unix paths, environment variables, command arguments, and byte streams flowing through tools like cut, comm, and tr. Every UTF-8 conversion at a Unix boundary is a potential bug.

ConversionBehaviorRisk
from_utf8_lossyReplaces invalid bytes with U+FFFDSilent data corruption
from_utf8 + ?Returns error on invalid bytesRefuses valid binary input
OsStr / &[u8]Preserves raw bytesCorrect for Unix tools

CVE-2026-35346 in comm used String::from_utf8_lossy on raw input bytes, silently corrupting binary files that GNU comm handled correctly. The fix replaced print! with BufWriter::write_all, keeping data in bytes end-to-end.

Category 5: Panics on Untrusted Input

Every panic!, unwrap(), expect(), slice index, and integer division on user input is a denial-of-service vector. Rust's type system encourages handling Result, but it does not force you to. A single .unwrap() on a parsed integer from stdin can crash a long-running daemon. Patterns we audit for at Fajarix:

  • Direct slice indexing with data[n] instead of data.get(n).
  • Integer division and modulo without checking the divisor.
  • unwrap() on Mutex::lock() in code paths that can be reached after a poisoned lock.
  • expect() on environment variables or config values.
  • Arithmetic that overflows in release mode but panics in debug, masking bugs during testing.

Category 6: Logic Errors and Business-Rule Violations

The largest bug category in any production system is logic errors: off-by-one in pagination, wrong sign on a refund, missing authorization check on an endpoint, race condition in a saga. Rust's type system can encode some invariants through newtypes and the typestate pattern, but most business rules live outside what the compiler verifies.

This is where rigorous testing, property-based testing with proptest, fuzzing with cargo-fuzz, and code review carry the load. Our staff augmentation teams pair Rust engineers with security reviewers specifically because the compiler doesn't replace human judgment on these.

How Should Engineering Teams Adopt Rust Safely?

Rust adoption succeeds when teams understand both what it guarantees and what it doesn't. Set realistic expectations: Rust eliminates a specific bug class, not all bugs. Build your CI and review process around the gaps. Here's the checklist we apply on Fajarix engagements:

  1. Run cargo clippy with -D warnings in CI, including pedantic lints for new code.
  2. Add cargo audit and cargo deny to catch vulnerable dependencies.
  3. Enable overflow checks in release builds for security-sensitive crates.
  4. Fuzz every parser and protocol boundary with cargo-fuzz or libafl.
  5. Forbid unwrap and expect in library code using clippy lints.
  6. Audit every syscall pair for TOCTOU patterns.
  7. Use OsStr and &[u8] at all OS boundaries; reach for String only for application-layer text.
  8. Commission an external security audit before any privileged or production deployment.

Common Misconceptions About Rust Safety

We hear these regularly from engineering leaders evaluating Rust for new systems, and they all need correcting before adoption begins.

"Rust prevents all security bugs."

False. Rust prevents memory-safety bugs in safe code. The uutils audit found 44 CVEs in production Rust, none of them memory-safety issues.

"If it compiles, it works."

False. Compilation proves the program respects ownership and types. It does not prove the program implements the right behavior, handles concurrency correctly across syscalls, or rejects malicious input.

"unsafe is the only place bugs live."

False. Every bug in the uutils audit was in safe Rust. The dangerous code is whatever interacts with the outside world: paths, sockets, parsers, integer math on untrusted input.

What Tools Help Catch the Bugs Rust Cannot Catch in Production?

No single tool covers the gap, but a layered toolchain gets close. Combine static analysis, dynamic analysis, and human review. The bugs Rust cannot catch in production fall to different tools:

  • cargo-fuzz and libafl for parsers, protocol implementations, and any code touching untrusted bytes.
  • proptest and quickcheck for property-based testing of business logic.
  • miri for catching undefined behavior in unsafe blocks.
  • cargo-mutants for verifying test coverage actually catches regressions.
  • loom for exhaustively testing concurrent code under different schedules.
  • semgrep with custom rules for project-specific anti-patterns like unwrap on parsed input.

For teams building production systems, we layer these tools into CI pipelines as part of our web development services and backend engagements. The same patterns apply whether you're shipping a CLI, a web service, or embedded firmware.

Where Rust Still Wins Despite These Gaps

Nothing in this article argues against Rust. The bugs Rust cannot catch in production are exactly the bugs every other language also fails to catch. Rust still eliminates the largest single category of historical CVEs, gives you fearless concurrency, produces fast binaries, and forces explicit error handling in a way that most languages don't.

The right framing is: Rust raises the floor, it doesn't raise the ceiling. You still need disciplined engineering, security review, fuzzing, and audits. Teams that adopt Rust expecting the compiler to do all the work end up shipping the same logic bugs they always shipped, just faster.

At Fajarix, we help teams in fintech, healthcare, and infrastructure adopt Rust without falling into these traps, combining rigorous code review with Fajarix AI automation tooling for static analysis at scale.

Ready to put these insights into practice? The team at Fajarix builds exactly these solutions. Book a free consultation to discuss your project.

Ready to build something like this?

Talk to Fajarix →