44个CVE。
全是Rust写的。
全是"懂行的人"写的。
借用检查器全程静音,Clippy集体装死,cargo audit直接躺平。
这不是什么野路子脚本,这是uutils——Canonical钦定的GNU coreutils Rust重写版,准备塞进Ubuntu 26.04 LTS的硬核基础设施。就在今年四月,官方一口气披露了44个安全漏洞。每一个都在提醒我们:Rust很安全,但Rust程序员不是。
路径是幽灵:TOCTOU的致命舞蹈
最扎眼的一群漏洞,叫TOCTOU(Time Of Check To Time Of Use)。
说人话就是:你检查文件的时候它是A,你操作的时候它变成了B。
Canonical的工程师发现,cp、mv、rm这些基础命令在Ubuntu 26.04 LTS里仍然是GNU版本。为什么?因为Rust版还没搞定符号链接攻击。
看这段看似无害的代码:
// 1. 清空目标文件
fs::remove_file(to)?;
// ...
// 2. 创建新文件
let mut dest = File::create(to)?;
死穴就在这"..."里。
攻击者在这两行间插一脚,把to换成指向/etc/shadow的符号链接。File::create会乖乖 follow symlink,然后你的特权进程就亲手把系统密码文件给覆盖了。
Rust的标准库在这里帮了倒忙。那些"人体工学"API——fs::metadata、File::create、fs::remove_file——全都在重复解析路径,而不是基于文件描述符操作。C语言老手本能地会用openat家族,Rust的std::fs却让你轻松写出竞态条件。
修复方案?用OpenOptions::create_new(true),确保原子性创建。但说实话,这API藏得够深。
UTF-8霸权与Unix的"字节乱炖"
Rust的String和&str强制UTF-8,这在Web世界是天降福音,在Unix系统编程里却是数据腐败的温床。
Unix路径、环境变量、管道内容,本质都是字节流,不是UTF-8字符串。uutils的comm命令曾经这么写:
print!("{}", String::from_utf8_lossy(ra));
from_utf8_lossy——这名字听着温柔,实则暴力。遇到非法UTF-8字节就偷偷替换成U+FFFD(那个菱形问号)。GNU comm能处理二进制文件,uutils版却静默篡改用户数据。
在系统工具里,UTF-8是错的默认。
该用OsStr,该用Vec<u8>,该用&[u8]。把字节流硬塞进String再拉出来,就像把生海鲜装进真空袋——看起来整洁,打开已经臭了。
Panic不是退出,是自杀
Rust的panic!会展开栈并终止进程。在CLI工具里,这等于给攻击者发DDoS武器。
审计发现sort --files0-from有个神操作:解析NUL分隔的文件名列表时,遇到非UTF-8字节直接expect()爆炸:
let path = std::str::from_utf8(bytes)
.expect("Could not parse string...");
GNU sort把文件名当原始字节处理,Rust版却因为一个字节让整个进程自杀。想象一下,你的定时任务脚本处理用户上传文件,一个unwrap()没包好,整个CI流水线直接暴毙。
每一个unwrap(),每一个expect(),都是埋在地雷上的棉花糖。
生产代码里,该用?,该用checked_*,该让错误上浮。别在边界上装死。
兼容性即安全
有些漏洞甚至不是"代码错了",而是**"跟GNU不一样"**。
kill -1在GNU里表示"信号1(SIGHUP)",uutils却理解成"PID -1"——在Linux上这意味着给所有可见进程发信号。一个打字错误变成核按钮。
这就是Hyrum's Law:当你的行为偏离原版,某个角落的shell脚本就会做出错误决策。uutils现在被迫在CI里跑GNU coreutils的完整测试套件,不是为了功能正确,是为了防止意外杀伤。
最阴险的一击:chroot后的动态库陷阱
CVE-2026-35368是全场MVP——本地root代码执行。
代码看起来清白:
chroot(new_root)?;
let user = get_user_by_name(name)?;
setuid(user.uid())?;
陷阱在第二行。
get_user_by_name内部通过NSS(Name Service Switch)解析用户名,而NSS会动态加载libnss_*共享库。当你已经chroot进攻击者控制的文件系统后,这个函数会去加载新根目录里的.so文件。
攻击者放个恶意动态库,你以uid 0执行它。游戏结束。
GNU chroot早就知道:先解析用户,再切根。Rust版把顺序搞反了,而且静态编译也救不了你,因为NSS的dlopen发生在运行时。
但等等,C语言更惨
读到这儿你可能想:Rust就这?
打住。
这44个CVE里,零个缓冲区溢出,零个Use-After-Free,零个双释放,零个数据竞争,零个空指针解引用。
看看GNU coreutils过去几年的成绩单:
pwd在深路径下缓冲区溢出(9.11, 2026)numfmt越界读取(9.9, 2025)split堆覆盖(CVE-2024-0684, 2024)tail -f栈缓冲区溢出(9.0, 2021)
Rust确实守住了内存安全的底线。
那些让C程序员夜不能寐的漏洞,在uutils里一个都没有。剩下的这些——TOCTOU、UTF-8语义、错误处理——是系统编程的新边疆,是代码与混乱现实世界接壤的边界。
诚实的代码
Rust的借用检查器能守住内存,但守不住时间(两次syscall之间),守不住语义(路径字符串比较),守不住兼容性(GNU的行为怪癖)。
所谓"地道Rust"(Idiomatic Rust),不该只是优雅的迭代器链和零成本抽象。
它应该包含丑陋的OsStr,包含繁琐的文件描述符操作,包含?运算符的冗余传播,包含对历史包袱的妥协。
类型系统能编码很多,但编码不了外部世界的混乱。
真正健壮的系统代码,敢于直面这种混乱,而不是用纸糊的抽象盖住它。
当你的代码从&Path变成文件描述符,从String变成Vec<u8>,从unwrap()变成?时,你写的不是妥协,是诚实。
【锐评】:Rust没翻车,只是扯下了"内存安全等于绝对安全"的遮羞布——系统编程的坑,终究要靠程序员脑子来填。
参考链接:
https://corrode.dev/blog/bugs-rust-wont-catch/