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
TerminalSessioncap for stdio, - a read-only bootstrap-granted
Directorycap rooted at a tiny in-rodata pseudo-fs (the resolver remainsNamespace-shaped for forward parity; the v0 manifest grants aDirectorybecause that is what Storage Phase 3 slice 2 ships as a kernelCapObject), - a
ProcessSpawnernarrowed to one allowed binary (ls-shim), - and a
Timercap.
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 ofcapos-rt,_startshim, fixed heap,malloc/free/calloc/realloc,console_write_line. - P1.2 UDP + DNS resolver smoke (
2026-05-05 21:21 UTC):libcapos-posixerrno TLS cell,clock_gettime/gettimeofdayoverTimer, fd-table dispatch shape,__errno_location(). - P1.3 Pipe + recording-shim fork-for-exec (
2026-05-07 09:55 UTC, fix-slice through05b528732026-05-07 21:07 UTC): kernelPipecap,ProcessSpawner.createPipe, fd-tableFdBacking::Pipe, recording-shimfork/execve/waitpid/_exit, directposix_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
d06dff6bat2026-05-14 19:31 UTC, slice 2b11ec9e4at2026-05-14 22:30 UTC, slice 3804a3f41at2026-05-14 23:23 UTC): RAM-backedFile/Directory/Store/NamespaceCapObjects withKernelCapSource::file/directory/store/namespacegrant 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 oninitConfig.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/shposture. - §7: confirm
TerminalSessionas the canonical fd 0 / 1 / 2 backing for the v0 smoke. AnFdBacking::Terminalvariant inlibcapos-posix/src/fd.rsplusposix_inherit_stdio()adoption is the implementation shape.
Promotion = strike the “Working answer” phrasing in the proposal,
replace with “Decided (P1.4 Slice 1,
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,closeover theFileinterface methods.DirectoryClient:open,list,mkdir,remove,subover theDirectoryinterface methods, returning typedFileClient/DirectoryClientprojections 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 kernelFilecap (the schema-level read/write take an explicit offset).FdBacking::Directory { client: DirectoryClient, iter: ... }– iteration state forreaddir.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
Namespacecap. - Walk
Namespace.sub()for each prefix segment; mint a leafFile/Directorycap withNamespace.resolve()+Store.get()or (for the in-rodata pseudo-fs case) directlyDirectory.open()/Directory.sub()from a bootstrap-granted rootDirectory. - v0 has no cwd /
chdir; relative paths are an error. - Returns typed
File/Directoryresult 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 oninitConfig.initinschema/capos.capnp. This is the only P1.4 schema touch; queue on the shared schema serial surface perdocs/plans/README.mdConcurrency Notes when selected. Regenerate the checked-in capnp bindings;make generated-code-check` must pass.
- Add a bounded
- 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. LaunchParametersremains 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.hsubset:printf,fprintf(fd 1 / fd 2 only),vprintf,vfprintf,snprintf,vsnprintf,putchar,puts,fputs,fputc. Nofopen/FILE *– those route through the fd-table surface.string.hsubset:memcpy,memmove,memset,memcmp,strlen,strcmp,strncmp,strchr,strrchr,strcpy,strncpy,strcat,strncat,strdup.stdlib.hsubset:atoi,strtol,strtoul,exit(already present via libcapos_exit),abort.ctype.hsubset: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. Addvendor/dash/VENDORED_FROM.mdrecording the upstream URL, commit, tag, and refresh procedure (mirror the existingvendor/dns-c-wahern/VENDORED_FROM.mdshape). - 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 tochild. Patches live undervendor/dash/patches/with one.patchper call site; the cumulative diff against upstream is < 50 lines. - Inter-call
dup2/closebetween fork and execve already records throughlibcapos-posixand 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
.cfiles, - compiles them with
clang --target=x86_64-unknown-none-elf -nostdlib -static -Iinclude -Ilibcapos/include -Ilibcapos-posix/include, - links the resulting
.ofiles withlibcapos.aandlibcapos_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 withopendir/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, importscapos.local/cue/defaults) grantingTerminalSession, a read-onlyDirectoryover an in-rodata pseudo- fs containing exactly the entries the heredoc references, aProcessSpawnernarrowed tols-shim, and aTimer.Makefilevendor-dash,libcapos-posix-shell,manifest-posix-shell.bin,capos-posix-shell.iso, andrun-posix-shell-smoketargets.tools/qemu-posix-shell-smoke.shhost harness: pipels; echo doneheredoc into fd 0, assertdone, 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:openan in-rodata file path, read to EOF, write to stdout.demos/grep-shim/main.c: read stdin line by line (using the P1.4stdiosubset), match a literal substring fromargv[1], write matching lines to stdout.- Extend
system-posix-shell.cueto 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/Namespaceservice 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-smokeexits 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 aVENDORED_FROM.mdand apatches/directory whose cumulative diff vs upstream is < 50 lines. libcapos-posixexposes the file / dir / stdio / env / printf / string / signal / time / identity surface listed above; the surface ships from headers underlibcapos-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, andmake run-posix-pipe-smokeall remain green.- The proposal stamps the phase closeout with merge SHA and a minute-precision timestamp.