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
148
src/produce.rs
148
src/produce.rs
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue