act on what the binary declares: callable Pulse shims, ConVars, string anchors; gen v2.1
This commit is contained in:
parent
3de955c4ff
commit
71ce34edd2
14 changed files with 1507 additions and 54 deletions
|
|
@ -8,6 +8,10 @@
|
|||
//! It also exercises the two indirections that resolve a callback (a static object's first virtual, and
|
||||
//! a constructor-stored member) against pointers the file chose, which is the same untrusted-chase shape
|
||||
//! the Valve table readers had to be hardened for.
|
||||
//!
|
||||
//! CONVAR extraction rides the same pass and is fuzzed here with it. It adds two things worth attacking:
|
||||
//! a per-registrar delegation walk (bounded call/tail-jump decoding at a file-chosen address) and a
|
||||
//! statistical argument-slot choice, both driven entirely by bytes the file controls.
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use source2rosetta::concmd;
|
||||
use source2rosetta::elf::CodeImage;
|
||||
|
|
@ -36,6 +40,40 @@ fuzz_target!(|data: &[u8]| {
|
|||
assert!(named.len() <= 12, "more flag names than there are flag bits");
|
||||
let _ = (c.description.len(), c.form.describe(), c.flags);
|
||||
}
|
||||
// ---- ConVars: same pass, different registrar test ----
|
||||
let cvs = concmd::convars(&img, "server");
|
||||
for c in &cvs {
|
||||
// The name gate is the only thing separating a convar registration from any other call that
|
||||
// happens to pass a string, so it must hold on every row.
|
||||
assert!(
|
||||
!c.name.is_empty() && c.name.len() <= 64,
|
||||
"an implausible convar name was recorded: {:?}",
|
||||
c.name
|
||||
);
|
||||
// Flags are optional (a registrar with no identifiable slot reports none), but when present the
|
||||
// decode is a pure bit test over a table of 9 and cannot exceed it.
|
||||
if !c.flags_raw.is_empty() {
|
||||
let raw = u64::from_str_radix(c.flags_raw.trim_start_matches("0x"), 16)
|
||||
.expect("flags_raw is written as hex by this reader");
|
||||
assert!(raw <= u64::from(u32::MAX), "a convar flags word exceeded 32 bits");
|
||||
assert_eq!(
|
||||
c.flags.len(),
|
||||
concmd::convar_flag_names(raw).len(),
|
||||
"decoded flag names disagree with the raw word for {:?}",
|
||||
c.name
|
||||
);
|
||||
} else {
|
||||
assert!(c.flags.is_empty(), "flag names without a raw word for {:?}", c.name);
|
||||
}
|
||||
let _ = (c.description.len(), c.addr.len(), c.library.len());
|
||||
}
|
||||
// ConVars are deduped on (name, object address) for the same reason commands are.
|
||||
let mut cseen: Vec<(&str, &str)> = cvs.iter().map(|c| (c.name.as_str(), c.addr.as_str())).collect();
|
||||
let cbefore = cseen.len();
|
||||
cseen.sort_unstable();
|
||||
cseen.dedup();
|
||||
assert_eq!(cbefore, cseen.len(), "a duplicate (name, object) convar survived");
|
||||
|
||||
// Commands are deduped on (name, address), so no pair may survive twice.
|
||||
let mut seen: Vec<(&str, u64)> = cmds.iter().map(|c| (c.name.as_str(), c.handler)).collect();
|
||||
let before = seen.len();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ fuzz_target!(|data: &[u8]| {
|
|||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue