64 lines
2.9 KiB
Rust
64 lines
2.9 KiB
Rust
#![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"
|
|
);
|
|
}
|
|
});
|