深入 Rust

unsafe Rust 與健全性契約(soundness contract)

有時借用檢查器那些必然保守的規則,會駁回其實正確的程式碼——與硬體對話、呼叫 C 函式庫、實作一個需要對同一緩衝區有兩個指標的快速資料結構。安全 Rust 會讓這些變得不可能。所以 Rust 提供一個逃生口,unsafe 關鍵字,它不是把檢查到處關掉;它在一個標記的區域內解鎖一小組特定的額外能力,並請「你」去維護編譯器再也無法驗證的那些規則。unsafe 不是「沒有安全的 Rust」;它是「Rust,但你接下了檢查器交還給你的幾項義務」。

具體來說,一個 unsafe 區塊(或 unsafe fn)恰好解鎖五件事、不多不少:解參考一個原始指標(*const T 或 *mut T)、呼叫一個 unsafe 函式或一個外部(FFI)函式、存取或修改一個可變 static、讀取一個聯合(union)的欄位、以及實作一個 unsafe 特徵(像親手實作 Send 或 Sync)。這就是全部清單。關鍵在於,在一個 unsafe 區塊內,平常的借用檢查器、型別檢查器、與其他所有安全分析,仍和先前一模一樣地運作——unsafe 不讓你可變別名、忽略生命週期、或略過型別檢查。它只授予那五個操作,而每一個都有一個編譯器無法檢查的前置條件。健全性契約就是這筆交易:你可以使用這些能力,但你必須保證這麼做永遠不造成未定義行為——沒有釋放後使用、沒有越界存取、不對同一份資料造出兩個 &mut、沒有資料競爭。若你守住這個承諾,你的 unsafe 程式碼就是「健全的」,而透過一個安全介面呼叫它的程式碼,無論做什麼都永遠不會觸發 UB。若你違背它,你就有了未定義行為,連同 C 那一切無聲損毀的後果。

為何重要:unsafe 正是 Rust 的安全抽象被建構出來的方式——Vec、Mutex、Rc、與標準函式庫內部滿是經審核的 unsafe,包在安全 API 裡,讓你不必親寫 unsafe 就能使用。最誠實、也最常被過度推銷的說法是:Rust 的安全保證涵蓋的是「安全」子集,而它之所以成立,正因為每個 unsafe 區塊都守住了它那一端的契約。一個不健全的 unsafe 區塊就能重新引入正是 Rust 承諾要防止的那些錯誤,包括那些會損毀遠處安全程式碼所用記憶體的錯誤。所以「Rust 是記憶體安全的」既為真又重要,但它的意思是「安全 Rust 不會造成 UB,『且』底下的 unsafe 區塊被寫成維護了契約」——絕非「Rust 沒有未定義行為」。盡量少寫 unsafe,讓每個區塊都小而經審查,並用 Miri 檢查它。

// unsafe 解鎖原始指標解參考;「你」必須確保它指向有效資料。 let x = 5; let p: *const i32 = &x; // 造出一個原始指標是「安全」的 let v = unsafe { *p }; // 解參考它是「不安全」的:你承諾 p 有效 println!("{v}"); // 5 // 契約:若 `p` 是懸置的,*p 就是未定義行為—— // 不保證當機,可能無聲損毀,正如 C 一樣。

unsafe 授予五項特定能力;作為回報你承諾沒有 UB。違背那承諾,你就在一個 Rust 程式裡得到 C 式的無聲損毀。

unsafe 不會關掉借用檢查器;它解鎖五個編譯器無法驗證的操作,並把它們的前置條件交給你。Rust 的安全主張之所以成立,全因每個 unsafe 區塊都維護了健全性契約——一個不健全的區塊就能重新引入正是 Rust 防止的那種 UB。

又称
unsafe blockunsafe fnthe unsafe keywordsoundness不安全區塊健全性