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

Technical specification — Version 1.0

Family overview

The air-sys-syscall::process module exposes the kernel primitives for managing processes, threads, and child processes. It is one of the foundational families of layer 0: everything concerning a process lifecycle (creation, waiting, termination), identity (PIDs, TIDs), capabilities, and rlimits.

Family scope.

Eighteen wrapped syscalls, organized into sub-categories:

  1. Identity: getpid, gettid.
  2. Process creation: clone3, execve, execveat.
  3. Waiting and termination: waitid, exit_group, pidfd_open, pidfd_send_signal, pidfd_getfd.
  4. Groups and sessions: setpgid, getpgid, setsid, getsid.
  5. Process control: prctl (operations exposed individually).
  6. Resource limits: getrlimit, setrlimit, prlimit.
  7. Capabilities: capget, capset.

Cross-cutting characteristics of the family.

  1. Preference for modern variants. clone3 rather than clone, pidfd_open rather than using bare PIDs for post-fork operations. The legacy syscalls are listed in UNSUPPORTED.md with justification.

  2. Systematic newtypes for identifiers. Pid, Tid, PidFd are typed newtypes. No bare i32 for a PID.

  3. Clear ownership semantics. PidFd is RAII: it automatically closes the FD on destruction. FDs received by operations are always returned as OwnedFd.

  4. Systematic upstream validation. In accordance with Engineering Principle 4, every public function validates its parameters at the start and returns Result or Option on violation.

Subsection 1: Identity

getpid

Signature.

#![allow(unused)]
fn main() {
pub fn getpid() -> Pid;
}

