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-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éthode AirProcessManager::resource_limit(pid, resource) -> AirResult<Rlimit> (binde prlimit couche 0 en lecture seule). Socle de getrlimit et de la mesure de pile.

  • air-runtime::process_context (nouveau module) : au bootstrap ([start::bootstrap]), capture une fois (a) l’adresse du vecteur auxiliaire auxv et (b) les bornes de la pile principale (pointeur de pile initial arrondi au sommet de page + RLIMIT_STACK soft, via la couche 1). Expose :

    • auxval(entry_type) -> Option<usize> — socle de getauxval ;
    • 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 de RLIMIT_STACK). air-runtime ne syscall jamais (ADR-052) : la limite passe par air-process.

    Fusion getauxval (ex-Lot E) : la même capture d’auxv sert getauxval — 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 ; getauxval obtenu par la même capture ; TLS dynamique correct sur les 2 arches sans risque de layout ; resource_limit réutilisable par getrlimit.
  • Coût : un module couche 1 de plus + un re-sceau additif (couche-1-v1.9) ; bornes de pile approximatives (mitigées par le raffinage std).
  • Alternatives rejetées : slot TCB (§2, faux sur aarch64) ; lecture des bornes via /proc/self/maps en 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.