# Process Management

How danos lists, supervises, and kills processes — the microkernel answer to
`ps`, `kill`, and `SIGCHLD`/`wait`.

## Why system calls, not `/proc`

Unix systems sit on a spectrum. Classic BSD/macOS list processes through
syscalls (`sysctl(KERN_PROC)`) and kill through `kill(2)`; Linux renders the
process table as `/proc` for *reading* but still kills through a syscall; Plan 9
made the file tree the whole interface (`echo kill > /proc/n/ctl`). Microkernels
mostly abandon ambient PIDs: Minix and QNX route everything through a user-space
process-manager server, and Fuchsia/seL4 control processes only through handles.

danos rules out `/proc` **as the primitive**: here a `/proc` would be served by
the VFS server — a user process — which would put the VFS in the path of process
control. If the VFS (or anything under it) hangs, nothing could be listed or
killed, *including the hung VFS*. The control plane for processes must not
depend on a process. So the primitives are kernel system calls; a read-only
`/proc` rendering can be layered on later, and a POSIX-style process-manager
server can be built *from* these primitives when one is needed.

## The three primitives

### `process_enumerate(buffer, maximum) -> total`

A snapshot of the task table into a caller buffer of `abi.ProcessDescriptor`
(id, supervisor, state, priority, name) — the exact shape of
`device_enumerate`, so `ps` is a user program over a snapshot, not a kernel
service. The total may exceed what fit; call again with a larger buffer. Kernel
tasks are included with an empty name — an honest listing shows the idle tasks
too. Ungated and read-only: what is running is not a secret between cooperating
bring-up processes.

### `system_spawn(..., exit_endpoint) -> child id`, and the supervision link

`system_spawn` records the caller as the child's **supervisor** and returns the
child's process id (ids are monotonic, never reused — a stale id can only miss).
That link is the kill authority: it answers "who may kill process 7?" without
inventing users or permissions, the same way a device *claim* is the capability
for `mmio_map`. It composes with the supervision hierarchy the device manager
already forms: init supervises the services it starts, the device manager
supervises the drivers it matches. (A transferable process *handle* — Fuchsia
style — can replace the id once the handle table grows types beyond endpoints.)

`exit_endpoint` (a handle, or `abi.no_cap`) is the supervisor's death-watch: when
the child ends — clean exit, CPU fault, or `process_kill` — the kernel posts an
asynchronous notification to that endpoint, exactly like a bound IRQ. The badge
carries `abi.notify_badge_bit | abi.notify_exit_bit | child_id`, so one endpoint
supervises many children and can even share with IRQ notifications. This is the
microkernel's SIGCHLD: no new mechanism, just the IRQ-as-IPC pattern reused, and
a supervisor's event loop (`ipc.replyWait`) already knows how to receive it. The
child holds a reference to the endpoint from birth, so the notification cannot
dangle even if the supervisor dies first.

### `process_kill(id) -> 0 / -ESRCH / -EPERM`

Only the supervisor may kill; kernel tasks are not killable processes. Like a
signal, delivery is prompt but asynchronous — 0 means the kill is accepted and
irrevocable; the exit notification confirms completion.

## How a kill lands (the kernel mechanics)

Everything below runs under the big kernel lock, where task states cannot move.

- **Target ready or blocked** (not on any core): reaped on the killer's own
  call. The reap releases what death always releases (IRQ bindings first, then
  a client the target still owed a reply to is failed with `-EPEER`, IPC handles
  closed, the exit notification posted last) — plus the unlinking only a
  *remote* death needs: out of the ready queue, out of an endpoint's sender FIFO
  (`Task.ipc_wait_endpoint`), out of a receive wait queue (`Task.wait_queue`),
  and out of any server's owed-reply slot, so nothing ever dequeues a dangling
  pointer. Destroying the address space is safe because no core can have it
  loaded: every switch away from a task loads the next task's tables.
- **Target running on another core**: it cannot be torn down mid-instruction,
  so it is condemned (`Task.kill_pending`) and dies at whichever comes first:
  - its next **system_call entry** — checked before dispatch, so a condemned
    process cannot spawn, claim, or message anything on its way out;
  - its core's next **timer tick** — but only when the task is not inside one
    of its own system calls (`Task.in_system_call`): the tick may have
    interrupted kernel code mid-operation, where teardown would leak whatever
    the operation held. User-mode execution is always a safe kill point. The
    tick-time terminate abandons the interrupt frame exactly like the fault
    path (the LAPIC is acknowledged before the tick hook runs);
  - any core's tick finding it **blocked or ready** (it entered a syscall and
    parked after being condemned) — reaped by the same remote-reap path.

  A pure user-mode spin loop that never makes a system call therefore dies
  within one tick; nothing a process does can outrun the kill.

The scheduler stays below the process layer: finishing a kill (IRQ bindings,
handles, the notification) is called *up* through two hooks process.zig
registers at boot (`terminate_current_hook`, `reap_task_hook`), mirroring how
the architecture layer calls up into `tick`.

## Known gaps (bring-up honesty)

- ~~Device claims are not released on death~~ Closed (M17.1): every path out of a
  process releases its device claims alongside its IRQ and MSI bindings
  (`releaseTaskResourcesLocked`), so a restarted driver can claim its hardware
  again — the cleanup half of [process-lifecycle.md](process-lifecycle.md)'s iron
  rule 1. The `claim-release` test proves the kill → release → re-claim cycle.
- Kernel stacks of dead tasks are leaked, as on every exit path (no reaper yet).
- ~~There is no exit status in the notification~~ Closed (M17.2): the kernel
  records how every process ends — exited, a fault class, or killed — before it
  posts the exit notification, and the supervisor reads it with
  `process_exit_reason` (`runtime.process.exitReason`). This is the input to
  restart policy ([process-lifecycle.md](process-lifecycle.md)); an exit *code*
  for the clean case can still ride alongside later.
- Enumerate writes through the caller's raw pointer under the bring-up trust
  model, like `device_enumerate` (an unmapped page is a self-DoS, not an
  isolation break).

## Tests

`process-list` (enumerate), `process-kill` (kernel-level kill paths, refusals,
notifications), `supervision` (the whole user-side surface via the process-test
service: spawn supervised → enumerate → kill blocked and spinning children →
notifications → gone), `claim-release` (a killed claim-holder's device is
claimable again). See test/qemu_test.py.
