2021年,一位同事告诉我Rust是"能在Bug诞生前就消灭它们的语言"。三年后,我在三个生产项目里逐行审计,找到了社区说"不存在"的那类错误。
从HN热帖到代码审计
![]()
那个648赞的Hacker News帖子"Bugs Rust won't catch"我没急着评论。先打开自己参与维护的三个项目——一个纯Rust项目,两个依赖Rust工具链的项目——拿着帖子的分类清单逐行排查。
结果:找到了。而且正是帖子里列的那几类。
我的核心发现:Rust保证内存安全,但不保证逻辑正确。社区经常把前者包装成后者,这两个不是一回事。我代码里的数字证明了这一点。
第一类:编译器笑着放过的逻辑错误
在一个处理配置文件的Rust CLI工具里,我发现这段代码:
「// Itera sobre los elementos y calcula el "siguiente" índice // El compilador no tiene idea de que este rango está mal fn procesar_ventana(datos: &[u32]) -> Vec { let mut resultado = Vec::new(); // Bug: debería ser datos.len() - 1 para comparar pares // Rust lo compiló feliz. No hay UB, no hay memory error. // Hay un error lógico puro que produce resultados incorrectos. for i in 0..datos.len() { if i + 1 < datos.len() { resultado.push(datos[i] + datos[i + 1]); } } resultado }」
编译器零警告。所有内存访问都合法,生命周期没问题。但业务逻辑是错的——我需要对照项目规格文档才能发现。
这让我想起之前用TypeScript 7 beta跑基准测试的经历:最耗时的错误不是编译器拒绝的,而是编译器热情接受、但语义完全错误的那种。不同语言,同一陷阱。
第二类:算术溢出与边界假设
另一个项目里有段处理像素坐标的代码:
「fn escalar_coordenada(x: u32, factor: u32) -> u32 { x * factor // 溢出在release mode是defined behavior: wrapping }」
Rust的release模式对整数溢出采用wrapping行为(回绕),不是panic。编译器不会拦你,运行时也不会报错。如果你的业务假设"放大后的坐标一定更大",这个假设在u32::MAX附近会静默失效。
我查了cargo.toml,这个项目没开overflow-checks = true。团队显然不知道这个默认行为。
第三类:并发逻辑与数据竞争的区别
在一个依赖Rust写的数据处理管道的项目里,发现这样的模式:
「use std::sync::Arc; use std::thread; fn procesar_en_paralelo(datos: Vec) -> Vec { let compartido = Arc::new(Mutex::new(HashMap::new())); let handles: Vec<_> = datos.into_iter().map(|d| { let clon = Arc::clone(&compartido); thread::spawn(move || { // 每个线程处理自己的数据,但写入同一个map let resultado = calcular(d); clon.lock().unwrap().insert(d.id, resultado); }) }).collect(); // ... join handles }」
Rust的borrow checker确保没有数据竞争(data race)——两个线程不会同时读写同一内存。但逻辑竞争(race condition)呢?如果下游代码假设"map里的键按处理顺序排列",这个假设在并发写入时完全不成立。
编译器没报错。Arc>是合法模式。但业务逻辑里的时序假设错了。
第四类:错误处理的路径遗漏
最隐蔽的在这个纯Rust项目里。团队大量使用?操作符:
「fn cargar_configuracion(ruta: &Path) -> Result { let contenido = std::fs::read_to_string(ruta)?; let parseado: Config = serde_json::from_str(&contenido)?; validar_config(&parseado)?; // 新增的检查 Ok(parsear_con_defaults(parseado)?) }」
问题在最后一行。如果parsear_con_defaults也返回Result,?会提前返回。但调用者拿到Err时,能区分是"文件不存在""JSON语法错误"还是"配置逻辑无效"吗?
我查了调用链,上层用match处理Err时,只打印了通用错误信息。用户看到"配置加载失败",不知道具体哪一步。运维排查时,三个完全不同的根因被埋在同一个错误变体里。
Rust强制你处理错误(Result不能忽略),但不强制你区分错误类型。?操作符让传播变得太容易,有时太容易了。
数字不会说谎
三个项目的审计结果:
• 纯Rust项目(约1.2万行):找到7处逻辑错误,0处内存安全问题
• 依赖Rust工具的项目A(调用Rust写的CLI):3处边界条件错误,其中1处导致生产事故
• 依赖Rust工具的项目B:2处并发时序假设错误,目前未触发但存在风险
12处问题,全部是"Rust承诺不存在的Bug"类别。不是内存泄漏,不是use-after-free,是业务逻辑与代码实现之间的裂缝。
为什么社区会混淆这两件事
我尊重Rust社区的技术深度,但必须指出一个传播现象:内存安全的营销话术被过度外推了。
borrow checker、所有权系统、零成本抽象——这些确实是工程杰作。但它们解决的是特定类别的问题。当技术布道者说"Rust消除Bug",听众容易理解为"消除所有Bug",而实际上只是"消除内存安全类Bug"。
这种混淆在招聘市场更明显。我见过JD写"用Rust重写以减少90%Bug",这个百分比没有来源,也无法验证。我的代码审计显示,逻辑错误占比远高于90%的补集。
对技术选型的实际影响
这不是劝退Rust。我的三个项目会继续用,但会调整工程实践:
• 强制开启overflow-checks = true,或显式使用checked_add等安全算术
• 错误类型必须细分,禁止裸Result>传播
• 关键逻辑路径强制代码审查,不依赖编译器替代人工审计
• 并发代码必须文档化时序假设,并写针对性测试
TypeScript 7的经历让我养成一个习惯:编译器通过只是第一步,语义正确需要第二步验证。Rust项目现在执行同样的两步流程。
给同样被营销话术吸引过的人
如果你正在评估Rust,或者被"零缺陷"承诺打动,建议做一件事:拿你现有的代码库,按Hacker News那个帖子的分类清单审计一遍。不要看别人的例子,用自己的代码。
我的发现可能不代表你的情况。但"内存安全≠逻辑正确"这个等式,在我的代码里被验证了12次。
语言是工具,不是咒语。Rust是很好的工具,但好工具也有适用范围。认清这个范围,比盲目崇拜更能减少生产事故。
你的代码库里,编译器笑着放过、但逻辑明显错误的那类问题,最近一次是什么时候发现的?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.