ADR-086 — Introspection thread cible pour la face libc : TLS dynamique, bornes de pile, auxv (chantier B — M5, Lot C)
Statut : Accepté (ratification BDFL 2026-07-09). Additif couche 1 servant le
jalon M5 (lier std sur libair, ADR-076).
Re-scellé couche-1-v1.9 le 2026-07-10 (périmètre implémenté 100 % couverture, 2 arches ;
tag signé sous la délégation des re-sceaux additifs du superviseur
ADR-065, cadre RFC d’extension de couches
scellées ADR-015). Numérotation : le Lot C prend
couche-1-v1.9 (prêt le premier) ; l’earmark couche-1-v1.9 d’ADR-078 (crypto/air-tls,
non ratifié) glisse en couche-1-v1.10 à sa ratification (décision BDFL 2026-07-10).
Catégorie : Additif couche 1 (aucun symbole existant modifié) + convention de la face libc cible (couche 2). S’inscrit dans les Managers de domaine (ADR-077) : le toit libc binde le manager couche 1, jamais la couche 0.
Contexte
Le lien de hello-std contre libair (M5) révèle 7 symboles pthread/unistd
indéfinis sur la cible :
- TLS dynamique POSIX :
pthread_key_create,pthread_key_delete,pthread_setspecific(std les utilise pour la registration des destructeurs TLS à la sortie de thread ; le stockage TLS lui-même passe par le#[thread_local]natif, déjà installé par [air_runtime::thread_local_storage],has-thread-local: true). - identité de thread :
pthread_self. - détection de pile :
pthread_getattr_np,pthread_attr_getstack,pthread_attr_getguardsize(std les appelle une fois à l’init pour poser la garde de débordement de pile du thread principal).
Ces symboles existent déjà en couche 2 (air-libc-thread/air-libc-capi) mais sont
gardés #[cfg(not(target_vendor = "air"))] — hôte uniquement, leurs implémentations
s’appuyant sur std (backend thread/TLS). Leur version cible était marquée
« REMONTÉE » (ADR-055 D3, M3 Phase B) faute d’un socle
couche 1. Cet ADR fournit ce socle.
État de la couche 1 : le TCB ([air_runtime::thread_control_block]) porte
tid/errno/canari + le TLS ELF statique ; il n’expose ni registre de clés
POSIX dynamiques ni accesseur de bornes de pile. La couche 0 fournit prlimit
(RLIMIT_STACK) mais la couche 1 ne le binde pas.
Décision
1. Additif couche 1 — capture du contexte de démarrage (re-sceau couche-1-v1.9)
-
air-process: méthodeAirProcessManager::resource_limit(pid, resource) -> AirResult<Rlimit>(bindeprlimitcouche 0 en lecture seule). Socle degetrlimitet de la mesure de pile. -
air-runtime::process_context(nouveau module) : au bootstrap ([start::bootstrap]), capture une fois (a) l’adresse du vecteur auxiliaireauxvet (b) les bornes de la pile principale (pointeur de pile initial arrondi au sommet de page +RLIMIT_STACKsoft, via la couche 1). Expose :auxval(entry_type) -> Option<usize>— socle degetauxval;main_thread_stack() -> Option<StackBounds>— socle de la détection de pile.
La logique pure (marche
auxv, arithmétique de bornes) est isolée et host-testable ; la capture l’est aussi (pile synthétique + lecture réelle deRLIMIT_STACK).air-runtimene syscall jamais (ADR-052) : la limite passe parair-process.Fusion
getauxval(ex-Lot E) : la même capture d’auxvsertgetauxval— un seul additif bootstrap couvre les deux besoins.
2. TLS dynamique per-thread cible = #[thread_local] natif, pas un slot TCB
Le pointeur des valeurs TLS per-thread (vecteur clé → valeur) est porté par un
#[thread_local] de couche 2 (air-libc-thread), sur le même modèle que l’errno
cible ([thread_control_block::errno], #[thread_local] natif prouvé sur les 2 arches).
La table globale des clés (allocateur + destructeurs, déjà en AirMutex) est
partagée hôte/cible.
Rejeté — slot dans le TCB (design initialement envisagé) : sur aarch64
(Variant I), les champs du TCB au-delà de 0x10 recouvrent le bloc #[thread_local]
(cf. [ThreadControlBlock::init], qui n’y écrit ni tid ni canari) ; un slot
dynamic_tls y corromprait le TLS. Le #[thread_local] natif est correct sur les
deux Variants sans toucher le layout du TCB — additif couche 1 réduit au seul
bootstrap.
3. pthread_self cible = air_thread::current_tid()
Façade directe sur gettid (couche 1, disponible sur toute cible) — aucun additif.
4. Détection de pile cible
pthread_getattr_np(self) + pthread_attr_getstack/getguardsize du thread
principal lisent air_runtime::main_thread_stack(). Bornes approximatives
assumées : std raffine lui-même via /proc/self/maps — nos valeurs ne font
qu’amorcer le calcul de garde.
5. Threads spawnés — câblés, validation différée
Le rejeu des destructeurs TLS à la sortie de thread et la détection de pile
per-thread spawné sont TARGET-ONLY et non exercés par hello-std
(mono-thread) ; le spawn natif panique sur aarch64 (dette connue). Les crochets sont
posés proprement (couche 1) ; leur validation runtime est différée avec note, sans
bloquer M5.
Conséquences
- Positives : socle couche 1 minimal (bootstrap seul) débloquant 7 symboles
std;getauxvalobtenu par la même capture ; TLS dynamique correct sur les 2 arches sans risque de layout ;resource_limitréutilisable pargetrlimit. - Coût : un module couche 1 de plus + un re-sceau additif (
couche-1-v1.9) ; bornes de pile approximatives (mitigées par le raffinagestd). - Alternatives rejetées : slot TCB (§2, faux sur aarch64) ; lecture des bornes via
/proc/self/mapsen couche 1 (dépendance file I/O au bootstrap, superflue puisque std raffine) ; dégrader la garde de pile (perte de sécurité).
Portée du re-sceau couche-1-v1.9
air-process::resource_limit + air-runtime::process_context (auxval,
main_thread_stack, StackBounds, capture). Tag signé posé en fin de Lot C, une
fois la face libc cible (C.2) implémentée et vérifiée on-target.