read what the binary says about itself: names, signatures, prototypes; gen v2
All checks were successful
CI / lint (push) Successful in 17s
CI / fuzz (push) Successful in 1m52s
CI / test (push) Successful in 24s

This commit is contained in:
Kamal Tufekcic 2026-07-29 20:09:21 +03:00
commit c458b4cb50
34 changed files with 58363 additions and 192 deletions

View 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"
);
}
});