↓推薦關注↓
Rust 因其強大的安全效能而備受青睞,尤其是在記憶體安全和執行緒安全方面。然而,這是否意味著只要使用 Rust,就一定能避免編寫出不安全的程式碼呢?事實並非如此。在某些場景下,開發者不得不使用 unsafe Rust 來完成任務,這也帶來了潛在的安全隱患。那麼,如何在這些不可避免的情況下,最大程度地降低風險,確保程式碼的可靠性呢?
特斯拉工程師 Colin Breck 針對此問題撰文,總結了三種有效的實踐方法,希望能對開發者有所裨益。
原文:https://blog.colinbreck.com/making-unsafe-rust-a-little-safer-tools-for-verifying-unsafe-code/
作者 | Colin Breck 責編 | 蘇宓
出品 | CSDN(ID:CSDNnews)
Rust 之所以能成為一種流行的系統程式語言,其中一個原因是它具有出色的效能,同時可以在編譯時消除記憶體和併發錯誤,而這些錯誤在其他具有類似效能特性的語言(如 C 和 C++)中很難避免。不過,開發者可以透過編寫 unsafe Rust 程式碼來繞過這些編譯時檢查。儘管絕大多數程式設計師不應編寫不安全的 Rust 程式碼,但一些庫出於效能需求、以及直接操作記憶體或硬體,或者與其他庫和系統呼叫整合的目的,使用了不安全的 Rust 程式碼。
接下來,本文將探討驗證不安全 Rust 程式碼的工具,包括從 C 或 C++ 編寫的庫中呼叫的不安全程式碼。現下我想要深入這一主題,主要目的也是想為運營技術(OT)和關鍵基礎設施編寫安全且可靠的軟體。

記憶體檢測工具 Sanitizers
Sanitizers 是一種執行時工具,專門用來檢測程式執行中的問題,比如記憶體損壞、記憶體洩漏或執行緒之間的資料競爭。它的工作原理是在編譯程式碼時自動插入檢查機制,幫助驗證程式的行為是否正常。在使用 Sanitizers 時雖然它會引入內存和效能開銷,但通常僅用於測試環境中。重要的是,與編譯器不同,Sanitizer 只能檢測在執行時實際被執行的程式碼路徑中的錯誤——這可以透過測試或直接執行程式來實現。
當我第一次得知 Rust 支援用於查詢錯誤的 Sanitizers 時,我感到很驚訝。因為過往,我比較熟悉如何在 C 和 C++ 中透過 Clang 和 LLVM 編譯器使用 Sanitizers。由於 Rust 的編譯器 rustc 也是基於 LLVM 構建的,它同樣可以使用這些 Sanitizers。
記憶體越界訪問/緩衝區溢位
看一下下面的程式:
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
xs[i as usize]
}
fn main() {
let v = bad_address(4);
println!("Value at offset: {}", v);
}
當我使用 RUST_BACKTRACE=1 cargo run –release 執行程式時,Rust 的邊界檢查檢測到了錯誤,程式會 panic(崩潰):
thread 'main' panicked at src/main.rs:3:5:
index out of bounds: the len is 4 but the index is 4
stack backtrace:
0: _rust_begin_unwind
1: core::panicking::panic_fmt
2: core::panicking::panic_bounds_check
3: sanitizers::main
程式被終止,這種情況可能是開發者極不願看到甚至是難以接受的,尤其當該軟體對關鍵基礎設施的執行至關重要時,可能會引發其他安全問題。然而,執行時檢查可確保程式永遠不會執行導致未定義行為的不安全程式碼。
現在考慮一種情況——如果該函式在一個 unsafe 程式碼塊中使用指標索引陣列會發生什麼:
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
unsafe { *xs.as_ptr().offset(i as isize) }
}
fn main() {
let v = bad_address(4);
println!("Value at offset: {}", v);
}
在不安全程式碼中,Rust 編譯器不再提供記憶體和執行緒安全的保障。程式設計師需要自己確保不安全程式碼是符合規則的,並且不會導致未定義行為。當我執行這段程式碼時,即使程式讀取了陣列邊界外的記憶體,也不會觸發 panic。
Value at offset: 24576
Rust 的 AddressSanitizer 可以幫忙檢查程式碼中對堆疊和堆的越界訪問。它的原理是,AddressSanitizer 透過在記憶體分配之間插入一些“紅區”(red-zones),這些區域不能被訪問,同時使用影子記憶體(shadow memory)追蹤記憶體是否被非法讀寫。如果程式訪問了不該碰的記憶體,AddressSanitizer 就會報錯。需要注意的是,這個工具只能在 Rust 的 nightly 版本中使用,不能用在穩定版上。但別擔心,nightly 和穩定版工具鏈可以同時安裝,不會互相影響。要安裝 nightly 工具鏈,你可以這樣操作:
rustup install nightly
然後啟動 AddressSanitizer 執行程式:
export RUSTFLAGS=-Zsanitizer=address
cargo +nightly run
程式會因為越界訪問而崩潰,並生成詳細的錯誤報告:
=================================================================
==96148==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016dce67b0 at pc 0x00010211bf70 bp 0x00016dce6770 sp 0x00016dce6768
READ of size 4 at 0x00016dce67b0 thread T0
#0 0x00010211bf6c in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb array_out_of_bounds_unsafe.rs:3
#1 0x00010211c170 in array_out_of_bounds_unsafe::main::hc84cbff8319e0a2b array_out_of_bounds_unsafe.rs:7
#2 0x00010211bd40 in core::ops::function::FnOnce::call_once::hc75a52fb9134d583 function.rs:250
#3 0x00010211bd8c in std::sys::backtrace::__rust_begin_short_backtrace::h9c09c1d17c8393c3 backtrace.rs:152
#4 0x00010211b888 in std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h3a3a442dfff79e34 rt.rs:195
#5 0x000102135230 in std::rt::lang_start_internal::hc996363c321dd410+0x440 (array_out_of_bounds_unsafe:arm64+0x10001d230)
#6 0x00010211b6c0 in std::rt::lang_start::hae3ff67dcefd99eb rt.rs:194
#7 0x00010211c2e0 in main+0x20 (array_out_of_bounds_unsafe:arm64+0x1000042e0)
#8 0x00019d87e0dc ()
#9 0xf4687ffffffffffc ()
Address 0x00016dce67b0 is located in stack of thread T0 at offset 48 in frame
#0 0x00010211bdbc in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb array_out_of_bounds_unsafe.rs:1
This frame has 1 object(s):
[32, 48) 'xs' (line 2) <== Memory access at offset 48 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow array_out_of_bounds_unsafe.rs:3 in array_out_of_bounds_unsafe::bad_address::h9a9dae85f9ad5feb
Shadow bytes around the buggy address:
0x00016dce6500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x00016dce6780: f1 f1 f1 f1 00 00[f3]f3 00 00 00 00 00 00 00 00
0x00016dce6800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6880: 00 00 00 00 f1 f1 f1 f1 f8 f8 f2 f2 f8 f8 f8 f8
0x00016dce6900: f8 f8 f2 f2 f2 f2 04 f3 00 00 00 00 00 00 00 00
0x00016dce6980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016dce6a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==96148==ABORTING
上述這一示例是在 debug 模式下執行的。如果在 release 模式下執行,由於編譯器最佳化,可能無法識別到該錯誤。因此,在 release 構建中使用 Sanitizer 時,務必停用編譯器最佳化:
export RUSTFLAGS="-C opt-level=0 -Zsanitizer=address"
cargo +nightly run --release
值得一提的是,AddressSanitizer 不是每次都能發現記憶體越界的問題。在前面的例子中,程式的表現取決於我訪問陣列時用的索引值:程式可能正常執行,也可能因為訪問了未知地址而報 SEGV 錯誤,或者因為堆疊溢位直接崩潰。
資料競爭
為了完整討論 Sanitizer(檢測工具),我還想分享另一個示例,討論一下在不安全 Rust 程式碼中出現的錯誤可以透過 Sanitizer 檢測到的方法。再來看一下以下程式碼,該程式碼從不同執行緒中的不安全程式碼訪問共享的可變變數:
fn main() {
static mut A: usize = 0;
let t = std::thread::spawn(|| {
unsafe { A += 1 };
});
unsafe { A += 1 };
t.join().unwrap();
}
正常執行此程式不會產生執行時錯誤,但當啟用 ThreadSanitizer 執行時,它會出現這種情況:
export RUSTFLAGS=-Zsanitizer=thread
cargo +nightly run
它將檢測到資料競爭並生成詳細的報告:
==================
WARNING: ThreadSanitizer: data race (pid=12331)
Read of size 8 at 0x000104f40460 by thread T1:
#0 sanitizers::main::_$u7b$$u7b$closure$u7d$$u7d$::h77c6a8d926b4ffd9 main.rs:5 (sanitizers:arm64+0x10000ae68)
#1 std::sys::backtrace::__rust_begin_short_backtrace::h3d7723e74dc43907 backtrace.rs:152 (sanitizers:arm64+0x100008f6c)
#2 std::thread::Builder::spawn_unchecked_::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h972cca723fc4d46a mod.rs:561 (sanitizers:arm64+0x1000033a4)
#3 _$LT$core..panic..unwind_safe..AssertUnwindSafe$LT$F$GT$$u20$as$u20$core..ops..function..FnOnce$LT$$LP$$RP$$GT$$GT$::call_once::h339263acc4287c3b unwind_safe.rs:272 (sanitizers:arm64+0x100004e64)
#4 std::panicking::try::do_call::h1720a438c6154692 panicking.rs:573 (sanitizers:arm64+0x1000090b8)
#5 __rust_try (sanitizers:arm64+0x10000351c)
#6 std::thread::Builder::spawn_unchecked_::_$u7b$$u7b$closure$u7d$$u7d$::h4a9e1cb91e992611 mod.rs:559 (sanitizers:arm64+0x100002cf0)
#7 core::ops::function::FnOnce::call_once$u7b$$u7b$vtable.shim$u7d$$u7d$::h4d54278169623269 function.rs:250 (sanitizers:arm64+0x100005254)
#8 std::sys::pal::unix::thread::Thread::new::thread_start::h5efa5b2bb0838bc2 (sanitizers:arm64+0x10002acd4)
Previous write of size 8 at 0x000104f40460 by main thread:
#0 sanitizers::main::he9b6ca8696085c08 main.rs:7 (sanitizers:arm64+0x100004b9c)
#1 core::ops::function::FnOnce::call_once::hba41c0d640901898 function.rs:250 (sanitizers:arm64+0x10000540c)
#2 std::sys::backtrace::__rust_begin_short_backtrace::hc59209a0a1d24814 backtrace.rs:152 (sanitizers:arm64+0x10000901c)
#3 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h915b5f69c928813d rt.rs:195 (sanitizers:arm64+0x100005148)
#4 std::rt::lang_start_internal::hc996363c321dd410 (sanitizers:arm64+0x10002428c)
#5 main (sanitizers:arm64+0x100004d6c)
Location is global 'sanitizers::main::A::h92ea287e34ba2e52' at 0x000104f40460 (sanitizers+0x100058460)
Thread T1 (tid=11227209, running) created by main thread at:
#0 pthread_create (librustc-nightly_rt.tsan.dylib:arm64+0xa0a8)
#1 std::sys::pal::unix::thread::Thread::new::h0b16ad3e3a52b1cf (sanitizers:arm64+0x10002ab38)
#2 std::thread::Builder::spawn_unchecked::hbd40c84e3aa877bf mod.rs:467 (sanitizers:arm64+0x100002028)
#3 std::thread::spawn::hd17317d53012bcc4 mod.rs:730 (sanitizers:arm64+0x100001fa0)
#4 sanitizers::main::he9b6ca8696085c08 main.rs:4 (sanitizers:arm64+0x100004b54)
#5 core::ops::function::FnOnce::call_once::hba41c0d640901898 function.rs:250 (sanitizers:arm64+0x10000540c)
#6 std::sys::backtrace::__rust_begin_short_backtrace::hc59209a0a1d24814 backtrace.rs:152 (sanitizers:arm64+0x10000901c)
#7 std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h915b5f69c928813d rt.rs:195 (sanitizers:arm64+0x100005148)
#8 std::rt::lang_start_internal::hc996363c321dd410 (sanitizers:arm64+0x10002428c)
#9 main (sanitizers:arm64+0x100004d6c)
SUMMARY: ThreadSanitizer: data race main.rs:5 in sanitizers::main::_$u7b$$u7b$closure$u7d$$u7d$::h77c6a8d926b4ffd9
==================
ThreadSanitizer: reported 1 warnings

Miri
Rust 支援的一套 Sanitizer 工具在查詢不安全程式碼錯誤方面非常有幫助,但它們不是萬能的,無法找到所有錯誤。而且,這些 Sanitizer 工具之間並不完全相容,每次只能單獨執行,這會增加測試次數和耗費的時間。
Miri 是另一種工具,它是一個直譯器,可以更準確地發現不安全程式碼中的問題,比如越界訪問、記憶體洩漏、使用未初始化資料、釋放後使用(use-after-free)以及資料競爭等。它的工作原理是解釋 Rust 的中間表示(MIR),這種方式介於編譯器的靜態分析和 Sanitizer 的動態分析之間,更有針對性地發現潛在問題。
與 Sanitizer 類似,Miri 依賴於 Rust 的 nightly 工具鏈,安裝也很簡單:
rustup +nightly component add miri
記憶體越界訪問
讓我們重新考慮之前的越界記憶體訪問示例:
fn bad_address(i: i32) -> i32 {
let xs: [i32; 4] = [0, 1, 2, 3];
unsafe { *xs.as_ptr().offset(i as isize) }
}
fn main() {
let v = bad_address(4000);
println!("Value at offset: {}", v);
}
使用 Miri 十分簡單:
cargo +nightly miri run
它會報告越界訪問錯誤並提供回溯資訊:
error: Undefined Behavior: out-of-bounds pointer arithmetic: expected a pointer to 16000 bytes of memory, but got alloc870 which is only 16 bytes from the end of the allocation
--> src/main.rs:3:15
|
3 | unsafe { *xs.as_ptr().offset(i as isize) }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ out-of-bounds pointer arithmetic: expected a pointer to 16000 bytes of memory, but got alloc870 which is only 16 bytes from the end of the allocation
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc870 was allocated here:
--> src/main.rs:2:9
|
2 | let xs: [i32; 4] = [0, 1, 2, 3];
| ^^
= note: BACKTRACE (of the first span):
= note: inside `bad_address` at src/main.rs:3:15: 3:45
note: inside `main`
--> src/main.rs:7:13
|
7 | let v = bad_address(4000);
| ^^^^^^^^^^^^^^^^^
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
雖然 Sanitizer 也檢測到了相同的錯誤,但 Miri 的輸出更加具體,易於理解,包括程式碼片段而不是記憶體地址和棧幀。此外,Miri 在單次執行中檢查多種未定義行為。與 Sanitizer 類似,Miri 只解釋實際執行的程式碼路徑(無論是在測試中還是在二進位制程式執行時),並且不會發現未被解釋的程式碼路徑中的錯誤。
資料競爭
為了完整性,我再次使用存在資料競爭的程式碼,但這次使用 Miri 在測試中執行它:
fn data_race() {
static mut A: usize = 0;
let t = std::thread::spawn(|| {
unsafe { A += 1 };
});
unsafe { A += 1 };
t.join().unwrap();
}
#[cfg(test)]
mod tests {
use crate::data_race;
#[test]
fn data_race_test() {
data_race();
}
}
執行 Miri 的測試命令如下:
cargo +nightly miri test
Miri 成功識別出資料競爭,幷包含具體的程式碼片段和錯誤資訊,比上面 Rust ThreadSanitizer 的輸出更易於理解:
running 1 test
test tests::data_race_test ... error: Undefined Behavior: Data race detected between (1) non-atomic write on thread `tests::data_race_test` and (2) non-atomic read on thread `unnamed-2` at alloc1. (2) just happened here
--> src/main.rs:5:18
|
5 | unsafe { A += 1 };
| ^^^^^^ Data race detected between (1) non-atomic write on thread `tests::data_race_test` and (2) non-atomic read on thread `unnamed-2` at alloc1. (2) just happened here
|
help: and (1) occurred earlier here
--> src/main.rs:7:14
|
7 | unsafe { A += 1 };
| ^^^^^^
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE (of the first span) on thread `unnamed-2`:
= note: inside closure at src/main.rs:5:18: 5:24
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error

C 和 C++ 庫的情況
Miri 是一款非常實用的工具,但它還有一個限制:目前它無法解釋透過 Rust 外部函式介面(FFI) 呼叫的程式碼,而 FFI 是 Rust 呼叫 C 和 C++ 庫的方式。例如,rusqlite 用 FFI 呼叫 C 語言編寫的 SQLite 資料庫,duckdb-rs 透過用 FFI 呼叫了用 C++ 編寫的 DuckDB,以及 open62541 呼叫用 C 語言編寫的 OPC UA 庫。由於 Miri 使用跨平臺的直譯器執行程式,程式無法訪問 FFI 或大多數平臺特定的 API。只有一些常見的功能,比如檔案系統訪問和標準輸出列印,被 Miri 支援。
好訊息是,我們可以返回使用 C 和 C++ 編譯器(如 GCC 或 Clang)提供的 Sanitizer。關鍵在於,C 或 C++ 程式碼必須在呼叫前透過啟動相應的 Sanitizer 進行編譯,然後才能在 Rust 呼叫它。
觀察下面 C 語言程式碼中的不安全示例:
#include <stdio.h>
#include <string.h>
void c_say_hello(const char *message) {
char buffer[10];
strcpy(buffer, message); // Unsafe: no bounds checking!
printf("Hello from C! %s\n", buffer);
}
可以透過一個 build.rs 檔案使用 Clang 編譯 C 程式碼,並啟用 AddressSanitizer:
fn main() {
let mut build = cc::Build::new();
build
.compiler("clang")
.file("c_src/c_code.c")
.flag("-Wall") // Enable warnings
.flag("-fsanitize=address") // Enable AddressSanitizer
.flag("-fno-omit-frame-pointer"); // Simplify stack tracing
build.compile("c_code");
// Ensure the build script reruns if the C file changes
println!("cargo:rerun-if-changed=c_src/c_code.c");
}
然後,可以使用 FFI 從不安全塊中的 Rust 庫呼叫 C 程式碼:
use std::ffi::{c_char, CString};
#[link(name = "c_code")] // Link to the compiled library
extern "C" {
fn c_say_hello(name: *const c_char);
}
pub fn say_hello(message: &str) {
let name = CString::new(message).expect("CString::new failed");
unsafe {
c_say_hello(name.as_ptr()); // Call the C function
}
}
最後,可以在程式中呼叫包裝 C 程式碼的“安全” Rust 函式:
use sanitizers::say_hello;
fn main() {
say_hello("This is far too long and will do bad things!");
}
在啟用 AddressSanitizer 的情況下執行程式:
export RUSTFLAGS=-Zsanitizer=address
cargo +nightly run
它會報告棧緩衝區溢位錯誤:
=================================================================
==51935==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016fa7e76a at pc 0x000100c32ad0 bp 0x00016fa7e730 sp 0x00016fa7dee0
WRITE of size 45 at 0x00016fa7e76a thread T0
#0 0x000100c32acc in strcpy+0x4ec (librustc-nightly_rt.asan.dylib:arm64+0x4aacc)
#1 0x000100383ee8 in c_say_hello+0x11c (sanitizers:arm64+0x100003ee8)
#2 0x000100383328 in sanitizers::say_hello::h7a86e3249bf087ea+0x1d8 (sanitizers:arm64+0x100003328)
#3 0x0001003815c0 in sanitizers::main::h11beac415f2c6ee0 main.rs:4
#4 0x0001003818d8 in core::ops::function::FnOnce::call_once::h6f36f80e70ecb8a5 function.rs:250
#5 0x000100381910 in std::sys::backtrace::__rust_begin_short_backtrace::h5a6edce2cadaf2f4 backtrace.rs:152
#6 0x000100381488 in std::rt::lang_start::_$u7b$$u7b$closure$u7d$$u7d$::h6d09ba155578db90 rt.rs:195
#7 0x00010039ccfc in std::rt::lang_start_internal::hc996363c321dd410+0x440 (sanitizers:arm64+0x10001ccfc)
#8 0x0001003812c0 in std::rt::lang_start::hf8df676e77f16e31 rt.rs:194
#9 0x0001003815ec in main+0x20 (sanitizers:arm64+0x1000015ec)
#10 0x00019d87e0dc ()
#11 0xb922fffffffffffc ()
Address 0x00016fa7e76a is located in stack of thread T0 at offset 42 in frame
#0 0x000100383dd8 in c_say_hello+0xc (sanitizers:arm64+0x100003dd8)
This frame has 1 object(s):
[32, 42) 'buffer' (line 5) <== Memory access at offset 42 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (librustc-nightly_rt.asan.dylib:arm64+0x4aacc) in strcpy+0x4ec
Shadow bytes around the buggy address:
0x00016fa7e480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x00016fa7e700: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00[02]f3 f3
0x00016fa7e780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e800: 00 00 00 00 f1 f1 f1 f1 f8 f8 f8 f8 f2 f2 f2 f2
0x00016fa7e880: 00 00 f3 f3 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00016fa7e980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==51935==ABORTING
需要注意的是,錯誤是發生在呼叫 c_say_hello 函數里的 strcpy 函式時,也就是 C 的棧幀中。這說明透過 FFI 呼叫的 C 程式碼不再是“看不見的黑盒”,可以被檢測到問題。如果你把這個例子中的 C 程式碼用普通方式編譯(不加 AddressSanitizer,試著從 build.rs 檔案裡去掉相關的那一行),而 Rust 程式碼仍然用 AddressSanitizer 執行,那麼程式還是會報錯,提示棧緩衝區溢位(stack-buffer-overflow)。不過,這時錯誤資訊會不那麼具體,並且會顯示問題出在 Rust 程式碼中,而不是 C 程式碼裡。

結論
本文探討了三種驗證不安全 Rust 程式碼的技術,以提高程式碼安全性並避免可能帶來嚴重後果的未定義行為。這些後果包括故障、安全漏洞、違反法規、經濟損失、人員傷害甚至死亡。本文並非旨在進行詳盡的調查,而是我為解決實際問題所探索的故事、推薦使用的工具。總結三種技術如下:
1. Sanitizer:在執行時檢測不安全 Rust 程式碼;
2. Miri 直譯器,用於檢查不安全 Rust 程式碼;
3. C 和 C++ Sanitizer:在 Rust 中呼叫 C 和 C++ 庫時進行執行時檢測。
大多數系統程式設計師和應用開發者都不應編寫不安全的 Rust 程式碼。不安全 Rust 主要應該屬於庫開發者的領域。但如果必須編寫不安全程式碼,或者透過所依賴的庫呼叫不安全程式碼,建議使用 Sanitizer 或 Miri 對程式碼進行測試,避免出現各種錯誤。
– EOF –
關注「程式設計師的那些事」加星標,不錯過圈內事
點贊和在看就是最大的支援❤️