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

Spec couche 0 — Famille fs, sous-module inotify (surveillance de fichiers)

Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS). Extension de la famille fs.

Position

Ce document spécifie le primitif inotify de la couche 0, manquant identifié en spécifiant air-filesystem (couche 1) : AirFileSystemWatcher en a besoin et aucun wrapper inotify n’existait. C’est une petite extension de la famille fs (sous-module air-sys-syscall::fs::inotify), dans l’esprit FD-événementiel de signalfd/timerfd/eventfd et du socket uevent de device.

Émergence (méthode doc-d’abord). Ce manque a été révélé par la spécification de la couche 1 — exactement le mécanisme attendu : consommer la couche 0 fait apparaître ses trous. Cette extension doit être implémentée avant les watchers d’air-filesystem (PR coordonnée).

Périmètre. inotify (notification de changements de fichiers, non privilégié, basé sur les chemins). Hors périmètre : fanotify (surveillance à l’échelle d’un mount/FS + décisions d’accès, privilégié) — primitif distinct, à produire plus tard pour un service de sécurité/audit (couche 5), pas requis ici.

Caractéristiques transverses (cohérentes couche 0) :

  1. CLOEXEC par défaut sur le FD inotify.
  2. Décodage = miroir de format kernel : la lecture du FD rend un flux de struct inotify_event de taille variable (un champ name suit) ; on le décode via un itérateur emprunté, zéro allocation (même approche que le parseur uevent de device, et précédent SignalFdInfo). 2 bis. Zéro perte (ADR-032) : tous les événements d’un buffer lu sont rendus ; un événement tronqué (buffer trop court) est signalé, pas avalé.
  3. Pas d’ioctl générique (ADR-021 c.3) — inotify a ses syscalls dédiés.
  4. name = octets (&[u8]), pas d’UTF-8 présumé (Principe 3).

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

#![allow(unused)]
fn main() {
/// Descripteur de surveillance (retour d'`inotify_add_watch`). Newtype typé
/// (jamais un `i32` brut) — ADR-029.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WatchDescriptor(i32);

bitflags! {
    /// Masque d'événements / d'options (`IN_*`). Noms kernel conservés (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;
        // bits de lecture (positionnés par le kernel dans l'événement) :
        const UNMOUNT       = 0x0000_2000;
        const Q_OVERFLOW    = 0x0000_4000;
        const IGNORED       = 0x0000_8000;
        const ISDIR         = 0x4000_0000;
        // options d'ajout de watch :
        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, posé par défaut)
    }
}
}

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

#![allow(unused)]
fn main() {
/// Instance inotify : possède un `OwnedFd`, fermé au `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;

    /// Ajoute (ou met à jour) un watch sur `path` pour les événements `mask`.
    /// `path` : `&CStr` (octets terminés NUL, convention couche 0).
    pub fn add_watch(&self, path: &CStr, mask: InotifyEventMask)
        -> Result<WatchDescriptor, Errno>;

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

    /// Lit un lot d'événements **dans `buffer`** et le décode sans allocation.
    /// Le `InotifyEvents` retourné **emprunte** `buffer`.
    pub fn read_events<'b>(&self, buffer: &'b mut [u8])
        -> Result<InotifyEvents<'b>, Errno>;
}
}

Syscalls sous-jacents. inotify_init1 (x86_64 n°294, ARM64 n°26), inotify_add_watch (x86_64 n°254, ARM64 n°27), inotify_rm_watch (x86_64 n°255, ARM64 n°28), read pour les événements. IN_CLOEXEC posé par défaut. Disponible depuis Linux 2.6.27 (inotify_init1).

Préconditions / erreurs. add_watch : ENOSPC (limite max_user_watches), ENOENT (chemin), EACCES. read_events : EAGAIN (NONBLOCK sans événement), EINVAL (buffer trop petit pour le prochain événement — l’appelant agrandit). remove_watch : EINVAL (wd invalide).

