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) :
- CLOEXEC par défaut sur le FD inotify.
- Décodage = miroir de format kernel : la lecture du FD rend un flux de
struct inotify_eventde taille variable (un champnamesuit) ; on le décode via un itérateur emprunté, zéro allocation (même approche que le parseur uevent dedevice, et précédentSignalFdInfo). 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é. - Pas d’ioctl générique (ADR-021 c.3) — inotify a ses syscalls dédiés.
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’estAirFileSystemWatcher(couche 1) qui la porte, pas la couche 0.
Récapitulatif
| Fonction | Rôle |
|---|---|
inotify_init | crée l’instance (RAII Inotify, CLOEXEC) |
Inotify::add_watch / remove_watch | ajoute/retire un watch sur un chemin |
Inotify::read_events | lit + décode (itérateur emprunté zéro-alloc) |
Inotify::as_fd / into_fd | inté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_init→add_watchsur un répertoire temporaire → créer/modifier/déplacer/supprimer un fichier →read_eventsrend les événements attendus (CREATE,MODIFY,MOVED_FROM/MOVED_TOcorrélés parcookie,DELETE), avec le bonname;remove_watch;Q_OVERFLOWsous 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 suralloc). - 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
- inotify en sous-module de
fs(fs::inotify), pas une famille à part — il est filesystem par nature, et petit. - Décodage zéro-alloc emprunté des
inotify_eventde taille variable (précédent uevent/SignalFdInfo) ; zéro perte (ADR-032). fanotifyhors périmètre — primitif distinct, privilégié, pour un service de sécurité/audit ultérieur (couche 5).- 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.