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éinvention — s’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/*-consumerne 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 aufree(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
mmappar segment, pas par allocation — essentiel sur Pi 4, ADR-014). - Très grande demande (≥ seuil) → cartographie
mmapdédiée, rendue parmunmapà 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 :
munmapdes cartographies dédiées à la libération + rognage léger detop; 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 (techniquememalignde dlmalloc : sur-allocation + détachement d’un chunk préfixe + ajustement des en-têtes,deallocate/reallocatelead-aware). Déclencheur : le PALstdsafe (ADR-088) —std::sync::mpsc::channelalloue un bloc aligné ligne-de-cache (align 64) ; l’ancien refus déterministealign > 16 → Nonefaisait échouerGlobalAlloc(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é carstden a besoin. Tests (matrice 32/64/128/256/page × tailles,realloc,dedicated,alloc_zeroed, OOM) + property-based + fuzz étendus ;align = 16inchangé (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 aummap, 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 :
allocrendnull(RustGlobalAlloc) /Errdé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 « pagesmmapdéjà à zéro » de dlmalloc est reprise). RemplaceAbortOnAllocen#[global_allocator]dansrt/. 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_alloc— même tas, deux faces ([[libc-air-dual-face]] ; précédentc_malloc/c_freede 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 /
mmaphô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) ;reallocpréservemin(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 selftestrt/; 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/freede 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-allocsurair-memory::raw_mapping/ verrouAirMutex: 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)
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) ; backingmmapdirect (segments 64 Ko + dédié ≥ 256 Ko) ;GlobalAlloc; reset-fork exposé. 100 % cov + property + fuzz + stress.air-runtime: enregistre le handlerchildqui reset le verrou d’air-alloc(dep air-runtime → air-alloc).rt/air-rt: remplaceAbortOnAllocparair_alloc::GlobalAllocatoren#[global_allocator].- Selftest
rt/: exercer une allocation réelle (Vec/String) on-target (exit code), 2 arches. - Doc : ADR-056, registre, SUMMARY, INDEX, DEPENDENCIES (air-alloc).