exav parses untrusted, attacker-controlled files. The parsing and scanning engine is written in safe Rust, which removes the memory-corruption bug class that dominates C scanners' CVE history.
exav's own code that reads hostile file content has no unsafe. exav-core
(file typing, signature/CVD parsing, PE parsing, the bytecode interpreter, the
YARA engine), exav-unpack (every archive/document decoder), exav-x86 (the
instruction decoder), exav-pe-emu (the packer emulator), exav-grep and
exav-update all carry #![forbid(unsafe_code)], which the compiler enforces.
Some third-party decoders they call do use unsafe internally; the
dependencies page counts it per
crate.
One crate does not: crates/exav-unpack-wasm, the WebAssembly bindings
published to npm. #[wasm_bindgen] expands to unsafe, and forbid cannot be
overridden from inside, so the attribute has to be absent for the crate to
build at all. The decoders it exposes are the same forbid-carrying
exav-unpack code; the exception covers the generated binding layer.
exav's own unsafe lives in the exav binary, almost all of it in
crates/exav/src/daemon.rs, and none of it touches scanned bytes:
- Process control for the prefork worker pool:
fork,waitpid,kill,_exit,getpid/getppid. - Signal and resource-limit setup:
sigaction/signal/sigemptyset,setitimer(the per-job watchdog),setrlimit(the kernel-enforced per-worker caps), and theSIGPIPEdisposition inmain.rs. - SCM_RIGHTS file-descriptor passing over the local clamd-compatible socket
(
recvmsg/sendmsg+File::from_raw_fd): the daemon'sFILDEScommand and the client's--send-as fd, required forclamdscan --fdpassinterop. The peer is a local-socket client. - Socket creation mode:
umaskaround the daemon'sbind, so the Unix socket is created with no permissions and carries the mode its address asked for before any client can reach it, whatever mask the daemon inherited.
The ICAP listener, whose parsers read straight off the network, carries
#![forbid(unsafe_code)] of its own. The residual unsafe in third-party
dependencies (compression/crypto SIMD, syscall shims) is inventoried at
https://exav.org/project/dependencies/.
The residual risks for a memory-safe scanner are denial of service (panics, unbounded memory, infinite loops) and detection evasion, which is where the hardening below focuses.
A scanned file is fully hostile and may be malformed or crafted. Against one, exav must:
- contain a parser panic to the container it came from, reported rather than called clean;
- never report a file as clean (
OK) unless it was actually fully scanned: anything that prevents a full scan is reportedLIMITS-EXCEEDED,UNSCANNABLEorPASSWORD-PROTECTED(statusPARTIAL, exit code 3); - stay within its budgets, which bound decompressed bytes, per-member size, compression ratio, file count, recursion depth and emulation steps.
A signature database is a trusted input. Load only databases you trust.
Detection content is code (.cbc bytecode programs run on an interpreter, and
YARA rules compile to automata), so the budgets that apply to it are failsafes
against exav's own bugs, not defences against a hostile author. The instruction
cap on a bytecode program bounds a runaway loop; it is not sized to make a
malicious program safe, and nothing checks that a database's rules are
well-intentioned. A database fetched over the network is trusted to the same
degree as the mirror it came from.
To scan with a database you do not trust, run exav under a sandbox that bounds it from outside: the WebAssembly build, or the prefork daemon, whose workers carry kernel-enforced address-space and CPU limits and are replaced when killed.
Bounds are not absolute. Two failure modes are outside what in-process
budgets can reach, in any deployment: an allocation large enough to abort the
process (Rust aborts rather than unwinding, so the panic boundary does not catch
it), and unbounded recursion inside a single file's parser, which the
container-nesting limit does not govern. The daemon contains both with
RLIMIT_AS and worker replacement: one hostile file costs one job. A one-shot
run applies the same rlimits when --max-process-bytes / --max-scan-secs are
given, but with no second process behind it the run ends rather than continuing.
A library embedding has neither unless the host process sets the rlimits itself,
and should not be treated as though it did.
Counting allocations in-process would not change this. A refusal only becomes a
recoverable error where the allocating code asked fallibly (try_reserve), which
across exav's decoder dependencies is a handful of files out of hundreds;
everywhere else Rust routes allocation failure to an abort, so a ceiling would
abort on allocations the machine could have served. Address space is the layer
that can refuse safely, which is why the bound lives there.
- Bounded extraction. Recursive unpacking is governed by a budget: total
decompressed bytes, per-member size, compression ratio, file count, and
recursion depth. The budget is reserved before each member is read, so peak
memory across an archive (and nested archives) cannot exceed the total cap.
Hitting any bound yields
LIMITS-EXCEEDED, never a clean result. - No extraction to named paths. Members are decoded in memory. An object too
large to hold goes to an unnamed temporary file under
--spill-dir(offto never write one), never to a path taken from the archive, which removes the zip-slip / path-traversal / symlink class. - No silent skips. A file too large to hold gets the full scan through a
block cache; a check that needs it whole and could not run makes it
LIMITS-EXCEEDEDrather than cleared. An unsupported codec isUNSCANNABLEand an encrypted memberPASSWORD-PROTECTED, neverOK. - 64-bit sizes/offsets throughout; release builds enable
overflow-checksso a wrapping size calculation panics (and is then contained) rather than silently bypassing a limit. - Panic isolation. Each container walk runs under
catch_unwind; a parser panic is reported for that container and does not abort a batch. - Fuzzing. Every parser (file typing, signature text, CVD container, archive
extraction, PE) and the full pipeline has a
cargo-fuzztarget underfuzz/. CI runs a short smoke pass per target; extended fuzzing is recommended before production use. - Dependency auditing. CI runs
cargo auditandcargo denyagainst the RustSec advisory database.
Please report security issues privately via GitHub Security Advisories ("Report a vulnerability" on the repository's Security tab) rather than a public issue. We aim to acknowledge within a few days.