| name | rust-unsafe-guide |
| description | Rust unsafe 使用指南技能。涵蓋 unsafe 區塊五種超能力(raw pointer 解引用、 呼叫 unsafe 函式、存取 mutable static、實作 unsafe trait、存取 union 欄位)、 安全封裝策略(safe abstraction over unsafe)、FFI unsafe 模式、 Miri 驗證流程、unsafe 程式碼審查準則、常見 UB(未定義行為)清單、 Pin 與自引用結構、raw pointer 算術、transmute 使用場景與風險。 觸發關鍵詞:unsafe, raw pointer, Miri, undefined behavior, UB, safe abstraction, transmute, Pin, self-referential, unsafe trait, union, mutable static, deref raw pointer
|
Rust Unsafe 使用指南
適用場景
- 需要與 C/C++ 函式庫透過 FFI 互操作
- 實作底層資料結構(自訂分配器、侵入式鏈結串列、無鎖佇列)
- 效能關鍵路徑中需跳過邊界檢查或避免不必要的初始化
- 實作
Send、Sync 等 unsafe trait
- 處理 raw pointer 運算(嵌入式、核心開發)
- 使用
Pin 建構自引用結構(async runtime 內部)
- 需要
transmute 做型別轉換(謹慎場景)
- 對 unsafe 程式碼進行 Miri 驗證與審查
核心知識
unsafe 五種超能力
| # | 能力 | 語法 | 風險等級 | 典型場景 |
|---|
| 1 | 解引用 raw pointer | *const T / *mut T | 高 | FFI、自訂集合 |
| 2 | 呼叫 unsafe 函式/方法 | unsafe fn / extern "C" | 中~高 | FFI、intrinsics |
| 3 | 存取/修改 mutable static | static mut | 高 | 全域狀態(盡量避免) |
| 4 | 實作 unsafe trait | unsafe impl Send for T | 中 | 跨執行緒型別保證 |
| 5 | 存取 union 欄位 | union.field | 中 | C 互操作、型別穿透 |
安全封裝原則
1. 最小化 unsafe scope
fn get_unchecked(slice: &[u8], idx: usize) -> u8 {
unsafe { *slice.get_unchecked(idx) }
}
unsafe fn get_value(slice: &[u8], idx: usize) -> u8 {
*slice.get_unchecked(idx)
}
2. invariant 文件化
每個 unsafe 區塊前必須加上 // SAFETY: 註解,說明:
- 為何此操作是安全的
- 呼叫端必須滿足的前置條件
- 哪些 invariant 正在被維護
3. 封裝為安全 API
pub struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> MyVec<T> {
pub fn get(&self, idx: usize) -> Option<&T> {
if idx < self.len {
Some(unsafe { &*self.ptr.add(idx) })
} else {
None
}
}
}
常見 UB(未定義行為)清單
| # | UB 類型 | 說明 | 觸發方式 | 預防方法 |
|---|
| 1 | Dangling pointer deref | 解引用已釋放記憶體的指標 | use-after-free | 借用檢查器 + lifetime |
| 2 | Data race | 多執行緒無同步存取 mutable data | static mut 併發 | Mutex/AtomicXxx |
| 3 | Invalid enum discriminant | enum 含無效的判別值 | transmute 亂轉 | 驗證再轉換 |
| 4 | Unaligned access | 在未對齊地址解引用型別指標 | packed struct + & | read_unaligned |
| 5 | Double free | 同一塊記憶體釋放兩次 | 手動 dealloc 流程錯誤 | RAII + ManuallyDrop |
| 6 | Null pointer deref | 解引用空指標 | FFI 回傳 null | NonNull / 檢查 |
| 7 | Out-of-bounds access | 超出配置範圍的記憶體存取 | 指標算術錯誤 | 邊界檢查 |
| 8 | Type punning 違規 | 違反嚴格別名規則 | transmute 不相容型別 | bytemuck crate |
| 9 | Uninitialized memory read | 讀取未初始化記憶體 | MaybeUninit 誤用 | assume_init 前確認 |
| 10 | Stack overflow via recursion | 無限遞迴或巨大分配 | 不受控遞迴 | 迭代取代遞迴 |
Miri 使用方法
Miri 是 Rust 的實驗性直譯器,可偵測 unsafe 程式碼中的 UB。
安裝與執行
rustup +nightly component add miri
cargo +nightly miri test
cargo +nightly miri run
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test
Miri 可偵測的問題
- 記憶體洩漏(需啟用
-Zmiri-leak-check)
- 越界存取
- Use-after-free
- 未初始化記憶體讀取
- 資料競爭(多執行緒場景)
- 違反 Stacked Borrows / Tree Borrows 規則
- 未對齊存取
Miri 的限制
- 無法執行實際系統呼叫(如檔案 I/O 的模擬有限)
- 執行速度比原生慢 10~100 倍
- 不支援所有 FFI(外部 C 函式需 shim)
- 不保證找到所有 UB(僅測試到的路徑)
transmute 使用場景與風險
let bytes: [u8; 4] = unsafe { std::mem::transmute(42u32) };
替代方案優先順序:
as 轉換(基本型別)
From/Into trait
bytemuck::cast / zerocopy(安全的位元轉換)
transmute 作為最後手段
Pin 與自引用結構
自引用結構在 Rust 中需要 Pin 保證記憶體位置不變:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
value: String,
ptr_to_value: *const String,
_pin: PhantomPinned,
}
impl SelfRef {
fn new(val: &str) -> Pin<Box<Self>> {
let s = SelfRef {
value: val.to_string(),
ptr_to_value: std::ptr::null(),
_pin: PhantomPinned,
};
let mut boxed = Box::pin(s);
let self_ptr: *const String = &boxed.value;
unsafe {
let mut_ref = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(mut_ref).ptr_to_value = self_ptr;
}
boxed
}
fn get_value_via_ptr(&self) -> &str {
unsafe { &*self.ptr_to_value }
}
}
程式碼範例
Basic: raw pointer 操作 + safe wrapper
Intermediate: 自訂 Vec-like 結構
Advanced: Pin + self-referential + Miri 可驗證
常見錯誤對照表
| 錯誤訊息 / 症狀 | 原因 | 修復方式 |
|---|
SIGSEGV / access violation (dangling pointer) | 解引用已釋放的 raw pointer | 確保 pointer 的 lifetime 不超過其指向的資料;使用 NonNull 或借用 |
| Miri: "data race detected" | 多執行緒無同步存取 static mut | 改用 AtomicXxx 或 Mutex;避免 static mut |
| Miri: "invalid enum discriminant" | transmute 將無效位元組轉為 enum | 先驗證數值再用 match 構建 enum;使用 num_enum crate |
| Miri: "accessing unaligned memory" | 對 #[repr(packed)] struct 取欄位參考 | 使用 std::ptr::read_unaligned 或 addr_of! 巨集 |
| double free / Miri: "use after free" | 同一記憶體手動釋放兩次 | 使用 ManuallyDrop 控制 drop 時機;確保 dealloc 僅呼叫一次 |
| Miri: "reading uninitialized memory" | 讀取 MaybeUninit 但未初始化 | 確保所有欄位已寫入後才呼叫 assume_init() |
error[E0133]: use of unsafe ... requires unsafe block | 在 safe context 呼叫 unsafe 函式 | 包裹在 unsafe { } 區塊中並加 SAFETY 註解 |
| Miri: "Stacked Borrows violation" | 違反指標別名規則(例如同時持有 & 和 *mut) | 審查借用模式;考慮使用 UnsafeCell |
| SIGBUS (unaligned access on ARM) | 在嚴格對齊架構上存取未對齊記憶體 | 使用 read_unaligned / write_unaligned |
記憶體洩漏(Miri -Zmiri-leak-check) | 手動分配的記憶體未釋放 | 實作 Drop trait 確保釋放;使用 RAII 模式 |
Cargo.toml 依賴模板
[package]
name = "unsafe-examples"
version = "0.1.0"
edition = "2024"
[dev-dependencies]
工具安裝:
rustup +nightly component add miri
cargo install cargo-careful
unsafe 程式碼審查準則
- 每個
unsafe 區塊是否有 // SAFETY: 註解? — 必須說明為何操作安全
- unsafe scope 是否最小化? — 只包裹必要的 unsafe 操作
- 是否對外暴露安全 API? — 內部 unsafe + 外部安全檢查
- 是否通過 Miri 測試? —
cargo +nightly miri test 無警告
- 是否有替代的 safe 方案? — 優先使用安全抽象
- FFI 指標是否驗證非 null? — 使用
NonNull 或明確檢查
transmute 是否有更安全的替代? — 優先 as、From、bytemuck
- 是否啟用
unsafe_op_in_unsafe_fn lint? — 強制 unsafe fn 內部也要 unsafe block
參考來源