2021年,一位同事告訴我Rust是"能在Bug誕生前就消滅它們的語(yǔ)言"。三年后,我在三個(gè)生產(chǎn)項(xiàng)目里逐行審計(jì),找到了社區(qū)說(shuō)"不存在"的那類(lèi)錯(cuò)誤。
從HN熱帖到代碼審計(jì)
![]()
那個(gè)648贊的Hacker News帖子"Bugs Rust won't catch"我沒(méi)急著評(píng)論。先打開(kāi)自己參與維護(hù)的三個(gè)項(xiàng)目——一個(gè)純Rust項(xiàng)目,兩個(gè)依賴(lài)Rust工具鏈的項(xiàng)目——拿著帖子的分類(lèi)清單逐行排查。
結(jié)果:找到了。而且正是帖子里列的那幾類(lèi)。
我的核心發(fā)現(xiàn):Rust保證內(nèi)存安全,但不保證邏輯正確。社區(qū)經(jīng)常把前者包裝成后者,這兩個(gè)不是一回事。我代碼里的數(shù)字證明了這一點(diǎn)。
第一類(lèi):編譯器笑著放過(guò)的邏輯錯(cuò)誤
在一個(gè)處理配置文件的Rust CLI工具里,我發(fā)現(xiàn)這段代碼:
「// 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 }」
編譯器零警告。所有內(nèi)存訪(fǎng)問(wèn)都合法,生命周期沒(méi)問(wèn)題。但業(yè)務(wù)邏輯是錯(cuò)的——我需要對(duì)照項(xiàng)目規(guī)格文檔才能發(fā)現(xiàn)。
這讓我想起之前用TypeScript 7 beta跑基準(zhǔn)測(cè)試的經(jīng)歷:最耗時(shí)的錯(cuò)誤不是編譯器拒絕的,而是編譯器熱情接受、但語(yǔ)義完全錯(cuò)誤的那種。不同語(yǔ)言,同一陷阱。
第二類(lèi):算術(shù)溢出與邊界假設(shè)
另一個(gè)項(xiàng)目里有段處理像素坐標(biāo)的代碼:
「fn escalar_coordenada(x: u32, factor: u32) -> u32 { x * factor // 溢出在release mode是defined behavior: wrapping }」
Rust的release模式對(duì)整數(shù)溢出采用wrapping行為(回繞),不是panic。編譯器不會(huì)攔你,運(yùn)行時(shí)也不會(huì)報(bào)錯(cuò)。如果你的業(yè)務(wù)假設(shè)"放大后的坐標(biāo)一定更大",這個(gè)假設(shè)在u32::MAX附近會(huì)靜默失效。
我查了cargo.toml,這個(gè)項(xiàng)目沒(méi)開(kāi)overflow-checks = true。團(tuán)隊(duì)顯然不知道這個(gè)默認(rèn)行為。
第三類(lèi):并發(fā)邏輯與數(shù)據(jù)競(jìng)爭(zhēng)的區(qū)別
在一個(gè)依賴(lài)Rust寫(xiě)的數(shù)據(jù)處理管道的項(xiàng)目里,發(fā)現(xiàn)這樣的模式:
「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 || { // 每個(gè)線(xiàn)程處理自己的數(shù)據(jù),但寫(xiě)入同一個(gè)map let resultado = calcular(d); clon.lock().unwrap().insert(d.id, resultado); }) }).collect(); // ... join handles }」
Rust的borrow checker確保沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)(data race)——兩個(gè)線(xiàn)程不會(huì)同時(shí)讀寫(xiě)同一內(nèi)存。但邏輯競(jìng)爭(zhēng)(race condition)呢?如果下游代碼假設(shè)"map里的鍵按處理順序排列",這個(gè)假設(shè)在并發(fā)寫(xiě)入時(shí)完全不成立。
編譯器沒(méi)報(bào)錯(cuò)。Arc>是合法模式。但業(yè)務(wù)邏輯里的時(shí)序假設(shè)錯(cuò)了。
第四類(lèi):錯(cuò)誤處理的路徑遺漏
最隱蔽的在這個(gè)純Rust項(xiàng)目里。團(tuán)隊(duì)大量使用?操作符:
「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)?) }」
問(wèn)題在最后一行。如果parsear_con_defaults也返回Result,?會(huì)提前返回。但調(diào)用者拿到Err時(shí),能區(qū)分是"文件不存在""JSON語(yǔ)法錯(cuò)誤"還是"配置邏輯無(wú)效"嗎?
我查了調(diào)用鏈,上層用match處理Err時(shí),只打印了通用錯(cuò)誤信息。用戶(hù)看到"配置加載失敗",不知道具體哪一步。運(yùn)維排查時(shí),三個(gè)完全不同的根因被埋在同一個(gè)錯(cuò)誤變體里。
Rust強(qiáng)制你處理錯(cuò)誤(Result不能忽略),但不強(qiáng)制你區(qū)分錯(cuò)誤類(lèi)型。?操作符讓傳播變得太容易,有時(shí)太容易了。
數(shù)字不會(huì)說(shuō)謊
三個(gè)項(xiàng)目的審計(jì)結(jié)果:
? 純Rust項(xiàng)目(約1.2萬(wàn)行):找到7處邏輯錯(cuò)誤,0處內(nèi)存安全問(wèn)題
? 依賴(lài)Rust工具的項(xiàng)目A(調(diào)用Rust寫(xiě)的CLI):3處邊界條件錯(cuò)誤,其中1處導(dǎo)致生產(chǎn)事故
? 依賴(lài)Rust工具的項(xiàng)目B:2處并發(fā)時(shí)序假設(shè)錯(cuò)誤,目前未觸發(fā)但存在風(fēng)險(xiǎn)
12處問(wèn)題,全部是"Rust承諾不存在的Bug"類(lèi)別。不是內(nèi)存泄漏,不是use-after-free,是業(yè)務(wù)邏輯與代碼實(shí)現(xiàn)之間的裂縫。
為什么社區(qū)會(huì)混淆這兩件事
我尊重Rust社區(qū)的技術(shù)深度,但必須指出一個(gè)傳播現(xiàn)象:內(nèi)存安全的營(yíng)銷(xiāo)話(huà)術(shù)被過(guò)度外推了。
borrow checker、所有權(quán)系統(tǒng)、零成本抽象——這些確實(shí)是工程杰作。但它們解決的是特定類(lèi)別的問(wèn)題。當(dāng)技術(shù)布道者說(shuō)"Rust消除Bug",聽(tīng)眾容易理解為"消除所有Bug",而實(shí)際上只是"消除內(nèi)存安全類(lèi)Bug"。
這種混淆在招聘市場(chǎng)更明顯。我見(jiàn)過(guò)JD寫(xiě)"用Rust重寫(xiě)以減少90%Bug",這個(gè)百分比沒(méi)有來(lái)源,也無(wú)法驗(yàn)證。我的代碼審計(jì)顯示,邏輯錯(cuò)誤占比遠(yuǎn)高于90%的補(bǔ)集。
對(duì)技術(shù)選型的實(shí)際影響
這不是勸退Rust。我的三個(gè)項(xiàng)目會(huì)繼續(xù)用,但會(huì)調(diào)整工程實(shí)踐:
? 強(qiáng)制開(kāi)啟overflow-checks = true,或顯式使用checked_add等安全算術(shù)
? 錯(cuò)誤類(lèi)型必須細(xì)分,禁止裸Result>傳播
? 關(guān)鍵邏輯路徑強(qiáng)制代碼審查,不依賴(lài)編譯器替代人工審計(jì)
? 并發(fā)代碼必須文檔化時(shí)序假設(shè),并寫(xiě)針對(duì)性測(cè)試
TypeScript 7的經(jīng)歷讓我養(yǎng)成一個(gè)習(xí)慣:編譯器通過(guò)只是第一步,語(yǔ)義正確需要第二步驗(yàn)證。Rust項(xiàng)目現(xiàn)在執(zhí)行同樣的兩步流程。
給同樣被營(yíng)銷(xiāo)話(huà)術(shù)吸引過(guò)的人
如果你正在評(píng)估Rust,或者被"零缺陷"承諾打動(dòng),建議做一件事:拿你現(xiàn)有的代碼庫(kù),按Hacker News那個(gè)帖子的分類(lèi)清單審計(jì)一遍。不要看別人的例子,用自己的代碼。
我的發(fā)現(xiàn)可能不代表你的情況。但"內(nèi)存安全≠邏輯正確"這個(gè)等式,在我的代碼里被驗(yàn)證了12次。
語(yǔ)言是工具,不是咒語(yǔ)。Rust是很好的工具,但好工具也有適用范圍。認(rèn)清這個(gè)范圍,比盲目崇拜更能減少生產(chǎn)事故。
你的代碼庫(kù)里,編譯器笑著放過(guò)、但邏輯明顯錯(cuò)誤的那類(lèi)問(wèn)題,最近一次是什么時(shí)候發(fā)現(xiàn)的?
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺(tái)“網(wǎng)易號(hào)”用戶(hù)上傳并發(fā)布,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。
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.