Skip to main content

global_state_detector/
lib.rs

1//! Detect persistent writable state between fuzzer iterations.
2//!
3//! Rust bindings for the [`global-state-detector`] C library. Operates
4//! on writable, non-executable ELF `PT_LOAD` segments (`.data` / `.bss`)
5//! of the main binary and every loaded shared object, filtering out
6//! libc / ld-linux / libpthread / libstdc++ / vDSO noise.
7//!
8//! # Usage
9//!
10//! Call [`init`] once after any one-time target initialization is done.
11//! Then, on each fuzzer iteration, [`rebaseline`] just before invoking
12//! the target and [`check`] just after. That pattern attributes drift
13//! to the target rather than to the fuzzer's own bookkeeping between
14//! callbacks.
15//!
16//! # Required linker flags for the final binary
17//!
18//! Cargo does not propagate `rustc-link-arg` from rlib dependencies, so
19//! consumers must arrange for the linker to receive `-rdynamic` and
20//! `-Wl,-z,now` themselves. The simplest way is a `.cargo/config.toml`
21//! in the consuming project (or its `fuzz/` subdirectory):
22//!
23//! ```toml
24//! [target.'cfg(target_os = "linux")']
25//! rustflags = ["-C", "link-arg=-rdynamic", "-C", "link-arg=-Wl,-z,now"]
26//! ```
27//!
28//! Without `-rdynamic`, symbol names in the main executable are not
29//! resolvable via `dladdr` and reports show `?+0x...` instead of real
30//! symbols. Without `-Wl,-z,now`, lazy PLT/GOT binding produces noise
31//! on the first iteration.
32//!
33//! # Rust-specific caveats
34//!
35//! * `static` and `static mut` live in `.data`/`.bss` — fully covered.
36//! * `AtomicU*`, `Mutex<T>` (the lock word), `RwLock` — covered.
37//! * `OnceCell`, `OnceLock`, `LazyLock`, `lazy_static!` — only the
38//!   pointer / discriminant in `.bss` is visible. The actual heap-
39//!   allocated payload is NOT tracked. You will see "this static was
40//!   first-used in this iteration" but not what value it took.
41//! * `thread_local!` lives in TLS, not `.data`/`.bss` — not tracked
42//!   here. TLS is per-thread anyway, less of a fuzzing concern.
43//! * Symbols come out Rust-mangled (`_ZN8...` / `_R...`). Pipe stderr
44//!   through `rustfilt` to demangle:
45//!       ./fuzzer 2>&1 | rustfilt
46//!   (cargo install rustfilt)
47//!
48//! # Thread safety
49//!
50//! The underlying C implementation is not thread-safe. Use it from a
51//! single-threaded harness, or add external synchronization.
52//!
53//! [`global-state-detector`]: https://github.com/AFLplusplus/global-state-detector
54
55use std::os::raw::c_int;
56
57extern "C" {
58    fn global_state_detector_init();
59    fn global_state_detector_check(rebaseline: c_int) -> c_int;
60    fn global_state_detector_rebaseline();
61}
62
63/// Snapshot all writable `PT_LOAD` segments of the main binary and
64/// every currently loaded shared object.
65///
66/// Call once after the target has been fully initialized (i.e. after
67/// any one-time setup your harness performs).
68pub fn init() {
69    unsafe { global_state_detector_init() }
70}
71
72/// Diff current memory against the last snapshot.
73///
74/// Returns the number of pages that changed. If `rebaseline` is true,
75/// the snapshot is updated to the current state so the next call only
76/// shows new deltas; pass `false` to keep comparing against the
77/// previous snapshot for cumulative drift.
78pub fn check(rebaseline: bool) -> i32 {
79    unsafe { global_state_detector_check(rebaseline as c_int) }
80}
81
82/// Force a full re-snapshot without reporting any diffs.
83pub fn rebaseline() {
84    unsafe { global_state_detector_rebaseline() }
85}