Underlying syscall. getpid (x86_64 #39, ARM64 #172). Man page getpid(2).

Behavior.

Returns the PID of the calling process. Cannot fail. POSIX guarantees that a returned PID is strictly positive.

Return type.

Pid is a newtype over NonZeroI32. The function is total (no Result).

Performance. Negligible (~30-50 ns via vDSO on recent kernels).

Tests.

  • Nominal test: getpid() returns a valid Pid.
  • Consistency test: two successive calls return the same value.

gettid

Signature.

#![allow(unused)]
fn main() {
pub fn gettid() -> Tid;
}

Underlying syscall. gettid (x86_64 #186, ARM64 #178). Man page gettid(2).

Behavior.

Returns the TID of the calling thread. On Linux, each thread has a distinct TID; the main thread of a process has a TID equal to the process PID.

Return type.

Tid is a newtype distinct from Pid to prevent confusion by type.

Performance. ~30-50 ns.

Tests.

  • Main thread test: gettid() equals getpid() for the main thread.
  • Multi-thread test: two threads of the same process have distinct TIDs.

Subsection 2: Process creation

clone3

Signature.

#![allow(unused)]
fn main() {
pub unsafe fn clone3(args: &CloneArgs) -> Result<CloneResult, Errno>;

pub enum CloneResult {
    Parent { child_pid: Pid, child_pidfd: Option<PidFd> },
    Child,
}

#[derive(Debug, Default)]
pub struct CloneArgs {
    pub flags: CloneFlags,
    pub pidfd: Option<PidFdReceiver>,
    pub child_tid: Option<TidReceiver>,
    pub parent_tid: Option<TidReceiver>,
    pub exit_signal: Option<Signal>,
    pub stack: Option<StackSpecification>,
    pub tls: Option<u64>,
    pub set_tid: Option<&[Pid]>,
    pub cgroup: Option<RawFd>,
}

bitflags! {
    pub struct CloneFlags: u64 {
        const VM = 0x00000100;
        const FS = 0x00000200;
        const FILES = 0x00000400;
        const SIGHAND = 0x00000800;
        const PIDFD = 0x00001000;
        const PTRACE = 0x00002000;
        const VFORK = 0x00004000;
        const PARENT = 0x00008000;
        const THREAD = 0x00010000;
        const NEWNS = 0x00020000;
        const SYSVSEM = 0x00040000;
        const SETTLS = 0x00080000;
        const PARENT_SETTID = 0x00100000;
        const CHILD_CLEARTID = 0x00200000;
        const DETACHED = 0x00400000;
        const UNTRACED = 0x00800000;
        const CHILD_SETTID = 0x01000000;
        const NEWCGROUP = 0x02000000;
        const NEWUTS = 0x04000000;
        const NEWIPC = 0x08000000;
        const NEWUSER = 0x10000000;
        const NEWPID = 0x20000000;
        const NEWNET = 0x40000000;
        const IO = 0x80000000;
        const CLEAR_SIGHAND = 0x100000000;
        const INTO_CGROUP = 0x200000000;
        const NEWTIME = 0x400000000;
    }
}

pub struct StackSpecification {
    pub addr: *mut u8,
    pub size: usize,
}
}

Underlying syscall. clone3 (x86_64 #435, ARM64 #435). Man page clone(2). Available since Linux 5.3 (September 2019).

Preconditions (Safety).

The API is unsafe because clone3 can create a thread sharing memory with the parent (CLONE_VM), which requires careful handling of memory aliasing that Rust cannot verify.

The # Safety documentation requires:

  • For “classic fork” uses (creation of an independent child process), no particular precondition. The Air wrapper may provide a safe fork_process helper that calls clone3 with the appropriate flags.

  • For uses with CLONE_VM, the caller must guarantee that the shared memory is manipulated in a thread-safe manner by both sides.

  • For uses with CLONE_NEWUSER, CLONE_NEWNS, etc. (namespaces), the caller must have the necessary capabilities.

Behavior.

clone3 is the modern unified operation for creating processes, threads, and any combination of the two. The precise behavior depends on the flags:

  • Without special flags: equivalent to fork(). Creates an independent child process.
  • With CLONE_VM | CLONE_THREAD | CLONE_SIGHAND: creates a thread in the current process.
  • With CLONE_NEW*: creates the process in new namespaces.
  • With CLONE_PIDFD: also returns a pidfd for the new process, avoiding races on the recycled PID.

The Air wrapper returns a CloneResult that clearly distinguishes the parent side from the child side. For the parent, the child’s PID is returned, plus optionally a PidFd if CLONE_PIDFD was requested.

Recommended pattern.

#![allow(unused)]
fn main() {
let mut args = CloneArgs::default();
args.flags = CloneFlags::PIDFD;
args.exit_signal = Some(Signal::SIGCHLD);

// SAFETY: no shared memory, classic fork with pidfd
let result = unsafe { clone3(&args)? };

match result {
    CloneResult::Parent { child_pid, child_pidfd } => {
        let pidfd = child_pidfd.expect("requested PIDFD");
        // Wait for the child via waitid on the pidfd
    }
    CloneResult::Child => {
        // Child code
        // Probably execve here
        unreachable!("execve should not return");
    }
}
}

Errors.

  • EAGAIN: system limits reached (maximum number of processes).
  • EINVAL: invalid flag combination.
  • ENOMEM: insufficient kernel memory.
  • EPERM: insufficient capabilities for the requested flags.
  • EUSERS: too many user namespaces.

Performance.

clone3 with classic fork: ~50-100 µs. The main cost is the duplication of page tables (copy-on-write).

clone3 for thread creation: ~20-50 µs.

clone3 with namespaces: variable, ~100-500 µs depending on the requested namespaces.

Portability.

Strictly Linux. No direct equivalent on other Unix systems (which have separate fork/vfork/pthread_create).

Tests.

  • Classic fork test: create a child process that exits immediately, waitid, verify exit code.
  • Pidfd test: create with CLONE_PIDFD, verify that the pidfd is valid.
  • Exec-in-child test: fork then execve, verify that the external binary is executed.
  • Namespaces test: create in a new PID namespace, verify isolation.
  • Capabilities test: attempt without privileges, verify EPERM.

execve

Signature.

#![allow(unused)]
fn main() {
pub fn execve(
    path: &CStr,
    argv: &[&CStr],
    envp: &[&CStr],
) -> Result<Infallible, Errno>;
}

Underlying syscall. execve (x86_64 #59, ARM64 #221). Man page execve(2).

Preconditions.

argv and envp are arrays of C-compatible strings. The wrapper converts to *const *const c_char internally.

Behavior.

Replaces the memory image of the current process with that of the binary at path. On success, never returns (the process now runs the new binary). On failure, returns with an Errno.

Return type.

Result<Infallible, Errno>: Infallible is the Rust idiom for “never happens on success”. The user can write let _: Infallible = execve(...)?; and then have an unreachable!().

Errors.

  • E2BIG: argv/envp too large.
  • EACCES: insufficient permissions to execute path.
  • ENOENT: path does not exist.
  • ENOEXEC: path is not a valid executable.
  • ENOMEM: insufficient memory.
  • ETXTBSY: the binary is open for writing by another process.

Performance.

Variable depending on the binary size and the complexity of the loader. Typically ~1-5 ms for a simple binary, more for dynamically linked binaries with many libraries.

Tests.

  • Nominal test: fork then execve of a simple binary (/bin/true), waitid.
  • ENOENT test: execve on a nonexistent path.
  • EACCES test: execve on a non-executable file.

execveat

Signature.

#![allow(unused)]
fn main() {
pub fn execveat(
    dirfd: DirFd,
    path: &CStr,
    argv: &[&CStr],
    envp: &[&CStr],
    flags: ExecveatFlags,
) -> Result<Infallible, Errno>;

bitflags! {
    pub struct ExecveatFlags: i32 {
        const EMPTY_PATH = 0x1000;
        const SYMLINK_NOFOLLOW = 0x100;
    }
}
}

Underlying syscall. execveat (x86_64 #322, ARM64 #281). Available since Linux 3.19.

Behavior.

Variant of execve that takes a directory FD (for relative path resolution) or a direct FD to the binary (with EMPTY_PATH).

Main use case: execute a binary referenced by a previously opened FD, without a race on the filesystem path.

#![allow(unused)]
fn main() {
let bin_fd = openat(DirFd::Cwd, c"/usr/bin/myapp", OpenFlags::PATH | OpenFlags::CLOEXEC, Mode::empty())?;
// Later, after possible checks:
execveat(DirFd::Fd(bin_fd.as_fd()), c"", &argv, &envp, ExecveatFlags::EMPTY_PATH)?;
}

Tests.

  • EMPTY_PATH test: openat on binary, execveat with FD.
  • Relative path test: execveat with dirfd + path.

Subsection 3: Waiting and termination

waitid

Signature.

#![allow(unused)]
fn main() {
pub fn waitid(
    target: WaitTarget,
    options: WaitOptions,
) -> Result<Option<WaitStatus>, Errno>;

pub enum WaitTarget {
    AnyChild,
    Pid(Pid),
    ProcessGroup(Pid),
    AnyProcessGroup,
    PidFd(BorrowedFd<'_>),
}

bitflags! {
    pub struct WaitOptions: i32 {
        const EXITED = 4;
        const STOPPED = 2;
        const CONTINUED = 8;
        const NOHANG = 1;
        const NOWAIT = 0x01000000;
    }
}

pub struct WaitStatus {
    pub pid: Pid,
    pub uid: u32,
    pub event: WaitEvent,
}

pub enum WaitEvent {
    Exited { code: i32 },
    Killed { signal: Signal, core_dumped: bool },
    Stopped { signal: Signal },
    Continued,
    Trapped { signal: Signal },
}
}

Underlying syscall. waitid (x86_64 #247, ARM64 #95). Man page waitid(2).

Preconditions.

WaitOptions must contain at least one of EXITED, STOPPED, or CONTINUED (otherwise waitid does not know which events to report).

Behavior.

Waits for an event to occur on the targeted process or group. More powerful than wait or waitpid because it can wait on a pidfd directly (avoiding races) and discriminate event types.

The wrapper returns Result<Option<WaitStatus>, Errno>. Ok(None) indicates that NOHANG was requested and that no event was available.

Pattern with pidfd.

#![allow(unused)]
fn main() {
let mut args = CloneArgs::default();
args.flags = CloneFlags::PIDFD;
// SAFETY: classic fork with pidfd
let result = unsafe { clone3(&args)? };

if let CloneResult::Parent { child_pidfd: Some(pidfd), .. } = result {
    let status = waitid(WaitTarget::PidFd(pidfd.as_fd()), WaitOptions::EXITED)?;
    match status {
        Some(WaitStatus { event: WaitEvent::Exited { code }, .. }) => {
            println!("Child exited with code {}", code);
        }
        _ => { /* other cases */ }
    }
}
}

Errors.

  • ECHILD: no child to wait for.
  • EINTR: interrupted by a signal (in accordance with convention 2 of ADR-021, propagated to the caller).
  • EINVAL: invalid options.

Performance.

Variable. With NOHANG, immediate return ~1 µs. Without NOHANG, blocking until an event.

exit_group

Signature.

#![allow(unused)]
fn main() {
pub fn exit_group(status: i32) -> !;
}

Underlying syscall. exit_group (x86_64 #231, ARM64 #94). Man page exit_group(2).

Behavior.

Terminates all threads of the calling process with the specified exit code. Never returns.

Return type.

! (never type): the function never returns.

Performance. Negligible on the caller side (the process disappears).

pidfd_open

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_open(pid: Pid, flags: PidFdOpenFlags) -> Result<PidFd, Errno>;

bitflags! {
    pub struct PidFdOpenFlags: u32 {
        const NONBLOCK = 0x800;
    }
}

pub struct PidFd { /* owns an internal OwnedFd */ }

impl PidFd {
    pub fn as_fd(&self) -> BorrowedFd<'_>;
    pub fn into_fd(self) -> OwnedFd;
}
}

Underlying syscall. pidfd_open (x86_64 #434, ARM64 #434). Available since Linux 5.3.

Behavior.

Opens an FD that references a process by PID. The FD remains valid even if the PID is recycled after the death of the original process (the FD does not “transfer” to a new process).

The pidfd can then be used for:

  • waitid (wait for the process to finish).
  • pidfd_send_signal (send a signal without a race on the PID).
  • pidfd_getfd (retrieve an FD from the target process).
  • Monitoring via poll/epoll (the FD becomes readable when the process terminates).

Errors.

  • EINVAL: invalid flags.
  • ESRCH: nonexistent process.
  • ENOMEM: insufficient kernel memory.

Performance. ~5-10 µs.

Tests.

  • Nominal test: fork child, pidfd_open on its PID, waitid on the pidfd.
  • ESRCH test: pidfd_open on a nonexistent PID.
  • Post-mortem test: create pidfd, kill process, verify that the pidfd remains valid until waitid.

pidfd_send_signal

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_send_signal(
    pidfd: BorrowedFd<'_>,
    signal: Signal,
    info: Option<&SignalInfo>,
) -> Result<(), Errno>;
}

Underlying syscall. pidfd_send_signal (x86_64 #424, ARM64 #424). Available since Linux 5.1.

Behavior.

Sends a signal to the process referenced by the pidfd. No race on the recycled PID: if the original process is dead, the signal is not delivered to a possible new process with the same PID.

To be preferred over kill(pid, sig) when a pidfd is available.

Errors.

  • EBADF: invalid pidfd.
  • EINVAL: invalid signal.
  • EPERM: insufficient permissions.
  • ESRCH: the referenced process no longer exists.

pidfd_getfd

Signature.

#![allow(unused)]
fn main() {
pub fn pidfd_getfd(
    pidfd: BorrowedFd<'_>,
    target_fd: RawFd,
    flags: u32,
) -> Result<OwnedFd, Errno>;
}

Underlying syscall. pidfd_getfd (x86_64 #438, ARM64 #438). Available since Linux 5.6.

Behavior.

Retrieves a duplicate of an FD open in another process. Requires CAP_SYS_PTRACE or being the same user as the target process.

Specialized use case: debuggers, monitoring tools, resource transfer between cooperating processes.

Subsection 4: Groups and sessions

setpgid, getpgid, setsid, getsid

Signatures.

#![allow(unused)]
fn main() {
pub fn setpgid(pid: Option<Pid>, pgid: Option<Pid>) -> Result<(), Errno>;
pub fn getpgid(pid: Option<Pid>) -> Result<Pid, Errno>;
pub fn setsid() -> Result<Pid, Errno>;
pub fn getsid(pid: Option<Pid>) -> Result<Pid, Errno>;
}

Underlying syscalls. setpgid (x86_64 #109, ARM64 #154), getpgid (#121, #155), setsid (#112, #157), getsid (#124, #156).

Preconditions.

Option<Pid> allows expressing “the current process” via None (instead of the kernel sentinel 0). Consistent with convention 1 of ADR-021.

Behavior.

Management of Unix process groups and sessions. Rarely used in modern applications but necessary for shells, daemons, and certain terminal applications.

Performance. Negligible (~1-2 µs).

Subsection 5: prctl (operations exposed individually)

prctl is a multiplexed syscall: 60+ distinct operations depending on the first argument. In accordance with convention 3 of ADR-021, Air does not wrap prctl generically but exposes each operation as a dedicated typed function.

Exposed operations

#![allow(unused)]
fn main() {
// PR_SET_PDEATHSIG / PR_GET_PDEATHSIG
pub fn set_parent_death_signal(signal: Option<Signal>) -> Result<(), Errno>;
pub fn get_parent_death_signal() -> Result<Option<Signal>, Errno>;

// PR_SET_NO_NEW_PRIVS / PR_GET_NO_NEW_PRIVS
pub fn set_no_new_privs() -> Result<(), Errno>;
pub fn get_no_new_privs() -> Result<bool, Errno>;

// PR_SET_NAME / PR_GET_NAME (thread name, max 16 bytes)
pub fn set_thread_name(name: &CStr) -> Result<(), Errno>;
pub fn get_thread_name() -> Result<CString, Errno>;

// PR_SET_DUMPABLE / PR_GET_DUMPABLE
pub fn set_dumpable(dumpable: DumpableMode) -> Result<(), Errno>;
pub fn get_dumpable() -> Result<DumpableMode, Errno>;

pub enum DumpableMode {
    NotDumpable,
    Dumpable,
    SuidDumpable,
}

// PR_SET_KEEPCAPS / PR_GET_KEEPCAPS
pub fn set_keep_caps(keep: bool) -> Result<(), Errno>;
pub fn get_keep_caps() -> Result<bool, Errno>;

// PR_SET_TIMERSLACK
pub fn set_timer_slack(slack_ns: u64) -> Result<(), Errno>;

// PR_CAP_AMBIENT (with sub-operations)
pub fn cap_ambient_raise(cap: Capability) -> Result<(), Errno>;
pub fn cap_ambient_lower(cap: Capability) -> Result<(), Errno>;
pub fn cap_ambient_clear_all() -> Result<(), Errno>;
pub fn cap_ambient_is_set(cap: Capability) -> Result<bool, Errno>;

// PR_SET_SECCOMP_MODE_FILTER (alternative to the direct seccomp syscall)
// Not exposed here, prefer seccomp_load_filter in the security family.
}

This list covers the most-used prctl operations. Rare operations (PR_TASK_PERF_EVENTS, PR_GET_TID_ADDRESS, etc.) are listed in UNSUPPORTED.md and can be added on a case-by-case basis if Air needs them.

Subsection 6: Resource limits

getrlimit, setrlimit, prlimit

Signatures.

#![allow(unused)]
fn main() {
pub fn getrlimit(resource: Resource) -> Result<Rlimit, Errno>;
pub fn setrlimit(resource: Resource, limit: Rlimit) -> Result<(), Errno>;
pub fn prlimit(
    pid: Option<Pid>,
    resource: Resource,
    new_limit: Option<Rlimit>,
) -> Result<Rlimit, Errno>;

pub enum Resource {
    Cpu,
    FileSize,
    Data,
    Stack,
    Core,
    Rss,
    NProc,
    NoFile,
    MemLock,
    As,
    Locks,
    SigPending,
    MsgQueue,
    Nice,
    RtPrio,
    RtTime,
}

pub struct Rlimit {
    pub soft: RlimitValue,
    pub hard: RlimitValue,
}

pub enum RlimitValue {
    Finite(u64),
    Infinity,
}
}

Underlying syscalls. getrlimit (x86_64 #97, ARM64 #163), setrlimit (#160, #164), prlimit64 (#302, #261).

Behavior.

Reads and modifies the resource limits of a process. prlimit is more powerful: it allows operating on another process (with appropriate privileges) and performs both operations (reading the old value + writing the new one) in a single atomic call.

Air recommendation. Prefer prlimit everywhere. getrlimit and setrlimit are exposed for compatibility but are essentially special cases of prlimit.

Errors.

  • EFAULT: should not happen via the safe API.
  • EINVAL: invalid resource or invalid limit.
  • EPERM: attempt to raise the hard limit without privileges.
  • ESRCH: nonexistent process (for prlimit).

Subsection 7: Capabilities

capget, capset

Signatures.

#![allow(unused)]
fn main() {
pub fn capget(target: CapabilityTarget) -> Result<CapabilitySet, Errno>;
pub fn capset(target: CapabilityTarget, set: &CapabilitySet) -> Result<(), Errno>;

pub enum CapabilityTarget {
    CurrentThread,
    Thread(Tid),
    Process(Pid),
}

pub struct CapabilitySet {
    pub effective: CapabilityMask,
    pub permitted: CapabilityMask,
    pub inheritable: CapabilityMask,
}

#[derive(Debug, Clone, Copy)]
pub struct CapabilityMask(u64);

#[derive(Debug, Clone, Copy)]
pub enum Capability {
    Chown,
    DacOverride,
    DacReadSearch,
    Fowner,
    Fsetid,
    Kill,
    Setgid,
    Setuid,
    SysAdmin,
    SysBoot,
    SysChroot,
    SysModule,
    SysNice,
    SysPtrace,
    SysRawio,
    SysResource,
    SysTime,
    NetAdmin,
    NetBindService,
    NetRaw,
    // ... other Linux capabilities
}
}

Underlying syscalls. capget (x86_64 #125, ARM64 #90), capset (#126, #91).

Behavior.

Management of Linux capabilities: fine-grained permissions replacing the binary root/non-root model.

Three sets per thread:

  • Effective: capabilities currently usable.
  • Permitted: capabilities the thread can enable.
  • Inheritable: capabilities transmitted across execve.

Performance. ~1-2 µs.

Tests.

  • Current capget test: read the capabilities, verify consistency.
  • capset drop test: remove a capability, verify that it disappears.
  • EPERM test: attempt to raise a non-permitted capability, verify the error.

Process family summary

Exposed functions:

CategoryMain functions
Identitygetpid, gettid
Creationclone3, execve, execveat
Waitingwaitid, exit_group
Pidfdpidfd_open, pidfd_send_signal, pidfd_getfd
Groupssetpgid, getpgid, setsid, getsid
prctl~12 individual operations
Limitsgetrlimit, setrlimit, prlimit
Capabilitiescapget, capset

Total: 18 wrapped syscalls, plus the individual prctl operations.

Non-wrapped syscalls (listed in UNSUPPORTED.md):

  • fork, vfork: replaced by clone3.
  • clone (without the 3): replaced by clone3.
  • wait, wait3, wait4, waitpid: replaced by waitid.
  • kill: exposed in the signal family, not in process.
  • tkill: deprecated, replaced by tgkill (in the signal family).
  • _exit: replaced by exit_group.
  • Rare prctl operations not listed above.

Types added to air-sys-types

  • Pid, Tid
  • PidFd, PidFdReceiver, TidReceiver
  • WaitTarget, WaitOptions, WaitStatus, WaitEvent
  • CloneFlags, CloneArgs, CloneResult
  • Signal, SignalMask (also used in the signal family)
  • Capability, CapabilityMask, CapabilitySet, CapabilityTarget
  • Resource, Rlimit, RlimitValue
  • DumpableMode
  • ExecveatFlags, PidFdOpenFlags

That is ~20 types for this family.

Underlying decisions that emerged in the process family

1. Pid and Tid as distinct newtypes.

Prevents, by typing, confusion between a process PID and a thread TID. Direct application of Principle 7.

2. Option<Pid> for “current process”.

Instead of the kernel sentinel 0 which means “the calling process”, Air uses None. Clearer, safer, application of convention 1 of ADR-021.

3. PidFd RAII with clear ownership.

The pidfd is encapsulated in a type that automatically closes the FD on destruction. No leak possible.

4. clone3 unsafe but with a safe helper to come.

The low-level API remains unsafe because CLONE_VM can violate Rust invariants. A safe fork_process helper covering the common case (classic fork without shared VM) will be exposed in layer 1.

5. prctl exposed operation by operation.

Refusal of the generic multiplexed form. Each prctl operation is a dedicated typed function. The cost in code volume is offset by the clarity.


Document license: MPL 2.0 Status: Technical specification of the air-sys-syscall::process module (layer 0).