Antivirus/crates/hound-api/src
Hound aa22ddbc38 houndd: execution gate over fanotify, with a fail-open watchdog
FAN_OPEN_EXEC_PERM hands us the open and waits for an answer, so a
binary can be refused before it runs. inotify could only report what
had already happened.

Verified end to end as root against a dedicated tmpfs (examples/
gate-smoke.rs, three phases):

  benign binary          ran      7.2 ms
  EICAR binary           blocked  1.8 ms   never executed
  scanner stalled 5 s    ran      1.6 s    watchdog rescued 3 events

The third phase is the one that matters. A gate that can hold a process
forever is a machine-wedging bug wearing a feature's clothes, so the
watchdog answers ALLOW for anything unanswered past DEADLINE and counts
it. A missed detection is a bad day; a frozen machine ends the product.

Two things this cost, both worth recording:

* Scanning by re-opening the path deadlocks the daemon against itself.
  The open() lands on the watched mount and queues a permission event
  behind the one we are currently answering, and we cannot answer that
  one until we finish this one. Allowing our own pid does not help —
  the thread never gets back to the queue to apply the rule. The gate
  reads through the descriptor the kernel already handed it, with
  pread so the gated process still sees its own file offset. This is
  what hung the first smoke run.

* The watchdog can only rescue events it has been told about, and it
  learns of them when the queue is drained. Scanning on the draining
  thread makes every event behind a slow scan invisible to the
  deadline. Reader and workers are therefore separate threads: the
  reader never blocks on a scan, so every event is registered within
  microseconds of arriving.

Also:

- ScanEngine::scan_bytes — the seam the gate needs, since it must never
  scan by path. Engines that cannot do it return None and simply are
  not usable behind the gate.
- Settings gain exec_gate and exec_gate_paths, defaulting to OFF. It
  needs CAP_SYS_ADMIN and a root-filesystem mark holds every process on
  the box; that is not a default to ship before Phase 2 soak testing.
- ABI constants are defined locally rather than taken from libc, so a
  version bump cannot quietly change what we ask the kernel for.

78 tests pass, up from 57.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:29:01 -05:00
..
lib.rs houndd: execution gate over fanotify, with a fail-open watchdog 2026-08-20 22:29:01 -05:00