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

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 with setuid/setgid alone 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), alongside fs::inotify, MmapRegion (resolved) and epoll.

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 not set*. setresuid/setresgid set all three identities (real/effective/saved) explicitly in one syscall. Setting the saved-set is what makes regaining privileges impossible — a guarantee that a plain setuid/setgid does 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

FunctionRoleSyscall
set_groups / get_groupsdrop / read the supplementary groupssetgroups / getgroups
set_resgid / get_resgidGID real+effective+savedsetresgid / getresgid
set_resuid / get_resuidUID real+effective+savedsetresuid / 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 via waitid), clean skip if not privileged → COVERAGE-EXCEPTIONS.md (PRIVILEGE category):
    • set_groups(&[]) then get_groups returns an empty list.
    • set_resgid then set_resuid to a non-root uid/gid, then verify the impossibility of reverting: set_resuid(Some(root), …)EPERM; get_resuid confirms real==effective==saved==target.
    • None (component unchanged): a single component modified.
  • Errors: EPERM without capability (covered on a non-root executor), EINVAL.
  • Property-based: encoding of Option<Uid/Gid>-1 for None, round-trip.
  • Cross-arch validation x86_64 + aarch64.

6. Foundational decisions

  1. setres* (and not set*) to set the saved-set — prohibition of regaining privileges by construction.
  2. setgroups(&[]) as first class — dropping supplementary groups, a mandatory step of the privsep.
  3. None = component unchanged (typed kernel -1, ADR-021 conv. 1).
  4. getres*/getgroups exposed for the defensive verification of drop_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.