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 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 asynchroneaprè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 son Drop (donc munmap) 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, munmap au 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. MmapRegion fait une allocation (l’Arc inner). C’est admis et documenté : (1) c’est opt-in — le chemin commun Mapping reste zéro-coût ; (2) le coût est négligeable et amorti devant le syscall mmap qui 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 :

  1. elle valide la plage/le mot futex contre les bornes de la région (Principe 4) ;
  2. elle clone une MmapRegionLiveness (compteur fort +1) ;
  3. 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). MmapRegion fait 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éutilise ProtectionFlags/MapFlags existants).
  • air-sys-syscall::mem : MmapRegion, MmapRegionLiveness (RAII appelant munmap → vit dans la crate des wrappers, comme Mapping).
  • Mapping reste inchangé (aucune modification de sa représentation ni de son Drop) → couverture mem existante intacte.

5. Tests

  • Sûreté (Miri) — le point central : parker une op (madvise/futex simulée) qui détient une MmapRegionLiveness, dropper la MmapRegion utilisateur → pas de munmap tant que la garde vit ; libérer le slot → munmap au 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/range hors bornes ou non alignés → EINVAL avant soumission.
  • Intégration (une fois consommé par io_uring) : madvise et futex_wait réveillé par futex_wake sur une MmapRegion partagée entre deux threads.
  • from_mapping : le Mapping consommé 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

  1. MmapRegion distinct de Mapping : Mapping reste unique/zéro-coût et intouché (couverture préservée) ; MmapRegion est l’opt-in partageable pour l’async io_uring.
  2. Compté par référence → ni UAF ni fuite : raffine le « fuite plutôt qu’UAF » envisagé (ADR-028/2a §6.2) ; munmap au dernier drop.
  3. Garde de vivacité MmapRegionLiveness garée dans le slot S1 : la sûreté mémoire de madvise/futex est par construction, sans unsafe exposé.
  4. Signature futex corrigée (&MmapRegion + offset, pas &AtomicU32) — réconcilie le 2c avec le modèle d’ownership asynchrone.
  5. 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).