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

Spécification technique — Version 1.0

Vue d’ensemble de la famille

Le module air-sys-syscall::fs expose les primitives kernel de gestion du filesystem : ouverture et fermeture de fichiers, lecture et écriture, métadonnées, opérations sur les répertoires, manipulations de chemins, opérations spéciales (truncate, fsync, fallocate, copy_file_range, splice, etc.).

C’est la plus grande famille de la couche 0 par volume, avec environ 35 syscalls wrappés.

Périmètre de la famille.

Catégories d’opérations :

  1. Ouverture et fermeture : openat2, openat, close.
  2. I/O synchrone : read, write, pread, pwrite, readv, writev, preadv, pwritev.
  3. Positionnement : lseek.
  4. Métadonnées : statx, faccessat.
  5. Répertoires : mkdirat, getdents64, unlinkat, renameat2, linkat, symlinkat, readlinkat, mknodat.
  6. Permissions : fchmodat, fchownat, utimensat.
  7. Opérations spéciales : ftruncate, truncate, fsync, fdatasync, sync_file_range, fallocate, copy_file_range, splice, tee, vmsplice, flock, statfs, fstatfs.
  8. fcntl (opérations exposées individuellement, cf. ADR-021 convention 3).
  9. Handles persistants : name_to_handle_at, open_by_handle_at.

Caractéristiques transverses de la famille.

  1. Variantes *at préférées systématiquement. openat2 plutôt que open, mkdirat plutôt que mkdir, unlinkat plutôt que unlink, etc. Les variantes *at prennent un FD de répertoire comme base, ce qui élimine les races sur les chemins.

  2. statx préféré à fstat/fstatat/lstat/stat. statx est l’API moderne unifiée qui couvre tous les cas et permet de sélectionner les champs à récupérer.

  3. openat2 préféré à openat. openat2 (Linux 5.6+) permet une résolution de chemin plus stricte via le champ resolve de OpenHow. Le wrapper Air détecte au runtime si openat2 est disponible et tombe sur openat en fallback.

  4. CLOEXEC par défaut universel. Tout FD ouvert via le wrapper Air a O_CLOEXEC activé par défaut. Pour les rares cas où on veut désactiver CLOEXEC, l’option doit être passée explicitement.

  5. Type DirFd pour les *at. Au lieu d’utiliser AT_FDCWD (sentinelle kernel -100), Air expose un type DirFd :

#![allow(unused)]
fn main() {
pub enum DirFd<'fd> {
    Cwd,
    Fd(BorrowedFd<'fd>),
}
}

Cohérent avec la convention 1 de l’ADR-021.

  1. Pas de support des chemins relatifs sans *at. Les fonctions wrapper exigent une base explicite (DirFd::Cwd ou un FD), pas de magie sur le cwd implicite.

Types transverses utilisés.

  • OwnedFd, BorrowedFd : ownership des FDs.
  • DirFd : base pour les opérations *at.
  • OpenFlags, Mode : flags d’ouverture et permissions.
  • OpenHow : structure complète pour openat2.
  • StatxResult, StatxMask : résultats et masques de statx.
  • Errno : erreurs.

Sous-section 1 : Ouverture et fermeture

openat2

Signature.

#![allow(unused)]
fn main() {
pub fn openat2(
    dirfd: DirFd<'_>,
    path: &CStr,
    how: OpenHow,
) -> Result<OwnedFd, Errno>;

#[derive(Debug, Clone, Copy, Default)]
pub struct OpenHow {
    pub flags: OpenFlags,
    pub mode: Mode,
    pub resolve: ResolveFlags,
}

bitflags! {
    pub struct OpenFlags: u64 {
        const RDONLY = 0;
        const WRONLY = 1;
        const RDWR = 2;
        const CREAT = 0o100;
        const EXCL = 0o200;
        const NOCTTY = 0o400;
        const TRUNC = 0o1000;
        const APPEND = 0o2000;
        const NONBLOCK = 0o4000;
        const DSYNC = 0o10000;
        const ASYNC = 0o20000;
        const DIRECT = 0o40000;
        const LARGEFILE = 0o100000;
        const DIRECTORY = 0o200000;
        const NOFOLLOW = 0o400000;
        const NOATIME = 0o1000000;
        const CLOEXEC = 0o2000000;
        const PATH = 0o10000000;
        const TMPFILE = 0o20200000;
    }
}

bitflags! {
    pub struct ResolveFlags: u64 {
        const NO_XDEV = 0x01;
        const NO_MAGICLINKS = 0x02;
        const NO_SYMLINKS = 0x04;
        const BENEATH = 0x08;
        const IN_ROOT = 0x10;
        const CACHED = 0x20;
    }
}

pub type Mode = u32;
}

