act on what the binary declares: callable Pulse shims, ConVars, string anchors; gen v2.1
All checks were successful
CI / lint (push) Successful in 17s
CI / fuzz (push) Successful in 2m6s
CI / test (push) Successful in 25s

This commit is contained in:
Kamal Tufekcic 2026-07-30 17:36:40 +03:00
commit 71ce34edd2
14 changed files with 1507 additions and 54 deletions

View file

@ -183,6 +183,7 @@ pub fn produce_cmd(a: ProduceArgs) -> Result<()> {
flagged: &derived.flagged,
unverified: &derived.unverified,
abi: &derived.abi,
anchors: &derived.anchors,
sig_cap,
version,
full_names,
@ -279,6 +280,11 @@ pub fn produce_cmd(a: ProduceArgs) -> Result<()> {
bindings.meta.pulse_typed,
prof.min_pulse_typed,
),
(
"host-callable Pulse shims",
bindings.meta.pulse_callable,
prof.min_pulse_callable,
),
(
"entity-IO records",
bindings.meta.entity_inputs + bindings.meta.entity_outputs,
@ -294,6 +300,7 @@ pub fn produce_cmd(a: ProduceArgs) -> Result<()> {
bindings.meta.commands,
prof.min_commands,
),
("ConVars", bindings.meta.convars, prof.min_convars),
] {
ensure!(
got >= floor,
@ -1192,6 +1199,40 @@ pub(crate) fn run_live_oracle(
}
}
// The Pulse shim contract, checked by calling. Independent of `--gamedata`: it verifies a claim
// `bindings-<game>.json` makes, not a gamedata locator, so it runs on every live oracle.
println!(
"\n=== Pulse invocation shims: calling every `args-only` binding with a sentinel handle ==="
);
let (shims_probed, shims_ok, shim_notes) = verify_pulse_shims(&live, pid, base, &img);
println!(" {shims_ok} / {shims_probed} returned cleanly");
for n in shim_notes.iter().take(8) {
println!(" {n}");
}
// Probing NOTHING must not read as a pass. `OracleCounts::pass_rate` returns 1.0 for zero checks —
// correct in general, since a stage with nothing to do is not a failure — but here zero means the
// eligibility filter stopped matching, which is precisely the silent collapse the emitted
// `call.needs` field would then be making claims about. The profile floor guarantees the callable
// rows exist, so an empty probe set is a contradiction worth shouting about.
if shims_probed == 0 {
println!(
" WARNING: no shim was eligible to probe. The floor guarantees host-callable rows exist, so \
this means the probe's own filter no longer matches them the emitted `call.needs` is \
UNVERIFIED for this build."
);
}
// A fault means the emitted argument contract is wrong, which is a claim the artifact should not be
// making. Anything else (a clean non-`-2` return) is a different status protocol, not a broken contract,
// so it counts as OK.
verdicts.push((
"pulse-shims",
OracleCounts {
checked: shims_probed as u32,
ok: shims_ok as u32,
faulted: shim_notes.iter().filter(|n| n.contains("FAULTED")).count() as u32,
},
));
let live_result = if gamedata.is_some() {
println!("\n=== validate-live: derived gamedata vs the running server ===");
// Parsed once, above — it feeds the CALL test's slot, sig/offset validation, and the pawn
@ -1259,6 +1300,113 @@ pub(crate) fn run_live_oracle(
Ok(live_result)
}
/// The sentinel entity handle the Pulse resolve preamble rejects before dereferencing anything.
const PULSE_INVALID_HANDLE: u32 = 0xffff_ffff;
/// `PVAL_EHANDLE`, from the shipped `PulseValueType_t`.
const PULSE_EHANDLE: i32 = 13;
/// Verify the emitted Pulse invocation shims by CALLING them — with a handle the engine must reject.
///
/// `bindings-<game>.json` states that a shim whose `call.needs` is `args-only` can be invoked by a host.
/// That is a claim about behaviour, so it is checked against behaviour rather than left as a derivation:
/// each eligible binding is called with a sentinel handle, and its resolve must return `-2` without
/// dereferencing anything. Confirming the ARGUMENT CONTRACT (the array at `r8+8+8k`, nulls in the slots the
/// measurement says are unread) is the point; the sentinel is what makes it free of side effects.
///
/// **Why this is safe to run in CI.** Every slot but the argument array is null, so a shim that misuses one
/// dereferences null and FAULTS — and a fault is caught, the signal suppressed and the thread restored. The
/// dangerous case is a valid-but-wrong pointer, which corrupts silently (see [`crate::taxonomy`]); this
/// passes none. The argument array points into the call's own dead stack scratch.
///
/// Eligibility is narrow on purpose: a shim, `args-only`, no declared return (so the output sink is never
/// needed), and a leading `PVAL_EHANDLE` (so the sentinel is rejected). Anything else is not probed.
///
/// Derived from the image with the same readers the fold uses, rather than read back from
/// `bindings-<game>.json`: the oracle runs for `integration-test` too, which never builds that artifact, and
/// threading it through both callers to re-parse hex strings would verify the same claim by a longer route.
fn verify_pulse_shims(
live: &live::LiveProcess,
pid: u32,
base: u64,
img: &CodeImage,
) -> (usize, usize, Vec<String>) {
let regs = crate::valvetab::pulse_bindings(img);
let pairs: Vec<(u64, u64)> = regs
.iter()
.map(|b| (b.descriptor, b.arg_descriptor))
.collect();
let sigs = crate::pulse::read_all(img, &pairs, 8).0;
let mut probed = 0usize;
let mut bailed = 0usize;
let mut bad: Vec<String> = Vec::new();
for (b, sig) in regs.iter().zip(sigs.iter()) {
let (Some(sig), true) = (sig.as_ref(), b.shim != 0) else {
continue;
};
let name = &b.name;
let callable =
crate::pulse::shim_reads(img, b.shim).is_some_and(|r| r.needs() == "args-only");
if !callable
|| !sig.returns.is_empty()
|| sig.args.first().map(|p| p.ty) != Some(PULSE_EHANDLE)
|| sig.args.len() > 2
{
continue;
}
let at = base + b.shim;
if !live.is_exec(at) {
continue;
}
// [0x00] padding — the array is addressed from +8 and nothing reads +0
// [0x08] pointer to argument 0 -> relocated to 0x20
// [0x10] pointer to argument 1 -> relocated to 0x24
// [0x20] the sentinel handle, [0x24] a zero second argument
let mut blob = [0u8; 0x28];
blob[0x20..0x24].copy_from_slice(&PULSE_INVALID_HANDLE.to_le_bytes());
let relocs: &[(usize, i64)] = if sig.args.len() >= 2 {
&[(0x08, 0x20), (0x10, 0x24)]
} else {
&[(0x08, 0x20)]
};
let args = [
live::Arg::Val(0),
live::Arg::Val(0),
live::Arg::Val(0),
live::Arg::Val(0),
live::Arg::Scratch(0),
live::Arg::Val(0),
];
probed += 1;
match live::call_remote_ex(
pid as i32,
at,
&args,
&[],
Some(live::Scratch {
bytes: &blob,
relocs,
}),
) {
Ok(r) if r.clean_return && r.rax as i32 == -2 => bailed += 1,
Ok(r) if r.clean_return => {
// A clean return that is not the bail path still proves the contract; only note it.
bailed += 1;
if bad.len() < 8 {
bad.push(format!(
"{name} returned {} (not -2), cleanly",
r.rax as i32
));
}
}
Ok(_) => bad.push(format!("{name} FAULTED — the argument contract is wrong")),
Err(e) => bad.push(format!("{name} could not be called: {e}")),
}
}
(probed, bailed, bad)
}
/// A launched, ready CS2 bots server the caller owns (must kill).
pub(crate) struct OwnedServer {
pub(crate) child: std::process::Child,