Daemon mode
--listen clamd://… loads the signature database once and serves scans over a
socket, so callers pay no per-scan load cost. The wire protocol is a subset of
ClamAV’s clamd protocol, so existing tooling (clamdscan, milters, clamd
client libraries) talks to exav unchanged.
The daemon refuses to start without signatures, so fetch them first (see
Signatures). A prebuilt .exavdb starts much faster than
raw .cvd files: see what it costs.
Start a daemon
Section titled “Start a daemon”exav --listen /run/clamav/clamd.ctl -d /var/lib/exav # a Unix socketexav --listen 0.0.0.0:3310 -d /var/lib/exav # TCP insteadexav --listen clamd://0.0.0.0:3310 -d /var/lib/exav # the same, spelled outThe protocol travels in the address, and with no scheme it is clamd. A leading
/ is a socket path; anything else is host:port. Nothing listens without
--listen. Creating a socket under /run/clamav needs root or the clamav
user; as yourself, use a path such as $XDG_RUNTIME_DIR/exav.sock.
Then scan through it:
exav --connect /run/clamav/clamd.ctl /data # exav as its own clientexav --connect /run/clamav/clamd.ctl --send-as contents /data # send the bytesexav --connect /run/clamav/clamd.ctl --send-as fd /data # send descriptorsclamdscan --stream file.bin # the official ClamAV clientclamdscan --fdpass file.bin # fd-passing also works--listen accepts connections and --connect makes one. clamdscan takes the
address from its clamd.conf (LocalSocket, or TCPSocket and TCPAddr).
By default the client sends paths, which the daemon opens itself, so the daemon
has to be able to see them. --send-as contents sends the bytes (INSTREAM) and
--send-as fd sends an open descriptor (FILDES, Unix socket only); either way
the daemon needs no access to the path, which suits a container, a remote scanner
or a daemon running as another user. - (stdin) is always sent as contents.
A directory names its whole tree in all three modes, as it does for clamdscan.
The client walks it and sends one request per file, so --exclude / --include
apply to the tree and each request has one reply. The parts of a byte-split
archive (x.001, x.002, …) are sent together so the daemon can rejoin them
(see split archives). By path, a directory holding such parts
goes as one CONTSCAN, and the client’s filters and --no-recursive do not
apply to it; with contents or fd, the parts of each directory go in one
EXINSTREAM MULTI request.
Who may connect
Section titled “Who may connect”The socket is created with mode 0600: only the user running the daemon can
connect. Anyone the mode admits can submit scans and read verdicts.
exav --listen 'clamd:///run/clamav/clamd.ctl?mode=660' -d /var/lib/exav?mode= takes octal permission bits, the value clamd.conf calls
LocalSocketMode. Prefer a group (660, with the daemon’s group shared with its
clients) over 666. The mode is applied at creation, whatever umask the daemon
inherited, and a value that is not a workable mode is refused.
The mode belongs to the address because it is a property of that socket. A
host:port has no file to set permissions on, so asking for one there is refused;
bind a TCP listener to the interface that should reach it and firewall the rest.
The other address options are listed in
what belongs to one listener.
Protocol coverage
Section titled “Protocol coverage”Supported commands: PING, VERSION, VERSIONCOMMANDS, STATS, RELOAD,
SCAN / CONTSCAN / MULTISCAN / ALLMATCHSCAN <path>, INSTREAM, FILDES
(fd passing), IDSESSION / SESSION / END, SHUTDOWN / QUIT (below), and
the exav extensions
EXINSTREAM, EXINSTREAM MULTI (below) and SCANURL <url>. SCANURL scans an
http(s):// object through range requests (see
scanning over HTTP) and is honoured, and
listed in VERSIONCOMMANDS, only in a build with http-scan
(--features http) and with --allow-http-scan. Commands may be z- or
n-framed.
EXINSTREAM takes INSTREAM’s framing and answers one line of JSON with the
verdict fields, for instance
{"v":1,"status":"FOUND","signature":"…","location":"a.zip/b.exe"}, or
{"v":1,"status":"PARTIAL","category":"LIMITS-EXCEEDED","reason":"…"}.
STATS carries the clamd fields clamdtop parses, then two exav lines: how much
this process scanned, how long it took, and (with --profile) which matcher the
time went to. See finding where the CPU went.
By default a file that could not be fully scanned comes back as … ERROR
carrying LIMITS-EXCEEDED, UNSCANNABLE or PASSWORD-PROTECTED, because an
unknown word would be read as OK by existing clients; --partial-as changes
that (see on the clamd wire).
The deprecated STREAM (separate port) command is not implemented; INSTREAM
replaces it. SHUTDOWN (and its alias QUIT) is refused by default so a client
cannot stop the daemon; send the process a signal instead, or pass
--allow-shutdown to honour it.
Validated against clamdscan (--fdpass, --stream, --multiscan), the Python
clamd library, and raw socat.
Split archives
Section titled “Split archives”An archive spread across big.zip.001, .002, .003 is one file cut at
arbitrary byte offsets. No part holds the archive whole, so scanning them one at
a time reports three clean files and never looks at what the archive holds. (The
first part of a 7z set is PARTIAL on its own: the header that says how to
decode it is in the last part.)
CONTSCAN and MULTISCAN rejoin such sets per directory and scan the archive
they make. The reply is still one line per file, and every part carries the
archive’s verdict, a FOUND replacing a part’s own PARTIAL:
/data/big.zip.001: Eicar-Signature FOUND (in big.zip)/data/big.zip.002: Eicar-Signature FOUND (in big.zip)A part of an infected archive is the file you have to act on. A set with a gap in
its numbering comes back UNSCANNABLE: those bytes belong to an archive nothing
can read any more. A missing last part cannot be seen from the names.
EXINSTREAM MULTI does the same for streamed input, with several files in one
request:
EXINSTREAM MULTI<u32 name_len><name> <u32 len><data>…<u32 0> per file<u32 0> ends the requestNames are labels; nothing opens or resolves them as paths. The reply is one line
of JSON, {"v":1,"files":[…]}, with one entry per file carrying the same verdict
fields as a single EXINSTREAM, plus "set" when the verdict came from a
rejoined archive. One request carries at most 1024 files with names up to 4096
bytes, and holds at most 256 MiB in RAM and 8 GiB in temporary files across
them; past any of these the request is answered with a JSON error. A file the
daemon could not hold in full has what it held scanned and is answered
LIMITS-EXCEEDED unless something is found; the rest of the request is still
scanned. The reply carries "truncated": true when the request ended
before its terminator, so some files never arrived.
RAR .partN and ZIP .zNN volumes are not rejoined this way: each carries its
own headers, so concatenating them produces garbage that still looks like an
archive. Each is scanned on its own.
Size over the wire
Section titled “Size over the wire”SCAN <path> sends only the path, and the daemon scans the file as the CLI would.
An INSTREAM body is buffered before it is scanned: in memory up to
--spill-threshold-bytes (16 MiB), then in a temporary file up to
--max-spill-bytes (2 GiB), with --max-total-spill-bytes bounding what one
process holds at once. A stream past those is scanned as far as it was held and
answered LIMITS-EXCEEDED. See
buffering a stream.
When the daemon has its answer before the stream ends, it reads and discards up to 64 MiB of the rest before replying, so the reply is not lost to a reset connection; the ICAP guide explains why. ICAP bodies are buffered the same way.
Worker pool and per-job limits
Section titled “Worker pool and per-job limits”By default the daemon runs a prefork pool of one worker process per CPU core
(--workers threads selects the in-process thread model). Workers are forked
after the database loads, so they share it copy-on-write. Each handles one job at
a time under kernel-enforced limits:
| Flag | Default | Bounds |
|---|---|---|
--max-scan-secs |
120 s | wall clock (and CPU time, RLIMIT_CPU) |
--max-process-bytes |
2 G | address space (RLIMIT_AS) |
--max-jobs-per-worker |
1000 | jobs before a worker is recycled (bounds leaks) |
The 2 G is lowered at startup to what the machine can back, and what a scan holds is kept inside it: one object at most a quarter, since an image decoded from it and its grey copy can take as much again twice, and the whole scan whatever leaves room for those. Sizing a server works this through for a 4 GiB machine.
A worker past its limit answers its client, exits and is replaced, without touching the rest of the pool. The in-engine limits (scan bytes, ratio, recursion, emulation steps) fire first.
Logging what was answered
Section titled “Logging what was answered”--log FILE records one line per scan the daemon serves, appended and flushed as
it goes:
exav --listen /run/clamav/clamd.ctl --log /var/log/exav/exav.log -d /var/lib/exavThe daemon refuses to start if it cannot open the file for appending, so its
directory must exist and be writable by the daemon.
The packaged unit makes /var/log read-only, so with systemctl edit exav add
LogsDirectory=exav (which creates /var/log/exav for the unit’s user) along
with the --log flag on ExecStart.
The daemon’s results otherwise go only to the client that asked, so this is the
operator’s record. A path scan is logged under its path, a streamed scan as
stream:, a passed descriptor as fd:. PING, VERSION and STATS are not
logged. The file is opened for appending and each line is written in a single
call, so lines from different workers never interleave.
SIGHUP reopens the file, as it does for clamd, so logrotate can move it:
/var/log/exav/exav.log { weekly postrotate systemctl kill -s HUP exav.service endscript}Hot-reloading signatures
Section titled “Hot-reloading signatures”The daemon refreshes signatures without a restart on any of:
- the clamd
RELOADcommand (whatfreshclam’sNotifyClamdsends; the reply isRELOADING); SIGUSR2(kill -USR2 <pid>,systemctl reload), as for clamd;- a change on disk at whatever
-dpoints at (see below); - a successful fetch by the built-in updater.
On each, the supervisor loads the new database and forks a new pool from it.
Workers of the old pool finish the job they are serving before they exit, so a
reload never cuts a scan short; ICAP requests in progress likewise finish, and
idle ICAP connections are closed. The old pool gets --max-scan-secs plus 5
seconds for this (10 minutes when --max-scan-secs is 0). A process still
there after that, held by an open IDSESSION or a client that never finishes
its request, is stopped and logged; a worker stopped in the middle of a job
answers it with ERROR. Under --workers threads, and for ICAP served alone,
the new database is swapped in instead: a connection already open keeps the one
it started with. If the new source is empty or corrupt, the swap is refused and
the daemon keeps serving the current database.
SIGHUP does not reload (it reopens the log).
SIGTERM and SIGINT stop the daemon.
The watch polls the source
Section titled “The watch polls the source”Every ~10 seconds the supervisor checks the -d source and reloads when it
changed, as clamd’s SelfCheck does. It compares the name, size,
modification time and inode of every entry with what it saw when the serving
database was read, so a new or renamed file counts even when it carries the
same timestamp as the rest (some filesystems stamp from a coarse clock), and a
database replaced by one with an older mtime is still picked up. It watches
whichever form -d names:
- a directory of signature files: the whole tree, subdirectories included;
- a single database file (
-d db.exavdb): that file, which an atomic swap replaces.
A file rewritten in place with the same size within one tick of the
filesystem’s clock looks unchanged; send RELOAD after such a write.
Polling rather than kernel events is what makes it work across Docker volumes,
bind mounts and NFS, where inotify events do not propagate. The
dual-container pattern relies on
it (the updater renames a new .cvd into the shared volume and the scanner’s
next poll reloads), and a prebuilt-database
deployment reloads on swap too, with no explicit RELOAD.
As a managed service
Section titled “As a managed service”A systemd unit (packaging/exav.service, also shipped in the .deb) runs exav
in place of clamav-daemon on the same socket, reading the raw signatures
freshclam keeps in /var/lib/clamav. It runs as the clamav user, which the
package does not create, so install it where ClamAV is installed:
sudo systemctl disable --now clamav-daemon.socket clamav-daemon.servicesudo systemctl enable --now exavclamdscan --ping 1Stopping clamav-daemon.socket matters on Debian and Ubuntu: while it holds the
socket, the first connection starts clamd again. systemctl reload exav sends
SIGUSR2.
See Migrating from ClamAV for the full swap, and the Docker guide for the containerised daemon.