bigywater.tech
Rust · 164

Sanitizer

上一篇的 Miri 能解释执行但很慢,也不能调 C 函数。Sanitizer 换了一条路:编译时在原生代码里插入检查,运行得快得多。这篇把常见的内存错误和数据竞争各跑一遍,看它们的报告长什么样、哪些在本机能用、代价多大。

5
本篇用 ASan 触发并报告的内存错误种类(释放后使用、堆越界、栈越界、重复释放、泄漏;另有 memcpy 越界)
×9.3
200 万次 Box::new:ASan 对原生 -O 的耗时倍数
2
本机不支持的 -Zsanitizer 值:leak、memory

释放后使用

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let v = vec![1, 2, 3];
      3 |     let p = v.as_ptr();
      4 |     drop(v);
      5 |     println!("{}", unsafe { *p });
      6 | }
  -Zsanitizer=address 编译运行:退出码 -6
    =================================================================
    ==92812==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000970 at pc 0x00010206d61c bp 0x00016dd92270 sp 0x00016dd92268
    READ of size 4 at 0x602000000970 thread T0
        #0 u164_uaf::main::h4690bcd4eeb5eab8 u164_uaf.rs:5
        #1 core::ops::function::FnOnce::call_once::h81b718fb6564e259 function.rs:250
        #2 std::sys::backtrace::__rust_begin_short_backtrace::h4808e6b10d96054f backtrace.rs:166
    0x602000000970 is located 0 bytes inside of 12-byte region [0x602000000970,0x60200000097c)
    freed by thread T0 here:
        #0 free+0x70
        #1 _$LT$alloc..raw_vec..RawVec$LT$T$C$A$GT$$u20$as$u20$core..ops..drop..Drop$GT$::drop::h9b52e4e4c87f2eea mod.rs:443
        #2 core::ptr::drop_glue$LT$alloc..raw_vec..RawVec$LT$i32$GT$$GT$::hfbdd768f8728e56a mod.rs:848
    previously allocated by thread T0 here:
        #0 malloc+0x6c
        #1 alloc::alloc::alloc::h4d231932ce95d638 alloc.rs:158
        #2 alloc::alloc::Global::alloc_impl_runtime::hec25b63c3797b59e alloc.rs:335
    SUMMARY: AddressSanitizer: heap-use-after-free u164_uaf.rs:5 in u164_uaf::main::h4690bcd4eeb5eab8
    ==92812==ABORTING

对比解释执行的 Miri 见Miri 检查未定义行为。

堆越界读

实测 · 本机执行输出
  源码:
      1 | use std::hint::black_box;
      2 | fn main() {
      3 |     let v = vec![10u32, 20, 30];
      4 |     let i = black_box(3);
      5 |     let x = unsafe { *v.as_ptr().add(i) };
      6 |     println!("{}", x);
      7 | }
  -Zsanitizer=address 编译运行:退出码 -6
    =================================================================
    ==92843==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000097c at pc 0x000102095630 bp 0x00016dd6a2b0 sp 0x00016dd6a2a8
    READ of size 4 at 0x60200000097c thread T0
        #0 u164_oob::main::h68f0db86d30f4ac0 u164_oob.rs:5
        #1 core::ops::function::FnOnce::call_once::h985e0e6e4afcdd18 function.rs:250
        #2 std::sys::backtrace::__rust_begin_short_backtrace::h7748453a8d8cbf95 backtrace.rs:166
    0x60200000097c is located 0 bytes after 12-byte region [0x602000000970,0x60200000097c)
    allocated by thread T0 here:
        #0 malloc+0x6c
        #1 alloc::alloc::alloc::hd44f808ba37a7cc0 alloc.rs:158
        #2 alloc::alloc::Global::alloc_impl_runtime::hdd866a2a3943b54e alloc.rs:335
    SUMMARY: AddressSanitizer: heap-buffer-overflow u164_oob.rs:5 in u164_oob::main::h68f0db86d30f4ac0
    ==92843==ABORTING
  源码:
      1 | use std::hint::black_box;
      2 | fn main() {
      3 |     let v = vec![10u32, 20, 30];
      4 |     let i = black_box(3);
      5 |     println!("{}", v[i]);
      6 | }
  -Zsanitizer=address 编译运行:退出码 101
    thread 'main' (7610322) panicked at <实验目录>/src/u164_safe_oob.rs:5:21:
    index out of bounds: the len is 3 but the index is 3
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

