A Rust library for Linux fanotify: it parses what the kernel reports, resolves
file handles to paths, and writes permission decisions.
fanotify watches operations on objects, reports the process, and
in its permission classes holds the operation inside the kernel until you answer.
What it hands you is a byte stream, in one of three identities: an open descriptor
per event, a file handle plus fsid (FID), or a mount id. The FID form is the
one that scales to a whole filesystem, because the kernel holds no descriptor per
watched object — and it is variable-length records behind a fixed header, so a
fixed-stride reader parses garbage. This crate reads all three, byte for byte.
- All three identities, all nine info record types, and the fields 0.7.x used to
drop: both sides of a
FAN_RENAME, the mount id, the access range, filesystem errors, and the pidfd. FidEvent<'buf>borrows the buffer it was parsed from, so a parse copies no record's bytes.EventReaderowns the buffer and the event storage: one allocation for the first batch, none after it.PathResolverturns handles into paths, reading the store through a borrow and teaching it through a separatePathMemo— so a hit copies nothing, a store behind a lock can answer, and a batch of events about one directory costs oneopen_by_handle_atinstead of one per event. Plug in your own cache, or none.- Reading, resolving and learning are three calls with three signatures, and each
one says what it costs:
EventReader::readborrows,resolve_eventsonly reads,resolve_events_memolearns, andFanotify::read_eventsis the owned form for a batch that has to move to another thread. - Permission answers in all their forms, including the audit record and any record type a newer kernel adds — built without a heap buffer.
- Every errno is the kernel's. Nothing is pre-validated, so a refusal is the kernel's own and says which syscall refused it.
- No hidden state: no table of marked paths, no eviction policy, no silent re-scan. Marks live in the kernel; caches are yours.
[dependencies]
fanotify-fid = "0.9"Rust 1.88 or newer, one dependency (libc). A FID group needs no privilege;
resolving its handles needs CAP_DAC_READ_SEARCH.
cargo run --example batch_to_worker
cargo test
sudo -E cargo test -- --ignored --test-threads=1The suite runs against a real kernel, not a mock. tests/baseline.rs asserts
every legal group configuration and every combination that must not exist, and
reads the capability actually held, because the privilege check runs before the
legality check. tests/allocation.rs counts allocations to hold the API to its
zero-copy claims. tests/properties.rs asserts the invariants no example can
show. fuzz/ has three targets for malformed input. The 32 unsafe blocks are
syscall calls, unaligned reads of the kernel's records, and two OwnedFd
constructions, each with a SAFETY comment.
SPEC.md — the behavioural contract, the five legal group configurations, the privilege and kernel-version tables, and the test gates.
rustdoc — the API, and the authority whenever anything disagrees with prose.
examples/ — batch_to_worker reads a batch, copies it once and parses
it on a worker thread; prelude_usage is the shortest path in;
resolve_after_rename shows which cached entries a rename invalidates.
MIT