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

ADR-056 — Allocateur global Air (air-alloc) : inspiré de dlmalloc, simple, possédé

Statut : Accepté (2026-06-30, ratifié par le BDFL). RFC de structure (ADR-015). S’appuie sur l’audit docs/notes/audit-dlmalloc-fr.md (2026-06-30). Companion d’ADR-049 (runtime) / ADR-052 (couche 1) ; fournit le malloc/free/ realloc de la future libc C-ABI (ADR-046/ADR-047).

Catégorie : Architecture (couche 1, fondateur). Toute évolution structurante = RFC.

Contexte

Le runtime air-runtime est implémenté, mesuré et prouvé on-target (étape 6). Mais l’allocateur est un placeholder AbortOnAlloc (rt/crates/air-rt/src/lib.rs : toute allocation ⇒ exit_process(102)). Conséquence : un binaire *-linux-air compile et démarre + spawn du code discipliné, mais ne peut pas exécuter de code qui alloue (Vec/String/Box, Command, build_envp, closure boxée). L’allocateur réel est le prérequis du « run réel » et un maillon de la libc Air.

L’audit dlmalloc (note de travail) a tranché la posture (décision BDFL) : ni vendoring boîte- noire, ni réinventions’inspirer du design éprouvé de dlmalloc en restant simple, et posséder la politique Air (sémantique d’échec, gardien post-fork, instrumentation). Cet ADR fige le design v1 et les 9 décisions ouvertes par la note.

