vatt'ghern jaskier's ballads
本文 1 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

把 curl 的一段五行程式餵進去會 segfault,沒人會說那是 curl 的漏洞—— 可同樣的記憶體誤用搬到一個不含 unsafe 的 Rust 程式裡,責任卻整個翻面落到 library 身上。 同一個崩潰,兩種語言裡連「算不算一個漏洞」都不是同一把尺。

記憶體 CVE 的雙重標準——Rust vs C/C++

CVE 是用來分類與通報軟體安全漏洞的資料庫, 其中最棘手的一類來自記憶體不安全(memory unsafety)—— use-after-free、buffer overflow、把 NULL 當有效指標解參考。 Kobzol 在 2026 年 6 月 15 日的這篇文章追問一個看似瑣碎、其實很要命的問題: 同樣是「程式在某種用法下踩爆記憶體」,為什麼在 Rust 會被算成 library 的 CVE, 在 C/C++ 卻常常被推回去說是「呼叫端用錯」。 他的答案不是去比每千行程式碼的 CVE 數量,而是先把「什麼才算一個記憶體安全漏洞」這把尺攤開—— 結果發現 Rust 與 C/C++ 量的根本不是同一件事。

這不是一篇統計研究,文章裡沒有 CVE 數量的對照表,作者刻意不去做那種比較。 它要拆的是定義:在 Rust 的世界觀裡, 「只要存在任何一種不靠 unsafe 就能觸發記憶體錯誤的用法,那就是 library 的 bug」; 在 C/C++ 的世界觀裡,所有程式碼本來就 implicitly unsafe,於是同一條準則根本套不上去, 責任自然往呼叫端滑。 這篇文章值得讀,是因為它把這個常被混為一談的「標準差異」講清楚—— 而標準差異一旦看懂,市面上那些「Rust 的 CVE 數沒有比較少」的比較,意義就得重新評估。

要把這個不對稱講透,得先抓住核心那條線:unsafe 關鍵字在不在使用端的程式裡。 下面這個 widget 把整件事壓成一個動作——選一種記憶體誤用、再切換語言, 看同一個崩潰怎麼在「呼叫端的錯,不算 library CVE」與「library 的 bug,算 CVE」之間翻面。 這條翻面的軸,就是整篇文章的骨幹。

切換語言與誤用情境,看同一個崩潰的 CVE 判定如何翻面 · 2 語言 × 3 情境

語言
誤用
使用端程式碼 curl_getenv(NULL); 這段使用端有 unsafe 嗎? 不適用——C 全部 implicitly unsafe 判定 不算 library CVE 歸為「呼叫端用錯」 library 不背這個責任 為什麼 C 缺乏型別系統手段去強制 precondition, 傳 NULL 的責任落在寫呼叫的人身上。
左軸固定不變:同一個記憶體誤用、同一段使用端程式。右側判定隨語言翻面。資料:Kobzol(2026-06-15)對 CVE 通報準則的論證。

這個翻面的支點只有一個詞:unsafe。 Kobzol 把 Rust 那一側的判準寫成一句近乎定義的話—— 「當你的使用端程式裡沒有用到 unsafe,卻仍然有任何一種可以想像的用法會造成記憶體錯誤, 那永遠是 library 的 bug,不是使用端的 bug」(in any conceivable way possible)。 注意這句的強度:不是「常常」,不是「多數情況」,是「任何一種可以想像的用法」。 safe API 只要存在一條不靠 unsafe 就能踩爆記憶體的路徑,那條路徑本身就是 library 的缺陷。

把這句話和 C/C++ 那側對照,差異不在「哪邊比較嚴格」,而在「準則能不能套上去」。 在 Rust 裡,all C code is implicitly unsafe——換句話說,C 沒有「safe 子集」這個概念, 整個語言都在 unsafe 那一格。 既然沒有一條「不靠 unsafe」的基準線可以畫,「safe API 必須真的 safe」這條判準在 C 裡就無從談起。 於是 C/C++ 退回另一條準則:我們替 library 的「特定誤用」開 CVE, 而不是替「一個可以被誤用的 API 之存在」開 CVE。

下面把這四個對照維度排開來——這就是這篇文章真正的比較骨架。 每一格不是在比「誰的 CVE 多」,是在比「同一件事,兩種語言怎麼歸類」。

RUST(SAFE CODE) C / C++ 基準線 存在「safe」子集 全部 implicitly unsafe 判準 不靠 unsafe 能踩爆= library 的 bug 替特定誤用開 CVE, 不替「可被誤用」開 傳 NULL/None 算 library CVE 歸「呼叫端用錯」 責任歸屬 落在 library 作者 落在使用端開發者 仍可能出事 unsafe 內的 bug、 邏輯漏洞、編譯器 bug 任何程式碼都可能 出記憶體錯誤
四個維度的對照表——這不是 CVE 數量的比較,是「同一件事歸到哪一格」的比較。最後一列是文章特意保留的誠實:Rust 並沒有把所有漏洞都消掉。

基準線:哪一格能畫出「safe」這條線

一切的根源是基準線:Rust 把語言切成 safe 與 unsafe 兩個世界, 而 C/C++ 只有一個世界,那個世界整個都是 unsafe。 Kobzol 的原話是 all C code is implicitly unsafe—— 從 Rust 的視角看出去,整份 C 程式碼都該被當成包在一個隱形的 unsafe 區塊裡。

這條線不是修辭。它決定了「責任」這個詞能不能被精準分配。 在 Rust 裡,使用端是否寫了 unsafe 是一個語法層級、可被編譯器檢查的事實: grep 一下原始碼就知道。 於是當崩潰發生,第一個問題有明確答案——使用端有沒有用 unsafe? 沒有,責任就不在使用端。 Kobzol 把這層保證寫得很乾脆: 只要你沒在程式裡用 unsafe(在絕大多數情況下根本不需要,除非你在寫作業系統或 lock-free 資料結構之類的東西), 又沒撞上編譯器 bug,你就「知道」任何可能的記憶體不安全來源都不是你的錯。

這個「你就知道(you know)」是 Rust 真正交付的東西, 不是「記憶體錯誤從此消失」,而是「責任歸屬從此可判定」。 在 C 裡這個問句沒有對應的乾淨答案:使用端有沒有「用錯」是一個語意層級、要靠人去爭論的判斷, 傳 NULL 算不算用錯?傳一個剛 free 掉的指標算不算用錯? 這些問題在 C 裡沒有編譯器替你裁決,只能靠文件約定與事後爭吵。

所以兩種語言量的根本不是同一個東西。 Rust 量的是「safe code 路徑上有沒有記憶體錯誤」,這是一個有明確邊界的集合; C 量的是「程式有沒有崩潰」,但崩潰之後責任往哪歸,沒有語言層級的依據。 把這兩者的 CVE 數量直接相除,分子分母量的不是同一件事。

判準:是「特定誤用」開 CVE,還是「可被誤用」就開

這是整篇最精準的一刀。Kobzol 的原句是: 我們替 library 的特定「誤用」開 CVE,而不是替「一個可以被誤用的 API 之存在」開 CVE (we create CVEs for specific misuses of a library, not for the existence of a library API that can be misused)。 這句講的是 C/C++ 的世界。

換到 Rust,判準整個翻過來。 不再是「等到有人實際誤用、造成具體事故,才回頭認定那是不是漏洞」, 而是「只要存在任何一種不靠 unsafe 就能踩爆的用法,這個 API 本身就有缺陷」。 這是把判定時機從「事後、具體誤用發生時」前移到「API 設計階段、可能性存在時」。 一個 safe API 有沒有漏洞,不取決於有沒有人真的踩到,取決於那條路徑存不存在。

這個差別在實務上很尖。 在 C 裡,一個會在 NULL 輸入下 segfault 的函式,只要文件寫了「不要傳 NULL」, 它就被當成合理的 API;踩爆的人是沒讀文件的那個。 在 Rust 裡,如果一個 safe 函式接受某個輸入會造成記憶體錯誤, 「文件寫了不要這樣傳」並不能讓它免責—— safe 的承諾是型別系統層級的,不是文件層級的。 要嘛把那條路徑用型別封死(讓壞輸入根本無法被建構), 要嘛把函式標成 unsafe(把責任明確交還給使用端), 沒有「文件免責」這個中間選項。

把這條判準量化成一個直覺:在 C 的模型裡,CVE 的觸發條件是「具體誤用 ∧ 造成事故」; 在 Rust 的模型裡,觸發條件是「safe 路徑上存在記憶體錯誤的可能性」。 後者的條件寬鬆得多——它不需要有人真的踩到。 這正是為什麼同一個 library,照 Rust 的尺去量,會數出更多「這算 library 的 bug」的情況。 CVE 數量比較之所以誤導,根子就在這裡。

換個角度看,這條判準真正動到的是「舉證責任」落在誰頭上。 在 C/C++ 的模型裡,要把一次崩潰算成 library 的漏洞,是通報的人得舉證—— 得說明這個崩潰不是自己傳錯了值、不是自己違反了某條寫在文件裡的前置條件; 只要 library 能指出「你傳了 NULL,文件早寫了不要傳」,舉證責任就被推回呼叫端,CVE 不成立。 Rust 把這個方向整個倒過來:使用端有沒有 unsafe 是可被機械檢查的事實, 一旦確認沒有 unsafe,舉證責任就翻到 library 作者身上—— 要主張「這不是我的 bug」,library 得證明那條 safe 路徑根本不存在, 而不是反過來要使用端證明自己沒用錯。 「任何不靠 unsafe 就能觸發 UB 的路徑都是 library 的 bug」這句話之所以重, 就在於它把預設值從「呼叫端有罪推定」翻成「library 有罪推定」, 這一翻,整個生態系裡誰該在崩潰發生時去自證清白,全變了。

同一個崩潰:curl_getenv(NULL) 與假想中的 hyper

文章用一組具體對照把抽象落地。 C 那側是一個五行的小程式,呼叫 curl_getenv(NULL), 它編譯時不出任何警告(compiles without any warnings),執行起來卻 segfault。 Kobzol 說,這會被講成是 wrong usage—— 他自己補了一句推測:如果他把這個程式當成 curl 的漏洞去通報, curl 的維護者「會(理直氣壯地)把我罵回去」(rightfully tell me off)。 這句是作者的合理推測,不是引用維護者的原話。

Rust 那側是一個假想:fn main() { hyper::foo(None); }, 假設它同樣會 segfault。 Kobzol 的判定是——這「絕對」會構成 hyper 的一個 CVE(would absolutely constitute a CVE), 因為使用端那段程式裡沒有任何 unsafe 區塊。 同一個崩潰、同一種「傳了一個函式不該收的值」的誤用,兩種語言裡判定相反。

把兩段程式並排,差別不在語法多漂亮,而在「這段程式有沒有 unsafe」這個可被機械檢查的事實。 拖動下方分隔線,左側是 C 的 curl_getenv(NULL) 與它的「不算 CVE」判定, 右側是假想的 hyper::foo(None) 與它的「算 CVE」判定—— 同一條 unsafe 軸,把責任推向相反的兩端。

拖動分隔線比對同一個崩潰在 C 與 Rust 裡的相反判定 · 拖曳

// C:使用端 #include <curl/curl.h> int main(void) { curl_getenv(NULL); // 編譯無警告 return 0; // 執行 → segfault } // 整份 C 都 implicitly unsafe, // 沒有「不靠 unsafe」的基準線。 判定:不算 curl 的 CVE // 歸成「wrong usage」, // 維護者會理直氣壯把通報退回。
// Rust:使用端(假想) fn main() { hyper::foo(None); // 假設它 segfault } // 這段使用端沒有任何 unsafe, // 卻能不靠 unsafe 觸發記憶體錯誤。 判定:絕對算 hyper 的 CVE // safe API 必須真的 safe, // 這條路徑本身就是 library 的 bug。
C · curl Rust · hyper
左:C 的 curl_getenv(NULL) segfault 被歸成 wrong usage。右:假想的 hyper::foo(None) 同樣崩潰,因使用端無 unsafe,照 Kobzol 判準絕對是 hyper 的 CVE。hyper 一例為文章設計的假想對照,非真實已知漏洞。

左:C 的 curl_getenv(NULL) segfault 被歸成 wrong usage

同一個崩潰,C 歸成呼叫端用錯;Rust 因使用端無 unsafe 而絕對算 library CVE。

這組對照最容易被誤讀成「Rust 比較嚴格所以比較好」。 Kobzol 的論點其實更冷靜:兩邊不是嚴格程度的差,是分類方式的差。 curl 的維護者把 curl_getenv(NULL) 退回去,並不是不負責任—— 在一個全部 implicitly unsafe 的語言裡,那本來就是使用端要守的契約。 Rust 把同樣的契約搬進型別系統,於是維護者不能再用「文件寫了」來推責。 這是工具能力的差異帶來的責任分配差異,不是道德高下。

可這個不對稱對 C/C++ 的 CVE 帳本來說,代價是實打實的。 當一整類「傳了不該傳的值就踩爆」的崩潰被系統性地歸到呼叫端、不進 library 的 CVE 記錄, 這些隱患並沒有消失,只是從「library 的漏洞」帳上轉到了「使用端的錯」這個沒人統計的灰色地帶。 於是 C/C++ library 的 CVE 數字,天生就少算了一整類在 Rust 那邊會被計入的問題—— 不是因為 C/C++ 的程式真的更少出這種事,而是因為它的記帳規則根本不把這種事記成 library 的帳。 這也說明了 Kobzol 為何刻意不去做逐行 CVE 數量的對照: 兩本帳的入帳標準不一樣,加總出來的數字放在一起相除,得到的比值說明不了「哪種語言的記憶體更安全」, 只說明了「兩邊把同一類崩潰記在不同的人頭上」。 一個 C library 的 CVE 列表看起來乾淨,未必是它真的乾淨,也可能只是它的記帳規則把髒東西掃到了帳本之外。

沒被消掉的那一格:Rust 不保證的東西

這篇文章最值得抄下來的,反而是它對 Rust 的限縮。 它沒有說 Rust 消掉了所有漏洞,只說消掉了「safe code 路徑上的記憶體不安全」這一格。 其他幾格原封不動地留著。

第一格是邏輯漏洞。Kobzol 直接說, 在 Rust 裡一樣可以製造出一般性的漏洞(指那些與記憶體不安全無關的)。 他舉的例子很接地氣:忘了加一道「這個 admin dashboard 只有 admin 能存取」的檢查, 這種事在任何語言都會發生(can happen in any language, after all)。 型別系統管不到「你忘了寫一個權限檢查」,access control 的漏洞不會因為換 Rust 就消失。

第二格是 unsafe 區塊裡的 bug。Rust 的記憶體安全保證只覆蓋 safe code; 一旦你寫了 unsafe——而文章也說了,在絕大多數情況下你不需要寫, 除非你在做作業系統或 lock-free 資料結構這類東西—— 那塊區域的記憶體錯誤就回到你頭上。 Kobzol 的措辭是:在絕大多數情況下,要造成記憶體錯誤,unsafe 關鍵字是必要的 (the unsafe keyword is required for this to happen)。 反過來說,unsafe 區塊就是那少數例外,是記憶體 bug 仍能進來的門。

第三格是編譯器 bug。文章把這格講得很乾脆: 「你就知道任何記憶體不安全來源都不是你的錯」這個保證, 本質上只剩編譯器 bug 這個例外(essentially only except for compiler bugs)。 如果 rustc 本身在某個 codegen 路徑上產生了不健全的機器碼, 再怎麼 safe 的原始碼也擋不住——但這時責任在編譯器,不在你的程式,也不在 library。

把這三格擺出來,是為了精準,不是為了潑冷水。 Rust 的記憶體安全是一個邊界清楚的承諾:safe code、不撞編譯器 bug,那一格內記憶體錯誤不是你的錯。 出了那一格——邏輯、unsafe、編譯器——保證就不延伸過去。 懂得這個邊界,才不會把「Rust 安全」誤讀成「Rust 沒漏洞」。

把這四格畫在一張圖上會更清楚 Rust 的承諾邊界落在哪裡—— 只有左上那一格(safe code 裡的記憶體不安全)被消掉,另外三格仍是漏洞的來源。

RUST 消掉了什麼/沒消掉什麼 已消掉 safe code 裡的 記憶體不安全 不靠 unsafe 就踩爆=library 的 bug 仍在:邏輯漏洞 忘了加 admin 權限檢查, 任何語言都會發生 仍在:unsafe 裡的 bug 絕大多數情況下 unsafe 是 觸發記憶體錯誤的必要條件; 那塊區域責任回到你頭上 仍在:編譯器 bug 那句「不是你的錯」的保證, 本質上只剩這一個例外; rustc 自己出 codegen 問題
左上一格(綠)是 Rust 真正消掉的;其餘三格(赭)原封不動留著。「記憶體安全」這四個字覆蓋的範圍,只有那一格。

怎麼用這把尺:當你下次看到 CVE 數量比較

這篇文章對一個正在選語言、或正在替既有 C/C++ 專案評估要不要遷移的工程師, 給的不是一個結論,是一把校準過的尺。 它的收束句講得很直白: 拿 Rust 與 C 或 C++ 每行程式碼的 CVE 原始數量去比,是各種意義上的誤導 (all kinds of misleading), 在比較記憶體安全時應該把這件事算進去。

具體怎麼用這把尺。看到一份「Rust 的 CVE 數沒有顯著比較少」的報告, 先問一個問題:這些 CVE 裡,有多少是 Rust 因為「safe API 必須真的 safe」這條更寬的判準、 把 C/C++ 會歸給呼叫端的東西算成了 library bug? 分類標準不對齊,數字就不可比。 這不是要你忽略數字,是要你先把分子分母對齊再看。

把這把尺收進日常,最直接的場景是審一個依賴該不該信任。 審一個 Rust crate 時,這條判準替你把工作量砍掉一大塊: 只要你自己的使用端沒寫 unsafe,你不必去想「我是不是哪裡傳錯了值才害它踩爆」, 那條路徑若真會踩爆,照判準就是 crate 的 bug,責任不在你; 你要盯的,是那三格沒被消掉的東西——它有沒有邏輯漏洞、它內部的 unsafe 區塊寫得穩不穩、 以及極少數情況下的編譯器 bug。審查的注意力於是能精準地集中到這幾處。 審一個 C/C++ library 時,這份省力完全不存在: 沒有「我沒用 unsafe 所以不是我的錯」這條乾淨的基準線, 每一個接受指標或索引的 API,你都得自己讀文件、自己推敲前置條件、自己守住每一個呼叫點, 一旦哪裡傳了不該傳的值,責任預設就在你這邊,而且編譯器不會替你攔。 同一個「這個依賴安不安全」的問題,兩種語言要你付出的審查成本,根本不在同一個量級。

The call:兩種語言量的根本不是同一件事—— Rust 把「safe code 裡的記憶體不安全」整格消掉並讓責任變得可判定, 卻沒碰邏輯漏洞、unsafe 裡的 bug 與編譯器 bug; 所以下次看到逐行 CVE 數量的對照,先確認分類標準有沒有對齊,再決定那個數字說明了什麼。