shell builtins: remove unsafe from rm/cp/yes/ls/mkdir and EnvStr
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
DirTasktree becomes an owned tree — aBox<DirTask>is owned linearly; a directory that spawned children parks its box inArc<Children { pending: AtomicUsize, parked: Guarded<Option<Box<DirTask>>> }>and whoever takespendingto 0 owns it. Per-root shared state isArc<RmTree>; the root carriesOption<Box<ShellRmTask>>and posts it viaShellTask::on_finish; verbose hops post theDirTaskbox (shell_dispatch!(ShellRmDirTask)). No hand-writtenunsafe 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]>) }withDrop;EnvMap::get(&[u8]) -> Option<&EnvStr>,inserttakes ownership; all manualref_()/deref()sites removed.PWD/OLDPWDare now copied (the old slices pointed into the mutable cwdVecacross dup'd envs). ParsedShellScript.this_jsvaluewas write-only — removed.- Primitives:
EventLoopHandle::enqueue_boxed_after_yield<T: Taskable>(no unsafe);MiniEventLoop::tick_platform_loop(one block under the existingloop_ptr()invariant, same astick_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.