栈越界与重复释放

实测 · 本机执行输出
  源码:
      1 | use std::hint::black_box;
      2 | fn main() {
      3 |     let a = [1u8, 2, 3, 4];
      4 |     let p = a.as_ptr();
      5 |     let i = black_box(4);
      6 |     println!("{}", unsafe { *p.add(i) });
      7 | }
  -Zsanitizer=address 编译运行:退出码 -6
    =================================================================
    ==92913==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016d6523d4 at pc 0x0001027accc8 bp 0x00016d652370 sp 0x00016d652368
    READ of size 1 at 0x00016d6523d4 thread T0
        #0 u164_stack::main::h0ee85ba49f3c565c u164_stack.rs:6
        #1 core::ops::function::FnOnce::call_once::h4c56f55464e056c3 function.rs:250
        #2 std::sys::backtrace::__rust_begin_short_backtrace::hda93cf726b48ae27 backtrace.rs:166
    Address 0x00016d6523d4 is located in stack of thread T0 at offset 84 in frame
        #0 u164_stack::main::h0ee85ba49f3c565c u164_stack.rs:2
      This frame has 3 object(s):
        [32, 48) 'args' (line 6)
        [64, 65) '_10' (line 6)
        [80, 84) 'a' (line 3) <== Memory access at offset 84 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)
    …(共 16 行)
  源码:
      1 | fn main() {
      2 |     let b = Box::new(5u32);
      3 |     let p = Box::into_raw(b);
      4 |     unsafe { drop(Box::from_raw(p)); drop(Box::from_raw(p)); }
      5 | }
  -Zsanitizer=address 编译运行:退出码 -6
    =================================================================
    ==92953==ERROR: AddressSanitizer: attempting double-free on 0x602000000970 in thread T0:
        #0 free+0x70
        #1 alloc::alloc::dealloc_nonnull::hd83b3ba2b3e2d01c alloc.rs:205
        #2 alloc::alloc::Global::deallocate_impl_runtime::h9ea61a1be044d64f alloc.rs:354
    0x602000000970 is located 0 bytes inside of 4-byte region [0x602000000970,0x602000000974)
    freed by thread T0 here:
        #0 free+0x70
        #1 alloc::alloc::dealloc_nonnull::hd83b3ba2b3e2d01c alloc.rs:205
        #2 alloc::alloc::Global::deallocate_impl_runtime::h9ea61a1be044d64f alloc.rs:354
    previously allocated by thread T0 here:
        #0 malloc+0x6c
        #1 alloc::alloc::alloc::h67cc3907dedf19bb alloc.rs:158
        #2 alloc::alloc::Global::alloc_impl_runtime::h0161344b0a1b8663 alloc.rs:335
    SUMMARY: AddressSanitizer: double-free (librustc-nightly_rt.asan.dylib:arm64+0x3ef54) in free+0x70
    ==92953==ABORTING

泄漏

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let b = Box::new([1u8; 16]);
      3 |     std::mem::forget(b);
      4 |     println!("done");
      5 | }
  -Zsanitizer=address 编译运行:退出码 1
    done
    =================================================================
    ==92982==ERROR: LeakSanitizer: detected memory leaks
    Direct leak of 16 byte(s) in 1 object(s) allocated from:
        #0 malloc+0x6c
        #1 alloc::alloc::alloc::h5c1c31a4651401fe alloc.rs:158
        #2 alloc::alloc::Global::alloc_impl_runtime::h8df4232c0085002b alloc.rs:335
    SUMMARY: AddressSanitizer: 16 byte(s) leaked in 1 allocation(s).
  -Zsanitizer=leak 编译: error: leak sanitizer is not supported for this target
  -Zsanitizer=memory 编译: error: memory sanitizer is not supported for this target

外部 C 函数

