Linux fanotify Permission Events: Building Access Mediation That Fails Safely
How fanotify groups mark filesystems, pause opens for an allow-or-deny response, expose queue overflow, and fail open when a policy daemon disappears.
Linux fanotify can report filesystem activity to a userspace process. In its permission-event modes it can do more: the kernel pauses selected operations, delivers an event to a policy daemon, and waits for an explicit allow or deny response. Antivirus scanners and access-control agents can use that mechanism to inspect a file before an open or execution continues.
That power comes with a difficult availability contract. A slow daemon stalls callers. A full queue loses observability. Closing the fanotify group allows permission events that have not received a response. A production design must therefore treat crash behavior, time limits, queue saturation, and filesystem coverage as policy decisions, not implementation details.
A group defines notification semantics
fanotify_init() creates a group and returns a file descriptor. The initialization flags select the notification class and event-reporting format. A notification-only group observes events but cannot request permission decisions. Permission events require a content or pre-content class and kernel support for fanotify access permissions.
The group file descriptor is readable and works with poll(2), epoll(7), and similar event loops. Each read returns one or more variable-length records beginning with fanotify_event_metadata. Code must advance using the documented event-length macros, validate the metadata version, and reject a malformed length rather than indexing into the next record optimistically.
Some report modes provide an open file descriptor for the affected object. Other modes identify objects through filesystem handles and additional information records. The selected format determines what the daemon can inspect, so the parser should be version-aware and ignore unknown optional information records by their declared lengths.
Marks choose the coverage boundary
fanotify_mark() attaches or removes event masks on an inode, mount, or filesystem. Marking a directory inode does not recursively watch every descendant as a general rule. For tree-wide coverage, a mount or filesystem mark is usually the intended boundary, subject to the event and filesystem constraints documented for that mark type.
A mount mark follows activity on the marked mount, while a filesystem mark can cover the filesystem across mount points. The choice affects containers, bind mounts, and remount operations. Record the filesystem ID and mount topology at startup, and watch for topology changes rather than assuming the initial namespace remains static.
Event masks can include notification events such as open, access, modification, close, move, and delete, plus permission events such as FAN_OPEN_PERM, FAN_OPEN_EXEC_PERM, and FAN_ACCESS_PERM. Newer kernels add further events, so compile-time availability and runtime support both need testing.
Every permission event needs a response
For a permission event, userspace writes a fanotify_response back to the group descriptor. The response identifies the event’s object descriptor and chooses FAN_ALLOW or FAN_DENY. The daemon must close per-event file descriptors after it finishes with them.
struct fanotify_response response = {
.fd = metadata->fd,
.response = trusted ? FAN_ALLOW : FAN_DENY,
};
ssize_t written = write(fanotify_fd, &response, sizeof(response));
close(metadata->fd);
if (written != sizeof(response))
handle_response_failure();
The decision should be tied to stable facts gathered from that event, not just a pathname resolved later. Names can change between lookup and inspection. Depending on report mode and policy, use the supplied descriptor or file handle, inspect metadata with race-aware APIs, and make the access decision before releasing the event.
The response path must remain responsive even when scanning is expensive. A bounded worker pool and explicit deadlines prevent one crafted file from consuming every decision worker. Define whether timeout allows or denies access and understand that the kernel has its own behavior if the group disappears.
Daemon loss is a fail-open path
When the fanotify group file descriptor closes, the kernel removes its marks. Pending permission events that have not been answered are permitted. This prevents abandoned kernel waiters from hanging forever, but it also means a crashed access-control daemon does not automatically produce fail-closed security.
If the threat model requires mandatory denial when userspace is unavailable, fanotify alone is not that guarantee. A service manager can restart the daemon, health checks can remove the host from service, and a separate kernel-enforced mechanism such as LSM policy can establish a baseline. None of those converts the close behavior retroactively for requests already released.
Test the exact crash path: stop the daemon while permission requests are outstanding and record what callers observe. Alert on any group reinitialization and verify that all marks are restored before the node returns to a trusted state.
Queue overflow destroys event continuity
The kernel queues events for the group up to its configured limit. When the queue overflows, it reports FAN_Q_OVERFLOW, but the lost events cannot be reconstructed from fanotify itself. An unlimited queue flag exists for sufficiently privileged callers, yet moving the limit to unbounded kernel memory creates a denial-of-service risk rather than removing it.
For audit-style notification, overflow means the observer no longer has a complete history and should start a documented reconciliation scan. For permission events, backpressure and blocked callers may appear before or alongside resource exhaustion. Monitor queue depth indirectly through processing delay, event rate, worker utilization, and overflow count.
Coalescing also matters. Identical notification events may be merged while they remain unread, so event count is not necessarily operation count. A security audit trail requiring every actor and every access belongs in a purpose-built audit mechanism, not in assumptions layered on fanotify notifications.
Know what fanotify cannot observe
The fanotify(7) manual documents important gaps. File changes caused through memory mappings are not comprehensively represented as ordinary modification events. Activity performed remotely on a network filesystem may not generate events on the local client. Directory monitoring is not automatically recursive, and notification describes filesystem objects rather than supplying a complete causal history.
Privileges and namespace behavior have also evolved across kernel versions. Creating powerful groups or using unlimited resources can require capabilities in the relevant user namespace, and some event combinations are invalid with some reporting modes. Probe the live kernel and fail startup if a mandatory mask or mark cannot be installed.
Do not silently downgrade a permission event to notification-only monitoring. Record the requested init flags, accepted masks, every mark result, kernel release, and filesystem type. That startup manifest becomes evidence of actual coverage.
Validate policy with adversarial I/O
Exercise allowed and denied opens, execution, renames during inspection, bind mounts, files deleted before a decision, large bursts, queue overflow, daemon pause, daemon crash, and service restart. Include callers in the namespaces and credential contexts used in production.
Measure decision latency at percentiles, not just the average. One slow tail can serialize application startup or package installation. Keep the policy daemon and its dependencies outside the paths it mediates, or add narrow exclusions that prevent it from deadlocking while opening its own executable, rules, libraries, logs, or quarantine store.
Fanotify permission events are a powerful interception primitive, but the system around them determines whether they are safe. A defensible deployment proves mark coverage, responds within a bound, detects overflow, understands fail-open group closure, and keeps a kernel-enforced baseline for controls that must survive userspace failure.
Related:
- Linux Capabilities: Fine-Grained Privileges Beyond root and setuid
- io_uring Explained: Linux’s Modern Asynchronous I/O Interface
Sources: