The Process Control Block
July 16, 2026 · View on GitHub
Every process in NØNOS is a ProcessControlBlock (src/process/core/pcb.rs:31). It
is a large structure, and this page organises it by concern rather than listing it
top to bottom: identity, scheduling, address space, authority, the kernel-to-user
transition, files, signals, and the POSIX-shaped bookkeeping. Two groups are the
NØNOS-specific core, the authority fields and the transition fields, and the rest
give a process the Linux-shaped semantics that let unmodified user code run. The
whole structure is shared behind Arc, so its fields are individually synchronised:
atomics for single hot values, a Mutex for state mutated together, and an RwLock
for the read-heavy capability token.
Identity and hierarchy
pid Pid the process id, immutable
tgid AtomicU32 thread-group id (the thread leader's pid)
ppid AtomicU32 parent pid
pgid AtomicU32 process-group id
sid AtomicU32 session id
name Mutex<String>
thread_group Option<Arc<ThreadGroup>>
pid is the only identity field that never changes; the rest of the hierarchy is
atomic so it can be reparented or moved between groups and sessions without a lock.
The thread_group, when present, is a shared ThreadGroup that the thread-related
predicates consult: is_thread is true when the group has more than one member or
this pid is not the group's tgid, and is_group_leader is true when this pid leads
its group (pcb.rs:205).
Scheduling
state Mutex<ProcessState> Ready, Running, Sleeping, Terminated, ...
priority Mutex<Priority> Idle, Low, Normal, High, RealTime
nice AtomicI32 POSIX nice value
rt_priority AtomicU32 realtime priority
policy AtomicU32 scheduling policy
processor AtomicU32 last CPU it ran on
cpus_allowed AtomicU64 affinity mask
voluntary_switches / involuntary_switches AtomicU64
wchan AtomicU64 what it is blocked on
state and priority are the two the scheduler reads on every
selection, held under Mutex because a state transition often changes more than one
thing at once. The switch counters and processor are the scheduler's own
bookkeeping, and cpus_allowed bounds which CPUs the process may run on.
Address space
memory Mutex<MemoryState> code range, VMAs, resident page count
mmap_va Mutex<MmapVa> the mmap-region allocator
cr3 AtomicU64 this process's page-table root
memory_info Mutex<ProcessMemoryInfo>
stack_base AtomicU64
tls_base AtomicU64
memory is the record the fault handlers and the exit
teardown walk: it holds the code range and the virtual memory areas. cr3 is the
page-table root loaded on a context switch into this process, and mmap_va tracks
the region from which mmap hands out addresses. tls_base and stack_base have
their own atomic accessors so the thread-local and stack pointers can be read and set
without taking the memory lock.
Authority
These four fields are the runtime side of the capability model,
and the source comments state their contract exactly (pcb.rs:44):
capability_token RwLock<Arc<CapabilityToken>> the source of truth
caps_bits AtomicU64 a derived bitmap cache
caps_manifest_installed AtomicBool one-shot install gate
revocation_epoch AtomicU64 per-capsule revoke counter
The capability_token is authoritative; caps_bits is a bitmap cache kept in sync by
process::caps so that the bitmap-only readers, IPC routing and inheritance, stay a
single atomic load rather than taking the token lock. caps_manifest_installed is the
one-shot gate: the PCB is born holding an inheritance-derived token, and
verified spawn's install_spawn flips this flag
exactly once to swap in the manifest-derived token, so a stale spawn path cannot
re-issue authority. revocation_epoch is bumped by process::caps::revoke and minted
into every token, and the syscall resolver compares a
token's epoch against it to reject authority minted before the most recent revoke.
The kernel-to-user transition
These fields hold everything the architecture layer needs to enter or resume user
mode, and their comments are the authority on the multi-arch shape (pcb.rs:100):
kernel_stack_top AtomicU64 TSS RSP0; 0 means no user mode expected
syscall_user_rsp AtomicU64 user RSP captured on syscall entry
pending_user_entry Mutex<Option<UserEntry>> first-entry-to-user record
saved_user_context Mutex<Option<SavedUser>> preemption snapshot
kstkesp / kstkeip AtomicU64
arch_fpu (aarch64/riscv64 only) per-PCB FP/SIMD slot
kernel_stack_top is the kernel stack installed into the TSS on a context switch, and
a value of zero is meaningful: it marks a process with no user mode expected, and the
scheduler hook refuses to dispatch a pending user entry for it. pending_user_entry is
the record consumed the first time the process drops to ring 3; on x86_64 it is the
iretq five-tuple, and on aarch64 and riscv64 it carries that architecture's entry
shape (ELR, SP_EL0, SPSR and the per-task kernel SP on aarch64; sepc, sstatus, user SP
and kernel SP on riscv64). saved_user_context is the snapshot the trap-entry path
writes when a user task is preempted, the general registers plus the return frame, and
it is what the resume path restores. syscall_user_rsp exists because a blocking
syscall parks the user RSP in per-CPU state, and if another task runs on the same CPU
before the syscall returns, that per-CPU slot has to be restored from the PCB. On
aarch64 and riscv64 the PCB carries a per-task FP/SIMD slot for the lazy-enable path,
while x86 keeps its FPU state in a pid-keyed side table instead, so there is no such
field there. This is where the process model and the context switch
meet.
Files, IPC, and signals
fd_table ProcessFdTable open file descriptors
umask Mutex<u32>
root_dir / cwd Mutex<String>
io_bitmap Mutex<[u8; 8192]> x86 port-IO permission bitmap
reply_inbox RwLock<Option<&'static str>> the process's IPC reply inbox
signals Mutex<SignalState>
pending_signals AtomicU64
exit_signal / alarm_time_ms
The fd_table is the process's descriptor table, and umask, root_dir, and cwd
are the filesystem context. io_bitmap is the 8 KiB x86 port-IO permission bitmap
that backs PIO grants: a driver that holds a port grant has
the corresponding bits cleared here so the CPU permits its in/out. reply_inbox
is the name of the private IPC inbox this process receives replies on, and
signals with pending_signals is the POSIX signal state.
Bookkeeping and compatibility
The remaining fields give a process the shape unmodified POSIX-style code expects:
argv and envp, creds (ProcessCredentials), time_info and io_stats,
start_time_ms and exit_code, tty_nr and tty_pgrp, clone_flags,
clear_child_tid and set_child_tid, no_new_privs and seccomp, thread_count,
and a flags word whose top bit is the continued flag managed by was_continued,
set_continued, and clear_continued (pcb.rs:217). A separate group of counters,
zk_proofs_generated, zk_proving_time_ms, zk_proofs_verified,
zk_verification_time_ms, and zk_circuits_compiled, tracks the process's use of the
in-kernel zero-knowledge machinery.
Key methods
Beyond the field accessors, terminate(code) (pcb.rs:193) records the exit code and
sets the state to Terminated(code) in one step, set_name truncates a new name to
256 bytes, and the identity accessors (pid, parent_pid, process_group,
session_id, thread_group_id, exit_status) read their atomics with the
appropriate ordering. The lifecycle that creates and destroys a PCB is on the
lifecycle page, and the table that holds every live PCB is on the
process table page.
Security analysis
The PCB is where a process's authority lives, so most of its security weight is in the authority group and the kernel-to-user transition group, not the POSIX bookkeeping. Three properties matter.
Authority is single-sourced and epoch-checked. The capability_token
(RwLock<Arc<CapabilityToken>>, pcb.rs:44) is the source of truth; caps_bits is only a derived
bitmap cache kept in sync by process::caps so hot readers do a single atomic load instead of taking the
token lock. The two never disagree by construction because one mutator maintains both. Authority cannot
be re-granted quietly: caps_manifest_installed is a one-shot AtomicBool that
verified spawn's install_spawn flips exactly once to swap the
inheritance-derived token for the manifest-derived one, so a stale spawn path cannot re-issue authority.
And revocation_epoch, bumped by process::caps::revoke and minted into every token, lets the syscall
resolver reject a token whose epoch predates the most recent revoke, so
authority minted before a revoke is dead even if the token is still held.
Port-IO authority defaults to none. io_bitmap (pcb.rs:100) is the 8 KiB x86 permission bitmap
the CPU consults on in/out, and it is born all-ones, every port denied. A PIO grant
works by clearing the specific bits it authorises, so a process with no grant can execute no port I/O at
all. This is least-authority made concrete in hardware: the default is deny, and the broker grant is the
only thing that opens a port.
The kernel-to-user fields are what make ring transitions safe. kernel_stack_top of zero is
meaningful (pcb.rs:100): it marks a process with no user mode expected, and the context-switch hook
refuses to drop it to ring 3 without a kernel stack for the next trap to land on. pending_user_entry
is consumed once on first entry so the transition is one-shot, and syscall_user_rsp exists precisely
because a blocking syscall parks the user RSP in per-CPU state that another task on the same CPU would
overwrite, so it has to be restored from the PCB. The honest boundary: the PCB is shared behind Arc
with per-field synchronisation (atomics, a Mutex for state mutated together, an RwLock for the
read-heavy token), so a field read outside its intended lock, or two fields expected to be consistent
read across a transition, sees per-field atomicity, not a snapshot of the whole block. The invariants
hold because each subsystem takes the field's own lock, not because the PCB is globally consistent.
Debugging the PCB
Most PCB-level bugs surface as a mismatch between a field and the behaviour it should drive. A capsule
that is refused an operation it was granted, or granted one it was not, is a caps_bits versus
capability_token divergence: the token is authoritative, so read it, and if the bitmap disagrees the
bug is a mutation that touched one and not the other outside process::caps. A syscall that fails with a
revoked-authority error after the capsule believed it still held the capability is the revocation_epoch
check firing: the token was minted before the last revoke, which is correct behaviour, and the trace
to follow is who called revoke. A driver whose in/out faults with a general-protection despite
holding a port grant points at io_bitmap, the grant cleared the wrong bits or was never applied, since
the default of all-ones denies everything. A process that will not enter user mode is the
kernel_stack_top == 0 case, read on the context switch page. The terminate(code)
method (pcb.rs:193) sets Terminated(code) in one step, so a PCB in Terminated that skipped Zombie
went through direct termination rather than the graceful two-phase exit, which narrows where to look.
Source map
src/process/core/pcb.rs the ProcessControlBlock and its methods
src/process/core/types.rs ProcessState, Priority, MemoryState, and the info types
src/process/core/thread_group.rs the shared ThreadGroup
src/process/caps.rs the authority-field mutators
Every reference above is verified against those trees. The authority fields tie into the
capability model and revocation;
the transition fields are consumed on the context switch page; the io_bitmap
default is set during creation on the process table page; and the lifecycle that
constructs and tears down a PCB is on the lifecycle page.