vatt'ghern jaskier's ballads

Reboot 沒有發明一套 C-to-Rust 的型別翻譯規則,而是把「翻一個直譯器」這件事拆成一連串各自能跑、能測的里程碑,再讓一群本質上不可靠的 coding agent 在自動驗證與回饋迴圈裡接力。六個 6k 到 23k 行的 C 直譯器,每個只需要 1 到 11 次簡短的人工介入。

Reboot:把 C 直譯器幾乎全自動翻成 safe Rust

C 寫的直譯器整包翻成 safe Rust,難的從來不是逐行 syntax 對應,而是「一次翻完才知道對不對」這個結構。一份 2 萬行的 C 直譯器丟給 LLM,吐出 2 萬行 Rust,編不過、跑不對、或某個邊角語意悄悄走樣——你拿不到任何中間訊號可以定位錯在哪。Reboot(論文題為「Mostly Automatic Translation of Language Interpreters from C to Safe Rust」,作者 Wang、Paulsen、Dodds、Kroening、Mathur、Saxena)的核心主張是把這個 all-or-nothing 的翻譯,重寫成一條「每一步都是完整、可測試程式」的階梯。它把直譯器這類「處理不可信輸入、又長年受記憶體漏洞所苦」的程式當成最值得翻譯的目標——而 safe Rust 給的正是這類程式最該拿到的那個保證。

click a milestone to grow the supported feature set · 6 milestones M0–M5

每個里程碑=一個能跑、能過測試的完整子集直譯器 simplest full interpreter M0 M1 M2 M3 M4 M5 ✓ pass ✓ pass ✓ pass ✓ pass ✓ pass ✓ pass
M0:最簡版——從原始 C 直譯器砍到只剩骨架的可執行程式。先把這個翻成 Rust 並驗證通過,後面每一步都站在一個「已知會跑」的底座上。
M1:在 M0 之上恢復一個 feature。論文的講法是「starts from the simplest version and incrementally restores features」——增量回填,而非整包重來。
M2:再回填一個 feature。關鍵是「each milestone validated before proceeding」——這一階沒驗過,不准往下走。
M3:每個里程碑都是「a complete, testable program」。錯誤被夾在兩個相鄰里程碑之間,定位範圍小到人能介入。
M4:階梯越高,被翻譯的語言子集越大,但每一階的「翻譯增量」始終很小——這是讓不可靠 agent 能收斂的前提。
M5:完整直譯器。走到頂端時,所有 feature 都回填完畢,而中途每一階都留下一個能跑能測的版本當證據。

下面四個小節對應 Reboot 的四個結構元件:feature reduction 為什麼讓翻譯變得可測試、multi-agent 怎麼編排一個不可靠的 coding agent、六個直譯器實測到什麼程度、以及 mujs 案例裡記憶體安全具體消掉了什麼。順序是有意的——先講為什麼能拆,再講拆完誰來做,最後才談量到的結果與安全保證。

feature reduction:把翻譯拆成可測試的階梯

整包翻譯的根本問題是「沒有中間訊號」。LLM 把一份大型 C 程式翻成 Rust,過程中任何一個語意偏差——整數溢位的 wrap-around 行為、指標別名、某個 union 的 tag 判斷——都會被埋進兩萬行裡,而你唯一拿到的回饋是「整個跑不對」。除錯範圍等於整份程式,這對人或對 agent 都太大。

Feature reduction 的做法是把一份大程式沿著「語言 feature」這條軸切開。論文的定義是把翻譯分解成「a sequence of milestones where each is a complete, testable program」——一連串里程碑,每一個都是完整、可測試的程式。流程則是「starts from the simplest version and incrementally restores features, with each milestone validated before proceeding」:從最簡版開始,逐步把 feature 一個個加回去,而且「每個里程碑都先驗證過才往下走」。

這句話裡有兩個各自獨立的設計決策。第一個是方向:不是把完整程式硬切成幾塊各翻各的,而是先把原始 C 直譯器「reduce」到一個最簡可執行版本,再沿著 feature 軸把它「restore」回去。最簡版好翻、好測,是整條階梯的底座;每一階只多翻一個 feature,翻譯的增量永遠很小。第二個是閘門:「validated before proceeding」是硬性的——某一階沒通過驗證,流程就停在那裡,不會把錯誤帶進下一階。

這兩個決策合起來把除錯範圍從「整份程式」壓縮到「相鄰兩個里程碑之間的那一個 feature 增量」。如果 M3 過了、M4 沒過,那麼出問題的幾乎一定落在 M3 到 M4 之間新加的那個 feature——這個範圍小到一個人類工程師看一眼就能定位,也小到自動回饋迴圈有機會自己修好。上面的階梯互動小工具把這件事視覺化:點過 M0 到 M5,看被支援的 feature 集合一階一階長高,而每一階頂端的綠色 ✓ 始終在——因為「每一階都是能跑能測的完整程式」正是 feature reduction 的全部重點。

有一個容易被忽略的細節值得點出來:feature reduction 不只是把問題切小,它還改變了「驗證」這個動作的性質。在 all-or-nothing 翻譯裡,你能驗的只有「最終整包對不對」——一個布林值,錯了不知道錯在哪。在 feature reduction 裡,每一階都有一份對應那個子集的測試,於是驗證從「一個終點的判斷」變成「沿途每一站的判斷」。這正是讓自動化能接手的關鍵:自動回饋迴圈需要的是密集、局部、可機讀的訊號,而不是一個遲到又粗糙的最終判決。階梯把訊號的密度從「一份程式一次」拉高到「一份程式好幾十次」。

也要看清楚這個方法的代價落在哪裡。把直譯器 reduce 到最簡版、再規劃出一條合理的 feature 回填順序,這件事本身需要對「這個語言有哪些 feature、彼此的相依關係是什麼」有判斷——算術要先於變數、變數要先於控制流、控制流要先於函式,這條順序不是隨便排的,排錯了某一階會依賴還沒回填的 feature 而卡住。論文把整體定位成「mostly automatic」(題目就是這個詞)而非 fully automatic,那 1 到 11 次的人工介入,合理的推測是有一部分就花在這些「階梯怎麼搭」的判斷上,而不只是修個別翻譯錯誤。

值得停下來想清楚這跟「multi-agent 翻譯」本身的差別。Reboot 的 ablation 實驗就是針對這一點設計的:論文說 ablation「confirms that feature reduction improves translation correctness compared to using multi-agent translation alone」——也就是說,光有 multi-agent、沒有 feature reduction,正確率會掉。具體數字是 validation 測試通過率有「6%--20% improvements」。換句話說,把翻譯拆成可測試的階梯這件事,本身就值 6 到 20 個百分點,不是 multi-agent 架構附帶的免費效果。這個 ablation 的設計很乾淨:固定住 multi-agent 那一半,只切換 feature reduction 的有無,量出來的差距就純粹是「拆階梯」這個決策的貢獻。對讀者的意義是——如果你只是把一個 coding agent 接上 build/test 迴圈、卻不先把翻譯拆成里程碑,你拿到的會是論文裡那條較低的曲線。

multi-agent:在不可靠的 agent 外面套迴圈

有了階梯,還需要有人爬。Reboot 在這裡的設計前提非常誠實:coding agent 是「inherently unreliable」的——本質不可靠。論文沒有假裝 agent 會一次翻對,而是承認它會翻錯、會在長流程裡偏離,然後把整套系統的重點放在「怎麼在這種不可靠之上維持一條長流程」。

它的答案是「a multi-agent architecture orchestrates inherently unreliable coding agents through automated validation and feedback, keeping long-running translation workflows on track with minimal human involvement」。拆開來看:multi-agent 架構去編排這些不可靠的 coding agent,靠的是「automated validation and feedback」——自動驗證與回饋——把「long-running translation workflow」維持在軌道上,而且只需要極少的人工介入。feature reduction 提供了驗證的著力點(每個里程碑都可測),multi-agent 編排則把「翻譯 → 建置 → 測試 → 回饋」這個迴圈自動轉起來。

一個里程碑的內迴圈——失敗就回饋重試,通過才推進 coding agent 翻一個 feature 增量 build + test 里程碑測試套件 validation 通過? fail → automated feedback(編譯/測試錯誤回灌 prompt) 推進下一里程碑 restore next feature pass ✓ human(1–11 次/直譯器) 只在迴圈卡死時才呼叫 automated validation + feedback 把不可靠 agent 維持在長流程上;人工介入是例外,不是主迴圈
編排的關鍵在閉合的內迴圈:build/test 的錯誤訊息被自動回灌成下一輪 prompt,agent 在同一個里程碑內反覆嘗試直到 validation 通過,才推進到下一個 feature。人工介入(每個直譯器 1–11 次)只發生在迴圈自己出不來的時候。

這個迴圈為什麼能成立,回到 feature reduction 給的那個性質:每個里程碑都可測。build 失敗、測試失敗,都會產生具體、可被機器讀的錯誤訊息——編譯器報哪一行型別不對、哪個測試 case 的輸出對不上。這些訊息就是「feedback」的內容,被自動回灌給 coding agent 當下一輪嘗試的輸入。agent 不可靠沒關係,因為它每一輪的嘗試都被一個客觀的閘門檢查,錯了就帶著錯誤訊息再來一次,對了才被放行到下一個里程碑。

「multi-agent」這個詞在這裡不是行銷標籤,它對應的是一個具體的工程選擇:把一個大任務交給單一 agent 一路做到底,跟把它拆給多個各司其職、彼此用驗證結果溝通的 agent,是兩種不同的可靠性模型。後者的好處正是論文強調的「keeping long-running translation workflows on track」——長流程不會因為單一 agent 在中途漂掉而整條失控。每個里程碑是一個可以重來的工作單元,agent 在這個單元裡反覆嘗試的成本是局部的;就算某一輪整個翻爛了,回到的也只是「這個里程碑的上一個已知良好狀態」,而不是把幾千行 Rust 全部作廢。

「minimal human involvement」這個說法在實測裡有具體數字撐著:每個直譯器只需要「1 to 11 brief user interventions」——1 到 11 次簡短的人工介入。合理的推測是,這些介入發生在自動迴圈自己鑽不出來的地方(某個 feature 的語意 agent 反覆翻錯、或某個 C 慣用法沒有乾淨的 safe Rust 對應),人類給一個提示把它推過去,迴圈再自己接手。論文用 brief 形容這些介入,本身就是在強調它們是短的、點狀的,不是「人坐在旁邊全程盯著翻」。1 到 11 這個區間本身也有資訊量:它不是「平均 0.3 次」那種近乎全自動的數字,也不是「幾百次」那種其實是人在主導的數字,而是落在「一個工程師花一個下午、在幾個卡點上各推一把」的量級——這恰好是 mostly automatic 該有的樣子。

把這個迴圈跟傳統的 C-to-Rust 自動工具(例如 c2rust 這類做機械式轉譯的)對照,差別就清楚了:機械式轉譯通常產出的是「能編譯但滿是 unsafe、跟原 C 一樣危險」的 Rust,因為它逐句保留了 C 的指標語意。Reboot 的目標不是「轉成 Rust」而是「轉成 safe Rust」——要拿掉 unsafe,就得真正理解每段程式在做什麼、再用 Rust 的所有權模型重新表達,這是 LLM 比語法轉譯器有優勢的地方,也正是為什麼需要那層驗證迴圈來壓住 LLM 的不可靠。語法轉譯器不會翻錯(它只做機械對應),但也給不了 safety;LLM 能給 safety,代價是會翻錯,於是驗證迴圈成了把這個交易做成的必要條件。

六個直譯器:測得多乾淨,缺口在哪

Reboot 在六個直譯器上做了實測,規模從 6k 到 23k 行 C 不等。論文給的兩個通過率數字要分開看,因為它們衡量的是不同的東西。第一個:所有翻譯都「pass 100% of the provided test suites」——通過原本附帶的測試套件全部。第二個:在「separately created validation tests」上,通過率落在「62%--92%」。

先把實測的三個區間擺在同一張圖上看尺度。論文只給了區間的上下界(並沒有逐一公布六個直譯器各自的點),所以下圖畫的是「六個直譯器全部落在這個範圍內」的包絡,而不是六個確切座標:程式規模 6k 到 23k 行、人工介入 1 到 11 次、validation 通過率 62 到 92%。三條軸放在一起,最該被讀出來的是它們的比例——介入次數始終是個位數到十位數出頭,相對於上萬行的 C 程式,幾乎可以忽略。

六個直譯器的實測包絡——上下界取自論文,非逐一座標 C 程式規模 6k 23k 人工介入 1 11 validation 通過率 62% 92% 100 介入次數相對於上萬行 C 程式幾乎可忽略——這正是 mostly automatic 的具體形狀
三條軸都是論文公布的上下界,不是六個直譯器的逐一資料點。前兩條軸刻意共用 0 到 25 的刻度,讓「上萬行程式」對上「個位數介入」的比例一眼可見;第三條是百分比軸,右端 100% 為參考。

這兩個數字之間的落差,正是這篇論文最該被認真看待的地方。「provided test suites」是直譯器原本就帶著的測試——翻譯出來的 Rust 全部通過,意味著「原作者覺得該測的東西,翻譯版都對」。但「separately created validation tests」是另外造的一組測試,刻意去測那些原本測試套件沒覆蓋到的角落。在這組上掉到 62% 到 92%,說的是:翻譯在「原測試覆蓋的範圍內」是可靠的,但一旦走出那個範圍,仍有 8% 到 38% 的 case 會露餡。

toggle the ablation baseline to see what feature reduction adds · two test suites

0 25 50 75 100 pass rate (%) 100% provided test suite 62–92% 62 92 validation separately created ablation:低 6–20 個百分點
六個直譯器全數通過原附測試套件(100%);另造的 validation 測試落在 62–92%——這道落差就是「原測試沒覆蓋到的語意角落」。

把這個缺口跟階梯接起來看就完整了:feature reduction 讓每一階都被「該里程碑的測試」驗過,但驗證的嚴格程度天花板就是那組測試本身。原測試套件覆蓋到的,翻譯版 100% 對;原測試沒覆蓋到、要靠另造的 validation test 才碰得到的角落,就還有 62% 到 92% 這段不確定。這不是 feature reduction 的失敗,而是它把問題誠實地暴露出來——翻譯的正確性上限,等於你拿來驗它的測試的覆蓋率。上面圖表的 ablation 對照可以疊上去看:拿掉 feature reduction,同一組 validation 測試的通過率還要再低 6 到 20 個百分點,這正是論文 ablation 的結論。

兩個數字分開報,這件事本身也是論文誠實的地方。如果只報「100% 通過測試套件」,讀者會以為翻譯是完美的;如果只報「62% 到 92%」,又會低估它在原測試範圍內的可靠程度。同時報這兩個、並且把後者建立在「另外造的、刻意去戳角落的」測試上,等於明說了「我們的方法能保證什麼、保證到哪裡為止」。62% 到 92% 這個區間的下緣特別值得留意——某個直譯器只有 62% 的 validation 通過率,意味著它有將近四成的角落 case 行為跟原版不同。哪個直譯器落在下緣、為什麼,是讀者真正想追問的,但這需要論文正文的逐一拆解,超出摘要能給的範圍。

對一個要在下週決定「該不該把某個 C 直譯器搬成 Rust」的工程師來說,這個缺口比那個漂亮的 100% 更有資訊量。它告訴你:自動翻譯能把你帶到「原測試全過」的程度幾乎不費人力,但要把最後那 8% 到 38% 的角落補起來,仍要靠你自己擴充驗證——而 feature reduction 至少把這件事變成「在哪個里程碑、哪個 feature 上補測試」的局部問題,而不是對著兩萬行 Rust 大海撈針。也就是說,這個方法給你的不只是一份翻譯,還是一份「按 feature 切好、每段都附帶測試掛點」的翻譯——後續要補強哪裡,你有現成的座標系。

mujs:safe Rust 消掉的那幾類漏洞

前面三節都在講「翻得對不對」,最後一節講「翻成 Rust 換到了什麼」。Reboot 對 mujs 做了一個 security case study,結論很直接:C 版本裡存在的記憶體漏洞——論文點名「heap buffer overflows and use-after-free」——在 safe Rust 翻譯版裡被消除了。論文原句是「memory vulnerabilities such as heap buffer overflows and use-after-free present in C are eliminated in the safe Rust translation」。

mujs case study:論文點名的兩類記憶體漏洞,在 C 與 safe Rust 翻譯版的狀態對照。漏洞類別與「eliminated」的結論取自論文;機制欄是這兩類漏洞在 safe Rust 下為何不再可能的標準解釋。
漏洞類別 C 版本 safe Rust 翻譯版 機制
heap buffer overflow present eliminated safe Rust 的陣列/slice 存取帶 bounds check,越界存取在 debug 是 panic、在 release 仍不會讀寫到相鄰 heap 配置,無法被當成可控的記憶體破壞原語。
use-after-free present eliminated 所有權與借用檢查在編譯期保證引用不會比它指向的配置活得更久;safe Rust 沒有手動 free,不存在「free 後仍持有指標再解參考」這條路徑。

為什麼是直譯器、為什麼是這兩類漏洞,論文在動機裡講得很清楚:直譯器是「particularly important targets for such translation, as they often handle untrusted inputs and suffer from memory-related vulnerabilities」——它們特別值得翻譯,因為它們經常處理不可信輸入、又長年受記憶體相關漏洞所苦。把這句話拆開:一個 JavaScript 直譯器像 mujs,它的輸入就是別人的 script,本質上是 attacker-controlled。在這種程式裡,一個 heap buffer overflow 或 use-after-free 不是普通 bug,而是直接可被惡意輸入觸發、用來讀寫程序記憶體的攻擊面。

safe Rust 把這兩類漏洞從「可能發生」變成「型別系統不允許」。heap buffer overflow 對應的是越界存取——safe Rust 的 slice/陣列存取帶 bounds check,越界不會悄悄踩進相鄰的 heap 配置。use-after-free 對應的是引用活得比配置久——所有權與借用檢查在編譯期就擋掉這種情況,safe Rust 裡也沒有手動 free 這個動作。這不是「比較不容易出錯」,而是這兩條攻擊路徑在 safe 子集裡根本不存在。對一個整天吃不可信輸入的直譯器,這正是它最該拿到、而 C 給不了的那個保證。

這也是為什麼「翻成 safe Rust」跟「翻成 Rust」是天差地別的兩件事,前面提過的差別在 mujs 上有了具體後果。如果翻譯出來的是一堆 unsafe Rust,那它跟原本的 C 一樣可以越界、可以解參考懸空指標,case study 就沒有東西可講。Reboot 的目標明確是 safe 子集——把整個程式約束在「編譯器有能力檢查記憶體安全」的範圍內,這個約束才是消除漏洞的來源。換句話說,消掉 heap buffer overflow 與 use-after-free 的不是某個聰明的修補,而是「整段程式都活在 safe Rust 的規則裡」這個前提;只要 mujs 的翻譯版守住這個前提,這兩類漏洞就沒有立足之地。

從攻防的角度再看一次這件事的份量。一個能被惡意 script 觸發的 heap buffer overflow,在 C 的世界裡往往是一條完整 exploit chain 的起點——先用越界寫改掉某個函式指標或物件 metadata,再導向任意程式碼執行。use-after-free 同理,攻擊者誘導程式 free 掉某個物件、再讓它被重新解參考,就拿到了一個可控的型別混淆原語。這些都是真實世界裡 JavaScript 引擎 CVE 的常見形狀。把直譯器搬進 safe Rust,等於是在語言層把這整類起點抽掉——不是讓 exploit 變難,而是讓它賴以起步的那個記憶體破壞原語在編譯期就不可能成立。對一個其輸入完全由攻擊者掌控的元件來說,這個層級的保證是值得拿翻譯成本去換的。

需要誠實標記一個邊界:論文的 case study 是 mujs 這一個直譯器,講的是「heap buffer overflow 與 use-after-free 被消除」,這是 safe Rust 子集本身的語意保證。它沒有、也不宣稱翻譯版「完全沒有任何 bug」——前面那個 62% 到 92% 的 validation 缺口提醒我們,語意正確性跟記憶體安全是兩件事。翻譯版可能在某個語意角落跟原版行為不同(那是 validation 在抓的),但同一個翻譯版,在記憶體安全這個維度上,把 C 版本那兩類最危險的漏洞拿掉了。這兩件事不衝突,而且剛好說明了 Reboot 想證的東西:自動翻譯可以同時逼近語意等價、又把安全保證從「靠人小心」升級成「靠型別系統」。

What this enables:把「翻一個直譯器」拆成可測試的里程碑階梯,再用 multi-agent 在自動驗證與回饋上編排不可靠的 coding agent,Reboot 讓 6k 到 23k 行的 C 直譯器幾乎全自動搬進 safe Rust——每個只要 1 到 11 次簡短人工介入,原測試全過、validation 到 62~92%,而 mujs 那兩類最該死的記憶體漏洞,在型別系統那一層就被消掉了。