实测 · 本机执行输出
  源码:
      1 | use std::hint::black_box;
      2 | unsafe extern "C" { fn memcpy(dst: *mut u8, src: *const u8, n: usize) -> *mut u8; }
      3 | fn main() {
      4 |     let mut dst = vec![0u8; 8];
      5 |     let src = [7u8; 16];
      6 |     unsafe { memcpy(dst.as_mut_ptr(), src.as_ptr(), black_box(16)) };
      7 |     println!("{}", dst[0]);
      8 | }
  -Zsanitizer=address 编译运行:退出码 -6
    =================================================================
    ==93022==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000978 at pc 0x0001007ed1dc bp 0x00016fc062f0 sp 0x00016fc05aa0
    WRITE of size 16 at 0x602000000978 thread T0
        #0 memcpy+0x48c
        #1 u164_ffi::main::h789552d2ef368346 u164_ffi.rs:6
        #2 core::ops::function::FnOnce::call_once::hcfe6ab0c5df18b36 function.rs:250
    0x602000000978 is located 0 bytes after 8-byte region [0x602000000970,0x602000000978)
    allocated by thread T0 here:
        #0 calloc+0x74
        #1 _RNvMs8_NtCs3OnNwH6WCZa_5alloc7raw_vecNtB5_11RawVecInner15try_allocate_inCs6isvwMtXNgT_5gimli+0x60
        #2 alloc::raw_vec::RawVecInner$LT$A$GT$::with_capacity_zeroed_in::h5e2911326f496cac mod.rs:610
    SUMMARY: AddressSanitizer: heap-buffer-overflow (librustc-nightly_rt.asan.dylib:arm64+0x3d1d8) in memcpy+0x48c
    ==93022==ABORTING

ThreadSanitizer

实测 · 本机执行输出
  源码:
      1 | use std::thread;
      2 | struct P(*mut u32);
      3 | unsafe impl Send for P {}
      4 | fn main() {
      5 |     let mut x = 0u32;
      6 |     let p = P(&mut x as *mut u32);
      7 |     let h = thread::spawn(move || {
      8 |         let p = p;
      9 |         for _ in 0..1000 { unsafe { *p.0 += 1 }; }
     10 |     });
     11 |     for _ in 0..1000 { unsafe { *(&mut x as *mut u32) += 1 }; }
     12 |     h.join().unwrap();
     13 |     println!("x = {}", x);
     14 | }
  只给自己的 crate 加 -Zsanitizer=thread(不重编标准库):退出码 101
    error: mixing `-Zsanitizer` will cause an ABI mismatch in crate `ts`
      |
      = help: the `-Zsanitizer` flag modifies the ABI so Rust crates compiled with different values of this flag cannot be used together safely
      = note: `-Zsanitizer=thread` in this crate is incompatible with `-Zsanitizer` being unset in dependency `std`
      = help: unset `-Zsanitizer` in this crate or set `-Zsanitizer=thread` in `std`
      = help: if you are sure this will not cause problems, you may use `-Cunsafe-allow-abi-mismatch=sanitizer` to silence this error
    …(共 134 行)
  加 -Zbuild-std(含重编标准库,共 12 秒):退出码 -6
    ==================
    WARNING: ThreadSanitizer: data race (pid=93031)
      Read of size 4 at 0x00016d7598bc by thread T1:
        #0 ts::main::_$u7b$$u7b$closure$u7d$$u7d$::hdf5edab4845b39f7 main.rs:9
        #1 std::sys::backtrace::__rust_begin_short_backtrace::h156f8089f55476ae backtrace.rs:166
        #2 std::thread::lifecycle::spawn_unchecked::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h53f13b1c4a46dbcc lifecycle.rs:70
      Previous write of size 4 at 0x00016d7598bc by main thread:
        #0 ts::main::hf31edbd70aaa0657 main.rs:11
        #1 core::ops::function::FnOnce::call_once::h56d34f4555891b70 function.rs:250
        #2 std::sys::backtrace::__rust_begin_short_backtrace::h65b730afc025650e backtrace.rs:166
      Location is stack of main thread.
      Thread T1 (tid=7611885, running) created by main thread at:
        #0 pthread_create <null>
        #1 std::sys::thread::unix::Thread::new::ha849696e57eca4aa <null>
        #2 std::thread::lifecycle::spawn_unchecked::h4ad50f56bd654a92 lifecycle.rs:116
    SUMMARY: ThreadSanitizer: data race main.rs:9 in ts::main::_$u7b$$u7b$closure$u7d$$u7d$::hdf5edab4845b39f7
    ==================
    ==================
    WARNING: ThreadSanitizer: data race (pid=93031)
      Write of size 4 at 0x00016d7598bc by thread T1:
        #0 ts::main::_$u7b$$u7b$closure$u7d$$u7d$::hdf5edab4845b39f7 main.rs:9
        #1 std::sys::backtrace::__rust_begin_short_backtrace::h156f8089f55476ae backtrace.rs:166
        #2 std::thread::lifecycle::spawn_unchecked::_$u7b$$u7b$closure$u7d$$u7d$::_$u7b$$u7b$closure$u7d$$u7d$::h53f13b1c4a46dbcc lifecycle.rs:70
      Previous write of size 4 at 0x00016d7598bc by main thread:
        #0 ts::main::hf31edbd70aaa0657 main.rs:11
        #1 core::ops::function::FnOnce::call_once::h56d34f4555891b70 function.rs:250
        #2 std::sys::backtrace::__rust_begin_short_backtrace::h65b730afc025650e backtrace.rs:166
      Location is stack of main thread.
      Thread T1 (tid=7611885, running) created by main thread at:
        #0 pthread_create <null>
        #1 std::sys::thread::unix::Thread::new::ha849696e57eca4aa <null>
        #2 std::thread::lifecycle::spawn_unchecked::h4ad50f56bd654a92 lifecycle.rs:116
    SUMMARY: ThreadSanitizer: data race main.rs:9 in ts::main::_$u7b$$u7b$closure$u7d$$u7d$::hdf5edab4845b39f7
    ==================
    ThreadSanitizer: reported 2 warnings
    x = 2000

