HN
Today

Bugs Rust won't catch

Rust's lauded safety features don't prevent every bug, as demonstrated by 44 CVEs found in uutils, the Rust rewrite of GNU coreutils. This deep dive dissects common security vulnerabilities that arise at the boundary between Rust's controlled environment and the messy Unix world. It offers crucial lessons for systems programmers on issues like TOCTOU, path handling, and error propagation, explaining why these slip past the borrow checker and how to mitigate them.

32
Score
0
Comments
#2
Highest Rank
8h
on Front Page
First Seen
Apr 29, 4:00 AM
Last Seen
Apr 29, 11:00 AM
Rank Over Time
42222244

The Lowdown

This article delves into the critical lessons learned from 44 CVEs discovered in uutils, the Rust reimplementation of GNU coreutils used in Ubuntu. Despite Rust's reputation for safety, these vulnerabilities bypassed the language's core safety mechanisms, highlighting a "new security boundary" for systems programming at the interface with the operating system.<ul><li>

TOCTOU Vulnerabilities: Many bugs stemmed from Time-Of-Check-To-Time-Of-Use issues, where path-based APIs in std::fs (like File::create or fs::remove_file) re-resolve paths across syscalls, allowing attackers to swap targets via symlinks. The solution involves anchoring operations on file descriptors or using create_new(true).

</li><li> **Permission Races**: Creating files or directories with default permissions and then setting stricter ones leaves a brief window for access. The recommendation is to set permissions at creation time using `OpenOptions::mode()` or `DirBuilderExt::mode()`. </li><li> **Path Identity**: Comparing paths using string equality is insufficient; `fs::canonicalize` or `(dev, inode)` pairs are needed to establish true filesystem identity, especially for sensitive operations like `--preserve-root` checks. </li><li> **Unix Boundary Types**: Rust's UTF-8 `String` is often unsuitable for Unix boundaries (paths, environment variables) which are byte-based. Lossy UTF-8 conversions or strict conversions leading to panics cause data corruption or DoS. The correct approach is to use byte-oriented types like `OsStr`, `Path`, and `&[u8]`. </li><li> **Panics as DoS**: Relying on `unwrap()`, `expect()`, or unchecked indexing on untrusted input can lead to `panic!`s, effectively creating Denial-of-Service vulnerabilities in CLI tools. The author advises converting bad input into explicit errors using `Result` and `?`. </li><li> **Error Propagation**: Discarding `Result`s can lead to silent failures, incorrect exit codes, and unexpected behavior. It's crucial to propagate errors or explicitly justify why a `Result` is ignored, often by collecting and reporting the "worst" error. </li><li> **Bug-for-Bug Compatibility**: For reimplementing core utilities, maintaining bug-for-bug compatibility with original GNU tools regarding exit codes and edge cases is a security feature, as users' scripts often rely on specific behaviors (Hyrum's Law). </li><li> **Trust Boundary Resolution**: Inputs must be fully resolved *before* crossing trust boundaries (e.g., calling `chroot`), as functions called inside an untrusted environment might load attacker-controlled libraries or resources. </li></ul> While these findings reveal vulnerabilities Rust's type system doesn't directly prevent, the article strongly emphasizes that Rust *did* successfully prevent entire classes of memory-safety bugs common in C codebases (buffer overflows, use-after-free). The identified issues represent a shift in the security landscape, focusing on the complex interactions between Rust code and the operating system's messy realities. The takeaway is to embrace "idiomatic Rust" that candidly reflects these real-world complexities, prioritizing robust error handling and precise type usage at system boundaries over aesthetic simplicity.