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 :
- Ouverture et fermeture :
openat2,openat,close. - I/O synchrone :
read,write,pread,pwrite,readv,writev,preadv,pwritev. - Positionnement :
lseek. - Métadonnées :
statx,faccessat. - Répertoires :
mkdirat,getdents64,unlinkat,renameat2,linkat,symlinkat,readlinkat,mknodat. - Permissions :
fchmodat,fchownat,utimensat. - Opérations spéciales :
ftruncate,truncate,fsync,fdatasync,sync_file_range,fallocate,copy_file_range,splice,tee,vmsplice,flock,statfs,fstatfs. - fcntl (opérations exposées individuellement, cf. ADR-021 convention 3).
- Handles persistants :
name_to_handle_at,open_by_handle_at.
Caractéristiques transverses de la famille.
-
Variantes
*atpréférées systématiquement.openat2plutôt queopen,mkdiratplutôt quemkdir,unlinkatplutôt queunlink, etc. Les variantes*atprennent un FD de répertoire comme base, ce qui élimine les races sur les chemins. -
statxpréféré àfstat/fstatat/lstat/stat.statxest l’API moderne unifiée qui couvre tous les cas et permet de sélectionner les champs à récupérer. -
openat2préféré àopenat.openat2(Linux 5.6+) permet une résolution de chemin plus stricte via le champresolvedeOpenHow. Le wrapper Air détecte au runtime siopenat2est disponible et tombe suropenaten fallback. -
CLOEXEC par défaut universel. Tout FD ouvert via le wrapper Air a
O_CLOEXECactivé par défaut. Pour les rares cas où on veut désactiver CLOEXEC, l’option doit être passée explicitement. -
Type
DirFdpour les*at. Au lieu d’utiliserAT_FDCWD(sentinelle kernel-100), Air expose un typeDirFd:
#![allow(unused)]
fn main() {
pub enum DirFd<'fd> {
Cwd,
Fd(BorrowedFd<'fd>),
}
}
Cohérent avec la convention 1 de l’ADR-021.
- Pas de support des chemins relatifs sans
*at. Les fonctions wrapper exigent une base explicite (DirFd::Cwdou 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 pouropenat2.StatxResult,StatxMask: résultats et masques destatx.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à dudirfd. 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: traitedirfdcomme 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 etEXCLétait demandé.EISDIR: tentative d’ouvrir un répertoire en écriture sansDIRECTORY.ENOENT: chemin inexistant.ENOTDIR: un composant du chemin n’est pas un répertoire.EXDEV: violation deNO_XDEV.EAGAIN: violation deBENEATHouNO_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 :
flags | Syscall noyau | Numéro (x86_64 / aarch64) |
|---|---|---|
empty() | faccessat (3-arg) | 269 / 48 |
| non vide | faccessat2 (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.nameest une copie propriétaire d’octets bruts (alloc::vec::Vec<u8>), sans leNULterminateur. Le design initial (&'buf CStremprunté) supposaitcore::ffi::CStret 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 appelsgetdents64). Le type octets (et nonOsString/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_direntsrendimpl Iterator<Item = DirEntry>(entrées possédées, pas de paramètre de durée de vie surDirEntry).
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égorie | Fonctions principales |
|---|---|
| Ouverture | openat2, openat, close |
| I/O sync | read, write, pread, pwrite, readv, writev, preadv, pwritev |
| Position | lseek |
| Métadonnées | statx, faccessat |
| Répertoires | mkdirat, getdents64, unlinkat, renameat2, linkat, symlinkat, readlinkat, mknodat |
| Permissions | fchmodat, fchownat, utimensat |
| Spéciales | ftruncate, truncate, fsync, fdatasync, fallocate, copy_file_range, flock, statfs, fstatfs |
| fcntl | ~10 opérations individuelles |
| Handles | name_to_handle_at, open_by_handle_at |
Syscalls non-wrappés (listés dans UNSUPPORTED.md) :
open(sansat) : remplacé paropenat2/openat.stat,fstat,lstat,fstatat: remplacés parstatx.mkdir(sansat) : remplacé parmkdirat.unlink,rmdir(sansat) : remplacés parunlinkat.rename,renameat: remplacés parrenameat2.link,symlink,readlink,mknod,access(sansat) : remplacés par variantesat.chmod,chown,utimes,lchown: remplacés par variantesat.getdents(sans 64) : deprecated, remplacé pargetdents64.chdir,fchdir: Air n’expose pas la manipulation du cwd implicite (cf. discussion convention).
Types ajoutés à air-sys-types
DirFdOpenFlags,Mode,OpenHow,ResolveFlagsStatxResult,StatxMask,StatxFlags,StatxTimestampAccessMode,AccessFlagsRenameFlags,FallocateModeSeekWhenceDirEntry,DirEntryType,DirEntryIterUtimeValueFdFlags,StatusFlags,FileLock,SealsFileHandleNameToHandleFlagsIoSlice,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).