Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
/docs/demo-output/
__pycache__/
*.py[cod]
/fuzz/target
/fuzz/Cargo.lock
/fuzz/corpus
/fuzz/artifacts
23 changes: 23 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "wraith-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.wraith]
path = ".."

[[bin]]
name = "maps_parse"
path = "fuzz_targets/maps_parse.rs"
test = false
doc = false
bench = false

[workspace]
8 changes: 8 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Fuzzing

Requires nightly Rust and `cargo install cargo-fuzz`.

cargo +nightly fuzz run maps_parse -- -max_total_time=60

Deterministic dependency-free property tests for the same parser run in normal CI
(`tests/maps_robustness.rs`).
18 changes: 18 additions & 0 deletions fuzz/fuzz_targets/maps_parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use wraith::maps::MemoryMap;

// Property: parsing arbitrary text never panics, regions come out sorted by start
// address, and lookups/formatting on whatever was parsed never panic.
fuzz_target!(|data: &[u8]| {
let text = String::from_utf8_lossy(data);
let map = MemoryMap::parse(&text);
assert!(map.regions().windows(2).all(|w| w[0].start <= w[1].start));
for r in map.regions() {
let _ = r.to_string();
let _ = map.region_at(r.start);
let _ = map.region_at(r.end);
}
let _ = map.region_at(0);
let _ = map.region_at(u64::MAX);
});
15 changes: 14 additions & 1 deletion src/tracer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,16 @@ enum SyscallStop {
Exit { result: i64, is_error: bool },
}

/// `PTRACE_GET_SYSCALL_INFO` request number (Linux >= 5.3, `<linux/ptrace.h>`).
const PTRACE_GET_SYSCALL_INFO: u32 = 0x420e;

fn syscall_stop(pid: Pid) -> io::Result<SyscallStop> {
let mut info = SyscallInfo::default();
// GET_SYSCALL_INFO requires Linux >= 5.3. Never guess phase on older kernels:
// an attached task's first stop may be an exit, where enforcement is unsafe.
let size = unsafe {
libc::ptrace(
0x420e,
PTRACE_GET_SYSCALL_INFO as _,
pid.as_raw(),
std::mem::size_of::<SyscallInfo>(),
&mut info as *mut SyscallInfo,
Expand Down Expand Up @@ -943,6 +946,16 @@ fn nix_err(e: nix::errno::Errno) -> io::Error {

#[cfg(test)]
mod tests {
#[cfg(target_env = "gnu")]
#[test]
fn syscall_info_request_matches_libc() {
assert_eq!(
super::PTRACE_GET_SYSCALL_INFO,
libc::PTRACE_GET_SYSCALL_INFO
);
assert_eq!(super::PTRACE_GET_SYSCALL_INFO, 0x420e);
}

use super::*;

#[test]
Expand Down
99 changes: 99 additions & 0 deletions tests/maps_robustness.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Deterministic property-style robustness tests for the `/proc/<pid>/maps`
//! parser (no external dependencies). A coverage-guided target lives in `fuzz/`.

use wraith::maps::{MemoryMap, RegionKind};

struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
// xorshift64*
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545F4914F6CDD1D)
}
fn pick<'a>(&mut self, xs: &[&'a str]) -> &'a str {
xs[(self.next() % xs.len() as u64) as usize]
}
}

fn check_invariants(map: &MemoryMap) {
assert!(map.regions().windows(2).all(|w| w[0].start <= w[1].start));
assert_eq!(map.is_empty(), map.regions().is_empty());
for r in map.regions() {
let _ = r.to_string();
let _ = r.label();
let _ = map.region_at(r.start);
let _ = map.region_at(r.end);
}
let _ = map.region_at(0);
let _ = map.region_at(u64::MAX);
}

#[test]
fn arbitrary_bytes_never_panic() {
let mut rng = Rng(0x9E3779B97F4A7C15);
for _ in 0..3000 {
let len = (rng.next() % 300) as usize;
let bytes: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
check_invariants(&MemoryMap::parse(&String::from_utf8_lossy(&bytes)));
}
}

#[test]
fn mutated_valid_lines_never_panic() {
let pieces = [
"7f0000000000-7f0000001000",
"ffffffffffffffff-ffffffffffffffff",
"0-0",
"-",
"rwxp",
"r-xp",
"rwxs",
"xx",
"00000000",
"00:00",
"103:02",
"0",
"123456",
"/usr/lib/libc.so.6",
"[stack]",
"[heap]",
"[vdso]",
"[vvar]",
"[vsyscall]",
"/tmp/a b (deleted)",
"\u{fffd}",
"",
];
let mut rng = Rng(0xDEADBEEFCAFEF00D);
for _ in 0..5000 {
let mut text = String::new();
for _ in 0..(1 + rng.next() % 6) {
for _ in 0..(rng.next() % 7) {
text.push_str(rng.pick(&pieces));
text.push(if rng.next() % 5 == 0 { '\t' } else { ' ' });
}
text.push('\n');
}
check_invariants(&MemoryMap::parse(&text));
}
}

#[test]
fn well_formed_map_is_parsed_and_sorted() {
// Deliberately out of order: parse() must sort so region_at stays correct.
let raw = "\
7ffc00000000-7ffc00021000 rw-p 00000000 00:00 0 [stack]\n\
55d000000000-55d000001000 r-xp 00000000 08:01 1234 /usr/bin/true\n\
7f0000000000-7f0000001000 rwxp 00000000 00:00 0\n";
let map = MemoryMap::parse(raw);
assert_eq!(map.regions().len(), 3);
check_invariants(&map);
let stack = map.region_at(0x7ffc00000800).expect("stack region");
assert_eq!(stack.kind, RegionKind::Stack);
let anon = map.region_at(0x7f0000000010).expect("anon region");
assert_eq!(anon.kind, RegionKind::Anonymous);
assert!(anon.write && anon.exec);
assert!(map.region_at(0x7f0000001000).is_none());
}
Loading