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

POSIX Adapter Phase P1.4: Running dash

Long-form decomposition for the POSIX adapter Phase P1.4 dash port. WORKPLAN.md selects the active POSIX track and links here; the executable per-step checklist is in docs/plans/posix-adapter.md Task 4; the design rationale and validation smoke contract are in docs/proposals/posix-adapter-proposal.md Phase P1.4 and Open Questions §1 (shell candidate) + §7 (fd 0 backing). Open Question §6 (fork policy = Variant A recording shim) is already a final decision in the proposal and does not gate P1.4.

What “Running dash” Means in v0

The validation smoke is make run-posix-shell-smoke. It boots a focused manifest that grants:

  • a TerminalSession cap for stdio,
  • a read-only bootstrap-granted Directory cap rooted at a tiny in-rodata pseudo-fs (the resolver remains Namespace-shaped for forward parity; the v0 manifest grants a Directory because that is what Storage Phase 3 slice 2 ships as a kernel CapObject),
  • a ProcessSpawner narrowed to one allowed binary (ls-shim),
  • and a Timer cap.

tools/qemu-posix-shell-smoke.sh pipes the heredoc ls; echo done into the shell’s fd 0, asserts done on the kernel log, asserts two clean-exit log entries (shell + ls-shim), and asserts clean QEMU exit. Stretch goal: cat foo | grep bar end-to-end against demos/cat-shim/ and demos/grep-shim/, exercising the P1.3 Pipe primitive through a shell pipeline.

This is intentionally narrow: no job control, no signal delivery, no chdir / cwd, no real filesystem persistence, no ulimit. The point is to prove that a real POSIX C program (not a capOS-native shell) boots, parses scripts, dispatches subprocesses through fork+execve, reads stdin, writes stdout, and exits cleanly under QEMU.

Prerequisites Already Landed

  • P1.1 libcapos C substrate (fe5f5208, 2026-05-05 13:28 UTC): Rust staticlib mirror of capos-rt, _start shim, fixed heap, malloc / free / calloc / realloc, console_write_line.
  • P1.2 UDP + DNS resolver smoke (2026-05-05 21:21 UTC): libcapos-posix errno TLS cell, clock_gettime / gettimeofday over Timer, fd-table dispatch shape, __errno_location().
  • P1.3 Pipe + recording-shim fork-for-exec (2026-05-07 09:55 UTC, fix-slice through 05b52873 2026-05-07 21:07 UTC): kernel Pipe cap, ProcessSpawner.createPipe, fd-table FdBacking::Pipe, recording-shim fork / execve / waitpid / _exit, direct posix_spawn / posix_spawn_file_actions_*. The Variant A contract: execve() returns the synthetic child pid on success.
  • Storage Phase 3 slices 1-3 (slice 1 d06dff6b at 2026-05-14 19:31 UTC, slice 2 b11ec9e4 at 2026-05-14 22:30 UTC, slice 3 804a3f41 at 2026-05-14 23:23 UTC): RAM-backed File / Directory / Store / Namespace CapObjects with KernelCapSource::file / directory / store / namespace grant sources. These are the v0 backing for the dash smoke’s read-only in-rodata pseudo-fs.
  • WASI bounded env grant (5f5028e7, 2026-05-13 11:05 UTC): reference shape for a bounded text env grant on initConfig.init (wasiEnv :Text). The dash port mirrors this for its env vector.

The Phase 2 Open Question §1 (dash candidate) and §7 (fd 0 backing = TerminalSession) still need to be promoted from working answers to final decisions; that promotion is the first dispatch slice of P1.4.

Decomposition

Slice 1: open-question closures (docs-only)

Two open questions in docs/proposals/posix-adapter-proposal.md must become final decisions before any code lands:

  • §1: confirm dash 0.5.13.x as the v0 candidate. Alternatives surveyed: busybox ash, oksh, toysh, custom Rust shell. dash wins on size, POSIX strictness, and single-purpose /bin/sh posture.
  • §7: confirm TerminalSession as the canonical fd 0 / 1 / 2 backing for the v0 smoke. An FdBacking::Terminal variant in libcapos-posix/src/fd.rs plus posix_inherit_stdio() adoption is the implementation shape.

Promotion = strike the “Working answer” phrasing in the proposal, replace with “Decided (P1.4 Slice 1, )” and the rationale.

Slice 2: typed clients in capos-rt

TerminalSessionClient and the TERMINAL_SESSION_INTERFACE_ID re-export already ship from capos-rt/src/client.rs and capos-rt/src/lib.rs; no work there. The net-new wrappers, mirroring the existing PipeClient / UdpSocketClient shape, are:

  • FileClient: read, write, stat, truncate, sync, close over the File interface methods.
  • DirectoryClient: open, list, mkdir, remove, sub over the Directory interface methods, returning typed FileClient / DirectoryClient projections of the transferred result caps.

