Bugs Rust won't catch

2026 年 4 月,Canonical 在 uutils 中披露了 44 个 CVE,uutils 是 GNU coreutils 的 Rust 重新实现,自 25.10 起默认发布。其中大多数来自 26.04 LTS 之前委托的外部审计

Bugs Rust won't catch

先看结论:2026 年 4 月,Canonical 在 uutils 中披露了 44 个 CVE,uutils 是 GNU coreutils 的 Rust 重新实现,自 25.

现在 Rust 生产代码库中,这些错误都是由那些知道自己在做什么的人编写的,并且没有一个被借用检查器、clippy lints 或货物审计发现。我写这篇文章并不是为了批评 uutils 团队。恰恰相反;我实际上要感谢他们如此详细地分享审计结果,以便我们都能向他们学习。最近,Ubuntu 工程副总裁 Jon Seager 也参与了我们的“Rust in Production”播客,许多听众赞赏他对 Canonical 的 Rust 状况的诚实态度。如果你用 Rust 编写系统代码,那么这是对 Rust 安全性的最集中关注,你现在可能会在任何地方找到它。不要信任跨两个系统调用的路径这是审计中最大的错误群。这也是 cp 、 mv 和 rm 在 Ubuntu 26.04 LTS 中仍然是 GNU 的原因。

核心内容

:( 模式总是相同的。您执行一个系统调用来检查有关路径的某些内容,然后执行另一个系统调用来对同一路径进行操作。在这两个调用之间,对父目录具有写访问权限的攻击者可以将路径组件交换为符号链接。内核在第二次调用时从头开始重新解析路径,特权操作将落在攻击者选择的目标上。Rust 的标准库使这种情况很容易出错。您首先使用的人体工学 API (fs::metadata、File::create、fs::remove_file、fs::set_permissions)每次都会获取一个路径并重新解析它,而不是获取一个文件描述符并相对于它进行操作,这对于普通程序来说没问题,但如果您正在编写需要防范本地攻击者的特权工具,则必须小心案例研究:CVE-2026-35355。

bug,从 src/uu/install/src/install.rs 简化。

// 1.
清除目标 fs::remove_file(to)?; // ...
// 2.

此处重新解析路径!

let mut dest = File::create(to)?; // 遵循符号链接,截断 copy(from, &mut dest)?; 在步骤 1 和步骤 2 之间,任何具有父目录写入权限的人都可以使用作为 /etc/shadow 的符号链接,然后 File::create 遵循该符号链接,特权进程会愉快地用其中包含的任何内容覆盖 /etc/shadow 。

该修复使用 OpenOptions::create_new(true) : fs::remove_file(to)?;让 mut dest = OpenOptions::new() .write(true) .create_new(true) .open(to)?;复制(来自,&mut dest)?; create_new 的文档说(强调我的):目标位置不允许存在文件,也不允许(悬空)符号链接。这样,如果调用成功,返回的文件就保证是新的。规则:锚定在文件描述符上 Rust 中的 &Path 看起来像一个值,但请记住,对于内核来说,它只是一个名称。从一个系统调用到下一个系统调用,该名称可以指向不同的事物。将您的操作锚定在文件描述符上。

create_new() 仅在创建新文件时有所帮助。对于其他所有内容,打开父目录一次并相对于该句柄进行工作。如果您在同一条路径上执行两次,则假设这是一个 TOCTOU(检查时间到使用时间)错误,直到您证明并非如此。在创建时而不是之后设置权限 这是 TOCTOU 的近亲。你想要一个具有限制性权限的目录,所以你写了这样的东西。

// 使用默认权限创建 fs::create_dir(&path)?; // 修复权限 fs::set_permissions(&path, Permissions::from_mode(0o700))?;暂时,路径以默认权限存在。系统上的任何其他用户都可以在该窗口期间打开()它。一旦他们有了文件描述符,后面的 chmod 就不会把它从他们身上夺走。规则:在创建时设置权限,在 OpenOptions::mode() 和 DirBuilderExt::mode() 达到之后不再设置权限,以便文件或目录天生就具有您想要的权限。内核将在顶部应用您的 umask,因此如果您确实关心的话,也请明确设置它。路径上的字符串相等性与文件系统标识不同 chmod 中的原始 --preserve-root 检查实际上是这样的: if recursive && preserve_root && file == Path::new("/") { return Err(PreserveRoot); }任何解析为 / 但未拼写为 / 的内容都会绕过该比较。因此 /../ 、 /./ 、 /usr/..
或指向 / 的符号链接。运行 chmod -R 000 /../ 并看到它直接通过您的检查并锁定整个系统。修复方法如下: fn is_root(file: &Path) -> bool { matches!(fs::canonicalize(file), Ok(p) if p == Path::new("/")) } if recursive && preserve_root && is_root(file) { return Err(PreserveRoot); }规则:在比较路径之前解析路径规范化解析 ..

, .

,并符号链接到真正的绝对路径。这比字符串比较要好得多。

哦,如果你想知道这一行: matches!(fs::canonicalize(file), Ok(p) if p == Path::new("/")) 我认为这只是一种奇特的说法 // 首先,解析其规范形式的路径 if let Ok(p) = fs::canonicalize(file) { // 如果成功,检查规范路径是否为 "/" p == Path::new("/") } else {在 --preserve-root 的特定情况下,这是有效的,因为 / 没有父目录,因此攻击者无法从您的下面交换任何内容。然而,在比较文件系统标识的两个任意路径的更一般情况下,您需要打开两个路径并比较它们的(dev,inode)对,就像 GNU coreutils 所做的那样。

(考虑身份,而不是字符串相等。)顺便说一句,这个组中我最喜欢的错误是 CVE-2026-35363: rm 。

延伸阅读:如果你想继续找可转化的工具入口,可以去工具合集和赚钱专题继续看。

进入 AI 工具导航页 查看更多 AI 写作