Recommandation de taille de buffer. Un événement = 16 octets d’entête + name (jusqu’à NAME_MAX+1). Recommander ≥ 4096 octets ; constante indicative INOTIFY_RECOMMENDED_BUFFER_SIZE = 4096.

Le flux décodé : InotifyEvents / InotifyEvent

#![allow(unused)]
fn main() {
/// Vue empruntée sur un buffer rempli par `read` : suite d'événements de taille
/// variable. Itérateur zéro-alloc.
pub struct InotifyEvents<'b> { /* curseur sur &'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,          // corrèle MOVED_FROM / MOVED_TO
    pub name: Option<&'b [u8]>, // nom (octets) si présent, sans le NUL de padding
}
}

Décodage (miroir ABI). Chaque enregistrement : wd:i32, mask:u32, cookie:u32, len:u32, puis len octets de name (padded NUL). Le décodeur avance de 16 + len à chaque pas, sans copie ; name est tranché dans le buffer de l’appelant (NUL de padding retiré). Slicing via get() (jamais d’indexation paniquante). Un enregistrement final tronqué (buffer coupé au milieu) → l’itérateur s’arrête proprement et read_events peut le signaler (cohérent ADR-032 : on n’avale pas un événement à moitié).

Non récursif (note d’usage). inotify surveille un répertoire par watch, pas ses sous-répertoires. La surveillance récursive (ajouter un watch par sous-répertoire, gérer les nouveaux dossiers via CREATE|ISDIR) est de la logique → c’est AirFileSystemWatcher (couche 1) qui la porte, pas la couche 0.

Récapitulatif

FonctionRôle
inotify_initcrée l’instance (RAII Inotify, CLOEXEC)
Inotify::add_watch / remove_watchajoute/retire un watch sur un chemin
Inotify::read_eventslit + décode (itérateur emprunté zéro-alloc)
Inotify::as_fd / into_fdintégration event loop / transfert d’ownership

Total : ~5 fonctions publiques. Types ajoutés à air-sys-types::fs : WatchDescriptor, InotifyEventMask, InotifyFlags, InotifyEvents<'b>, InotifyEvent<'b> (≈ 5).

Tests

  • Intégration (kernel réel) : inotify_initadd_watch sur un répertoire temporaire → créer/modifier/déplacer/supprimer un fichier → read_events rend les événements attendus (CREATE, MODIFY, MOVED_FROM/MOVED_TO corrélés par cookie, DELETE), avec le bon name ; remove_watch ; Q_OVERFLOW sous rafale.
  • Décodeur pur : buffers synthétiques (multi-événements, avec/sans name, enregistrement final tronqué) — zéro panique, zéro OOB, zéro alloc (allocateur de test qui panique sur alloc).
  • Property-based (proptest) : pour tout &[u8], l’itérateur termine et ne panique jamais.
  • Fuzzing (cargo-fuzz) : le décodeur ingère des données externes (buffer kernel) → harnais de fuzz obligatoire sur InotifyEvents::parse (Principe 3).
  • Couverture 100 % lignes + branches ; erreurs ressources (ENOSPC/EMFILE) non déclenchables en CI → COVERAGE-EXCEPTIONS.md (catégorie STRUCTURAL).

Décisions de fond

  1. inotify en sous-module de fs (fs::inotify), pas une famille à part — il est filesystem par nature, et petit.
  2. Décodage zéro-alloc emprunté des inotify_event de taille variable (précédent uevent/SignalFdInfo) ; zéro perte (ADR-032).
  3. fanotify hors périmètre — primitif distinct, privilégié, pour un service de sécurité/audit ultérieur (couche 5).
  4. Récursivité = couche 1 (AirFileSystemWatcher), pas la couche 0.

Licence du document : MPL 2.0 Statut : Spécification technique du sous-module air-sys-syscall::fs::inotify (couche 0), cible kernel 6.12 LTS. Extension de la famille fs.