SIGN IN SIGN UP

spec+plan: the arg-width validator — check a declared shape against the callee's machine code (#83)

* spec+plan: the arg-width validator — check a declared shape against the callee's machine code

A hook descriptor names a shape (the callee's exact C signature) and nothing checks that name against
the function it is about to detour. check-hook-shapes.sh proves core and the shim agree with EACH
OTHER; on TerminateRound both were confidently wrong together, and the only thing that noticed was a
SIGSEGV on a live server.

The failure is asymmetric, and that asymmetry is the whole design: declaring a param NARROWER than
the engine's truncates a pointer (a memory-safety bug that detonates far from the hook), while
declaring it WIDER is harmless, because SysV leaves the upper half of a 32-bit arg undefined. So
there is exactly one direction to police and it is mechanically detectable — a callee tells you how
wide an argument is by how it stores it. `mov %rdx,-0xe0(%rbp)` is 64-bit; `mov %esi,%r15d` is not.

Scoped deliberately narrow: only MOV r/m,r with a memory destination, only integer args, absence of
evidence is a PASS. A validator that failed on "I could not tell" would refuse most functions and be
switched off within a week. It is a smoke detector for the class that burned us, and the spec says so
rather than letting a pass be over-read.

Takes NO gamedata — the expectation is derived from the shape the descriptor already declares. The
bug this prevents was a hand-written ABI claim drifting from reality; checking it with a SECOND
hand-written ABI claim would be the same mistake with more steps.

Not a place SourceMod did better: SM's detour signatures are C++ macros a human wrote while reading
the disassembly. Making the shape data removed that human step, and this buys the safety back.

* spec: record the three review corrections — terminators, the ABI table, and the OOB

10.1 the kVocabulary line contradicted 'takes no gamedata'; 10.2 the scan had to stop at the
callee's own ret/jmp/call (490 of 3497 corpus refusals came from walking into the next function);
10.3 the widths had to come from the shape's ABI, not its addressable params, which made the
kParamI64 branch dead code. Also records the OOB in the bounded-window copy, why refusal is kept
over warn-and-log now that the false-fail classes are fixed, and the guards that remain untested.
G
Gabriel Hirakawa committed
1b442d86f0759ee257ff02c8f27e100c688c0717
Parent: abe9c83
Committed by GitHub <noreply@github.com> on 8/3/2026, 10:51:41 PM