shell builtins: remove unsafe from rm/cp/yes/ls/mkdir and EnvStr

JA
21h ago
+746-1485

What

Same programme as #40055 … #40261. Stacked on #40228 (base branch claude/shell-zero-unsafe → #40204; retarget as those land). With this, every file under src/runtime/shell/ except subproc.rs (#40204) is at zero.

builtin/rm.rs 46 → 0, yes.rs 6 → 0, cp.rs 3 → 0, ls.rs 2 → 0, mkdir.rs 1 → 0, EnvStr.rs 4 → 0 (RefCountedStr.rs deleted), ParsedShellScript.rs 1 → 0.

  • rm: the raw-pointer parent/child DirTask tree becomes an owned tree — a Box<DirTask> is owned linearly; a directory that spawned children parks its box in Arc<Children { pending: AtomicUsize, parked: Guarded<Option<Box<DirTask>>> }> and whoever takes pending to 0 owns it. Per-root shared state is Arc<RmTree>; the root carries Option<Box<ShellRmTask>> and posts it via ShellTask::on_finish; verbose hops post the DirTask box (shell_dispatch!(ShellRmDirTask)). No hand-written unsafe impl Send.
  • cp / yes: Option<Box<ShellCpTask>> / Option<Box<YesTask>> taken → queued → put back; cp_on_finish(self: Box<Self>).
  • EnvStr: enum { Empty, Refcounted(Rc<[u8]>), Slice(BackRef<[u8]>) } with Drop; EnvMap::get(&[u8]) -> Option<&EnvStr>, insert takes ownership; all manual ref_()/deref() sites removed. PWD/OLDPWD are now copied (the old slices pointed into the mutable cwd Vec across dup'd envs).
  • ParsedShellScript.this_jsvalue was write-only — removed.
  • Primitives: EventLoopHandle::enqueue_boxed_after_yield<T: Taskable> (no unsafe); MiniEventLoop::tick_platform_loop (one block under the existing loop_ptr() invariant, same as tick_once).

Fixes the #40228 review finding: rm's per-operand stderr chunks now take a sequence number (exec.next_chunk(cmd)), so rm nope1 nope2 2>&1 | true no longer hangs — new test rm errors for several operands into a closed pipe finish (hangs on 1.4.0 and on the base build, passes here). Also fixed in passing: the non-Linux ENOTEMPTY re-spawn path in delete_after_waiting_for_children stranded the directory; Expansion's leading-tilde path leaked a ref on get_homedir().

Instruction counts (release, taskset -c 56-63, interleaved, perf stat -e instructions:u, median): 1000× bun run --shell=bun true −0.021 %; 200× rm -rf of a 100-file dir −0.025 %; 100× rm -rf of a 131-dir tree +0.090 % (-v +0.037 %); 3000× JS $`true` +0.047 %.

Testing

Debug+ASAN: rm/cp/yes/mv/bunshell/exec/export 476/476; test/js/bun/shell/ overall 939 pass with the known sandbox artifacts (root-user ls permission cases, file-io non-existent dir, relative-bunExe ls case) and debug-load timeouts in leak.test.ts/shell-load that pass 43/43 on the release build; run-shell + test/regression/issue/*shell* 6/6. Hand-driven vs base release output: rm -rf on a 2184-file 5-level tree, rm -rfv | head -1, rm error paths, cp -r/-rv | head, ls -R | head, yes | head -1000, seq | tail, mv, export/assign/cd -/OLDPWD, mkdir -pv — identical modulo concurrent ordering; a Worker terminated mid rm -rfv walk × 6 clean; no ASAN output. clippy clean (bun_runtime, bun_event_loop); rust-check-all windows-msvc + apple-darwin pass.