8000 shell/ash: run ash on wasm through callback clone by tombl · Pull Request #5 · tombl/busybox · GitHub
[go: up one dir, main page]

Skip to content

shell/ash: run ash on wasm through callback clone - #5

Open
tombl wants to merge 4 commits into
vfork-child-exitfrom
wasm-ash
Open

tombl wants to merge 4 commits into
vfork-child-exitfrom
wasm-ash

Conversation

@tombl
@tombl tombl commented Oct 27, 2026
Copy link
Copy Markdown
Owner

Moves distro#198's four in-tree busybox patches onto the fork as commits. Tom's rule is that each upstream project is either a fork or in-tree patches, never both, and busybox is a fork. The changes are the same as in #198's patch files; only the commit messages were adapted.

Stacked on #4 (vfork-child-exit, 0ef30c9). The first four commits in this diff are #4's; this PR's own commits are the last four.

  1. shell/ash: allow ash without an MMU: drops the !NOMMU Kconfig dependencies and the #error in ash.c.
  2. shell/ash: fork through callback clone: forkshell takes a continuation. Its five callers (here-doc writer, $(...), subshell, pipeline member, external command) move their pid == 0 branch into a *_child callback. The child runs on a copy-clone with an 8MiB stack and installs its own exception handler, which calls exitshell().
  3. shell/ash: enter the kernel once per evaluated node: one getppid() per node in evaltree. wasm Linux only schedules and delivers signals on kernel entry, so without it a builtin-only loop like while :; do :; done & can't be preempted or killed. This is the janky one. It's here because busybox is a fork; distro#198's body has the benchmark.
  4. timeout: detach the watcher's stdio: fixes this fork's own callback-clone conversion of timeout, which left the watcher holding the caller's stdout. Under ash, $(timeout 15 prog) therefore waited the full 15s.

Merge order

  1. busybox#4: fast-forward master to 0ef30c9 (LANDING S2).
  2. This PR: fast-forward master to 7a7adb6. Don't squash or rebase, so distro's pin stays on master with no repin.
  3. distro#203 (pins 0ef30c9).
  4. distro#198 (pins this head, 7a7adb6; it's stacked on #203).

If this PR is squashed instead, repin #198 to the squash commit and recompute its hash.

Tested by distro#198's CI: busybox smoke/processes/networking/shell/interactive checks and the full flake check set.

tombl added 4 commits October 27, 2026 22:06
BusyBox gates ash on !NOMMU because every subshell, pipeline member, command
substitution and external command goes through fork(). The next commit
replaces that fork with callback clone, so lift the Kconfig dependency and
the compile-time #error.
wasm Linux has no return-twice fork or vfork. A clone without CLONE_VM
copies the parent's memory, so it gives ash real fork semantics as long as
the code after fork in the child is a callback rather than a second return.

forkshell gains a continuation argument. Each of its five callers (here-doc
writer, command substitution, subshell, pipeline member, external command)
moves its "pid == 0" branch into a *_child function and passes its locals in
a struct; the parent half stays inline. The child runs on a fresh 8MiB stack,
matching the main process, because subshells recurse through evaltree.

The parent's exception handlers are frames the child's stack does not have,
so the callback installs its own. A forked child would unwind to ash_main's
handler, which always calls exitshell because shlvl > 0; the callback does
the same.
wasm Linux cannot interrupt userspace: the scheduler and signal delivery run
only when a task enters the kernel. Hush happened to call waitpid() for each
command, but ash runs a loop of builtins with no syscalls at all, so
`sh -c 'while :; do :; done' &` held its CPU until a reboot and could not be
killed. ash now makes one cheap syscall per evaluated node. getppid() is
used because its entry polls the timer deadline (driving the tick and
preemption) and its exit runs pending signals; sched_yield() would give the
CPU away on every builtin regardless of the timeslice.
Upstream timeout daemonizes its watcher, which points the watcher's stdio at
/dev/null. Our fork's callback-clone conversion of timeout dropped that, so
the watcher kept the caller's stdout. ash execs the last command of $(...)
in place and reaps it only after reading EOF, so `$(timeout 15 prog)` waited
the full 15 seconds after prog exited. Hush hid this because an intermediate
shell reaped prog.
tombl added a commit to tombl/distro that referenced this pull request Oct 27, 2026
busybox is a fork, so its changes live on tombl/busybox rather than in
patch files here. The four patches become the wasm-ash branch
(tombl/busybox#5), stacked on vfork-child-exit (tombl/busybox#4).
@tombl
tombl changed the base branch from master to vfork-child-exit October 27, 2026 14:36
tombl added a commit to tombl/distro that referenced this pull request Oct 27, 2026
busybox is a fork, so its changes live on tombl/busybox rather than in
patch files here. The four patches become the wasm-ash branch
(tombl/busybox#5), stacked on vfork-child-exit (tombl/busybox#4).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0