Spec couche 0 — Famille process : extension privsep (setgroups / setresgid / setresuid + getres*)
Spécification technique — Version 1.0 (cible kernel : Linux 6.12 LTS). Extension
de la famille process.
Position
Cette extension ajoute les primitives manquantes indispensables à une
réduction de privilèges correcte (drop_privileges d’air-process, couche 1,
Principe 10). La famille process n’expose ni setgroups, setresgid,
setresuid, ni les getres* de vérification (ni même setuid/setgid), et
pas de types Uid/Gid — ces types sont ajoutés par cette extension
(constaté à l’implémentation, PR #34).
Émergence (méthode doc-d’abord). Manque révélé en spécifiant
air-process: faire un privsep avecsetuid/setgidseuls est un footgun de sécurité — les groupes supplémentaires ne sont pas largués, et le saved-set-uid/gid n’est pas fixé → possibilité de regagner les privilèges. 4ᵉ manque couche 0 identifié, et le plus prioritaire (sécurité), à côté defs::inotify,MmapRegion(résolu) etepoll.
Sous-module : air-sys-syscall::process (extension). Privilégié : ces
opérations exigent les capabilities adéquates (CAP_SETUID/CAP_SETGID) ou d’être
root au moment de l’appel.
1. Types
Réutilise les newtypes typés Uid/Gid de la famille process (ADR-029 :
jamais d’u32 brut pour un identifiant). S’ils n’existent pas encore, les
ajouter (air-sys-types::process) en même temps — Uid(u32)/Gid(u32),
#[repr(transparent)], avec as_raw/from_raw. (À confirmer à l’implémentation.)
2. Largage des groupes supplémentaires
#![allow(unused)]
fn main() {
/// `setgroups` — remplace la liste des groupes supplémentaires du processus.
/// Pour un privsep, on passe **`&[]`** (largage total) tant qu'on est privilégié.
/// # Errors `EPERM` (sans `CAP_SETGID`), `EINVAL` (trop de groupes), `EFAULT`.
pub fn set_groups(groups: &[Gid]) -> Result<(), Errno>;
/// `getgroups` — lit la liste courante (vérification défensive). Buffer fourni,
/// zéro alloc ; retourne la tranche remplie.
pub fn get_groups<'b>(buffer: &'b mut [Gid]) -> Result<&'b [Gid], Errno>;
}
Syscalls. setgroups (x86_64 n°116, ARM64 n°159), getgroups (x86_64 n°115,
ARM64 n°158). Pas de sentinelle ; la liste vide est la valeur normale du
privsep (et non un cas magique).
3. Identité réelle/effective/sauvegardée
#![allow(unused)]
fn main() {
/// `setresgid` — fixe **real + effective + saved** GID en un appel. À faire
/// **avant** `setresuid`. `None` pour un composant = « inchangé » (sentinelle
/// kernel `-1` typée en `Option`, ADR-021 conv. 1).
/// # Errors `EPERM`, `EINVAL`.
pub fn set_resgid(real: Option<Gid>, effective: Option<Gid>, saved: Option<Gid>)
-> Result<(), Errno>;
/// `setresuid` — fixe **real + effective + saved** UID en un appel. Après le GID.
/// `None` = « inchangé » (`-1` typé).
/// # Errors `EPERM`, `EINVAL`.
pub fn set_resuid(real: Option<Uid>, effective: Option<Uid>, saved: Option<Uid>)
-> Result<(), Errno>;
/// `getresgid` / `getresuid` — lecture des trois composantes (vérification
/// défensive : confirmer qu'on **ne peut plus** revenir en arrière).
pub fn get_resgid() -> Result<ResGid, Errno>;
pub fn get_resuid() -> Result<ResUid, Errno>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResUid { pub real: Uid, pub effective: Uid, pub saved: Uid }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResGid { pub real: Gid, pub effective: Gid, pub saved: Gid }
}
Syscalls. setresgid (x86_64 n°119, ARM64 n°149), setresuid (x86_64 n°117,
ARM64 n°147), getresgid (x86_64 n°120, ARM64 n°150), getresuid (x86_64 n°118,
ARM64 n°148). (Numéros vérifiés sur l’uapi 6.12 par arche à l’implémentation —
setresgid ARM64 corrigé 143 → 149, PR #34.)
Pourquoi
setres*et nonset*.setresuid/setresgidfixent explicitement les trois identités (real/effective/saved) en un syscall. Fixer le saved-set est ce qui rend le retour aux privilèges impossible — garantie qu’unsetuid/setgidsimple ne donne pas de façon fiable. C’est la raison d’être de cette extension.None(=-1kernel) laisse une composante inchangée, typé proprement (pas de-1magique).
4. Récapitulatif
| Fonction | Rôle | Syscall |
|---|---|---|
set_groups / get_groups | larguer / lire les groupes supplémentaires | setgroups / getgroups |
set_resgid / get_resgid | GID real+effective+saved | setresgid / getresgid |
set_resuid / get_resuid | UID real+effective+saved | setresuid / getresuid |
Total : ~6 fonctions. Types ajoutés à air-sys-types::process : ResUid, ResGid
(+ Uid/Gid si absents).
5. Tests
- Privilégiés (root /
CAP_SETUID+CAP_SETGID), en sous-processus isolés (fork+ observation viawaitid), skip propre si non privilégié →COVERAGE-EXCEPTIONS.md(catégorie PRIVILEGE) :set_groups(&[])puisget_groupsrend une liste vide.set_resgidpuisset_resuidvers un uid/gid non-root, puis vérifier l’impossibilité de revenir :set_resuid(Some(root), …)→EPERM;get_resuidconfirme real==effective==saved==cible.None(composante inchangée) : une seule composante modifiée.
- Erreurs :
EPERMsans capability (couvert sur exécuteur non-root),EINVAL. - Property-based : encodage des
Option<Uid/Gid>→-1pourNone, round-trip. - Validation cross-arch x86_64 + aarch64.
6. Décisions de fond
setres*(et nonset*) pour fixer le saved-set — interdiction du retour aux privilèges par construction.setgroups(&[])de première classe — largage des groupes supplémentaires, étape obligatoire du privsep.None= composante inchangée (-1kernel typé, ADR-021 conv. 1).getres*/getgroupsexposés pour la vérification défensive dedrop_privileges(Principe 5 : confirmer la réduction).
Licence du document : MPL 2.0
Statut : Spécification technique de l’extension privsep de
air-sys-syscall::process (couche 0), cible kernel 6.12 LTS. Prérequis de
air-process::drop_privileges.