Three honest axes of “100%”
“100% reverse engineered” is a precise, three-part claim. Each axis addresses a different failure mode.Running the check
verify command reads the file, runs the determinism check, and prints a colored summary.
It then calls differential_plan and reports whether the optional behavioral harness is ready:
stable_id is unstable, the exit line prints in red and the offending
function indices are listed. verify takes no options beyond the .wasm path. The determinism
check always runs; differential readiness is always reported.
What runs today: verify_determinism
verify_determinism(wasm_bytes: bytes) -> DeterminismReport is the zero-dependency check that
runs on every call to warden verify. It requires only the Python standard library.
How it works
The function parses the same raw bytes twice viaparse_module, calls fingerprint_function on
each pair of defined functions, and compares the resulting stable_id strings byte-exactly. The
returned DeterminismReport carries:
total: number of defined (non-import) functions examined.stable: count whosestable_idmatched across both parse runs.unstable: list of function indices where the id differed.ok:Truewhenunstableis empty.summary: a one-liner such as"42/42 functions fingerprint deterministically".
Why it matters for carry-over
Thestable_id is WARDEN’s stable function identity: a composite of the structural fingerprint,
call-neighborhood, and type signature that stays constant across rebuilds even when table indices
shift. The entire annotation carry-over mechanism (the feature that makes RE incremental rather
than Sisyphean) depends on every stable_id being reproducible from the binary bytes alone.
Because the check operates on raw bytes with no external tooling, it runs identically in CI, in a
sandboxed environment, and on a laptop with nothing native installed.
Differential execution (runs today)
Thewarden.interp mini-interpreter is the middle tier: it makes behavioral equivalence
runnable right now, with zero native toolchain required, for the integer subset and the f32
float subset that together cover the vast majority of Emscripten arithmetic and glue code.
What the interpreter covers
execute_function is a pure-Python stack-machine evaluator for the i32 integer subset. It models:
- Arithmetic:
add,sub,mul, and both signed and unsigned division and remainder (div_s,div_u,rem_s,rem_u). - Bitwise:
and,or,xor, the shiftsshl,shr_s,shr_u, and the rotatesrotl,rotr. - Bit counting:
clz(count leading zeros),ctz(count trailing zeros), andpopcnt(count set bits). - All i32 comparisons, signed and unsigned (
eq,ne,lt_s,lt_u,gt_s,gt_u,le_s,le_u,ge_s,ge_u,eqz). - The
selectopcode (pick one of two values on a condition). - Structured control:
block,loop,if/else/end,br,br_if,return. - Local variable operations:
local.get,local.set,local.tee. - Direct function calls (with recursive fuel tracking).
- Linear memory loads and stores at full and narrow widths:
i32.load,i32.store, the narrow loadsi32.load8_s,i32.load8_u,i32.load16_s,i32.load16_u, and the narrow storesi32.store8,i32.store16.
- f32 constants (
f32.const). - f32 arithmetic:
add,sub,mul,div,min, andmax. - f32 unary math:
neg,abs, andsqrt. - All f32 comparisons:
eq,ne,lt,gt,le, andge.
UnsupportedExecution. The harness catches this and records
the pair as “undecided” rather than crashing, so a partially-modeled module still yields useful
results for the functions that are covered.
UnsupportedExecution rather than returning a wrong number, and the signed forms apply
two’s-complement semantics before dividing. Float division does not trap: f32.div by zero
follows IEEE-754 and yields an infinity or NaN, exactly as a real engine would. The narrow
loads come in signed and unsigned pairs: load8_s sign-extends the byte into the full i32,
while load8_u zero-extends it.Running one function
exec command looks up the named project, parses its .wasm, locates function <index>,
and executes it on the provided arguments.
execute_function accepts optional keyword arguments:
The deterministic input corpus: generate_inputs
A differential check is only as good as the inputs it tries, and those inputs must be the same on
every run and on every machine. verify.corpus.generate_inputs produces a fixed corpus of
argument vectors from the function’s arity alone, with no use of the wall clock and no use of the
random module:
generate_inputs(param_count, *, count=64, seed=0) returns a list of up to count vectors, each a
list of param_count i32 values. The corpus always leads with the cases that catch the most bugs,
then fills the rest from a seeded integer recurrence:
- Boundary values that probe sign and overflow edges:
0,1,2,-1(as0xFFFFFFFF),0x7FFFFFFF(the largest positive signed i32),0x80000000(the most negative),7, and1024. - Pseudo-random spreads drawn from a hand-written, seeded linear congruential recurrence
(
x = (1103515245 * x + 12345) & 0xFFFFFFFF) so the sameseedandcountalways yield the same corpus, byte for byte.
param_count of 0 yields a single empty argument vector, [[]], regardless of count.
The corpus generator also produces deterministic float inputs for f32 parameters through
generate_float_inputs(param_count, *, count=32, seed=0). It leads with float boundary cases that
probe the float edges (0.0, 1.0, -1.0, 0.5, 2.0, and a large and a small magnitude), then
fills the rest from the same seeded recurrence mapped to floats, so the same seed and count
always yield the same float corpus, value for value. Float inputs are generated the same way
integer inputs are: from the function’s arity alone, with no wall clock and no random module. A
param_count of 0 yields a single empty argument vector, [[]], just like the integer corpus.
To map a stored function’s type signature to a parameter count, corpus.parse_param_count reads a
signature string such as "(i32, i32) -> (i32)" and returns the count. It tolerates the
unknown-type placeholder "(?) -> (?)" and an empty parameter list, returning 0 for both.
random module. Determinism is a hard
requirement here for the same reason it is for the fingerprint: a check that depends on the
clock or on an unseeded RNG cannot be reproduced in CI, so its result could not be trusted.
Pass an explicit seed to sweep a different but still fully reproducible corpus.Differential execution
differential_execute runs two functions from two modules over the same input corpus and reports
per-input agreement:
match field is True when both sides return identical result stacks. If either side raises
UnsupportedExecution, that side records None and match is False (undecided, not wrong).
The comparison is NaN-safe: two float results that are both NaN count as a match, even though
NaN is never equal to itself under normal float equality, so a function that legitimately returns
NaN on both sides is not flagged as a spurious divergence.
Concrete example. parse_token v1 and v2 differ structurally (v2 adds a bounds check), but
the bounds-check result is dropped before the return. differential_execute proves they are
behaviorally equivalent across the full input corpus: every row shows match: True.
internal_crc, by contrast, shows match: False on most inputs, which is a genuine behavioral change.
differential_execute never mutates the modules or functions it receives, and each input gets a
fresh zeroed memory. You can run it in a loop across a large corpus without side effects.Proving two versions equivalent: differential_versions
differential_versions is the one-call answer to the question diffing raises: a function’s
structure changed across an update, but did its behavior change? It works from the knowledge base,
pairs generate_inputs with differential_execute so you do not have to hand-write a corpus, and
it needs no native toolchain at all:
differential_versions(kb, from_version_id, to_version_id, *, func_index=None, samples=64) parses
both versions from their recorded .wasm paths (via kb.version_paths), picks the function to
compare (the one at func_index, or every defined index present in both versions when func_index
is None), generates the deterministic corpus from each function’s type signature, runs both sides,
and aggregates the result. The corpus is chosen by parameter type: a function whose parameters are
all floats (f32/f64) gets the float corpus from generate_float_inputs, and every other
function gets the integer corpus from generate_inputs. Because the corpus is fixed and the
interpreter is deterministic, the same two versions always produce the same report.
Resolve the functions
func_index set, just that function is compared, if it is defined in both versions.
Otherwise every defined function index present in both versions is compared.Read the parameter count
parse_param_count reads the count, and the parameter types select the corpus:
generate_float_inputs for all-float parameters, generate_inputs otherwise.Execute both versions over every input
differential_execute does.Classify and tally each row
matched. A row where they returned
different stacks increments mismatched and is appended to divergences. A row where either
side raised UnsupportedExecution (the interpreter returned None) increments undecided.parse_token (index 3), whose v2 adds a bounds check whose result
is dropped, and on internal_crc (index 5), whose v2 genuinely adds one to the returned value:
Three-tier verification model
With the interpreter in place, WARDEN now has three stacked tiers for behavioral equivalence:The optional differential harness
The behavioral harness activates only when the right native toolchain is present. Nothing silently pretends a check ran. WARDEN reports readiness honestly.Tooling detection: tooling_status()
tooling_status() uses shutil.which to probe PATH for four tools:
can_differential property is True when (wasm2c or w2c2) and cc. That is the minimum
required to execute the harness. wabt_validate is independent, useful for confirming the input
is well-formed before spending time lifting it.
The plan: differential_plan(wasm_path)
differential_plan(wasm_path: str) -> dict calls tooling_status() and returns a dict
describing what would run and whether it can:
ready is False, the note field tells you exactly what to install. The concrete steps
it describes, in order:
Transpile the original to C
wasm2c (from WABT) produces a self-contained C file that is functionally equivalent to the
original by construction.Compile the lifted C to a native executable
wasm-rt-impl.c is included with WABT.Compile the agent/LLM reconstruction the same way
Differential execution over a fuzzer corpus
Running the pipeline: run_differential
differential_plan describes the work; run_differential does it when it can. It is the
orchestration entry point that activates the wasm2c/w2c2 differential pipeline the moment a C
toolchain is detected, and honestly reports a plan instead of pretending when one is not present:
run_differential(wasm_path, *, reference_path=None, func_index=None, samples=64, runner=None) is
the orchestration entry point that activates the wasm2c/w2c2 differential pipeline the moment a C
toolchain is detected, and honestly reports a plan instead of pretending when one is not present.
The control flow is deliberately conservative.
Detect the toolchain
run_differential calls tooling_status() first. If can_differential is False, it does
no work: it returns {"ran": False, "reason": ..., "plan": ...}, where plan is exactly what
differential_plan returns. Nothing is faked.Build the commands
(wasm2c or w2c2) and cc holds, the command lists are built by small pure helpers
(_wasm2c_cmd, _w2c2_cmd, _cc_cmd). The transpiler it uses is whichever of wasm2c or
w2c2 was detected.Run through an injectable runner
runner, which defaults to a thin wrapper over subprocess.run with
output captured and a non-zero exit raised. Passing a runner is what lets a test drive the
full orchestration without wasm2c or a compiler installed.Report the outcome
run_differential shells out through subprocess and shutil from the standard library only.
The runner indirection keeps it testable and keeps it honest: every native command it runs is
exactly the one the helpers build, so the plan and the run never drift apart.Activating the harness
warden verify app_v1.wasm will report
differential equivalence ready: True.
What behavioral equivalence actually claims
When the differential harness reports a function as verified, the precise claim is: corpus-bounded behavioral equivalence: the reconstruction agrees with the original on every input tried, to the depth the fuzzer reached. This is strong, automatable evidence. It is not a formal proof. The corpus is finite, and an adversarial input outside it could in principle expose a divergence. Formal equivalence checking via SMT-based symbolic execution over all inputs is a future direction for cryptographically critical functions, but it is not what the current harness provides.Future dynamic ground-truth hooks
Three additional sources of behavioral evidence are identified in the architecture vision and scaffolded as future work.SeeWasm: symbolic cross-check
SeeWasm: symbolic cross-check
magic == 0xCAFE”), SeeWasm
can confirm the condition symbolically rather than relying on the model’s assertion. This
would plug in as an additional gate in the verifier after the differential harness, providing
path-condition soundness for security-critical branches.Wasabi / Frida / Chrome DevTools: dynamic ground truth
Wasabi / Frida / Chrome DevTools: dynamic ground truth
Oracle-as-oracle: free verification for matched functions
Oracle-as-oracle: free verification for matched functions
Verification in the confidence economy
Every symbol written to the KB carries aprovenance and a confidence score. The verifier
controls whether an agent proposal is promoted:
- Oracle matches (
provenance="oracle") are inherently verified against compiled ground truth. They receive the highest confidence. - Agent proposals (
provenance="agent") are gated by the verifier before write-back. Until behavioral equivalence is confirmed, they carry a confidence below 1.0 and are marked unverified. - Human names (
provenance="human",locked=True) are sovereign. The verifier does not touch them and they cannot be overwritten by agent passes.