Architecture
exav is a small set of focused crates around a constant-memory scanning core.
This is the high-level picture; deeper implementation detail lives in the
repository’s docs/.
Two input modes
Section titled “Two input modes”exav scans through one of two paths, and the difference decides what analysis is possible:
- Stream (
Read: stdin, pipes) —scan_streamruns the constant-memory pattern + hash core. Unlimited size, but structural unpacking needs seeking, so a pure pipe does pattern+hash only. - Seekable (
Read + Seek: local files, or an HTTP range reader over HTTP range GETs) —scan_seekableadditionally drives archive extraction through the reader, fetching only the directory and members it actually scans.
See Streaming & memory for the core.
The code is split across a handful of crates — exav and the WASI build are
the front-ends, exav-core is the engine, exav-unpack does extraction (and
backs the standalone exav-grep). See
How the crates compose below, and
Subprojects for what each one owns.
Not one monolithic engine
Section titled “Not one monolithic engine”Different ClamAV signature types compile to different runtime structures — there is deliberately not one giant matcher:
| Source | Runtime structure | Matching |
|---|---|---|
.ndb bodies + .ldb literal subsignatures |
a shared Aho-Corasick automaton keyed on a literal anchor per body | an automaton hit fans out to every body sharing that anchor; each candidate is then verified (wildcards / gaps / nibbles / alternation / offset / nocase) |
.ldb PCRE subsignatures |
regex objects compiled lazily, gated by the trigger expression |
linear-time, DoS-safe regex |
.hsb / .hdb |
a size-keyed hash table | whole-file digest lookup |
.mdb / .msb |
a section-hash table | per-PE-section digest lookup |
.cdb |
container-metadata matchers | matched on archive members (name/size/encryption/position) |
.imp |
a size-constrained import-hash map | PE imphash lookup |
.cbc |
a sandboxed bytecode interpreter (no JIT) | trigger-gated programs run on extracted buffers |
.yar / .yara |
a native YARA engine | near-full YARA, no runtime codegen |
So the engine is really: one Aho-Corasick automaton (fed by ndb + ldb literal subsigs) + several cheap hash tables + lazy regex + interpreters. Almost all of the memory cost is the automaton — see the database internals.
How the crates compose
Section titled “How the crates compose”exav is a small Cargo workspace, not one binary. Four front-ends drive one
engine: the CLI (which is also the daemon), the WASI build, the archive grep,
and the WebAssembly bindings published to npm. exav-core is the engine;
exav-unpack does extraction; exav-update sits to the side, feeding fresh
signatures out of band.
Every arrow points down. Nothing below calls anything above it, which is what lets the extraction crates be taken on their own.
A scan flows through it like this:
The five verdicts are the whole output vocabulary. Anything that stops a scan
early produces one of the last three rather than Clean — see
Verdicts & exit codes.
The split is not cosmetic. Extraction touches hostile bytes first and hardest,
so it lives in its own #![forbid(unsafe_code)] crate with its own budget and
panic containment; a malformed archive cannot reach the engine, let alone the
host. It also means you can take one piece — a build system that needs to look
inside archives does not need a virus scanner, and a reverse engineer who wants
to unpack a packed executable needs neither.
exav-pe-emu sits below the extractor for the same reason. Running a packer’s stub
is the one place where the scanner executes attacker-authored control flow
rather than parsing attacker-authored data, so it is its own crate with its own
budgets, its own #![forbid(unsafe_code)], and no way to reach a syscall.
Each crate has its own page under Subprojects, with install instructions, API, and examples.
Recursive unpacking, bounded
Section titled “Recursive unpacking, bounded”The exav-unpack crate recursively extracts archives and structured documents
(see Supported formats). Every path runs under
decompression-bomb defenses: output-byte, ratio, file-count, recursion-depth,
and cumulative scan-byte budgets. A bomb is LIMITS-EXCEEDED, never OK; a
member with an unsupported codec or encryption is UNSCANNABLE /
PASSWORD-PROTECTED, never silently dropped — the
never-silent invariant reaching all the way down.
Content-based typing
Section titled “Content-based typing”File type is decided by magic bytes, never by extension — an executable
renamed .jpg is still typed and scanned as an executable.