Skip to content

WASM sandbox

Two reasons to run the scanner inside a WASM sandbox:

  • You don’t trust the signatures: third-party .ndb sets, community YARA rules, anything you did not build yourself.
  • You don’t trust exav. It is a young parser of hostile input, and #![forbid(unsafe_code)] is not a boundary the kernel or a runtime enforces. The sandbox needs no assumption that exav’s own code is correct.

The same exav-core engine compiles to wasm32-wasip1 and runs under any WASI runtime (wasmtime, wasmer, wazero): you bring your own runtime, and a bug inside exav costs a crashed sandbox rather than a compromised host.

  1. Download exav-wasm-<tag>-wasm32-wasip1.wasm from a release and rename it exav.wasm, or build it from a clone of the repository (Rust 1.92 or newer):

    Terminal window
    git clone https://github.com/sylvinus/exav && cd exav
    rustup target add wasm32-wasip1
    cargo build --release --target wasm32-wasip1 -p exav-core --features wasi-bin
    cp target/wasm32-wasip1/release/exav-wasm.wasm exav.wasm
  2. Optionally strip debug names to make it smaller:

    Terminal window
    cargo install wasm-tools # if not installed
    wasm-tools strip -a exav.wasm -o exav.min.wasm && mv exav.min.wasm exav.wasm
  3. Scan a file under wasmtime, with the signatures mounted at /db and the files to scan at /files:

    Terminal window
    wasmtime \
    --dir /path/to/sigs::/db \
    --dir /path/to/files::/files \
    exav.wasm \
    /db /files/malware.exe

The first argument is the database: a directory of signature files, or a prebuilt .exavdb file. One that loads no real signatures is refused, as the native exav refuses it. The other arguments are files; a directory is not walked. Mount only the directories the module needs.

Each file gives one JSON object on stdout, in argument order: the ScanReport plus "file", or "file" and "error" for a file that could not be read. Progress goes to stderr:

Terminal window
wasmtime --dir ./sigs::/db --dir ./in::/in exav.wasm /db /in/a.exe /in/b.zip /in/gone \
2>/dev/null | jq -c .
{"file":"/in/a.exe","findings":[{"detail":"PE","label":"type"}],"verdict":"Clean"}
{"file":"/in/b.zip","findings":[{"detail":"ZIP","label":"type"}],"verdict":{"Infected":{"method":"Pattern","offset":4096,"signature":"Win.Trojan.Agent-1234"}}}
{"error":"No such file or directory (os error 44)","file":"/in/gone"}

verdict is "Clean" or an object named after the variant: Infected as above, or LimitsExceeded, Unscannable or PasswordProtected with a reason, such as {"Unscannable":{"reason":"..."}}. The exit code follows the native exav’s precedence.

Loading third-party signatures makes the signature parser part of your attack surface: a malicious .ndb could aim for a parser bug, excessive allocation, or an escape from the process. Natively, any of these reaches the host. In a WASM sandbox:

Native exav WASM sandbox
Signature trust full host access a memory-limited WASM instance
Runtime exav itself wasmtime or another WASI runtime
What a bug can reach the host the module’s own memory and mounted directories
Speed native slower
  • Memory is bounded: the module cannot reach memory outside its own linear memory.
  • No ambient system access: WASI limits file access to the mounted paths, and the module has no network.
  • Instruction limits: wasmtime can give the module a budget of executed instructions (“fuel”) and stop it when that runs out.
  • Panic isolation: a panic becomes a wasmtime trap; the host is unaffected.
  • No custom ABI: the module is a standard WASI command, so the host is the runtime itself, not code exav wrote.

A wasm32 module may grow to 4 GiB unless the runtime caps it. With wasmtime, set both limits on the command line (wasmtime run -W help lists them):

Terminal window
wasmtime run -W max-memory-size=3221225472 -W fuel=100000000000 \
--dir ./sigs::/db --dir ./in::/in exav.wasm /db /in/a.exe
  • No daemon mode: each run loads the database from scratch. For high-throughput scanning use a native exav --listen.
  • No stdin: files must be on a mounted filesystem.
  • No HTTP(S) scanning: the module cannot make network requests.
  • No flags: every scan uses the defaults. There is no --detect (PUA only from a .exavdb built with it), --all-matches, --passwords (only .pwdb passwords are tried) or --partial-as, and YARA’s filename variables are undefined.
  • Memory: the database and each scanned file, read whole, share the 4 GiB wasm32 address space. Loading raw signatures costs far more than a prebuilt database built natively, so give it one.
  • Native exav: full speed and features, with host access.
  • A container: similar isolation, but a full OS image rather than one .wasm file.