Spec couche 0 — Famille mem : MmapRegion (mapping partageable + handle de vivacité io_uring)
Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS). Extension
de la famille mem.
Position
Ce document spécifie MmapRegion, le mapping partageable et compatible
io_uring dont dépendent deux opérations io_uring jusqu’ici différées :
submit_madvise (Temps 2a §6.2) et submit_futex_wait/wake/waitv
(Temps 2c §6.1). Il débloque les deux.
Émergence (méthode doc-d’abord). Le besoin est apparu en spécifiant io_uring (madvise au 2a, confirmé par futex au 2c) : une opération io_uring qui référence de la mémoire userspace la lit de façon asynchrone — après le retour de la façade, jusqu’à la complétion. La mémoire doit rester valide jusque-là. Le type
Mapping(RAII unique,Drop=munmap) ne le garantit pas : rien n’empêche sonDrop(doncmunmap) pendant qu’une op est en vol → use-after-unmap (le kernel lit de la mémoire démappée). Il faut un mapping dont la durée de vie peut être partagée avec le slot S1 d’une op en vol.
Le mystère du nommage, résolu
Les specs io_uring nomment MmapRegion ; la famille mem a Mapping. Ce sont
deux types distincts, à dessein :
Mapping(existant, inchangé) : RAII unique (move-only),Drop=munmap, zéro-coût (pas d’allocation, ADR-021 c.4). Pour tout usage synchrone ordinaire. On ne le touche pas — sa couverture 100 % reste intacte.MmapRegion(nouveau) : mapping partageable, compté par référence,munmapau dernier drop. C’est le seul type passable aux opérations mémoire asynchrones d’io_uring (madvise, futex). Opt-in : on ne paie le partage que si on en a besoin.
1. MmapRegion
#![allow(unused)]
fn main() {
/// Mapping mmap **partageable** et compatible io_uring : ses pages restent
/// valides tant qu'une opération io_uring en vol (madvise/futex) y référence de
/// la mémoire. Compté par référence — `munmap` au **dernier** drop (la région
/// **et** toutes les gardes de vivacité d'ops en vol).
#[derive(Clone)]
pub struct MmapRegion { /* Arc<MmapRegionInner> : ptr, len, prot */ }
impl MmapRegion {
/// Mapping anonyme partageable (équivalent `mmap_anonymous` mais `MmapRegion`).
/// # Errors `Errno` (cf. famille `mem`).
pub fn new_anonymous(len: usize, prot: ProtectionFlags, flags: MapFlags)
-> Result<Self, Errno>;
/// Mapping de fichier partageable.
pub fn from_file(fd: BorrowedFd<'_>, len: usize, prot: ProtectionFlags,
flags: MapFlags, offset: u64) -> Result<Self, Errno>;
/// Promeut un `Mapping` unique en région partageable : **transfère** la
/// responsabilité du `munmap` à l'inner partagé (le `Mapping` est consommé,
/// son `Drop` ne munmap plus). Une seule allocation (l'inner). Le `prot`
/// **doit refléter** celui avec lequel le `Mapping` a été créé (un `Mapping`
/// nu ne mémorise pas ses protections) — **pas de défaut `READ|WRITE`**,
/// sinon `bytes()`/`futex_word()` décideraient de l'accessibilité sur une
/// base fausse (référence qui faute à l'usage).
pub fn from_mapping(mapping: Mapping, prot: ProtectionFlags) -> Self;
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
/// Tranche d'octets en lecture (si `prot` lisible). Accès **sûr**, borné.
pub fn bytes(&self) -> &[u8];
/// Référence à un **mot futex** (`AtomicU32`) à `offset` octets. La référence
/// est **mutable par partage** (`store`/`load` via `&`) : la région doit donc
/// être **inscriptible**.
/// # Errors `EINVAL` si `prot` ne contient pas `WRITE`, ou si `offset` est
/// hors bornes (`offset + 4 > len`) ou non aligné sur 4.
pub fn futex_word(&self, offset: usize) -> Result<&AtomicU32, Errno>;
/// Pointeur brut + longueur (consommateurs avancés ; pas de propriété).
pub fn as_ptr(&self) -> *const u8;
}
}
Modèle de propriété. MmapRegion enrobe un Arc<MmapRegionInner> ;
MmapRegionInner::Drop appelle munmap (en ignorant l’erreur, comme Mapping).
Cloner une MmapRegion incrémente le compteur fort ; munmap n’a lieu qu’au
dernier drop. Aucune copie de pages — seulement du partage de propriété.
Validation amont (Principe 4). futex_word vérifie inscriptibilité
(prot ⊇ WRITE, car la réf rendue est mutable), bornes et alignement (un
futex est un u32 aligné) avant de rendre la référence — EINVAL sinon. Les
accès bytes() sont bornés par construction (tranche vide si prot sans READ).
Exception « pas d’alloc dans le happy path » (ADR-021 c.4), justifiée.
MmapRegionfait une allocation (l’Arcinner). C’est admis et documenté : (1) c’est opt-in — le chemin communMappingreste zéro-coût ; (2) le coût est négligeable et amorti devant le syscallmmapqui crée la région (qui le domine de plusieurs ordres de grandeur) ; (3) le partage de durée de vie est intrinsèquement nécessaire à la sûreté de l’usage asynchrone (cf. ADR-021 c.4, clause « nécessité documentée »). À consigner dans les notes de la famille.
2. Handle de vivacité (consommé par le slot S1 d’io_uring)
#![allow(unused)]
fn main() {
/// Garde de vivacité retenue par le slot S1 d'une opération io_uring en vol qui
/// référence la région. Garde les pages **mappées** sans donner d'accès :
/// tant qu'une garde existe, `munmap` ne peut pas se produire.
pub struct MmapRegionLiveness { /* clone de l'Arc inner */ }
impl MmapRegion {
/// Produit une garde de vivacité (clone interne). Utilisé par les façades
/// io_uring `submit_madvise`/`submit_futex_*` : la garde est **garée dans le
/// slot**, et libérée à la complétion → `munmap` au dernier drop.
pub(crate) fn liveness_handle(&self) -> MmapRegionLiveness;
}
}
Mécanique de sûreté (le cœur). Quand une façade io_uring soumet une op référençant la région :
- elle valide la plage/le mot futex contre les bornes de la région (Principe 4) ;
- elle clone une
MmapRegionLiveness(compteur fort +1) ; - elle gare la garde dans le slot S1 à côté de l’op.
À la complétion, le slot est libéré → la garde tombe (compteur −1). munmap ne
survient que lorsque la région utilisateur ET toutes les gardes en vol sont
droppées. Le kernel ne lit donc jamais de mémoire démappée : ni
use-after-unmap, ni fuite (munmap au dernier drop, pas « fuite plutôt qu’UAF »).
Raffinement d’ADR-028 / Temps 2a §6.2. La rédaction initiale envisageait, pour madvise, un schéma « fuite plutôt qu’UAF » (ne pas munmap si une op est en vol → fuite).
MmapRegionfait strictement mieux : compté par référence, il ne fuit pas (munmap au dernier drop) et ne peut pas UAF. C’est le mécanisme retenu.
3. Consommation par io_uring (signatures corrigées)
madvise (Temps 2a §6.2) — déjà conforme
#![allow(unused)]
fn main() {
pub fn submit_madvise(&mut self, region: &MmapRegion, range: Range<usize>, advice: MadviseAdvice)
-> Result<SubmissionToken, Errno>;
}
range.end ≤ region.len() validé en amont ; le slot retient
region.liveness_handle().
futex (Temps 2c §6.1) — CORRECTION de signature
La spec 2c montrait futex: &AtomicU32 : insuffisant pour l’async (le borrow
ne survit pas au retour de la façade). On le remplace par une référence dans une
MmapRegion + offset (le slot retient la garde de vivacité) :
#![allow(unused)]
fn main() {
pub fn submit_futex_wait(&mut self, region: &MmapRegion, offset: usize,
expected: u64, mask: u64, flags: FutexFlags)
-> Result<SubmissionToken, Errno>; // OP 51
pub fn submit_futex_wake(&mut self, region: &MmapRegion, offset: usize,
nr: u64, mask: u64, flags: FutexFlags)
-> Result<SubmissionToken, Errno>; // OP 52
pub fn submit_futex_waitv(&mut self, waiters: Vec<FutexWaiter>, flags: FutexFlags)
-> Result<SubmissionToken, Errno>; // OP 53
/// Un attendu de `futex_waitv` : la région + l'offset du mot et la valeur
/// attendue. La région est gardée vivante par le slot. **Pas de masque** : le
/// `struct futex_waitv` kernel n'en porte pas (cf. note ci-dessous).
pub struct FutexWaiter {
pub region: MmapRegion, // possédée (clone) ⇒ garde de vivacité naturelle
pub offset: usize,
pub expected: u64,
}
}
offset localise le mot futex dans la région (inscriptible, aligné 4, borné —
EINVAL sinon). Note ABI : struct futex_waitv kernel = { val, uaddr, flags, __reserved } (pas de masque par-attendu) ; FutexWaiter n’expose donc
pas de masque (un champ silencieusement ignoré serait un footgun, ADR-032). Pour
une attente masquée, utiliser submit_futex_wait (mono-attente, qui garde
mask). La spec io-uring-2c-async.md et la famille mem sont réconciliées
sur ces signatures.
4. Répartition des types
air-sys-types::mem: aucun nouveau type pur requis (réutiliseProtectionFlags/MapFlagsexistants).air-sys-syscall::mem:MmapRegion,MmapRegionLiveness(RAII appelantmunmap→ vit dans la crate des wrappers, commeMapping).Mappingreste inchangé (aucune modification de sa représentation ni de sonDrop) → couverturememexistante intacte.
5. Tests
- Sûreté (Miri) — le point central : parker une op (madvise/futex simulée) qui
détient une
MmapRegionLiveness, dropper laMmapRegionutilisateur → pas demunmaptant que la garde vit ; libérer le slot →munmapau dernier drop. Vérifier aucun UAF (la mémoire reste lisible tant qu’une garde existe) et aucune fuite (munmap survient bien au dernier drop). Compteur fort exact sous loom (clones/drops concurrents). - Validation :
futex_word/rangehors bornes ou non alignés →EINVALavant soumission. - Intégration (une fois consommé par io_uring) :
madviseetfutex_waitréveillé parfutex_wakesur uneMmapRegionpartagée entre deux threads. from_mapping: leMappingconsommé ne munmap plus ; la région le fait au dernier drop (pas de double munmap).- Couverture 100 % lignes + branches ; erreurs ressources non déclenchables →
COVERAGE-EXCEPTIONS.md(STRUCTURAL).
6. Décisions de fond
MmapRegiondistinct deMapping:Mappingreste unique/zéro-coût et intouché (couverture préservée) ;MmapRegionest l’opt-in partageable pour l’async io_uring.- Compté par référence → ni UAF ni fuite : raffine le « fuite plutôt qu’UAF »
envisagé (ADR-028/2a §6.2) ;
munmapau dernier drop. - Garde de vivacité
MmapRegionLivenessgarée dans le slot S1 : la sûreté mémoire de madvise/futex est par construction, sansunsafeexposé. - Signature futex corrigée (
&MmapRegion+ offset, pas&AtomicU32) — réconcilie le 2c avec le modèle d’ownership asynchrone. - Une allocation (Arc) assumée (ADR-021 c.4 « nécessité documentée ») : opt-in,
négligeable devant
mmap.
7. Travail à reprendre
Cette spec débloque : implémentation MmapRegion (PR mem coordonnée), puis
submit_madvise (Temps 2a, reliquat) et submit_futex_* (Temps 2c, reliquat) —
réconcilier les signatures dans io-uring-2a-filesystem.md et
io-uring-2c-async.md. fanotify, epoll (selon retour 2c) : indépendants.
Licence du document : MPL 2.0
Statut : Spécification technique de MmapRegion (extension air-sys-syscall::mem),
cible kernel 6.12 LTS. Débloque madvise (2a) et futex (2c).