Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Layer 0 spec — fs family, inotify sub-module (file monitoring)

Technical specification — Version 1.0 (target kernel: Linux 6.12 LTS). Extension of the fs family.

Position

This document specifies the inotify primitive of layer 0, a missing piece identified while specifying air-filesystem (layer 1): AirFileSystemWatcher needs it and no inotify wrapper existed. It is a small extension of the fs family (sub-module air-sys-syscall::fs::inotify), in the FD-event-driven spirit of signalfd/timerfd/eventfd and the uevent socket of device.

Emergence (doc-first method). This gap was revealed by the specification of layer 1 — exactly the expected mechanism: consuming layer 0 surfaces its holes. This extension must be implemented before the watchers of air-filesystem (coordinated PR).

Scope. inotify (file change notification, unprivileged, path-based). Out of scope: fanotify (monitoring at the scale of a mount/FS + access decisions, privileged) — a distinct primitive, to be produced later for a security/audit service (layer 5), not required here.

Cross-cutting characteristics (consistent with layer 0):

  1. CLOEXEC by default on the inotify FD.
  2. Decoding = mirror of the kernel format: reading the FD yields a stream of struct inotify_event of variable size (a name field follows); it is decoded via a borrowed, zero-allocation iterator (same approach as the uevent parser of device, and the SignalFdInfo precedent). 2 bis. Zero-loss (ADR-032): all events from a read buffer are yielded; a truncated event (buffer too short) is signaled, not swallowed.
  3. No generic ioctl (ADR-021 c.3) — inotify has its dedicated syscalls.
  4. name = bytes (&[u8]), no presumed UTF-8 (Principle 3).

Types (pure → air-sys-types::fs)

#![allow(unused)]
fn main() {
/// Watch descriptor (returned by `inotify_add_watch`). Typed newtype
/// (never a raw `i32`) — ADR-029.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WatchDescriptor(i32);

bitflags! {
    /// Event / option mask (`IN_*`). Kernel names preserved (ADR-029).
    pub struct InotifyEventMask: u32 {
        const ACCESS        = 0x0000_0001;
        const MODIFY        = 0x0000_0002;
        const ATTRIB        = 0x0000_0004;
        const CLOSE_WRITE   = 0x0000_0008;
        const CLOSE_NOWRITE = 0x0000_0010;
        const OPEN          = 0x0000_0020;
        const MOVED_FROM    = 0x0000_0040;
        const MOVED_TO      = 0x0000_0080;
        const CREATE        = 0x0000_0100;
        const DELETE        = 0x0000_0200;
        const DELETE_SELF   = 0x0000_0400;
        const MOVE_SELF     = 0x0000_0800;
        // read bits (set by the kernel in the event):
        const UNMOUNT       = 0x0000_2000;
        const Q_OVERFLOW    = 0x0000_4000;
        const IGNORED       = 0x0000_8000;
        const ISDIR         = 0x4000_0000;
        // watch-add options:
        const ONLYDIR       = 0x0100_0000;
        const DONT_FOLLOW   = 0x0200_0000;
        const EXCL_UNLINK   = 0x0400_0000;
        const MASK_ADD      = 0x2000_0000;
        const ONESHOT       = 0x8000_0000;
    }
}

bitflags! {
    pub struct InotifyFlags: i32 {
        const NONBLOCK = 0x800;    // IN_NONBLOCK (= O_NONBLOCK)
        const CLOEXEC  = 0x80000;  // IN_CLOEXEC (= O_CLOEXEC, set by default)
    }
}
}

RAII + wrappers (air-sys-syscall::fs::inotify)

#![allow(unused)]
fn main() {
/// inotify instance: owns an `OwnedFd`, closed on `Drop`.
pub struct Inotify { /* OwnedFd */ }

pub fn inotify_init(flags: InotifyFlags) -> Result<Inotify, Errno>;

impl Inotify {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;

    /// Adds (or updates) a watch on `path` for the `mask` events.
    /// `path`: `&CStr` (NUL-terminated bytes, layer 0 convention).
    pub fn add_watch(&self, path: &CStr, mask: InotifyEventMask)
        -> Result<WatchDescriptor, Errno>;

    /// Removes a watch.
    pub fn remove_watch(&self, wd: WatchDescriptor) -> Result<(), Errno>;

    /// Reads a batch of events **into `buffer`** and decodes it without allocation.
    /// The returned `InotifyEvents` **borrows** `buffer`.
    pub fn read_events<'b>(&self, buffer: &'b mut [u8])
        -> Result<InotifyEvents<'b>, Errno>;
}
}

Underlying syscalls. inotify_init1 (x86_64 no.294, ARM64 no.26), inotify_add_watch (x86_64 no.254, ARM64 no.27), inotify_rm_watch (x86_64 no.255, ARM64 no.28), read for the events. IN_CLOEXEC set by default. Available since Linux 2.6.27 (inotify_init1).

Preconditions / errors. add_watch: ENOSPC (limit max_user_watches), ENOENT (path), EACCES. read_events: EAGAIN (NONBLOCK with no event), EINVAL (buffer too small for the next event — the caller enlarges it). remove_watch: EINVAL (invalid wd).

Buffer size recommendation. One event = 16 bytes of header + name (up to NAME_MAX+1). Recommend ≥ 4096 bytes; indicative constant INOTIFY_RECOMMENDED_BUFFER_SIZE = 4096.

The decoded stream: InotifyEvents / InotifyEvent

#![allow(unused)]
fn main() {
/// Borrowed view over a buffer filled by `read`: a sequence of variable-size
/// events. Zero-alloc iterator.
pub struct InotifyEvents<'b> { /* cursor over &'b [u8] */ }

impl<'b> Iterator for InotifyEvents<'b> {
    type Item = InotifyEvent<'b>;
    fn next(&mut self) -> Option<Self::Item>;
}

pub struct InotifyEvent<'b> {
    pub wd: WatchDescriptor,
    pub mask: InotifyEventMask,
    pub cookie: u32,          // correlates MOVED_FROM / MOVED_TO
    pub name: Option<&'b [u8]>, // name (bytes) if present, without the padding NUL
}
}

Decoding (ABI mirror). Each record: wd:i32, mask:u32, cookie:u32, len:u32, then len bytes of name (NUL-padded). The decoder advances by 16 + len at each step, without copying; name is sliced into the caller’s buffer (padding NUL removed). Slicing via get() (never panicking indexing). A final truncated record (buffer cut mid-way) → the iterator stops cleanly and read_events can signal it (consistent with ADR-032: a half event is not swallowed).

Non-recursive (usage note). inotify watches one directory per watch, not its sub-directories. Recursive monitoring (adding one watch per sub-directory, handling new folders via CREATE|ISDIR) is logic → it is AirFileSystemWatcher (layer 1) that carries it, not layer 0.

Summary

FunctionRole
inotify_initcreates the instance (RAII Inotify, CLOEXEC)
Inotify::add_watch / remove_watchadds/removes a watch on a path
Inotify::read_eventsreads + decodes (borrowed zero-alloc iterator)
Inotify::as_fd / into_fdevent loop integration / ownership transfer

Total: ~5 public functions. Types added to air-sys-types::fs: WatchDescriptor, InotifyEventMask, InotifyFlags, InotifyEvents<'b>, InotifyEvent<'b> (≈ 5).

Tests

  • Integration (real kernel): inotify_initadd_watch on a temporary directory → create/modify/move/delete a file → read_events yields the expected events (CREATE, MODIFY, MOVED_FROM/MOVED_TO correlated by cookie, DELETE), with the right name; remove_watch; Q_OVERFLOW under a burst.
  • Pure decoder: synthetic buffers (multi-event, with/without name, final truncated record) — zero panic, zero OOB, zero alloc (test allocator that panics on alloc).
  • Property-based (proptest): for any &[u8], the iterator terminates and never panics.
  • Fuzzing (cargo-fuzz): the decoder ingests external data (kernel buffer) → mandatory fuzz harness on InotifyEvents::parse (Principle 3).
  • Coverage 100 % lines + branches; resource errors (ENOSPC/EMFILE) not triggerable in CI → COVERAGE-EXCEPTIONS.md (STRUCTURAL category).

Core decisions

  1. inotify as a sub-module of fs (fs::inotify), not a separate family — it is filesystem by nature, and small.
  2. Borrowed zero-alloc decoding of the variable-size inotify_event (uevent/SignalFdInfo precedent); zero-loss (ADR-032).
  3. fanotify out of scope — a distinct, privileged primitive, for a later security/audit service (layer 5).
  4. Recursion = layer 1 (AirFileSystemWatcher), not layer 0.

Document license: MPL 2.0 Status: Technical specification of the air-sys-syscall::fs::inotify sub-module (layer 0), target kernel 6.12 LTS. Extension of the fs family.