Add re-exports for the existing FILE_INTERFACE_ID / DIRECTORY_INTERFACE_ID constants (already defined in capos-config/src/lib.rs) from the pub use capos_config::{...} block in capos-rt/src/lib.rs.

Slice 3: fd backing for File / Directory / Terminal

Extend libcapos-posix/src/fd.rs with three new FdBacking variants:

  • FdBacking::File { client: FileClient, pos: u64 } – the seek position lives in the fd table, not the kernel File cap (the schema-level read/write take an explicit offset).
  • FdBacking::Directory { client: DirectoryClient, iter: ... } – iteration state for readdir.
  • FdBacking::Terminal { client: TerminalSessionClient }.

Route the existing read / write / close C entry points through these variants. Add file-path-only C entry points (open, lseek, stat, fstat, access, unlink, opendir, readdir, closedir) in libcapos-posix/src/file.rs and libcapos-posix/src/directory.rs.

Slice 4: path resolver over Namespace

A read-only absolute-path resolver in libcapos-posix/src/path.rs:

  • Input: an absolute UTF-8 path and a bootstrap-granted root Namespace cap.
  • Walk Namespace.sub() for each prefix segment; mint a leaf File / Directory cap with Namespace.resolve() + Store.get() or (for the in-rodata pseudo-fs case) directly Directory.open() / Directory.sub() from a bootstrap-granted root Directory.
  • v0 has no cwd / chdir; relative paths are an error.
  • Returns typed File / Directory result caps that flow into the fd-table backing.

The v0 dash smoke uses the Directory.open / Directory.sub shape (bootstrap-granted root Directory, no Store / content-addressed hashes). The Namespace.resolve + Store.get shape is recorded in the resolver for parity with how a real filesystem service would back the resolver later.

Slice 5: stdio over TerminalSession

posix_inherit_stdio() already adopts pipe-backed fds 0 / 1 / 2 from the recording-shim execve path. Extend it to also adopt a bootstrap-granted TerminalSession cap as fd 0 / 1 / 2 when the manifest supplies one (the posix-pipe pipeline children stay on the existing pipe path). The shell binary calls posix_inherit_stdio() once from main() before reading the heredoc.

Slice 6: env vector + getenv / setenv / putenv

Mirror the WASI host adapter’s wasiEnv :Text shape:

  • Add a bounded posixEnv :Text (or per-key `posixEnvEntries
    List(Text)) grant on initConfig.initinschema/capos.capnp. This is the only P1.4 schema touch; queue on the shared schema serial surface per docs/plans/README.mdConcurrency Notes when selected. Regenerate the checked-in capnp bindings;make generated-code-check` must pass.
  • Read the grant from the bootstrap CapSet at startup; populate a per-process env vector in libcapos-posix/src/env.rs.
  • C entry points: getenv, setenv, putenv, unsetenv.
  • LaunchParameters remains a follow-on for non-v0 callers.

Slice 7: printf / string subset

A focused C library subset shipped from libcapos-posix (not a full libc, not a musl port):

  • stdio.h subset: printf, fprintf (fd 1 / fd 2 only), vprintf, vfprintf, snprintf, vsnprintf, putchar, puts, fputs, fputc. No fopen / FILE * – those route through the fd-table surface.
  • string.h subset: memcpy, memmove, memset, memcmp, strlen, strcmp, strncmp, strchr, strrchr, strcpy, strncpy, strcat, strncat, strdup.
  • stdlib.h subset: atoi, strtol, strtoul, exit (already present via libcapos _exit), abort.
  • ctype.h subset: isspace, isdigit, isalpha, isalnum, isupper, islower, tolower, toupper.

malloc / free / calloc / realloc already ship from libcapos.

Slice 8: signal stubs

Header-and-stub-only signal, kill, sigaction, plus a TLS-stored handler table that accepts handler registration but never delivers a signal. dash registers a SIGCHLD handler at startup; the stub records the handler pointer and returns 0. Documented out of scope: real SIGCHLD / SIGTSTP delivery, job control, controlling terminals.

Slice 9: time additions

time(2), nanosleep, sleep over the existing Timer cap; clock_gettime / gettimeofday already landed under P1.2 Phase B.

Slice 10: identity stubs

getpid, getuid, getgid, geteuid, getegid. uid / gid hardcoded (e.g. 0 for the bootstrap process); pid reuses the recording-shim child-pid space already used by execve().

Slice 11: dash vendoring + Variant A patch

  • Vendor dash 0.5.13.x under vendor/dash/ at a pinned tag, mirror-as-is. Add vendor/dash/VENDORED_FROM.md recording the upstream URL, commit, tag, and refresh procedure (mirror the existing vendor/dns-c-wahern/VENDORED_FROM.md shape).
  • Apply the Variant A per-call-site patch: at each fork-exec site, capture execve()’s synthetic pid return value, bail on -1, and assign back to child. Patches live under vendor/dash/patches/ with one .patch per call site; the cumulative diff against upstream is < 50 lines.
  • Inter-call dup2 / close between fork and execve already records through libcapos-posix and needs no per-call patching.

Slice 12: C-build pipeline for vendored multi-file C sources

The existing c-build helper compiles single-file demos/*/main.c smokes against libcapos.a + libcapos_posix.a. dash is a multi-translation-unit C codebase (main.c, eval.c, exec.c, expand.c, input.c, jobs.c, mail.c, memalloc.c, miscbltin.c, mystring.c, nodes.c, options.c, output.c, parser.c, redir.c, show.c, trap.c, var.c, plus generated tables).

Add a Makefile rule shape that:

  • accepts a list of vendored .c files,
  • compiles them with clang --target=x86_64-unknown-none-elf -nostdlib -static -Iinclude -Ilibcapos/include -Ilibcapos-posix/include,
  • links the resulting .o files with libcapos.a and libcapos_posix.a,
  • produces a userspace ELF without dragging in an external libc.

This rule is reusable for future C ports (busybox utilities, etc.).

Slice 13: ls-shim + manifest + smoke harness

  • demos/ls-shim/main.c: open a hardcoded in-rodata directory path, iterate with opendir / readdir / closedir, print each entry name, exit cleanly. This is the only allowed spawn target in the smoke.
  • system-posix-shell.cue: a focused-proof manifest (own CUE package, imports capos.local/cue/defaults) granting TerminalSession, a read-only Directory over an in-rodata pseudo- fs containing exactly the entries the heredoc references, a ProcessSpawner narrowed to ls-shim, and a Timer.
  • Makefile vendor-dash, libcapos-posix-shell, manifest-posix-shell.bin, capos-posix-shell.iso, and run-posix-shell-smoke targets.
  • tools/qemu-posix-shell-smoke.sh host harness: pipe ls; echo done heredoc into fd 0, assert done, two clean-exit log entries (shell + ls-shim), the scheduler halt line, and QEMU exit status 1 (isa-debug-exit).

Slice 14 (stretch): cat | grep pipeline

Extend the smoke to drive cat foo | grep bar end-to-end:

  • demos/cat-shim/main.c: open an in-rodata file path, read to EOF, write to stdout.
  • demos/grep-shim/main.c: read stdin line by line (using the P1.4 stdio subset), match a literal substring from argv[1], write matching lines to stdout.
  • Extend system-posix-shell.cue to allow both shims as spawn targets.
  • Smoke harness asserts the expected grep output line.

This stretch slice proves the P1.3 Pipe primitive end-to-end through dash’s pipeline parser (not just through the recording-shim posix_spawn_file_actions path).

Conflict Surface Coordination

P1.4 does not touch kernel/src/cap/, kernel/src/sched.rs, or any device-driver foundation file. The schema half is limited to the optional posixEnv bounded text grant on initConfig.init (Slice 6); queue on the shared schema serial surface per docs/plans/README.md Concurrency Notes when that slice dispatches. Every other slice is parallel-safe with the kernel-core selected milestone (DDF).

Out of Scope for P1.4

  • Job control, real signal delivery, controlling terminals.
  • chdir / cwd / relative paths.
  • ulimit.
  • A userspace Store / Namespace service over a real backing store – that remains the next Phase 3 item in the storage proposal and is not required for the v0 dash smoke.
  • Real filesystem persistence (block device, virtio-blk, FAT).
  • A POSIX terminal line discipline owned by libcapos-posix – cooked-mode line discipline still lives kernel-side until networking proposal Phase C.
  • Hosted C++. Tracked separately in docs/proposals/userspace-binaries-proposal.md.

Success Criteria

  • make run-posix-shell-smoke exits cleanly under QEMU with the asserted heredoc, done, two clean-exit log entries, and the scheduler halt line.
  • The vendored dash source under vendor/dash/ is mirror-as-is at a pinned tag with a VENDORED_FROM.md and a patches/ directory whose cumulative diff vs upstream is < 50 lines.
  • libcapos-posix exposes the file / dir / stdio / env / printf / string / signal / time / identity surface listed above; the surface ships from headers under libcapos-posix/include/capos/posix/ with no dependency on an external libc.
  • make workflow-check, make fmt-check, make generated-code-check, cargo test-config, cargo test-lib, cargo build-demos-capos, make capos-rt-check, make run-smoke, make run-c-hello, make run-posix-dns-smoke, and make run-posix-pipe-smoke all remain green.
  • The proposal stamps the phase closeout with merge SHA and a minute-precision timestamp.