# Process model

Every ordinary command runs in a fresh private wasm64 memory and Worker. The
kernel ([`process-kernel.c`](../src/process-kernel.c)) owns files, open-file
descriptions, pipes, local sockets, pseudo-terminals, process records, signals and the terminal; processes
inherit handles and values, never another address space. The call path is in
[architecture](architecture.md#system-calls); packet layouts are in
[`process.h`](../include/dolly/process.h) and each [host module](../host/README.md)'s header.

```mermaid
sequenceDiagram
  participant Sh as Parent (Slop)
  participant K as Kernel
  participant S as Supervisor
  participant C as Child Worker
  Sh->>K: SPAWN path, argv, env, fd mappings
  K-->>Sh: child pid
  S->>K: next launch
  K-->>S: executable bytes
  S->>C: new Worker, fresh memory and gate
  Sh->>K: WAIT pid (deferred)
  Note over K,C: Ctrl+C or kill: SIGINT is pending
  K-->>C: next call returns -EINTR
  C->>K: INTERRUPT_POLL, run handler, SIGNAL_ACKNOWLEDGE
  Note over S,C: handler unfinished 500 ms later: Worker terminated
  C->>K: EXIT status, or signal termination
  S->>K: Worker retired
  K-->>Sh: WAIT returns exit status or signal
```

## Executables

- Import one private shared memory64 and one function,
  `dolly_process_0.call`; export `_start`; carry `dolly.process` stamps
  ([`dolly-process-0.wat`](../abi/dolly-process-0.wat)) and a `dolly.host`
  record with the ABI digest of each host module they link
  ([`dolly-host-0.wat`](../abi/dolly-host-0.wat)). Memory is at most 8 GiB.
- A start section may initialize memory/TLS but must not call the kernel.
- Found through `PATH`; `#!` lines name an absolute in-Wasm interpreter, nested at most 4 deep.
- `readlink("/proc/self/exe")` returns the loaded image's canonical path, and
  `readlink("/proc/PID/cwd")` the directory a process that has not exited
  stands in; there is no general `/proc`. `dladdr` names that path for the
  program's own functions and static data, and nothing for the heap, the stack
  or a module `dlopen` loaded.
- Opening `/proc/processes` for reading gives the processes that have not
  exited, a line each in PID order, `PID PARENT STATE ARGUMENTS`: the state is
  `starting` until the program runs and `running` from then, the arguments
  are joined by spaces with each control character as `?`, and a line ends
  after 1023 bytes. It is the kernel's answer: the file is empty, so that
  `ls /proc` lists it and `stat` finds it, as on Linux, and each open of it,
  by any path, is its own copy of the table in a pipe; neither the file nor
  its descriptor takes a write, and sessions do not save `/proc`.
  [`ps`](../Dollyfile-ps) prints it under a heading.
- The target's identity: a program sees `__dolly__`, `__unix__` and
  `__wasm64__`, and no macro of Emscripten, Linux or WASI; `cc -dumpmachine`
  prints `wasm64-unknown-dolly` and `uname` `Dolly wasm64`. The libc is a
  bootstrap whose headers test `__dolly__`; LLVM and Zig still generate
  code under its Emscripten name, which no program can test.
- The compiler is itself a private process behind `cc`, `c++`, `ld` and `ar`
  ([`compiler.cpp`](../src/compiler.cpp)). It defaults to Clang's `-std=gnu17` and
  `-std=gnu++17` but to `-O2`, and follows Clang's suffix rules; objects are always position
  independent, `-m64` is the only target, `-lc -lm -ldl -lrt -lpthread -lutil`
  add nothing, and `-Wl,--no-undefined`, `--allow-shlib-undefined` and
  `--as-needed` are accepted and ignored: Dolly links no ELF shared libraries,
  and the exact typed import validation after linking decides.
- The supervisor caches compiled modules by SHA-256 (64 entries, 256 MiB), never
  instances. It admits a spawn while fewer than 56 processes exist and either
  their committed Wasm memory is under 4 GiB or the spawning process has no
  live child: the budget bounds fan-out such as `make -j`, and a chain of
  processes each waiting for the next always progresses. A spawn over the
  budget waits until a process ends, however long, since make and Cargo end a
  build at a spawn's `EAGAIN`; one past the count waits up to 5 s, then fails
  `EAGAIN`. Either wait is a blocking call a signal interrupts. So a pipeline
  started over the budget waits forever if its first stage fills the pipe
  before the next stage is admitted; Ctrl+C ends it.
  A start does not read an executable whose module is cached: the kernel opens the
  file and gives its version (`dolly_process_image_version`, a number every write and
  size change of the file renews), and only a version the supervisor has not compiled
  is read (`dolly_process_image_read`) and hashed.
- A refusal is reported to the program that asked, not to the person at the
  page. An executable the loader refuses (a wrong stamp or import, a host module
  the session does not have or whose layout differs, a thread client without
  `dolly_thread_start`) and a Worker that fails while running exit with status
  126 after one line on the process's own descriptor 2 that names the cause
  ([`process-supervisor.mjs`](../src/process-supervisor.mjs)); unrelated
  processes are unaffected. `cc` refuses to link a thread client without
  `-pthread`, so what it links the loader runs.
- A malformed call returns an errno and the process keeps running: `EFAULT` for
  a packet outside its memory, `E2BIG` over 1 MiB, `ENOSYS` for an unknown
  operation, `EINVAL` for a wrong layout
  ([`process-worker.mjs`](../src/process-worker.mjs)). FFI packets (`dso@0`) carry
  pointers of the process itself; a wild one is `EFAULT` too, while a trap in
  the function an FFI call reaches ends the process like any other trap.
- Wasm calls nest on a browser stack that a page cannot size; a program that
  exhausts it fails with 126 like any other trap. The Worker enters an
  executable that asks for it, which only the compiler does, through
  `WebAssembly.promising` where that stack is measured half again as deep:
  Chrome 151, about 950 KB instead of a Worker's 500 KB, and not Firefox 155
  ([browser stack](browser-stack.md)). A program `stack-wrap` rewrote records
  `stack@0` and continues on fresh stacks instead of ending.
  `RLIMIT_STACK` describes only the stack inside the process's memory.
- `cc`, `c++`, `ld` and `ar` retry status 126 up to twice
  ([`runtime-adapter.c`](../src/process/runtime-adapter.c)), so long
  source builds survive a transient browser Worker allocation failure without
  hiding deterministic source errors.

## Descriptors and files

- 1024 descriptors per process, Linux's default soft limit: Zig compiling
  Ghostty holds more than 256. Descriptor flags are per handle; offsets and status
  flags belong to the shared open description. `FD_CLOEXEC` works everywhere.
  `getrlimit` reports the fixed limits (`RLIMIT_NOFILE` 1024, `RLIMIT_NPROC` 56,
  `RLIMIT_AS` 8 GiB, `RLIMIT_STACK` 8 MiB); `sysconf(_SC_OPEN_MAX)` is still
  libc's constant 1024.
- Pipes hold 64 KiB. Empty reads and full writes return `EAGAIN` when
  nonblocking; closing all writers gives EOF, also to a reader that was
  waiting. Writing with no reader, or waiting to write when the last reader
  closes, returns `EPIPE`, and libc raises `SIGPIPE` in the writer before
  `write` returns ([`libc-adapter.c`](../src/process/libc-adapter.c)): the
  process ends with that signal unless it ignores, handles or blocks it. A
  process's descriptors close when it exits, before it is waitable.
- Opening `/dev/stdin`, `/dev/stdout` or `/dev/stderr` duplicates the caller's
  descriptor 0, 1 or 2.
- `/dev/tty` opens the terminal for reading and writing, for every process
  but one on a [pseudo-terminal](#pseudo-terminals), which opens that.
  `ttyname` fails: a terminal has no name of its own. No process group is a
  terminal's foreground: `tcgetpgrp` and `tcsetpgrp` fail `ENOTTY`, as for a
  process without a controlling terminal, so a shell runs without job control.
- `poll` covers files, pipes, sockets, pseudo-terminals and the terminal; signals wake it with `EINTR`.
- A terminal edits a line while `ICANON` is set (the default): a read waits for
  Enter, which arrives as LF, and returns one line; Backspace and Ctrl+U erase,
  Ctrl+D ends a line without LF or, on an empty one, is end of file; `ECHO`
  shows what is typed. With `ICANON` clear a read returns the bytes as typed.
  The raw read of `runtime.h` (`dolly_terminal_read_raw_timeout`), Slop's and
  Janis's, takes one byte unedited in either mode. `OPOST`/`ONLCR` map LF to
  CRLF on output. While `ISIG` is set (the default), the interrupt key, `VINTR`,
  sends SIGINT to the foreground: Ctrl+C unless a program sets another (Emacs
  sets C-g, and `VQUIT` to the same key); a program that clears `ISIG` (raw
  mode) reads that key as its byte. Under `ISIG` there is always a key, no quit
  key of its own and no job control, so `VINTR` is on, `VQUIT` off or `VINTR`'s
  key and `VSUSP` off.
  `ICRNL` reads an input CR as LF and `INLCR` an LF as CR, when a program sets
  them (a line editor such as zsh's does). termios reports no other input
  mapping, no flow control or other signal keys, and `VMIN` 1, `VTIME` 0;
  `tcsetattr` rejects anything else with `EINVAL`.
  `TCSAFLUSH` and `tcflush` discard unread input; break and flow control fail
  with `ENOTSUP`.
- There is one user and no permission bits: `chmod`, `chown` and `access` only
  check that the file exists, and nothing changes a file's mode
  ([why](architecture.md#decisions)).
- `statvfs` reports kernel memory: its maximum as capacity, unallocated memory
  as free, and no inode limit (zero files).
- The cwd is an open directory handle: it follows renames, `getcwd` fails with
  `ENOENT` after unlink, `fchdir` works.
- `mmap` makes private copies; `MAP_SHARED` writes back on `msync` and whole
  `munmap` ([`mmap.c`](../src/process/mmap.c)). Mappings are not coherent with
  other writers, do not extend the file past EOF, and cannot trap there: Wasm
  cannot protect or revoke part of linear memory. So `mprotect` grants read
  and write, which memory always is (a shared mapping made without write then
  writes back, `EACCES` if its descriptor is read-only), and any other
  protection fails `ENOTSUP`.
- Advisory locks live in one kernel table of 1024 for all processes; a request
  that needs one more fails with `ENOLCK`. `flock` locks the whole file for an
  open file description: descriptors made by `dup` or inherited by a spawned
  child share the lock, and it goes with the last of them. Converting gives
  the held lock up first, as on Linux, so a refused `LOCK_NB` conversion leaves
  none. `fcntl` `F_SETLK`, `F_SETLKW` and `F_GETLK` (and `lockf`) lock byte
  ranges for the process, splitting and merging what it holds; as POSIX says,
  closing **any** descriptor of a file drops all the process's locks on that
  file. Not the descriptors libc keeps for itself, for a shared mapping or
  inside `truncate`: those are marked `DOLLY_PROCESS_FD_KEEP_LOCKS`. The two
  kinds do not see each other, and neither stops `read` or `write`. A waiting request is parked like a pipe read: a release wakes it at
  once and a signal interrupts it. Waiters are not ordered and deadlocks are
  not detected (no `EDEADLK`). Exit, a kill and a failed Worker release every
  lock of the process. `F_OFD_*` is `EINVAL`; a pipe or socket cannot be
  locked (`ENOTSUP`).

## Local sockets

`sockets@0` ([`host/sockets/dolly-sockets-0.wat`](../host/sockets/dolly-sockets-0.wat),
[`host/sockets/kernel.c`](../host/sockets/kernel.c)): stream sockets between
processes of this kernel. The bytes stay in kernel memory, as a pipe's do, and
the module has no browser import: nothing here reaches a network.

A program asks for them when it links: `cc ... -ldolly-sockets` takes the
socket calls from the module's client, which records `sockets@0`, and the
image that keeps the program declares `REQUIRES HOST sockets@0`. A program
that does not ask is unchanged: every socket call is libc's refusal,
`ENOSYS`, and nothing is recorded, so a tool with a network path it never
takes here needs no line. The rest of this section is what a program gets
that asked.

- `socketpair(AF_UNIX, SOCK_STREAM)` gives two connected descriptors, which
  spawn inherits like any other. A server binds a path, listens and accepts;
  a client that shares no descriptor with it connects to the path.
- A socket reads, writes, polls, duplicates and closes like a pipe, 64 KiB
  each way; `send` and `recv` take `MSG_DONTWAIT`, `send` also `MSG_NOSIGNAL`.
  A stream has no record boundaries: a write takes what fits and a blocking
  one waits for the rest. A closed, shut down or dead peer is end of file to
  a reader, also one that was waiting, and `EPIPE` to a writer, with `SIGPIPE`
  as for a pipe. `poll` reports a pending connection as readable and a closed
  peer as hung up; `shutdown` ends one direction.
- A bound path is an empty file that `stat` reports as a socket; `bind` fails
  with `EADDRINUSE` when the name exists. `connect` finds the listener by the
  file, so a renamed path still connects; an unlinked one leaves the listener
  and its connections alone and can be bound again. The file stays when its
  socket closes or its process ends: `connect` then fails with
  `ECONNREFUSED`, as for any path nobody listens at, and a server unlinks a
  stale path before it binds.
- `connect` returns once the listener has room for the connection, before it
  is accepted, and the client may write at once. A listener holds at most 16
  unaccepted connections (`listen`'s backlog, 1 to 16): a blocking `connect`
  waits for room, a nonblocking one fails with `EAGAIN`. Connections nobody
  accepted end with their listener.
- The kernel holds at most 128 sockets, a connection being two; one more
  fails with `ENFILE`. A socket goes with its last descriptor, so exit, a
  kill and a failed Worker release what a process held.
- Refused by name: every other family (`AF_INET` and `AF_INET6` fail with
  `EAFNOSUPPORT`, so a program takes the path it has for a host without a
  network), datagram and seqpacket sockets (`EPROTOTYPE`), descriptor
  passing (`sendmsg` with control data, `ENOTSUP`), abstract names
  (`ENOENT`) and every `setsockopt` (`ENOPROTOOPT`). `getsockopt` answers
  `SO_TYPE` and `SO_ERROR`. A socket call on another descriptor fails with
  `ENOTSOCK`.

## Pseudo-terminals

`pty@0` ([`host/pty/dolly-pty-0.wat`](../host/pty/dolly-pty-0.wat),
[`host/pty/kernel.c`](../host/pty/kernel.c)): a terminal one program gives
another, as a terminal multiplexer gives each of its panes. The bytes stay in
kernel memory, as a pipe's do, and the module has no browser import.

A program asks when it links: `cc ... -ldolly-pty` takes `openpty` from the
module's client, which records `pty@0`, and the image that keeps the program
declares `REQUIRES HOST pty@0`. Without it `openpty` is libc's, which finds
no `/dev/ptmx` and fails with `ENOENT`. The program on the slave links
nothing and declares nothing: any program built for Dolly runs on one.

- `openpty` gives two descriptors, a master and a slave, neither inherited by
  a spawned program unless it is mapped. Each reads, writes, polls,
  duplicates and closes like a pipe: what is written to the master is the
  slave's input (4 KiB unread), what is written to the slave is read from the
  master (64 KiB). A write takes what fits; a master that does not wait gets
  `EAGAIN` for the rest.
- The slave is a terminal to the program that holds it, with a mode and a
  size of its own: `isatty`, termios and `TIOCGWINSZ` answer as for the
  display's terminal, with the same discipline (`ICANON`, `ECHO` to the
  master, `OPOST` with `ONLCR`, `ISIG`, `ICRNL` and `INLCR`). A process whose descriptor 0, 1 or 2 is a
  slave opens it as `/dev/tty`: the same open file again, so `O_NONBLOCK` set
  there is set for every process that holds the slave. The master is not a
  terminal, but termios and `TIOCGWINSZ` on it reach the slave's mode and
  size, as on Linux, where a terminal emulator sets its slave's mode that way.
- `ioctl(TIOCSWINSZ)` on the master sets the size, and the processes that
  hold the slave get `SIGWINCH`.
- While `ISIG` is set, byte 3 written to the master is `SIGINT` for the
  processes that hold the slave. The one the others descend from is spared
  while it has them: a shell, while its command runs. A process group is one
  process, so none names a foreground.
- Closing the master is `SIGHUP` to the processes that hold the slave, end of
  file to their reads and `EIO` to their writes. A read of the master returns
  end of file once every descriptor of the slave is closed and its output is
  read.
- The supervisor delivers these signals as it delivers a kill
  ([`dolly-pty-supervisor-0.wat`](../host/pty/dolly-pty-supervisor-0.wat)):
  a program that does not answer `SIGINT` or `SIGHUP` within 500 ms is ended.
- The kernel holds at most 32 pseudo-terminals; one more fails with `ENFILE`.
  One goes with the last descriptor of its two ends, so exit, a kill and a
  failed Worker release what a process held.
- A shell on a slave publishes no command result to the page: the page
  follows the shell at the display's terminal.
- Not there: `/dev/ptmx` and a path for a slave (`posix_openpt`, `ptsname`
  and `forkpty` fail), sessions and controlling-terminal ioctls. The name
  `openpty` returns, `/dev/pts/N`, is for display.

## Spawn and wait

- Spawn inherits no descriptors, the standard streams, or all non-CLOEXEC
  descriptors, then applies explicit parent-to-child mappings, all read from the
  parent at once. A spawn may set the child's cwd.
- `posix_spawn` inherits all non-CLOEXEC descriptors and replays `adddup2`
  and `addclose` actions in order as mappings, a closed one keeping the child
  from inheriting the descriptor ([`runtime-adapter.c`](../src/process/runtime-adapter.c)).
  A child starts with default dispositions and an empty signal mask; other file
  actions, a session, process group or scheduler, and a non-empty mask return
  `ENOTSUP`. There is no `fork`: it fails with `ENOSYS`.
- Each process is its own process group and its own session, which it leads:
  `getpgid`, `getpgrp` and `getsid` give its PID (the caller's for 0, `ESRCH`
  for none), `setpgid` of a process to itself succeeds and any other fails
  `EPERM`, as `setsid` does ([`libc-adapter.c`](../src/process/libc-adapter.c)).
  `kill(0)` signals the caller and `kill(-PID)` that one process, not its
  descendants; `kill(-1)` fails `ENOTSUP`.
- `waitpid` accepts a child PID, `-1` or `0`, with `WNOHANG`, `WUNTRACED` and
  `WCONTINUED`: no child ever stops or continues. Wait records distinguish signal termination from exit:
  `exit(130)` is not SIGINT.
- Timed spawns carry an absolute monotonic deadline at most one day away; the
  supervisor ends the child with status 124 even inside a pure CPU loop.
- `SPAWN_FOREGROUND` and `SPAWN_INTERACTIVE` are explicit roles. Ctrl+C with
  `ISIG` set sends SIGINT to the foreground tree; like a job-control shell, an
  interactive owner with running children is spared. Slop clears `ISIG` while
  it reads a line, so Ctrl+C at its prompt is input. Image scripts
  use `/bin/foreground`; the browser knows no command names. Only the
  foreground tree can pass the role on; retirement returns it to the nearest
  foreground ancestor.
- Retirement drops the Worker, memory and gate before the child is waitable.
- A child outlives a parent that exits, as on POSIX: a child still running
  loses it (its parent is 0) and is reaped when it ends, as init reaps
  orphans. The terminal goes back to the parent's foreground ancestor, as a
  shell takes it back when a job ends. A parent a signal ends takes its
  descendants with it, by that signal, as a signal to its process group
  would: a Dolly group is one process, so the command of `slop -c` is the shell's
  child unless the shell `exec`s it, and a closed terminal's `SIGHUP` ends its jobs. A timed
  command's deadline ends its whole tree, as `timeout` signals the command's
  process group.
- Worker termination has no completion event, so a large interactive process
  gets 500 ms of reclamation before its exit is acknowledged, sparing the
  recovery shell. Nothing guarantees against browser memory pressure; this is
  one reason Slop itself is [serial](architecture.md#decisions) and parallel
  builds need a modest `-jN`.

## Exec

`execve` and its family (`execv`, `execvp`, `execl`, ...) replace the program
of the calling process (`DOLLY_PROCESS_EXEC`, [`process.h`](../include/dolly/process.h)).

- The process keeps its pid, parent and children (the new program waits for
  them), cwd, locks, alarm, pending signals, terminal roles (foreground,
  interactive) and a timed spawn's deadline, and its descriptors without
  `FD_CLOEXEC`. The new program starts with the old one's signal mask,
  ignored signals and signals pending in it; every handled signal is back at
  its default action. A parent's `waitpid` gets the new program's status.
- What goes: the old program's memory and Workers, its threads (exec from any
  thread ends the others), mappings, DSOs, `atexit` handlers, and what its
  host modules held for it. `MAP_SHARED` writes are not written back, as at
  exit. stdio is not flushed: a program flushes before it calls `exec`.
- A program that cannot run fails the call and the old one goes on: a missing
  file is `ENOENT`, a file that is no Wasm and has no `#!` line `ENOEXEC`, and
  an executable the loader refuses (a stamp, an import, a host module the
  session has not) `ENOEXEC` after one line on the process's descriptor 2
  that names the cause. The kernel reads the program and the supervisor
  compiles and admits it while the old program waits in the call; only then
  does the old program go ([`process-supervisor.mjs`](../src/process-supervisor.mjs)).
- A signal delivered while the new program is prepared interrupts the call:
  the old program handles it, and libc calls `exec` again. Between the old
  Worker's end and the new one's start the process is `starting`, as a spawned
  one is, and a signal then ends it.
- A large interactive program gets the 500 ms of reclamation it gets at exit
  before its successor's memory is made.

## Signals

- Supported: `SIGHUP`, `SIGINT`, `SIGQUIT`, `SIGABRT`, `SIGKILL`, `SIGPIPE`,
  `SIGALRM`, `SIGTERM`, `SIGCHLD`, `SIGWINCH`; others are rejected.
- Handlers run in Wasm at syscall boundaries ([`signal.c`](../src/process/signal.c))
  with masks, `SA_RESTART`, `SA_RESETHAND`, `SA_NODEFER` and `SA_SIGINFO`.
  Alternate stacks, `sigwait`, asynchronous preemption and `SA_NOCLDWAIT` are
  unsupported.
- A process that does not finish a delivered signal within 500 ms is terminated.
  The filesystem and the shell survive; kernel or supervisor failure is outside
  this guarantee. Until a handler is finished the kernel delivers the process
  no other signal.
- A handler is finished when it returns, and when it leaves by `longjmp`,
  `_longjmp` or `siglongjmp`, as a pager or a shell leaves a blocked read.
  `sigsetjmp(env, 1)` saves the signal mask and `siglongjmp` restores it,
  delivering what that unblocks before it jumps; after `sigsetjmp(env, 0)`
  and after `setjmp` the jump leaves the handler's mask, as on Linux, and the
  program unblocks its signal itself. libc sees the jump because `<setjmp.h>`
  declares these as its own functions: a program that declares `longjmp`
  itself is not seen leaving, and its handler stays unfinished. A jump made
  and caught inside a handler counts as leaving it.
- Normal exit runs `atexit` handlers; default signal termination and forced
  termination do not, so named temporary files may remain.
- Delivered handlers interrupt sleep, `poll` and `pause`; `SA_RESTART` restarts
  read, write and wait. Ignored or blocked signals do not shorten sleeps.
- Because handlers run only when a syscall starts, `pselect` is the race-free
  way to wait for a descriptor or a blocked signal: a pending signal its mask
  unblocks interrupts it before the wait; `sigsuspend` is that wait without
  descriptors, and `select` the wait without a mask.
- `SIGCHLD` is queued once a child is waitable. `SIGWINCH` follows terminal
  resizes and never forces termination.
- `alarm`, `setitimer` and `getitimer` support only `ITIMER_REAL` (others fail
  with `EINVAL`); the supervisor raises due timers on its 16 ms tick. A
  default-action SIGALRM is delivered like `kill`, ending even a CPU loop. libc
  tells the kernel while SIGALRM is handled or ignored; it then only becomes
  pending and never forces termination.
- Clock reads use the Worker's clock aligned to the kernel's origin, but enter the
  kernel at least once per millisecond so signals arrive in clock-only loops.
- CPU time is not accounted: `clock()`, `CLOCK_PROCESS_CPUTIME_ID`,
  `getrusage` and `times` report the monotonic time since the program started
  (since its `exec`, if it was exec'd) as its user time, an upper bound that is
  exact while it computes without blocking; system time and children's times
  are zero ([`libc-adapter.c`](../src/process/libc-adapter.c)).

## Threads, DSOs and FFI

- `threads@0` ([`host/threads/dolly-threads-0.wat`](../host/threads/dolly-threads-0.wat),
  [`host/threads/threads.mjs`](../host/threads/threads.mjs)): statically linked `-pthread`
  programs, one Worker per thread sharing the process memory, at most 32 per
  process and 64 in total; `sysconf` reports 4 processors. Handlers run on the
  main thread, also while it waits in `pthread_join`. FFI calls work in every
  thread; no DSOs, FFI closures, cancellation or directed signals.
- `dso@0` ([`host/dso/dolly-dso-0.wat`](../host/dso/dolly-dso-0.wat),
  [`host/dso/process.mjs`](../host/dso/process.mjs)): process-local DSOs and
  FFI, served in the Worker of an executable that records the module
  ([host modules](../host/README.md#modules-served-in-the-process-worker)).
  - `cc -rdynamic` builds a host: it exports the program's symbols and links
    the loader behind `dlopen`. A DSO (`cc -shared`) shares its owner's
    memory, table and allocator; the loader checks exact import types before
    instantiation.
  - FFI calls and closures (`dolly_ffi_*`, `<dolly/dso.h>`) stay in the process
    Worker; libffi supports CPython `_ctypes`. In a program using `threads@0`
    each thread's Worker serves its calls, and loading or a closure, which
    would add a table entry to one Worker only, is refused with `ENOTSUP`.
  - A program passed through Binaryen's cast emulation (`wasm-opt
    --fpcast-emu`), whose table holds nothing but thunks of one signature,
    sixteen or more `i64` parameters and an `i64` result, is called in it; loading and
    closures, which would add an entry of another signature, are refused.
    GObject programs need it: they call functions through casts to types of
    other parameters.
  - Without the record a program loads nothing: `dlopen` returns `NULL` with
    `ENOSYS` and a `dlerror()` naming `-rdynamic` and `dso@0`, and the raw
    operations are unknown ones. With it, the image must declare
    `REQUIRES HOST dso@0` or the program is refused before it starts.

## Host modules at run time

- An image's host modules are the `REQUIRES HOST` lines of its recipe. A
  session of an image that declares `nena@0`
  ([`host/nena/dolly-nena-0.wat`](../host/nena/dolly-nena-0.wat),
  `<dolly/nena.h>`), as `default` does, may enable and disable modules while
  it runs; any other image's list is fixed, and nothing in Wasm changes it
  ([browser boundary](browser-boundary.md#host-modules)).
- `nena` lists every module of the page with its state and what it gives;
  `nena enable NAME...` and `nena disable NAME...` switch them. The loader's
  refusal of a program and the engine's refusal of a package name the command:
  `host module gpu@0 is not enabled: nena enable gpu`.
- Enabling starts the module as at boot; where the browser lacks what it
  needs, the refusal says so. Disabling takes it away at once: programs that
  record it are refused, and a running program's next call through it fails
  with `ENOSYS`. Sockets and other state a module keeps in the kernel stay
  with their processes; a threaded program ends with status 126 when
  `threads@0` is disabled under it.
- Not switched: `runtime@0`, `http@0`, `build@0`, `display@0`, `input@0` and
  `snapshot@0`; `nena` says why for each.
- `nena disable nena` gives the privilege up until the page is reloaded: the
  session then is as an image without `nena@0`, and its refusals read so.
- The modules a session enabled are listed in `/etc/dolly/enabled`, one
  `name@version` a line, written by the page's provider. A saved session
  holds the file, and loading it enables them again: ENTRY waits three
  seconds for them and starts without one the browser has not given by then.
  The page says in one line which were enabled; one that cannot be is said
  there too, with the command. A module the
  image declares and the session disabled is back when the page loads.

## Unsupported

`fork`, job control, joining another process group or session, network
sockets, and changing user or group identity fail explicitly.
