source2rosetta/fuzz/fuzz_targets/fuzz_pulse.rs
Kamal Tufekcic 71ce34edd2
All checks were successful
CI / lint (push) Successful in 17s
CI / fuzz (push) Successful in 2m6s
CI / test (push) Successful in 25s
act on what the binary declares: callable Pulse shims, ConVars, string anchors; gen v2.1
2026-07-30 17:36:40 +03:00

48 lines
2.4 KiB
Rust

#![no_main]
//! A Pulse binding's typed signature is reconstructed by DECODING the accessor that returns its
//! descriptor: the reader follows arbitrary control flow, constant-propagates through it, and then
//! dereferences whatever addresses that produced — a returned element count, a base pointer, a name
//! pointer per element, and a receiver it chases one call deep. Every one of those is whatever the file
//! says it is, so a crafted (or truncated) `.so` can aim them anywhere, into non-code, off the end of a
//! section, or into a cycle. The reader must answer with fewer signatures, never a panic and never a
//! runaway. Also exercises the invariant the whole stage rests on: a recovered list has exactly as many
//! parameters as the accessor's own count says, so a shifted layout cannot ship as a short signature.
use libfuzzer_sys::fuzz_target;
use source2rosetta::elf::CodeImage;
use source2rosetta::{pulse, valvetab};
fuzz_target!(|data: &[u8]| {
let Ok(img) = CodeImage::from_bytes(data.to_vec()) else {
return;
};
let bindings = valvetab::pulse_bindings(&img);
// The invocation shim's read-measurement walks a file-chosen address with its own span arithmetic,
// and its verdict is emitted as `call.needs`, so it must degrade rather than panic or over-claim.
for b in &bindings {
if b.shim == 0 {
continue;
}
if let Some(r) = pulse::shim_reads(&img, b.shim) {
assert!(r.reads.len() <= 7, "more argument slots than the shim has");
assert!(
matches!(r.needs(), "args-only" | "output-sink" | "pulse-context" | "other-slots"),
"needs() left its closed vocabulary"
);
}
}
let pairs: Vec<(u64, u64)> = bindings
.iter()
.take(64)
.map(|b| (b.descriptor, b.arg_descriptor))
.collect();
let (sigs, stride, votes, _) = pulse::read_all(&img, &pairs, 1);
assert_eq!(sigs.len(), pairs.len(), "one verdict per binding");
assert!(votes == 0 || stride > 0, "a voted-for stride is never zero");
for s in sigs.into_iter().flatten() {
for p in s.args.iter().chain(&s.returns) {
// A parameter that survived is fully formed: the name gate and the type gate both passed.
assert!(!p.name.is_empty(), "shipped a nameless parameter");
assert!(p.ty >= -1, "shipped a type below PVAL_VOID");
}
}
});