Skip to content

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

Open
Jarred-Sumner wants to merge 2 commits into
claude/shell-zero-unsafefrom
claude/shellbuiltins-zero-unsafe
Open

Jarred-Sumner wants to merge 2 commits into
claude/shell-zero-unsafefrom
claude/shellbuiltins-zero-unsafe

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

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.

@robobun

robobun commented Aug 23, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 11:52 AM PT - Sep 8th, 2026

✅ @autofix-ci[bot], your commit c01429200a4f280b86670252770dae7080c012a4 passed in Build #113082! 🎉


🧪   To try this PR locally:

bunx bun-pr 40262

That installs a local version of the PR into your bun-40262 executable, so you can run:

bun-40262 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it's a substantial redesign of concurrency-sensitive ownership (rm's cross-thread DirTask tree, the Children pending/parked handoff, new event-loop primitives, and EnvStr's refcount model), a human look would still be worthwhile.

What was reviewed:

  • Children::release/done ordering: the parked store is sequenced before the parent's SeqCst fetch_sub, so the last decrementer always sees the box; the reset-to-1 lets delete_after_waiting_for_children re-release on the ENOTEMPTY re-spawn path.
  • EnvMap::insert under EnvStr: Drop: get_or_put drops the duplicate key on found-existing and default-initialises the value slot on insert, so the *value_ptr = val assignment drops Empty/the old value correctly.
  • Per-DirTask poster lifecycle: each task is arm()ed via schedule_no_ref, new_child clones the parent's poster before the parent's own on_finish consumes it, and the root ShellRmTask is armed separately.
  • remove_entry_dir's handed_off gate keys on self.children.is_some(), which is only set once a child is actually scheduled (the errored() early-outs precede get_or_insert_with).
Extended reasoning...

Overview

This PR completes the zero-unsafe programme for src/runtime/shell/ (except subproc.rs): 46→0 in rm.rs, plus cp/yes/ls/mkdir/EnvStr/ParsedShellScript. The largest change is a full redesign of rm -r's cross-thread DirTask tree: the raw-pointer parent/child graph with subtask_count/need_to_wait/pending_main_callbacks atomics is replaced by linear Box<DirTask> ownership handed through an Arc<Children { pending: AtomicUsize, parked: Guarded<Option<Box<DirTask>>> }>, with per-root shared state in Arc<RmTree>. EnvStr becomes a plain enum { Empty, Refcounted(Rc<[u8]>), Slice(BackRef<[u8]>) } with Drop; RefCountedStr.rs is deleted and every manual ref_()/deref() call site is removed. cp/yes move to Option<Box<Task>> taken/put-back. New primitives: EventLoopHandle::enqueue_boxed_after_yield and MiniEventLoop::tick_platform_loop. Also fixes a hang (rm's per-operand stderr chunks now numbered) with a new regression test, plus two incidental fixes (non-Linux ENOTEMPTY re-spawn stranding, and a leaked ref on get_homedir() in Expansion's leading-tilde path).

Security risks

None identified. This is internal ownership/lifetime restructuring; no new user-facing surface, parsing, or trust boundaries. The changes generally reduce risk by removing hand-written unsafe impl Send, raw-pointer container_of recovery, and manual refcount balancing.

Level of scrutiny

High. The rm rewrite replaces a subtle atomic protocol (whose lost-wakeup window already required a stress test) with a different subtle atomic protocol. The Children handoff, the delete_after_waiting_for_children loop that can re-spawn and re-release, the per-DirTask ShellTask poster lifecycle across schedule_no_ref/new_child/on_finish, and the interaction between handed_off and self.children.is_some() under concurrent errored() short-circuits are all exactly the class of code REVIEW.md flags for careful maintainer review. The EnvStr change touches every env-map insert/get site and changes insert from ref-on-insert to move-in semantics.

Other factors

The PR is stacked on #40228 (base claude/shell-zero-unsafe → #40204). Test coverage is solid (476/476 shell builtin tests debug+ASAN, hand-driven diff vs base release, Worker termination mid-walk, rust-check-all for windows/darwin) and instruction-count deltas are within noise. The new closed-pipe test targets the specific hang fixed. But the sheer scope of the ownership redesign — and the fact that it's stacked — means a maintainer should walk the Children protocol and the DirTask state machine before this lands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and the bug-hunting pass found no issues. Given the scope — a full redesign of rm's concurrent DirTask ownership (the Arc<Children> park/hand-off protocol), the EnvStr refcount → Rc<[u8]>/Drop migration touching every env-map insert/get site, and new event-loop primitives — a human look at the concurrency hand-off and the EnvMap::insert key-leak question would still be worthwhile.

Checked: the Children::release/done sequencing (park-then-fetch_sub, reset to 1) against lost wakeups; delete_after_waiting_for_children now loops on re-spawn (fixes the non-Linux ENOTEMPTY strand noted in the description); EnvMap::insert on an existing key drops the old value via get_or_put overwrite and the passed-in duplicate key drops on move; cp's CowSlice args borrow this.src_absolute/tgt_absolute before this moves into create_for_shell — the borrow ends at the call boundary and the Vecs ride inside the box.

Extended reasoning...

Overview

This PR completes the zero-unsafe programme for src/runtime/shell/ (except subproc.rs), touching 23 files. The largest piece is a ground-up redesign of rm's recursive delete tree: the raw-pointer DirTask graph with subtask_count/need_to_wait/pending_main_callbacks atomics and hand-written unsafe impl Send is replaced by linearly-owned Box<DirTask> handed between threads via an Arc<Children { pending: AtomicUsize, parked: Guarded<Option<Box<DirTask>>> }> parking protocol, with per-root shared state in Arc<RmTree>. EnvStr moves from a packed u128 + intrusive RefCountedStr to a plain enum { Empty, Refcounted(Rc<[u8]>), Slice(BackRef<[u8]>) } with Clone/Drop, and every manual ref_()/deref() call site (EnvMap, cd, which, export, Assigns, Expansion, interpreter's PWD/OLDPWD) is removed. cp and yes switch to Option<Box<Task>> take/put-back ownership. RefCountedStr.rs and ParsedShellScript.this_jsvalue (write-only) are deleted. New event-loop primitives enqueue_boxed_after_yield and MiniEventLoop::tick_platform_loop are added. It also fixes a hang (rm's per-operand stderr chunks now take sequence numbers so a closed pipe calls each back) with a regression test, and a leak in Expansion's leading-tilde path.

Security risks

None identified. This is an internal memory-ownership refactor of shell builtins; no new user-facing surface, no parsing of untrusted input changes, no auth/crypto/permissions code touched. The rm path traversal / NOFOLLOW behaviour is unchanged.

Level of scrutiny

High. The rm redesign replaces a subtle raw-pointer concurrent hand-off protocol with a different subtle safe one — Children::release parks the box under a mutex then does a SeqCst fetch_sub; the last decrementer takes it and resets pending to 1 for the next round. This is production-critical (rm -rf on the thread pool) and a stranded task hangs the process. The EnvStr change alters ownership semantics at every insert/get: EnvMap::insert no longer refs — it moves — and clone() relies on ArrayHashMap::clone deep-cloning EnvStr (which now derives Clone). These are exactly the shapes REVIEW.md calls out under "Reference counts provably balanced on every terminal path" and "Know the thread affinity of every line you touch".

Other factors

The PR is stacked on #40228/#40204, is well-tested (476/476 shell tests, ASAN-clean, instruction-count deltas within noise, cross-target check-all), and the description is thorough. The bug-hunting system flagged nothing. I traced the Children protocol against the old need_to_wait/subtask_count dance and it appears equivalent (the store-into-parked-before-fetch_sub ordering mirrors the old need_to_wait.store(true)-before-fetch_sub), and the delete_after_waiting_for_children loop now correctly re-parks on ENOTEMPTY re-spawn (the old code set do_post_run = false and returned, stranding the dir on non-Linux). But this is a ~1000-line concurrent-ownership redesign with new architectural primitives in bun_event_loop; it warrants maintainer eyes on the hand-off protocol and the EnvMap key lifecycle rather than an automated approval.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from a09c68f to 393343a Compare August 27, 2026 08:38
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from ae77500 to 4da140e Compare August 27, 2026 09:18
Comment thread test/js/bun/shell/commands/rm.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from 393343a to 550a539 Compare August 29, 2026 07:50
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch 3 times, most recently from 99e10d9 to 38befe7 Compare August 29, 2026 08:00

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from 38befe7 to 550b4be Compare August 29, 2026 08:40

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from 0ffcbbd to 2dead19 Compare August 31, 2026 05:43
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from 550b4be to 6cb4a62 Compare August 31, 2026 05:57
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from 2dead19 to 2288ef8 Compare August 31, 2026 06:19
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from 6cb4a62 to 4c490c9 Compare August 31, 2026 06:21

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from 2288ef8 to c8ea1ef Compare September 6, 2026 19:26
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from 4c490c9 to 5f469e8 Compare September 6, 2026 19:35

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

rm: the recursive DirTask tree is an owned tree. Each directory is a
Box<DirTask>; a directory that spawned child tasks parks its box in an
Arc<Children> (its own slot plus one per unfinished child) and whoever
takes the count to zero owns the box and deletes the directory. Per-root
shared state is Arc<RmTree>; the root DirTask carries the ShellRmTask and
posts it back when the tree is done; verbose output hops post the DirTask
box itself. On non-Linux, a directory found non-empty after its children
finished (and re-spawned) now releases its slot again instead of never
completing. rm numbers its per-root error chunks like its verbose chunks,
so `rm nope1 nope2 2>&1 | true` no longer waits for callbacks the closed
pipe collapsed into one.

cp: the fs.cp task owns the ShellCpTask box and hands it back through
cp_on_finish. yes: the bounce payload is a Box taken from the builtin,
queued through EventLoopHandle::enqueue_boxed_after_yield and put back
when it runs. mkdir collects verbose output through a RefCell; ls -l
writes the mode/time bytes directly. ParsedShellScript drops a write-only
JsRef.

EnvStr is an enum (Empty / Rc<[u8]> / BackRef<[u8]> for literals, script
text and the dotenv loader) instead of a packed tagged pointer with manual
ref/deref; EnvMap looks up by &[u8] and owns what it stores. PWD/OLDPWD
hold copies rather than slices of the cwd buffers.

Instruction counts vs the base branch (release, perf stat, pinned,
interleaved): 1000x `bun run --shell=bun` true -0.02%, 200x rm -rf of a
100-file dir -0.03%, 100x rm -rf of a 131-dir tree +0.09% (-v: +0.04%),
3000x JS $`true` +0.05%.
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shell-zero-unsafe branch from 9d93c46 to c014292 Compare September 8, 2026 16:41
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/shellbuiltins-zero-unsafe branch from 5f469e8 to 93a4ba0 Compare September 8, 2026 16:41

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
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.

2 participants