ADR-067 — air-account (couche 1) : gestion des comptes (/etc/passwd + /etc/shadow + /etc/group), bindée par la libc
Statut : Accepté (2026-07-05, ratification BDFL). RFC de structure (ADR-015). Applique le modèle de re-sceau additif de la couche 1 (ADR-062) sous la délégation d’ADR-065.
Catégorie : Nouvelle crate couche 1 + descellement additif de la couche 1
scellée (couche-1-v1.2 → couche-1-v1.3). Aucune rupture d’ABI ni de signature
existante ; les jalons antérieurs sont conservés intégralement.
Contexte
Le principe fondateur (rappel BDFL, 2026-07-05)
Il n’y a pas une implémentation C et une implémentation Rust. Il y a une seule implémentation Rust — proposée en couche 1, réutilisable par des développeurs Rust via
air-base-lib— qui est ensuite bindée en libc C.
La libc (couche 2) ne porte aucune logique métier : elle n’est que la frontière
ABI C (disposition des struct, empaquetage de buffer, convention errno) posée sur
une brique couche 1. C’est le prolongement de la doctrine libc « une brique, deux
faces » (BDFL) : f() fidèle-POSIX + air_f() restituant le Result, toutes deux
au-dessus de la même brique couche 1.
Le défaut à corriger (M4 comptes, PR #228)
Le jalon M4 comptes (getpwnam_r/getpwuid_r, crate air-libc-pwd) a violé ce
principe : le parseur /etc/passwd a été écrit dans la crate libc. C’est une
logique métier (comprendre le format d’un fichier système) qui n’a rien de spécifique
au C et doit donc vivre en couche 1, réutilisable par tout code Rust — pas seulement
par la libc. La correction fait l’objet du présent ADR.
Le besoin réel
La cible OpenSSH exige une famille comptes complète : getpwnam/getpwuid
(mot de passe), getgrnam/getgrgid (groupes), getspnam (shadow), initgroups
(groupes supplémentaires d’un utilisateur). sshd s’en sert pour résoudre
l’utilisateur cible puis larguer ses privilèges (initgroups + setgroups +
setresgid/setresuid). Il faut donc une brique couche 1 qui comprenne les trois
bases de comptes, indépendamment de la libc.
Ce qui n’est PAS concerné (précision, contre une erreur d’analyse antérieure)
Les identifiants/privilèges (uid/gid) n’ont aucun manque couche 0 : air-sys- syscall::process expose déjà get_resuid/set_resuid/get_resgid/set_resgid
(triplets ResUid/ResGid typés) et get_groups/set_groups. Le seul manque de ce
côté est une surface fine couche 1 (à-la-carte), traité séparément (cf. § Suite),
et ne relève pas de cet ADR.
Décision
Une nouvelle crate couche 1 : air-account
Crate indépendante (crates/air-account), spécialisée dans les bases de comptes
système. Elle porte trois codecs et une couche d’accès.
(a) Codecs purs — zéro I/O, zéro unsafe, fuzzés (entrée = fichier système,
surface d’attaque au sens du Principe 3) ; decode et encode symétriques :
- codec
/etc/passwd→PasswdEntry { name, passwd, uid: Uid, gid: Gid, gecos, dir, shell }(7 champs,uid/gidvalidésu32checked). - codec
/etc/group→GroupEntry { name, passwd, gid: Gid, members: Vec<AirOsString> }(4 champs ; le 4ᵉ est une liste de membres séparés par,). - codec
/etc/shadow→ShadowEntry { name, hash, last_change, min, max, warn, inactive, expire }(9 champs ; sensible — cf. § Sécurité).
(b) Couche d’accès — lit les fichiers via air-filesystem (couche 1, lecture
bornée) et offre le lookup, en types riches (Result/Option lossless) :
lookup_user_by_name / lookup_user_by_uid -> AirResult<Option<PasswdEntry>>
lookup_group_by_name / lookup_group_by_gid -> AirResult<Option<GroupEntry>>
groups_of_user(name) -> AirResult<Vec<Gid>> // socle d'initgroups
shadow_by_name(name) -> AirResult<Option<ShadowEntry>> // root-only
Le graphe de dépendances
air-account (couche 1 ; deps : air-filesystem, air-sys-types, air-base-core)
├── importée par air-process (résolution user->uid/gid/groupes avant drop_privileges)
├── ré-exposée par air-base-lib (surface Rust de première classe, pour les devs)
└── bindée par air-libc-pwd / air-libc-grp / air-libc-shadow (couche 2, ABI C)
air-account ne dépend PAS d’air-process : c’est air-process qui l’importe
(sens imposé). Aucun cycle (air-filesystem ignore air-account).
La libc devient un pur binder
air-libc-pwd (et les futures air-libc-grp/air-libc-shadow) ne gardent que l’ABI C :
getpwnam_r(name, …) -> air_account::lookup_user_by_name(name)? -> PACK borné dans le buffer C
getgrnam_r -> lookup_group_by_name getspnam_r -> shadow_by_name
initgroups(user,gid)-> air_account::groups_of_user(user) + [gid] -> set_groups (couche 1 creds)
La face Air (dual-face) tombe gratuitement : air_getpwnam(...) -> code surfaçant
le Result, puisque air-account le calcule déjà.
air-base-lib surface air-account
air-base-lib ré-exporte l’API d’air-account afin qu’un développeur Rust dispose
de la gestion des comptes de première classe, sans passer par la libc C. C’est la
matérialisation du principe : une implémentation, deux consommateurs (dev Rust + libc).
Réciprocité avec air-process
air-process gagne un drop_to_user(name) : résout via air-account
(lookup_user_by_name + groups_of_user) puis construit un AirPrivilegeDrop et
appelle drop_privileges — exactement le geste de sshd. drop_privileges (orchestré,
vérifié, seccomp/Landlock) reste inchangé et recommandé.
Sécurité (/etc/shadow)
shadow_by_name lit des hachages de mot de passe. Postures obligatoires :
- Root-only : la lecture échoue
EACCESsans privilège (comportement kernel viaair-filesystem) — jamais de dégradation silencieuse. - Zéro fuite :
ShadowEntry.hashn’est jamais journalisé ni inclus dans unDisplay/Debugpar défaut (redaction) ; la comparaison de mot de passe (temps constant) n’appartient pas àair-account(relève deair-crypto). - Zéro copie durable : les tampons contenant le hachage sont zeroïsés après usage côté brique (pas de rémanence heap).
Ces points étant des considérations de sécurité, ils sont remontés au BDFL (la délégation d’ADR-065 réserve au BDFL les compromis de sécurité) ; le présent ADR les consigne pour ratification explicite.
Gouvernance
- Nouvelle crate couche 1 + surface additive → re-sceau
couche-1-v1.3(tag signé) au merge de l’implémentation. Purement additif : aucune signature scellée existante n’est modifiée ;air-process/air-base-libgrandissent seulement. - Sous la délégation d’ADR-065 (additifs révélés par les toits traités par le
superviseur), avec remontée explicite ici du volet sécurité
/etc/shadow. - Le refactor de #228 (déplacer le parseur
air-libc-pwd→air-account) est non-rupturant : l’ABI C (getpwnam_r/getpwuid_r,struct passwd, symbolesnm -D) reste identique ; seule l’implémentation interne bouge d’une couche.
Conséquences
- Création de
crates/air-account(codecs + accès), fuzz-cibles pour les 3 codecs. air-processimporteair-account(drop_to_user) ;air-base-lible ré-exporte.air-libc-pwdre-câblé en binder ; ajout des bindersair-libc-grp(groupes) etair-libc-shadow(shadow) sur la même brique.- Tag
couche-1-v1.3posé au merge (signé) ; note dansINDEX.md+etat-avancement.md. - Registre
docs/libc-conformance.md:getpwnam_r/getpwuid_rre-documentés « bind d’air-account» ; ajoutgetgrnam_r/getgrgid_r/getspnam_r/initgroups.
Alternatives rejetées
- Parseur dans la crate libc (ce que faisait #228). Rejeté : viole le principe « une impl Rust bindée en C » ; la logique n’est pas réutilisable par les devs Rust ; duplication à venir (pal, outillage).
- Loger la logique comptes dans
air-process. Rejeté :air-processgère l’exécution de processus ; la lecture des bases de comptes est une préoccupation distincte et réutilisable → crate dédiée qu’air-processimporte. - Modèle NSS (bases pluggables : LDAP, SSSD…). Différé :
air-accountv1 cible les fichiers (/etc/*) — le cas OpenSSH courant. Une abstractionAccountSource(fichiers | NSS | …) est un additif ultérieur, non requis maintenant (Principe 5 : pas de généralité spéculative). - Additif couche 0 pour les credentials uid/gid. Sans objet : la couche 0 les a
déjà (
get_resuid/set_resuid/… typés). Cf. § Contexte.
Suite
- Implémenter
air-account(3 codecs + accès + fuzz), puis refactor #228 en binder, puis bindersgrp/shadow+initgroups. - Surface fine credentials (
air-process::credentials:AirProcessCredentials:: current(),set_uids/set_gidsà-la-carte sur la couche 0) — ADR séparé (même famille OpenSSH, mais sujet distinct des bases de comptes). - Tag
couche-1-v1.3après merge. Note de re-sceau dansINDEX.md+etat-avancement.md.