Modèle mental (cadrage BDFL) — producteur / consommateur : air-alloc produit de la mémoire (à partir du noyau) ; air-memory (arènes/pools/slabs) la consomme. Les noms restent air-alloc

  • air-memory (la symétrie *-producer/*-consumer ne serait qu’esthétique).

Décision

D1 — Crate dédiée air-alloc (couche 1, no_std), dépendances minimales

Nouvelle crate crates/air-alloc, #![no_std], placée sous air-memory::{arena,pool,slab,backing} (qui sont clients du global allocator). Dépendances minimales (anti-cycle) : air-sys-types (Errno, MapFlags/ProtectionFlags) + air-sys-syscall (mem pour mmap/munmap, futex pour le verrou). Ne dépend ni d’air-memory, ni d’air-thread, ni d’air-runtime.

D2 — Mémoire brute : appel système direct (air-sys-syscall::mem)

air-alloc obtient ses segments par mmap directement via air-sys-syscall::mem (le plus bas, zéro détour). On ne réutilise pas air-memory::raw_mapping (qui ferait dépendre le producteur de son consommateur — smell architectural). La duplication éventuelle avec raw_mapping sera réconciliée plus tard (extraction d’une crate-socle si elle se justifie).

D3 — Verrou futex maison dans air-alloc

v1 = une arène globale unique protégée par un verrou futex propre à air-alloc (mot d’état + air-sys-syscall::futex), pas l’AirMutex d’air-thread (pour ne dépendre de rien en couche 1). La logique d’arène reste mono-thread (modèle Dlmalloc<A> : Send, non Sync) ; le verrou est la couche au-dessus.

D4 — Cœur emprunté à dlmalloc (plancher de correction, non négociable)

  • Étiquettes de frontière (en-tête taille | bits occupé, footer recopié) → fusion O(1) des voisins libres (anti-fragmentation) et taille stockée par bloc (réponse au free(ptr) C sans taille).
  • Étagères par classe de taille + table de bits (plus petite étagère non vide en O(1)) : smallbins exacts pour les petits objets (cas dominant).
  • Segments de 64 Ko demandés au noyau et sous-alloués (un mmap par segment, pas par allocation — essentiel sur Pi 4, ADR-014).
  • Très grande demande (≥ seuil) → cartographie mmap dédiée, rendue par munmap à la libération. Réintroduit consciemment (le port Rust 0.2.14 l’a laissé vestigial — cf. audit §2).
  • Reliquat privilégié (dv) + frontière (top), free-lists intrusives (métadonnées en-bande → bootstrap sain, aucun autre tas requis).

D5 — Simplifications v1 (optimisations différées, jamais la correction)

  • Grands blocs : liste triée simple (pas le treemap digital de dlmalloc — sa partie la plus subtile/risquée). Recherche O(n) sur les grands libres (rares) ; réintroduction d’un index rapide en v2 si mesure (Principe 5).
  • Pas de cache par-thread en v1 (arène globale verrouillée) → v2 (perf concurrente mesurée).
  • Reddition minimale : munmap des cartographies dédiées à la libération + rognage léger de top ; pas de reddition fine multi-segments (différée).
  • Alignement naturel (16 octets = max_align_t) seul en v1 ; l’alignement fort (aligned_alloc/posix_memalign, sur-allocation+rognage) est différé (vient avec la libc C).

    AMENDEMENT 2026-07-11 — D5 (alignement fort) LEVÉ. L’alignement fort (align > 16, puissance de deux arbitraire) est désormais implémenté dans l’arène (technique memalign de dlmalloc : sur-allocation + détachement d’un chunk préfixe + ajustement des en-têtes, deallocate/reallocate lead-aware). Déclencheur : le PAL std safe (ADR-088) — std::sync::mpsc::channel alloue un bloc aligné ligne-de-cache (align 64) ; l’ancien refus déterministe align > 16 → None faisait échouer GlobalAlloc (abort SIGILL). Ce n’est pas un changement de décision (D5 était un report explicite, « vient avec la libc C ») mais son achèvement, avancé car std en a besoin. Tests (matrice 32/64/128/256/page × tailles, realloc, dedicated, alloc_zeroed, OOM) + property-based + fuzz étendus ; align = 16 inchangé (lead = 0).

  • Garde-fou : invariant « granularité = puissance de deux ≥ alignement » (sinon le résidu de reddition corrompt les bits de flag — bug documenté dans dlmalloc). L’arithmétique tailles/flags est impitoyable : on emprunte le design éprouvé de Lea pour ces parties précisément.

D6 — Réglages de taille (constantes réglables)

  • Granularité de segment = 64 Ko (valeur dlmalloc) ; constante ajustable.
  • Seuil « cartographie dédiée » = 256 Ko (valeur C de dlmalloc) ; réglable, à ajuster après mesure Pi 4.

D7 — Politique d’échec : « échoue franchement, ne se bloque pas »

  • Réserver d’avance : pas de MAP_NORESERVE. Le noyau engage la mémoire à la demande → l’échec (ENOMEM) remonte au mmap, le plus tôt et le plus déterministe possible. Coût assumé : plus gourmand sur petite machine (cohérent avec la raison d’être d’Air : la prévisibilité prime).
  • Sur échec : alloc rend null (Rust GlobalAlloc) / Err déterministe, sans boucle, sans hang.
  • Honnêteté (à documenter dans le code) : un déterminisme total est impossible sous Linux (la politique globale de surengagement n’appartient pas à Air) ; la borne est documentée.

D8 — Gardien post-fork : reset natif via air-runtime::fork

Le deadlock classique (verrou tenu au fork → enfant mono-thread bloqué) est résolu nativement : air-alloc expose une fonction de reset de son verrou ; air-runtime (qui dépend d’air-alloc) enregistre un handler child qui remet le mot futex à l’état déverrouillé dans l’enfant, intégré à reset_after_fork_in_child. Sens des dépendances : air-runtime → air-alloc (jamais l’inverse), zéro cycle. Plus simple et plus sûr que l’atfork acquire-around-fork de dlmalloc.

D9 — Instrumentation (fuites / doubles libérations) différée en v3

Le mode diagnostic LIB_AIR_DEBUG (table des allocations vivantes, bootstrap-safe, dans une cartographie dédiée) est reporté en v3. La conception v1 réserve la place (ne ferme pas la porte : en-tête extensible sous cfg ou table latérale), sans en payer le coût en v1.

D10 — API & étalement

  • v1 — face Rust : core::alloc::GlobalAlloc (alloc/dealloc/realloc/alloc_zeroed — l’optimisation « pages mmap déjà à zéro » de dlmalloc est reprise). Remplace AbortOnAlloc en #[global_allocator] dans rt/. Débloque le run réel (Command/spawn/tout code allouant) sur *-linux-air.
  • v2 — perf (mesurée) : arènes/cache par-thread, index rapide des grands bins si le profil le justifie, reddition/segments plus fins.
  • v3 — instrumentation + gardien complet : table live-allocs, double-free/fuite, canaris, comptabilité par-composant (cf. AirMemoryTracker).
  • Face C-ABI (future, ADR-046) : malloc/free(sans taille, résolu par l’en-tête)/realloc/ calloc/aligned_allocmême tas, deux faces ([[libc-air-dual-face]] ; précédent c_malloc/c_free de dlmalloc-rs).

D11 — Mandat de test (Principe 1, couche 1)

  • Logique host-testable à 100 % : l’arène est testée sur un backing fourni (buffer hôte / mmap hôte injecté à la place du vrai syscall — patron de dlmalloc-rs). Couverture 100 % lignes+branches.
  • Property-based : roundtrip alloc/free ; alignement respecté ; aucun chevauchement entre blocs vivants ; fusion (deux libres adjacents → un) ; realloc préserve min(ancien,nouveau) octets.
  • Fuzzing (cargo-fuzz) : séquences aléatoires alloc/free/realloc/calloc, modèle fantôme (non-chevauchement + contenu), différentiel possible.
  • Stress concurrent : N threads sous le verrou (deadlock/corruption).
  • Frontière TARGET-ONLY (ADR-035) : seul le câblage réel (#[global_allocator], reset-fork on-target) se valide par le selftest rt/ ; la logique est host-testable et doit l’être à 100 % (ne pas gonfler les exceptions de couverture).

Conséquences

  • Run réel débloqué sur *-linux-air (tout code allouant) — le grand verrou restant après l’étape 6.
  • Maillon libc posé (le malloc/free de la libc C-ABI viendra se brancher sur le même tas).
  • Possession de la politique (échec déterministe, gardien fork, futur diagnostic) sans réinventer l’algèbre d’allocation ni dépendre d’une boîte noire.
  • Couche 1 enrichie d’une crate fondatrice à tester avec le plus grand soin (l’arithmétique d’allocation est impitoyable — d’où le mandat property+fuzz+stress).

Alternatives rejetées

  • Vendorer dlmalloc (boîte noire) : politique Air (gardien fork, instrumentation, échec déterministe) difficile à greffer ; et le port Rust a un chemin mmap-direct vestigial qu’un vendoring aveugle aurait hérité. Rejeté au profit de l’inspiration possédée.
  • Réinventer from-scratch sans modèle : l’arithmétique tailles/flags + coalescence est un nid à corruptions ; on emprunte le design éprouvé de Lea pour le cœur.
  • air-alloc sur air-memory::raw_mapping / verrou AirMutex : font dépendre le producteur de ses consommateurs / d’air-thread ; rejeté (D2/D3) au profit de l’appel direct + verrou maison.
  • treemap / cache par-thread / instrumentation en v1 : optimisations/fonctions différées (D5/D9) pour réduire la surface de bug de la fondation.

Mise en œuvre (esquisse, code headless après ratification — staging v1)

  1. crates/air-alloc (no_std, deps air-sys-types + air-sys-syscall) : verrou futex maison ; arène (étiquettes de frontière, smallbins+bitmap, liste triée des grands, dv/top) ; backing mmap direct (segments 64 Ko + dédié ≥ 256 Ko) ; GlobalAlloc ; reset-fork exposé. 100 % cov + property + fuzz + stress.
  2. air-runtime : enregistre le handler child qui reset le verrou d’air-alloc (dep air-runtime → air-alloc).
  3. rt/air-rt : remplace AbortOnAlloc par air_alloc::GlobalAllocator en #[global_allocator].
  4. Selftest rt/ : exercer une allocation réelle (Vec/String) on-target (exit code), 2 arches.
  5. Doc : ADR-056, registre, SUMMARY, INDEX, DEPENDENCIES (air-alloc).