oracle.
Alpha status. The matching engine, signature store, MinHash-LSH index, and the matrix
corpus farm (the
build_matrix.sh matrix builder, its manifest.json, and the
harvest_directory harvest step) are all implemented and working, along with the robustness
work covered below: deterministic identify tie-breaking, calibrated version-inference
confidence, SignatureStore.stats() introspection, and the evaluate_identification
precision/recall harness. The seed store that ships with a fresh clone is intentionally empty.
You build your own. The large multi-thousand-signature corpus that would let the Oracle
identify most of a real target still needs the full emsdk matrix run (Docker plus emsdk); the
precision/recall numbers you can produce today are calibration on the labeled modules you have,
not a published accuracy figure. A shared community corpus is on the
roadmap.Compile your own ground truth
No other FLIRT- or BinDiff-style workflow auto-builds its corpus from the toolchain’s own source across the version/flag matrix. The critical ingredient is--profiling-funcs.
Without it, an optimized Emscripten build strips the wasm name section. The corpus builder,
extract_signatures, looks up each function’s name from:
module.names.function_names: the wasm name section (primary source).func.export_names[0]: used if the function is exported and the name section is absent.
--profiling-funcs (or -g / -gsource-map) on
every corpus build. This flag forces Emscripten to emit the name section even at -O2 and -Oz
where it would otherwise be stripped.
The signature store
Signature
Defined in src/warden/oracle/signatures.py. One Signature is a labeled fingerprint of a
single function extracted from a corpus build.
Signature.fingerprint() reconstructs a FunctionFingerprint so the same similarity()
function used everywhere in WARDEN can compare a corpus entry against a live target function.
Library classification is automatic, based on name prefixes. classify_library
walks an ordered prefix table and returns the first library whose prefix matches the
name. Anything that matches no prefix keeps the fallback --library value (default
musl). The full table:
The
compiler-rt row covers the soft-float and wide-integer builtins Emscripten
links from compiler-rt. These show up in almost every non-trivial module and used to
land in the musl fallback bucket, which made the per-library breakdown misleading.
SignatureStore
A SignatureStore is an ordered list of Signature objects with JSON persistence. The on-disk
format is plain JSON (portable, git-diffable, and shareable without a database).
Introspecting a store with stats()
store.stats() returns one deterministic summary dict for a store, so you can audit
what a corpus actually covers without loading the JSON by hand. It rolls up the counts
that matter when you are deciding whether a corpus is broad enough to identify a given
target:
Every dict is ordered for stable output: keys are sorted so the same store always prints
the same summary, which keeps it safe to snapshot in a test or diff across runs, and the
result does not depend on the order signatures were added. A store whose
by_emscripten_version covers only one version will identify fewer functions in a target
built differently. stats() makes that gap visible before you run identify and wonder why
coverage is low.
Signature dedup on extend
The matrix farm compiles the same reference programs across many versions and opt
levels, so the same function body recurs constantly. extend deduplicates as it adds:
a new signature is dropped when an existing entry already shares the same identity key.
The key is the tuple of (name, library, emscripten_version, opt_level, exact_hash),
so two builds of memcpy that produced a byte-identical body at the same version and
-O level collapse to one entry, while a genuinely different -Oz body is kept. This
keeps oracle.json from ballooning when you re-run the farm or harvest the same
directory twice, and it makes the build idempotent: harvesting the same tree again does
not grow the store.
Dedup happens on
extend only, the path every corpus and harvest build takes. add
is the raw single-signature append and does not deduplicate, so a manual add loop
can still create duplicates if you bypass extend.The shipped seed store is empty
src/warden/oracle/seed_signatures.json ships with "count": 0 and an empty signatures
array. This is deliberate: a pre-built corpus would encode assumptions about which emsdk
versions and flag combinations matter for your target. You build the store that matches your
target and commit it alongside your project.
Identifying functions: identify and ORACLE_THRESHOLD
identify is defined in src/warden/oracle/match.py. Given a KnowledgeBase, a version_id,
and a SignatureStore, it:
1
Fetch all defined functions for the version
Imports are excluded. They already have names from the JS glue.
2
Reconstruct a fingerprint from the KB row
Uses
fingerprint_from_record. No re-parsing of the original .wasm is required.3
Score every corpus signature against the target fingerprint
The
similarity() function combines four signals:4
Accept or reject
The best-scoring signature wins. If
score >= threshold, it is an Oracle match.Deterministic tie-breaking
Two signatures can score identically against one target function. The matrix farm makes this common: the samememcpy body compiled at the same -O level under two different
emscripten versions produces two signatures that are byte-identical apart from their
version label, so both score 1.0. Picking whichever happened to come first in the store
would make the result depend on insertion order, and the same target could be labeled
3.1.50 on one run and 3.1.55 on the next.
To keep identify deterministic, ties are broken by a fixed, total ordering rather than
by store position. Among signatures that tie on score, the Oracle prefers, in order:
- the higher
score(the primary key, already decided above), - then the lexicographically smaller
name, - then the lexicographically smaller
source_ref(a missingsource_refis treated as the empty string, so it sorts first).
OracleMatch regardless of the order
signatures were added to the store. identify_indexed applies the same tie-break over its
candidate pool, so the indexed path returns the same match as the linear scan.
The acceptance threshold is:
write=True (the default), every accepted match is written back to the KB with:
- Provenance
"oracle"andconfidence = score. - A
Symbolentry with the matched name, type signature, and a summary noting library and version. - An evidence trail:
{"kind": "oracle", "detail": "<library> <emver> @<opt> score=<N>"}. - A row in
oracle_matcheskeyed to the internal function ID.
--threshold flag overrides ORACLE_THRESHOLD at the CLI. Lower values catch more functions
at the cost of false positives; higher values are conservative. The default of 0.82 is a
reasonable starting point for functions of moderate size, but you should audit matches after
the first run.
The OracleMatch return type
identify returns list[OracleMatch]:
Scaling: the MinHash-LSH index
The defaultidentify scan is O(targets × signatures). For a corpus of a few hundred signatures
that is negligible, but once the matrix farm has accumulated thousands of entries across many
versions and opt levels the linear pass becomes a bottleneck. warden.oracle.index provides
SignatureIndex, a banded MinHash + structural-hash index that reduces each function’s search
space to a small set of candidates before any scoring happens.
How it works
SignatureIndex.build(store, *, bands=None) splits each signature’s 32-element MinHash into
bands equal slices and hashes each slice into a bucket. Two signatures that agree on any
whole band land in the same bucket. The probability of that collision grows with their true
Jaccard similarity, so near-matches across -O levels still surface. Signatures are also indexed
by their exact structural_hash, so structurally identical functions are always found regardless
of their MinHash values.
Automatic band selection
bands controls the recall/precision tradeoff of the index. Few wide bands cast a wide net
(high recall, larger candidate sets); many narrow bands are selective (smaller candidate sets,
slightly lower recall). The right number depends on how many signatures are in the store, so
you do not have to guess: when you pass bands=None (the default), build calls
recommended_bands(len(store)) to pick a value scaled to the store size.
The function returns a divisor of 32 (the MinHash width) so the bands partition the signature
evenly with no remainder. As the matrix farm grows the corpus from a few hundred to tens of
thousands of signatures, the index narrows its bands automatically and the candidate sets stay
small. Pass an explicit integer to
bands to override the heuristic.
index.candidates(fp) returns the deduplicated union of every signature sharing a band-bucket
or the structural hash with fp. This is a pure dictionary lookup on the hot path.
identify_indexed is the LSH-accelerated mirror of identify. When the index yields no
candidates for a function it falls back to the full store, so no function is ever silently
dropped. The result is identical to the linear scan at ORACLE_THRESHOLD = 0.82.
Version inference
Afteridentify returns, call infer_version to infer which Emscripten version the target was
built with:
emscripten_version across all matches that carry one. The
version with the highest vote count (the plurality) wins. Ties on vote count are broken by
the smaller version string, so the winner is deterministic when two versions draw. Matches
with no emscripten_version are ignored, and the histogram reports only the
version-bearing votes.
Calibrated confidence
Raw vote share alone overstates certainty. Two matches that both vote3.1.55 give a vote
share of 1.0, but two matches is thin evidence, and a target whose runtime spans several
versions can still produce a clear plurality winner that does not deserve full confidence.
The reported confidence is therefore the plurality share calibrated down by sample size:
- Plurality share. The base figure is the winner’s votes over all version-bearing votes.
A clean sweep approaches
1.0; a split field scores proportionally lower (a 2-2 tie gives the winner a share of0.5). - Sample size damping. A handful of matches cannot pin a version as firmly as hundreds
can, so the share is multiplied by
total / (total + 1)over the version-bearing match count. That factor is0.5for a single vote and rises toward1.0as evidence accumulates, so a lone match is deliberately modest even though its raw share is1.0.
[0.0, 1.0] and is fully deterministic: identical matches give an
identical confidence, with no clock or random input. When 75% of a few hundred Oracle
matches are tagged 3.1.55, the calibrated confidence is high; when only two matches agree,
it is held down by the damping factor.
This version pin matters downstream:
- It is stored on the
ModuleVersionrow and shown bywarden versions. - The diff engine uses it to classify toolchain churn: if
v1infers3.1.50andv2infers3.1.61, functions that changed can be re-examined against the Oracle and reported as “changed due to Emscripten upgrade” rather than “application change”. - It sharpens dynCall/elem-table convention assumptions for any subsequent JS glue analysis.
Measuring identification quality: evaluate_identification
A threshold of 0.82 is a starting point, not a proof. Before you trust the Oracle on a
real target you want numbers: how many of its matches are correct, and how many real runtime
functions does it miss. evaluate_identification is a precision/recall harness for exactly
that, and it runs against a labeled module without needing emsdk or Docker on the machine.
The idea is to evaluate against ground truth you already have. A corpus build is itself a
labeled module: every function in it carries its real name from the wasm name section. So you
can build a store from one set of labeled modules, run identify against a held-out labeled
module whose true names are known, and compare the Oracle’s answers to those names. Because the
held-out module still has its name section, the harness knows the correct answer for every
function and can score the run.
raw_name survived
ingest from the wasm name section or an export) the harness compares the Oracle’s predicted
name against that ground truth and bins the outcome. Functions with no raw_name carry no
label, so they contribute nothing to either metric.
A match on an unlabeled function is ignored: there is no ground truth to confirm or deny it,
so it never inflates the false-positive count.
The whole evaluation is read-only and deterministic: it never writes back to the KB (it scores
the matches
identify would produce), and the same store, target, and threshold always yield
the same report. That makes it a clean way to sweep the threshold. Run it at several values,
watch precision rise and recall fall as you raise the bar, and pick the operating point that
fits your tolerance for false positives.
This harness measures quality on a held-out labeled module, which you can produce on any
machine. It does not replace a broad corpus. A genuinely confident precision/recall number,
and a corpus large enough to identify most of a real target, still needs the full
multi-thousand-signature emsdk matrix run described below, which requires Docker and emsdk.
Until that run exists, treat these numbers as a calibration tool on the modules you have, not
as a published accuracy figure.
CLI usage
Step 1: Build a signature store
<wasm>: one or more.wasmfiles with a name section (--profiling-funcsat compile time).--out/-o: the output (or existing)oracle.json. If the file already exists, new signatures are appended; the CLI reports+N new.--emver: the Emscripten version string to tag these signatures with.--opt: the optimization level (e.g.-O2,-Oz).--library: fallback label for names that do not match any known prefix (defaultmusl).
Inspect a store before you trust it
warden oracle inspect prints what a store covers, so you can audit a corpus without opening
the JSON. It is the CLI surface over SignatureStore.stats():
Versions line maps to by_emscripten_version and is the one to read closely. A store
that covers only one version (as above) will identify fewer functions in a target built with a
different toolchain. A broad corpus from the matrix farm shows several versions here. Every
line is sorted, so the same store always prints the same summary.
warden oracle inspect <store> is the CLI surface over SignatureStore.stats(). It is being
wired into the CLI separately; stats() is the stable library entry point in the meantime.
Both read the same oracle.json and report the same counts.Step 2: Identify runtime functions in a target
v1: a version label already ingested withwarden ingest.--store/-s: path to theoracle.jsonbuilt above (defaultoracle.json).--threshold: overrideORACLE_THRESHOLD.--indexed: use the MinHash-LSH index (identify_indexed) instead of the linear scan. Produces identical matches but is significantly faster once the store grows large. The index is built in memory at the start of each run. No separate build step is required.
infer_version succeeds, the toolchain line:
Step 3: Check coverage
The emsdk matrix corpus farm
Building a corpus by hand for each version and flag combination is tedious. The scripts inscripts/corpus/ provide a containerized, reproducible alternative. The only host dependencies
are Docker and the warden CLI.
The farm runs in two stages. First build_matrix.sh compiles the reference programs across the
emscripten-version times opt-level matrix and writes a directory of labeled .wasm files plus a
manifest.json that records how each file was built. Then a single harvest pass turns that whole
directory into one oracle.json. Splitting compile from harvest means the expensive Docker
builds happen once and you can re-harvest (after a classifier change, say) without recompiling.
Stage 1: build_matrix.sh
For each (emscripten_version, opt_level) pair:
1
Pull the official emsdk image
emscripten/emsdk:<version> is pulled from Docker Hub. Emscripten never touches your host.2
Compile reference programs inside the container
Every
.c file under scripts/corpus/reference/ is compiled with emcc, passing
--profiling-funcs and -sEXPORT_ALL=1. --profiling-funcs forces the name section to be
emitted even at high -O levels. Without it, the wasm has no labels and extract_signatures
extracts nothing useful.3
Write the labeled .wasm and a manifest row
Each artifact is written to the output directory under a tag built from the version and opt
level (for example
3.1.55_O2/strings.wasm). The container is discarded; only the compiled
artifacts and the manifest remain. A row describing the build flags is appended to
manifest.json next to the .wasm files.The manifest
build_matrix.sh writes one manifest.json at the root of the output directory. It is the
contract between the compile stage and the harvest stage: it records, for every artifact, the
labels the harvester needs to tag the extracted signatures (the emscripten version, the opt level,
the extra flags, and the relative path to the .wasm). Because the labels travel with the files,
the harvester never has to re-derive them.
Stage 2: harvesting the directory
A single harvest pass walks the output directory, reads the manifest, fingerprints every named function in every listed.wasm, and folds the results into one store. The library does this with
harvest_directory:
harvest_directory(root) loads <root>/manifest.json, and for each entry it parses the .wasm,
runs extract_signatures with the entry’s emscripten_version, opt_level, and flags, and
extends them into the store. Because extend deduplicates, byte-identical functions that recur
across the matrix collapse to a single signature, so harvesting a large matrix does not produce a
bloated store. The whole pass is deterministic: the same directory always yields the same
oracle.json.
warden oracle harvest <dir> is the CLI surface over harvest_directory. It is being wired
into the CLI separately; the library function is the stable entry point in the meantime. Both
read the same manifest.json and write the same oracle.json.Why the matrix matters
Emscripten codegen varies across optimization levels in ways that affect fingerprints. A function compiled at-O0 has a different structural skeleton than the same function at -Oz after
inlining and dead-code elimination. A corpus that covers only one opt level will miss matches in
targets compiled at a different level. The MinHash fuzzy similarity (ORACLE_THRESHOLD = 0.82)
tolerates some variation, but breadth of coverage in the corpus is the primary lever for match
rate.
Reference programs
scripts/corpus/reference/ currently contains strings.c, a program that exercises musl
string functions and the Emscripten runtime. The design calls for more: programs exercising
pthreads, exceptions, different allocator configurations, and ideally portions of the Emscripten
runtime itself. To widen the Oracle’s reach, add .c files to reference/ and extend the
FLAGS array in build_matrix.sh to cover -pthread, -sPROXY_TO_PTHREAD, -fexceptions,
and LTO. Each extra flag set becomes its own matrix entry, with its flags recorded in the
manifest flags field, so the harvest step labels those signatures with the build that produced
them.
The corpus accumulates into a single
oracle.json. Because the format is plain JSON, it can be
committed to your project repository and shared with collaborators. Everyone runs against the
same ground truth without rebuilding.Using the Oracle as a library
load_seed_store() loads src/warden/oracle/seed_signatures.json (the empty store that ships
with the package). It is exported from warden.oracle for completeness but has no signatures
until you build them.
See also
Ingestion
What happens before the Oracle: parsing, fingerprinting, seeding from exports and the name
section.
The agent crew
The Oracle handles runtime code; the agent crew names what remains.
Diff and carry-over
How the inferred Emscripten version feeds into toolchain-churn suppression during diffing.
CLI reference
Full flag listing for
warden oracle build, warden oracle harvest,
warden oracle inspect, and warden oracle identify.