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-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.2couche-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/passwdPasswdEntry { name, passwd, uid: Uid, gid: Gid, gecos, dir, shell } (7 champs, uid/gid validés u32 checked).
  • codec /etc/groupGroupEntry { name, passwd, gid: Gid, members: Vec<AirOsString> } (4 champs ; le 4ᵉ est une liste de membres séparés par ,).
  • codec /etc/shadowShadowEntry { 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 EACCES sans privilège (comportement kernel via air-filesystem) — jamais de dégradation silencieuse.
  • Zéro fuite : ShadowEntry.hash n’est jamais journalisé ni inclus dans un Display/Debug par défaut (redaction) ; la comparaison de mot de passe (temps constant) n’appartient pas à air-account (relève de air-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-lib grandissent 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-pwdair-account) est non-rupturant : l’ABI C (getpwnam_r/getpwuid_r, struct passwd, symboles nm -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-process importe air-account (drop_to_user) ; air-base-lib le ré-exporte.
  • air-libc-pwd re-câblé en binder ; ajout des binders air-libc-grp (groupes) et air-libc-shadow (shadow) sur la même brique.
  • Tag couche-1-v1.3 posé au merge (signé) ; note dans INDEX.md + etat-avancement.md.
  • Registre docs/libc-conformance.md : getpwnam_r/getpwuid_r re-documentés « bind d’air-account » ; ajout getgrnam_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-process gè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-process importe.
  • Modèle NSS (bases pluggables : LDAP, SSSD…). Différé : air-account v1 cible les fichiers (/etc/*) — le cas OpenSSH courant. Une abstraction AccountSource (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 binders grp/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.3 après merge. Note de re-sceau dans INDEX.md + etat-avancement.md.