数据竞争在安全代码里是编译错误,见数据竞争为什么编译不过;这里为了触发它用了 unsafe 的裸指针。

代价

实测 · 本机执行输出
  源码:
      1 | use std::hint::black_box;
      2 | use std::time::Instant;
      3 | fn main() {
      4 |     let n = 20_000_000usize;
      5 |     let v: Vec<u32> = (0..n as u32).collect();
      6 |     let t = Instant::now();
      7 |     let mut s = 0u64;
      8 |     for i in 0..v.len() { s += unsafe { *v.as_ptr().add(black_box(i)) } as u64; }
      9 |     black_box(s);
     10 |     let a = t.elapsed().as_secs_f64() * 1000.0;
     11 |     let t = Instant::now();
     12 |     for i in 0..2_000_000u32 { let b = Box::new([i; 4]); black_box(&b); }
     13 |     let b = t.elapsed().as_secs_f64() * 1000.0;
     14 |     println!("{:.2} {:.2}", a, b);
     15 | }
                   遍历 2000 万元素 (ms)     200 万次 Box::new (ms)       峰值内存 (MiB)        可执行文件 (KiB)
  原生 -O                         6.1                     21.1               78                500
  ASan -O                      14.5                    196.5              180                502

模糊测试通常与 sanitizer 一起运行,见Fuzz 测试。

分步演示

Sanitizer
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。nightly 工具链用 rustup 装在隔离目录(不影响 brew 的 rustc):rustc 1.101.0-nightly (21b707e3f 2026-09-30)、miri 0.1.0 (21b707e3f9 2026-09-30)。-Zsanitizer=address(直接用 rustc);-Zsanitizer=thread 配合 cargo -Zbuild-std(rust-src 组件);栈符号用 -Zunstable-options -C symbol-mangling-version=legacy 以便阅读
    • 没有验证:Linux 上的 sanitizer 行为与支持范围(例如 leak、memory)、-Zbuild-std 对 ASan 的影响(本篇的 ASan 没有重编标准库,因此标准库内部代码没有被插桩)、stack-use-after-return 与 stack-use-after-scope、ASAN_OPTIONS 的其它选项、sanitizer 与 Miri 同时使用
    • 本篇的调用栈只截取了前 3 帧、省略了 Shadow bytes 部分以便阅读;其余内容为原样输出
    京ICP备2026067502号-1京公网安备11010502063487号