Layer 0 spec — process family: privsep extension (setgroups / setresgid / setresuid + getres*)
Technical specification — Version 1.0 (target kernel: Linux 6.12 LTS). Extension
of the process family.
Position
This extension adds the missing primitives essential to a correct
privilege reduction (drop_privileges of air-process, layer 1,
Principle 10). The process family exposes neither setgroups, setresgid,
setresuid, nor the verification getres* (not even setuid/setgid), and
no Uid/Gid types — these types are added by this extension
(observed at implementation time, PR #34).
Emergence (doc-first method). Gap revealed while specifying
air-process: performing a privsep withsetuid/setgidalone is a security footgun — the supplementary groups are not dropped, and the saved-set-uid/gid is not set → possibility of regaining privileges. 4th layer 0 gap identified, and the highest priority (security), alongsidefs::inotify,MmapRegion(resolved) andepoll.
Submodule: air-sys-syscall::process (extension). Privileged: these
operations require the appropriate capabilities (CAP_SETUID/CAP_SETGID) or
being root at call time.
1. Types
Reuses the typed newtypes Uid/Gid from the process family (ADR-029:
never a raw u32 for an identifier). If they do not yet exist, add them
(air-sys-types::process) at the same time — Uid(u32)/Gid(u32),
#[repr(transparent)], with as_raw/from_raw. (To be confirmed at implementation time.)
2. Dropping supplementary groups
#![allow(unused)]
fn main() {
/// `setgroups` — replaces the process's supplementary groups list.
/// For a privsep, pass **`&[]`** (full drop) while still privileged.
/// # Errors `EPERM` (without `CAP_SETGID`), `EINVAL` (too many groups), `EFAULT`.
pub fn set_groups(groups: &[Gid]) -> Result<(), Errno>;
/// `getgroups` — reads the current list (defensive verification). Caller-provided
/// buffer, zero alloc; returns the filled slice.
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). No sentinel; the empty list is the normal privsep value
(not a magic case).
3. Real/effective/saved identity
#![allow(unused)]
fn main() {
/// `setresgid` — sets **real + effective + saved** GID in one call. To be done
/// **before** `setresuid`. `None` for a component = "unchanged" (kernel
/// sentinel `-1` typed as `Option`, ADR-021 conv. 1).
/// # Errors `EPERM`, `EINVAL`.
pub fn set_resgid(real: Option<Gid>, effective: Option<Gid>, saved: Option<Gid>)
-> Result<(), Errno>;
/// `setresuid` — sets **real + effective + saved** UID in one call. After the GID.
/// `None` = "unchanged" (typed `-1`).
/// # Errors `EPERM`, `EINVAL`.
pub fn set_resuid(real: Option<Uid>, effective: Option<Uid>, saved: Option<Uid>)
-> Result<(), Errno>;
/// `getresgid` / `getresuid` — reads the three components (defensive
/// verification: confirm that reverting is **no longer** possible).
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). (Numbers verified against uapi 6.12 per architecture at
implementation time — setresgid ARM64 corrected 143 → 149, PR #34.)
Why
setres*and notset*.setresuid/setresgidset all three identities (real/effective/saved) explicitly in one syscall. Setting the saved-set is what makes regaining privileges impossible — a guarantee that a plainsetuid/setgiddoes not reliably provide. This is the raison d’être of this extension.None(= kernel-1) leaves a component unchanged, cleanly typed (no magic-1).
4. Summary
| Function | Role | Syscall |
|---|---|---|
set_groups / get_groups | drop / read the supplementary groups | setgroups / getgroups |
set_resgid / get_resgid | GID real+effective+saved | setresgid / getresgid |
set_resuid / get_resuid | UID real+effective+saved | setresuid / getresuid |
Total: ~6 functions. Types added to air-sys-types::process: ResUid, ResGid
(+ Uid/Gid if absent).
5. Tests
- Privileged (root /
CAP_SETUID+CAP_SETGID), in isolated subprocesses (fork+ observation viawaitid), clean skip if not privileged →COVERAGE-EXCEPTIONS.md(PRIVILEGE category):set_groups(&[])thenget_groupsreturns an empty list.set_resgidthenset_resuidto a non-root uid/gid, then verify the impossibility of reverting:set_resuid(Some(root), …)→EPERM;get_resuidconfirms real==effective==saved==target.None(component unchanged): a single component modified.
- Errors:
EPERMwithout capability (covered on a non-root executor),EINVAL. - Property-based: encoding of
Option<Uid/Gid>→-1forNone, round-trip. - Cross-arch validation x86_64 + aarch64.
6. Foundational decisions
setres*(and notset*) to set the saved-set — prohibition of regaining privileges by construction.setgroups(&[])as first class — dropping supplementary groups, a mandatory step of the privsep.None= component unchanged (typed kernel-1, ADR-021 conv. 1).getres*/getgroupsexposed for the defensive verification ofdrop_privileges(Principle 5: confirm the reduction).
Document license: MPL 2.0
Status: Technical specification of the privsep extension of
air-sys-syscall::process (layer 0), target kernel 6.12 LTS. Prerequisite of
air-process::drop_privileges.