Syscall sous-jacent. openat2 (x86_64 n°437, ARM64 n°437). Disponible depuis Linux 5.6 (mars 2020).

Préconditions.

path est une chaîne C-compatible (terminée par NUL). Pour les ouvertures à FD direct (sans path relatif), passer c"".

Le wrapper Air ajoute systématiquement OpenFlags::CLOEXEC aux flags, sauf si l’appelant a explicitement utilisé un mode raw qui désactive cette discipline.

Comportement.

Ouvre un fichier (ou un répertoire avec DIRECTORY, ou un FD path-only avec PATH). La résolution du chemin est contrôlée par resolve :

  • ResolveFlags::BENEATH : refuse de résoudre au-delà du dirfd. Empêche les attaques par “../../”.
  • ResolveFlags::NO_SYMLINKS : refuse de suivre les liens symboliques.
  • ResolveFlags::NO_MAGICLINKS : refuse les liens magiques (/proc/self/fd/X).
  • ResolveFlags::IN_ROOT : traite dirfd comme une nouvelle racine.

Ces flags permettent une discipline stricte de résolution, utile pour les contextes sandboxés.

Pattern recommandé.

#![allow(unused)]
fn main() {
let how = OpenHow {
    flags: OpenFlags::RDONLY,
    mode: 0,
    resolve: ResolveFlags::BENEATH | ResolveFlags::NO_SYMLINKS,
};
let fd = openat2(DirFd::Cwd, c"./config.toml", how)?;
}

Erreurs.

  • EACCES : permissions insuffisantes.
  • EEXIST : fichier existe et EXCL était demandé.
  • EISDIR : tentative d’ouvrir un répertoire en écriture sans DIRECTORY.
  • ENOENT : chemin inexistant.
  • ENOTDIR : un composant du chemin n’est pas un répertoire.
  • EXDEV : violation de NO_XDEV.
  • EAGAIN : violation de BENEATH ou NO_SYMLINKS (résolution refusée).

Performance. ~5-20 µs selon la complexité du path.

Tests.

  • Test ouverture en lecture : fichier existant, vérifier FD valide et CLOEXEC.
  • Test ENOENT : chemin inexistant.
  • Test BENEATH : tentative de remonter avec ../, vérifier EAGAIN.

openat (fallback)

Signature.

#![allow(unused)]
fn main() {
pub fn openat(
    dirfd: DirFd<'_>,
    path: &CStr,
    flags: OpenFlags,
    mode: Mode,
) -> Result<OwnedFd, Errno>;
}

Comportement.

Version sans resolve. Pour les kernels qui ne supportent pas openat2 (< 5.6). Le wrapper Air utilise openat automatiquement si openat2 retourne ENOSYS.

close

Signature.

#![allow(unused)]
fn main() {
pub fn close(fd: OwnedFd) -> Result<(), Errno>;
}

Comportement.

Ferme un FD. Le wrapper consomme l’OwnedFd (prend l’ownership), garantissant qu’il ne peut pas être réutilisé après close.

Note. Le Drop automatique d’OwnedFd appelle close en silence (ignorant les erreurs). La fonction explicite close permet de récupérer une erreur éventuelle (rare mais possible : EIO, EINTR sur certains systèmes).

Sous-section 2 : I/O synchrone

read, write

Signatures.

#![allow(unused)]
fn main() {
pub fn read(fd: BorrowedFd<'_>, buf: &mut [u8]) -> Result<usize, Errno>;
pub fn write(fd: BorrowedFd<'_>, buf: &[u8]) -> Result<usize, Errno>;
}

Syscalls sous-jacents. read (x86_64 n°0, ARM64 n°63), write (x86_64 n°1, ARM64 n°64).

Comportement.

Lecture/écriture séquentielle au FD. Retourne le nombre d’octets effectivement lus/écrits, qui peut être inférieur à buf.len() (read partiel pour les sockets, pipes, fichiers en fin).

Erreurs courantes.

  • EAGAIN : FD non-bloquant et pas de données disponibles (ou pipe plein).
  • EINTR : interrompu par signal (cf. ADR-021 convention 2).
  • EIO : erreur I/O bas niveau.
  • EBADF : FD invalide.

pread, pwrite

Signatures.

#![allow(unused)]
fn main() {
pub fn pread(fd: BorrowedFd<'_>, buf: &mut [u8], offset: u64) -> Result<usize, Errno>;
pub fn pwrite(fd: BorrowedFd<'_>, buf: &[u8], offset: u64) -> Result<usize, Errno>;
}

Comportement.

Variantes positionnées : lisent/écrivent à un offset spécifique sans modifier la position courante du FD. Sûres pour usage concurrent depuis plusieurs threads.

readv, writev, preadv, pwritev

Signatures.

#![allow(unused)]
fn main() {
pub fn readv(fd: BorrowedFd<'_>, iov: &mut [IoSliceMut<'_>]) -> Result<usize, Errno>;
pub fn writev(fd: BorrowedFd<'_>, iov: &[IoSlice<'_>]) -> Result<usize, Errno>;
pub fn preadv(fd: BorrowedFd<'_>, iov: &mut [IoSliceMut<'_>], offset: u64) -> Result<usize, Errno>;
pub fn pwritev(fd: BorrowedFd<'_>, iov: &[IoSlice<'_>], offset: u64) -> Result<usize, Errno>;
}

Comportement.

Variantes scatter-gather : lecture/écriture vers/depuis plusieurs buffers en un seul syscall. Utile pour les protocoles avec header + payload.

Sous-section 3 : Positionnement

lseek

Signature.

#![allow(unused)]
fn main() {
pub fn lseek(fd: BorrowedFd<'_>, offset: i64, whence: SeekWhence) -> Result<u64, Errno>;

pub enum SeekWhence {
    Set,
    Current,
    End,
    Data,    // Linux-specific: next data after offset
    Hole,    // Linux-specific: next hole after offset
}
}

Comportement.

Modifie la position courante d’un FD seekable. Retourne la nouvelle position absolue.

Sous-section 4 : Métadonnées

statx

Signature.

#![allow(unused)]
fn main() {
pub fn statx(
    dirfd: DirFd<'_>,
    path: &CStr,
    flags: StatxFlags,
    mask: StatxMask,
) -> Result<StatxResult, Errno>;

bitflags! {
    pub struct StatxFlags: u32 {
        const EMPTY_PATH = 0x1000;
        const NO_AUTOMOUNT = 0x800;
        const SYMLINK_NOFOLLOW = 0x100;
    }
}

bitflags! {
    pub struct StatxMask: u32 {
        const TYPE = 0x0001;
        const MODE = 0x0002;
        const NLINK = 0x0004;
        const UID = 0x0008;
        const GID = 0x0010;
        const ATIME = 0x0020;
        const MTIME = 0x0040;
        const CTIME = 0x0080;
        const INO = 0x0100;
        const SIZE = 0x0200;
        const BLOCKS = 0x0400;
        const BTIME = 0x0800;
        const MNT_ID = 0x1000;
        const ALL = 0xfff;
    }
}

pub struct StatxResult {
    pub mask: StatxMask,
    pub blksize: u32,
    pub attributes: u64,
    pub nlink: u32,
    pub uid: u32,
    pub gid: u32,
    pub mode: u16,
    pub ino: u64,
    pub size: u64,
    pub blocks: u64,
    pub atime: StatxTimestamp,
    pub btime: StatxTimestamp,
    pub ctime: StatxTimestamp,
    pub mtime: StatxTimestamp,
    pub rdev_major: u32,
    pub rdev_minor: u32,
    pub dev_major: u32,
    pub dev_minor: u32,
    pub mount_id: u64,
}

pub struct StatxTimestamp {
    pub seconds: i64,
    pub nanoseconds: u32,
}
}

Syscall sous-jacent. statx (x86_64 n°332, ARM64 n°291). Disponible depuis Linux 4.11.

Comportement.

Lit les métadonnées d’un fichier. mask sélectionne les champs souhaités (perf : le kernel ne lit que ce qui est demandé). Le champ mask du résultat indique les champs effectivement remplis (peut être un sous-ensemble du mask demandé si certaines informations ne sont pas disponibles).

Pattern.

#![allow(unused)]
fn main() {
let result = statx(
    DirFd::Cwd,
    c"./config.toml",
    StatxFlags::empty(),
    StatxMask::SIZE | StatxMask::MTIME,
)?;

if result.mask.contains(StatxMask::SIZE) {
    println!("Size: {}", result.size);
}
}

faccessat

Signature.

#![allow(unused)]
fn main() {
pub fn faccessat(
    dirfd: DirFd<'_>,
    path: &CStr,
    mode: AccessMode,
    flags: AccessFlags,
) -> Result<(), Errno>;

bitflags! {
    pub struct AccessMode: u32 {
        const F_OK = 0;
        const R_OK = 4;
        const W_OK = 2;
        const X_OK = 1;
    }
}

bitflags! {
    pub struct AccessFlags: i32 {
        const EACCESS = 0x200;
        const SYMLINK_NOFOLLOW = 0x100;
    }
}
}

Comportement.

Teste les permissions d’accès à un fichier sans l’ouvrir. Sujet à des races (le résultat peut être obsolète au moment où on agit dessus) : préférer ouvrir directement et gérer l’erreur.

Routage faccessat / faccessat2 (honorer les flags).

Le syscall noyau faccessat historique ne prend que 3 arguments (dirfd, path, mode) : il ignore silencieusement tout flag comportemental passé par-dessus. Honorer AT_SYMLINK_NOFOLLOW (0x100) / AT_EACCESS (0x200) exige le syscall faccessat2 (numéro 439 sur x86_64 et aarch64, Linux 5.8+, 4 arguments). Comme la glibc, le wrapper route donc selon la valeur de flags, sans changer sa signature publique :

flagsSyscall noyauNuméro (x86_64 / aarch64)
empty()faccessat (3-arg)269 / 48
non videfaccessat2 (4-arg)439 / 439

Le chemin flags-vide conserve le syscall 3-arg, universellement disponible. Le chemin flags-non-vide emprunte faccessat2, le seul qui applique réellement les flags ; sur un noyau < 5.8 dépourvu de faccessat2, un appel avec flags retourne ENOSYS (jamais un résultat où le flag aurait été ignoré en silence — la garantie « on n’implémente pas un flag qu’on ignore »).

Sous-section 5 : Répertoires

mkdirat

#![allow(unused)]
fn main() {
pub fn mkdirat(dirfd: DirFd<'_>, path: &CStr, mode: Mode) -> Result<(), Errno>;
}

getdents64

#![allow(unused)]
fn main() {
pub fn getdents64(fd: BorrowedFd<'_>, buf: &mut [u8]) -> Result<usize, Errno>;

pub struct DirEntry {
    pub inode: u64,
    pub offset: i64,
    pub file_type: DirEntryType,
    pub name: Vec<u8>, // octets bruts (sans NUL) — cf. note std-free ci-dessous
}

pub enum DirEntryType {
    Unknown,
    Fifo,
    Character,
    Directory,
    Block,
    Regular,
    Symlink,
    Socket,
}

pub struct DirEntryIter<'buf> { /* ... */ }

impl<'buf> Iterator for DirEntryIter<'buf> {
    type Item = DirEntry<'buf>;
    // ...
}

pub fn parse_dirents<'buf>(buf: &'buf [u8], length: usize) -> DirEntryIter<'buf>;
}

Comportement.

Lit les entrées d’un répertoire ouvert avec DIRECTORY. Le format binaire kernel est complexe ; le wrapper fournit un itérateur typé.

Note std-free (ADR-048, re-sceau couche-0-v1.6). DirEntry.name est une copie propriétaire d’octets bruts (alloc::vec::Vec<u8>), sans le NUL terminateur. Le design initial (&'buf CStr emprunté) supposait core::ffi::CStr et un emprunt sur le buffer ; l’implémentation a retenu une copie possédée pour découpler l’entrée du buffer de lecture (réutilisable entre appels getdents64). Le type octets (et non OsString/String) honore la doctrine « chemins = octets » : sur Unix un nom de fichier est une suite d’octets opaque, potentiellement non-UTF-8 (Principe 3, zéro présomption d’encodage). parse_dirents rend impl Iterator<Item = DirEntry> (entrées possédées, pas de paramètre de durée de vie sur DirEntry).

unlinkat, renameat2, linkat, symlinkat, readlinkat, mknodat

Signatures classiques, suivant la convention *at. Détails standards.

renameat2

#![allow(unused)]
fn main() {
pub fn renameat2(
    old_dirfd: DirFd<'_>,
    old_path: &CStr,
    new_dirfd: DirFd<'_>,
    new_path: &CStr,
    flags: RenameFlags,
) -> Result<(), Errno>;

bitflags! {
    pub struct RenameFlags: u32 {
        const NOREPLACE = 1;
        const EXCHANGE = 2;
        const WHITEOUT = 4;
    }
}
}

RenameFlags::EXCHANGE est particulièrement utile pour les updates atomiques (échanger deux fichiers atomiquement).

Sous-section 6 : Permissions

fchmodat, fchownat, utimensat

Signatures classiques. utimensat peut prendre UTIME_NOW ou UTIME_OMIT comme valeurs spéciales pour les timestamps, exprimées via un enum typé :

#![allow(unused)]
fn main() {
pub enum UtimeValue {
    Now,
    Omit,
    Time(StatxTimestamp),
}
}

Sous-section 7 : Opérations spéciales

ftruncate, truncate, fsync, fdatasync

Signatures standards.

fallocate

#![allow(unused)]
fn main() {
pub fn fallocate(
    fd: BorrowedFd<'_>,
    mode: FallocateMode,
    offset: i64,
    length: i64,
) -> Result<(), Errno>;

bitflags! {
    pub struct FallocateMode: i32 {
        const KEEP_SIZE = 0x01;
        const PUNCH_HOLE = 0x02;
        const COLLAPSE_RANGE = 0x08;
        const ZERO_RANGE = 0x10;
        const INSERT_RANGE = 0x20;
        const UNSHARE_RANGE = 0x40;
    }
}
}

copy_file_range

#![allow(unused)]
fn main() {
pub fn copy_file_range(
    fd_in: BorrowedFd<'_>,
    off_in: Option<&mut u64>,
    fd_out: BorrowedFd<'_>,
    off_out: Option<&mut u64>,
    length: usize,
    flags: u32,
) -> Result<usize, Errno>;
}

Copie zero-copy entre fichiers (sur le même filesystem si possible).

splice, tee, vmsplice

Couvert dans la famille ipc mais référencé ici parce que ce sont aussi des opérations FS.

flock, statfs, fstatfs

Signatures standards.

Sous-section 8 : fcntl (opérations individuelles)

Comme prctl, fcntl est un syscall multiplexé. Air l’expose opération par opération conformément à la convention 3 de l’ADR-021 :

#![allow(unused)]
fn main() {
// F_DUPFD, F_DUPFD_CLOEXEC
pub fn dup_fd(fd: BorrowedFd<'_>, min_fd: RawFd) -> Result<OwnedFd, Errno>;

// F_GETFD, F_SETFD
pub fn get_fd_flags(fd: BorrowedFd<'_>) -> Result<FdFlags, Errno>;
pub fn set_fd_flags(fd: BorrowedFd<'_>, flags: FdFlags) -> Result<(), Errno>;

// F_GETFL, F_SETFL
pub fn get_status_flags(fd: BorrowedFd<'_>) -> Result<StatusFlags, Errno>;
pub fn set_status_flags(fd: BorrowedFd<'_>, flags: StatusFlags) -> Result<(), Errno>;

// F_SETPIPE_SZ, F_GETPIPE_SZ
pub fn set_pipe_size(fd: BorrowedFd<'_>, size: usize) -> Result<usize, Errno>;
pub fn get_pipe_size(fd: BorrowedFd<'_>) -> Result<usize, Errno>;

// F_SETLK, F_SETLKW, F_GETLK
pub fn try_lock(fd: BorrowedFd<'_>, lock: &FileLock) -> Result<bool, Errno>;
pub fn lock(fd: BorrowedFd<'_>, lock: &FileLock) -> Result<(), Errno>;

// F_ADD_SEALS, F_GET_SEALS (pour memfd)
pub fn add_seals(fd: BorrowedFd<'_>, seals: Seals) -> Result<(), Errno>;
pub fn get_seals(fd: BorrowedFd<'_>) -> Result<Seals, Errno>;

// ... autres opérations selon besoins Air
}

Sous-section 9 : Handles persistants

name_to_handle_at, open_by_handle_at

#![allow(unused)]
fn main() {
pub fn name_to_handle_at(
    dirfd: DirFd<'_>,
    path: &CStr,
    flags: NameToHandleFlags,
) -> Result<FileHandle, Errno>;

pub fn open_by_handle_at(
    mount_fd: BorrowedFd<'_>,
    handle: &FileHandle,
    flags: OpenFlags,
) -> Result<OwnedFd, Errno>;
}

Permet de référencer un fichier par un handle opaque persistant (même après changement de chemin). Cas d’usage avancé.

Récapitulatif de la famille fs

Fonctions exposées : ~35 syscalls principaux + opérations fcntl individuelles.

CatégorieFonctions principales
Ouvertureopenat2, openat, close
I/O syncread, write, pread, pwrite, readv, writev, preadv, pwritev
Positionlseek
Métadonnéesstatx, faccessat
Répertoiresmkdirat, getdents64, unlinkat, renameat2, linkat, symlinkat, readlinkat, mknodat
Permissionsfchmodat, fchownat, utimensat
Spécialesftruncate, truncate, fsync, fdatasync, fallocate, copy_file_range, flock, statfs, fstatfs
fcntl~10 opérations individuelles
Handlesname_to_handle_at, open_by_handle_at

Syscalls non-wrappés (listés dans UNSUPPORTED.md) :

  • open (sans at) : remplacé par openat2/openat.
  • stat, fstat, lstat, fstatat : remplacés par statx.
  • mkdir (sans at) : remplacé par mkdirat.
  • unlink, rmdir (sans at) : remplacés par unlinkat.
  • rename, renameat : remplacés par renameat2.
  • link, symlink, readlink, mknod, access (sans at) : remplacés par variantes at.
  • chmod, chown, utimes, lchown : remplacés par variantes at.
  • getdents (sans 64) : deprecated, remplacé par getdents64.
  • chdir, fchdir : Air n’expose pas la manipulation du cwd implicite (cf. discussion convention).

Types ajoutés à air-sys-types

  • DirFd
  • OpenFlags, Mode, OpenHow, ResolveFlags
  • StatxResult, StatxMask, StatxFlags, StatxTimestamp
  • AccessMode, AccessFlags
  • RenameFlags, FallocateMode
  • SeekWhence
  • DirEntry, DirEntryType, DirEntryIter
  • UtimeValue
  • FdFlags, StatusFlags, FileLock, Seals
  • FileHandle
  • NameToHandleFlags
  • IoSlice, IoSliceMut

Soit ~25 types pour cette famille, portant le total de air-sys-types à ~45 types après les familles process et fs.

Décisions de fond émergées dans la famille fs

1. Variantes *at exclusives.

Pas de wrapper pour les variantes non-at. Toute opération filesystem prend une base explicite (DirFd::Cwd ou FD). Cohérent avec la sécurité et la convention 1 de l’ADR-021.

2. statx comme unique fonction de métadonnées.

stat, fstat, lstat, fstatat ne sont pas wrappés. statx les couvre tous, plus performant grâce au mask.

3. openat2 préféré, fallback openat transparent.

Le wrapper bascule automatiquement si openat2 retourne ENOSYS. Pas de friction côté appelant.

4. CLOEXEC universel.

Tout FD ouvert via Air a CLOEXEC. Pour les rares exceptions (héritage volontaire vers un fork), une API raw existe.

5. Pas de manipulation du cwd.

chdir/fchdir non exposés. Le cwd est une variable globale du processus, source de bugs en code concurrent. Air préfère que les chemins relatifs partent d’un DirFd explicite.


Licence du document : MPL 2.0 Statut : Spécification technique du module air-sys-syscall::fs (couche 0).