diff --git a/.gitignore b/.gitignore index 7a2fe13..ca5b8ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ /docs/demo-output/ __pycache__/ *.py[cod] +/fuzz/target +/fuzz/Cargo.lock +/fuzz/corpus +/fuzz/artifacts diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..61c009b --- /dev/null +++ b/fuzz/Cargo.toml @@ -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] diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..8cad07b --- /dev/null +++ b/fuzz/README.md @@ -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`). diff --git a/fuzz/fuzz_targets/maps_parse.rs b/fuzz/fuzz_targets/maps_parse.rs new file mode 100644 index 0000000..126ac5c --- /dev/null +++ b/fuzz/fuzz_targets/maps_parse.rs @@ -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); +}); diff --git a/src/tracer.rs b/src/tracer.rs index 9b26d8a..7e8ee03 100644 --- a/src/tracer.rs +++ b/src/tracer.rs @@ -97,13 +97,16 @@ enum SyscallStop { Exit { result: i64, is_error: bool }, } +/// `PTRACE_GET_SYSCALL_INFO` request number (Linux >= 5.3, ``). +const PTRACE_GET_SYSCALL_INFO: u32 = 0x420e; + fn syscall_stop(pid: Pid) -> io::Result { 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::(), &mut info as *mut SyscallInfo, @@ -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] diff --git a/tests/maps_robustness.rs b/tests/maps_robustness.rs new file mode 100644 index 0000000..b57839e --- /dev/null +++ b/tests/maps_robustness.rs @@ -0,0 +1,99 @@ +//! Deterministic property-style robustness tests for the `/proc//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 = (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()); +}