read what the binary says about itself: names, signatures, prototypes; gen v2
This commit is contained in:
parent
54ef572202
commit
c458b4cb50
34 changed files with 58363 additions and 192 deletions
64
fuzz/fuzz_targets/fuzz_valvetab.rs
Normal file
64
fuzz/fuzz_targets/fuzz_valvetab.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#![no_main]
|
||||
//! Valve's in-binary name tables are read by walking every writable data section in 8-byte steps and
|
||||
//! treating each position as a candidate record — chasing a name pointer, two code pointers and, for the
|
||||
//! entity-IO table, an input-name pointer. Every one of those fields is whatever the file says it is, so a
|
||||
//! crafted (or truncated) `.so` can aim them anywhere; the readers must answer with fewer records, never a
|
||||
//! panic. Also exercises the accessors the derivation reads off each record, and the ambiguity rule that
|
||||
//! decides which names are safe to ship as locators.
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use source2rosetta::elf::CodeImage;
|
||||
use source2rosetta::valvetab;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let Ok(img) = CodeImage::from_bytes(data.to_vec()) else {
|
||||
return;
|
||||
};
|
||||
let pulse = valvetab::pulse_bindings(&img);
|
||||
for b in &pulse {
|
||||
let _ = (
|
||||
b.name.len(),
|
||||
b.display.as_ref().map(String::len),
|
||||
b.description.as_ref().map(String::len),
|
||||
b.descriptor,
|
||||
b.arg_descriptor,
|
||||
b.flags.raw,
|
||||
);
|
||||
}
|
||||
let inputs = valvetab::datadesc_inputs(&img);
|
||||
for i in &inputs {
|
||||
let _ = (i.handler.len(), i.io_name.len(), i.func);
|
||||
}
|
||||
// Array segmentation walks BACKWARD and forward from a confirmed input over addresses the file
|
||||
// controls, so every step is arithmetic on untrusted values — the same shape as the `r.base + 8`
|
||||
// overflow the Pulse reader had to be hardened against. It must yield fewer arrays, never a panic.
|
||||
let arrays = valvetab::datadesc_arrays(&img);
|
||||
for a in &arrays {
|
||||
// An array is only recorded because it holds an input, and a field descriptor's offset is read
|
||||
// as a u32 — so neither list may come back as something the caller has to re-validate.
|
||||
assert!(!a.inputs.is_empty(), "an array with no inputs was recorded");
|
||||
for (n, o) in &a.fields {
|
||||
let _ = (n.len(), *o);
|
||||
}
|
||||
}
|
||||
// Every input the array walk finds must also be one the direct reader finds: the two disagree only
|
||||
// if one of them is reading a record the other rejects, which is a contradiction worth catching.
|
||||
assert!(
|
||||
arrays.iter().map(|a| a.inputs.len()).sum::<usize>() <= inputs.len(),
|
||||
"the array walk claimed more inputs than the record reader accepts"
|
||||
);
|
||||
// The shipping gate: a name reaches gamedata only through here, so it is the part that must never
|
||||
// panic AND never over-claim — no name may survive with more than one address behind it.
|
||||
let (names, _dropped) = valvetab::names(&inputs);
|
||||
for n in &names {
|
||||
assert_eq!(
|
||||
inputs
|
||||
.iter()
|
||||
.filter(|i| i.handler == n.name)
|
||||
.map(|i| i.func)
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len(),
|
||||
1,
|
||||
"shipped an ambiguous handler name"
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue