shell

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: BSD-3-Clause

README

shell

Docs CI Go Reference status

A POSIX shell for WASI containers, in pure Go, built for GOOS=wasip1 GOARCH=wasm. It is what makes shell-form commands work in go-wasmcontainers: RUN a && b in a Containerfile, wctr run IMAGE sh -c '…', wctr exec -it CONTAINER sh.

A wasip1 module cannot start another program: Go's port has no fork or exec, and WASI preview 1 has no process calls. This repository uses a small subset of WASIX, in WASIX's own import module (wasix_32v1) and ABI — proc_spawn3, proc_join, fd_pipe, proc_id, proc_parent, tty_get, tty_set — and nothing else of it (no fork, no exec, no threads, no signals).

wasix //go:wasmimport bindings for the subset, with a Go API: Spawn(name, argv, *Attr), Wait(ctx, pid), Pipe(), TtyGet/TtySet. The struct encoders and decoders are portable and fuzzed, for hosts to reuse.
cmd/sh sh: the mvdan.cc/sh v3.14.1 interpreter, whose external commands are started with proc_spawn3 and waited for with proc_join.
internal/run PATH lookup (<dir>/<name>, then <dir>/<name>.wasm, the pkgx bottle layout), #! scripts, plain scripts (POSIX ENOEXEC rule), the fd plan of a child.
cmd/mkimage builds the base image (below) as an OCI image layout, and pushes it to a registry.

The base image

ghcr.io/go-wasmcontainers/shell, platform wasip1/wasm, one layer:

path
/bin/sh this shell
/bin/coreutils.wasm wasmdesk/coreutils, the multicall module
/bin/cat, /bin/ls, … one symbolic link to coreutils.wasm per tool (42 tools); the module picks the tool from argv[0]
/etc/passwd, /etc/group, /etc/os-release root only
/tmp (1777), /root

Config: Cmd ["/bin/sh"], Env PATH=/bin, WorkingDir /, and Shell ["/bin/sh","-c"], which is what a shell-form RUN, CMD or ENTRYPOINT in a Containerfile runs. The build is reproducible (every mtime is the commit time). It is published by the image workflow when a release tag is pushed (tags X.Y.Z and latest); every CI run keeps the layout as an artifact.

go run ./cmd/mkimage --sh sh.wasm --coreutils coreutils.wasm --version 0.1.0 --layout ./layout
wctr pull oci:./layout:0.1.0

It runs on a host that implements the WASIX process subset: the go-wasmcontainers engine from v0.17.0 (wctrd --wasi engine, the default), where pipelines through the coreutils work.

What sh does

  • Pipelines between builtins and programs (fd_pipe), redirections, here-documents, command substitution, cd (the child gets the directory both as a chdir fd op and as $PWD, which is where Go's wasip1 port reads it), exported variables, $?, set -e/-u/-x/-f/-a, exec, sh -c, sh file, sh -s, an interactive prompt on a TTY.
  • Exit statuses as POSIX says: 127 not found, 126 found but cannot run, 128+n for a signal reported by the host.
  • Empty arguments and arguments with newlines reach the child intact (proc_spawn3, not the line-feed separated proc_spawn2).
  • Children inherit only fds 0–2: the shell closes its pipe ends and redirection files in the child (fd ops), so readers see EOF.

Not available, by design of the subset: signals (kill, trap on signals, Ctrl-C forwarding), job control (& runs in-process, fg, bg), process substitution <(…). exec runs the program and exits with its status rather than replacing the shell.

Build

GOOS=wasip1 GOARCH=wasm go build -o sh ./cmd/sh

It needs a host that implements the subset: the go-wasmcontainers engine (v0.17.0 and later), or wasmer (used in CI as the independent oracle). Outside such a host, starting a program fails with status 126 and says why. A redirection to /dev/null works in a container without /dev.

wasmer run --volume ./rootfs:/ --env PATH=/bin rootfs/bin/sh -- -c 'echo a | cat; exit 3'

Tests

  • natively (go test -race ./...): the ABI encoders, the fd plan (fuzzed against a simulated fd table), PATH lookup, the exec handler with a fake host, the sh command line;
  • under wasmer: the wasix package against a real WASIX host, and the shell end to end (internal/e2e);
  • in the engine itself (TestBaseImageUnderEngine): the base image's layer, sh and the real coreutils, pipelines included (a 20000-line one), which wasmer cannot run.

Four defects of the oracle found on the way are listed in docs/REFERENCES.md, with the bibliography (WASIX header and libc shims, wasmer's implementation, mvdan.cc/sh, Go's wasip1 port, POSIX).

mvdan.cc/sh is patched

third_party/mvdan-sh is mvdan.cc/sh v3.14.1 with one change, interp.SetPipeFunc: upstream creates pipes with os.Pipe, which is ENOSYS on wasip1. See third_party/mvdan-sh/PATCHED; a test fails if go.mod and the copy disagree. Because of the replace directive, go install …@version does not work for cmd/sh; build from a clone.

License

BSD-3-Clause. third_party/mvdan-sh keeps its own BSD-3-Clause license.

Directories

Path Synopsis
cmd
mkimage command
Command mkimage builds the shell base image (wasip1/wasm): /bin/sh, the coreutils multicall module and its links, a minimal /etc.
Command mkimage builds the shell base image (wasip1/wasm): /bin/sh, the coreutils multicall module and its links, a minimal /etc.
sh command
Command sh is a POSIX shell for WASI containers.
Command sh is a POSIX shell for WASI containers.
internal
base
Package base lays out the root filesystem of the shell base image: /bin/sh, the coreutils multicall module with one symbolic link per tool, a minimal /etc, /tmp and /root.
Package base lays out the root filesystem of the shell base image: /bin/sh, the coreutils multicall module with one symbolic link per tool, a minimal /etc, /tmp and /root.
oci
Package oci builds a single-layer OCI image for wasip1/wasm, writes it as an OCI image layout and pushes it to a registry.
Package oci builds a single-layer OCI image for wasip1/wasm, writes it as an OCI image layout and pushes it to a registry.
run
Package run is the part of the shell that runs external programs: it finds them on PATH, decides how to start them (WebAssembly module, #! script or plain shell script) and hands them to a Spawner.
Package run is the part of the shell that runs external programs: it finds them on PATH, decides how to start them (WebAssembly module, #! script or plain shell script) and hands them to a Spawner.
shell
Package shell is the sh command: mvdan.cc/sh's POSIX interpreter wired to WASIX process spawning (internal/run).
Package shell is the sh command: mvdan.cc/sh's POSIX interpreter wired to WASIX process spawning (internal/run).
Package wasix binds the subset of WASIX that a shell needs to start and wait for other programs: proc_spawn3, proc_join, fd_pipe, proc_id, proc_parent, tty_get and tty_set, in the import module "wasix_32v1".
Package wasix binds the subset of WASIX that a shell needs to start and wait for other programs: proc_spawn3, proc_join, fd_pipe, proc_id, proc_parent, tty_get and tty_set, in the import module "wasix_32v1".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL