Repository navigation
Conversation
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughChangesThe rm refusal handling
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 7:05 AM PT - Jul 29th, 2026
❌ @robobun, your commit a9438ff has some failures in 🧪 To try this PR locally: bunx bun-pr 34906That installs a local version of the PR into your bun-34906 --bun |
There was a problem hiding this comment.
No new issues found; my earlier inline notes are all resolved (thanks for splitting the ... test out for Windows). Deferring to a human because this relocates the preserve-root guard into the Exec state and rewires the completion accounting (had_refusal, the extra output_count bump for the refusal write, the total_tasks == 0 early-out) around concurrently-scheduled ShellRmTasks — I traced the needs_io vs write_no_io paths and they look consistent, but this is destructive-path state-machine surgery that's worth a maintainer's eyes.
What was reviewed:
./..basename refusal against the operand matrix in the tests, plus.../dotfiles negative cases- preserve-root now resolving via
Builtin::shell().cwd()instead ofbun_sys::getcwd_z - removal of
RmState::WaitingWriteErr/write_failing_error— no remaining references, each builtin has its own copy - completion paths when all operands are refused, with and without
stderr.needs_io()
Extended reasoning...
Overview
Two-file change: src/runtime/shell/builtin/rm.rs (~200 lines net) and test/js/bun/shell/commands/rm.test.ts (+~100 lines). The runtime change adds the POSIX-mandated ./.. operand refusal, moves the preserve-root guard from ParseOpts into the start of Exec, switches its resolution base from the process cwd to the shell instance's cwd, and reworks how refused operands feed into the exit-code / output-drain accounting (new had_refusal on ExecState, new exit_code() helper used at all three completion sites, an output_count bump for the batched refusal message when stderr needs async I/O, and a total_tasks == 0 short-circuit to Done). The now-unreachable WaitingWriteErr state and write_failing_error helper are deleted.
Security risks
The change is on a destructive filesystem operation, but strictly narrows what rm will act on (refuses ./.. up front) and fixes a case where the guard could be side-stepped by process-vs-shell cwd mismatch. No new inputs are trusted; refused operands are skipped rather than processed. I don't see a way this widens the delete surface.
Level of scrutiny
High. rm -rf is as destructive as it gets, and the diff touches the async completion protocol between worker-pool ShellRmTasks and the main-thread on_io_writer_chunk/on_shell_rm_task_done callbacks. I walked the four cases (refusal + tasks × needs_io/no_io, refusal + no tasks × needs_io/no_io) and the output_count/output_done/tasks_done/total_tasks invariants hold in each — the comment about task completions bouncing to the main thread is what makes the post-schedule output_count.fetch_add non-racy. It reads correct, but a maintainer who owns the shell I/O writer contract should confirm.
Other factors
Test coverage is solid: a test.each over seven operand spellings asserting exact stderr/exit/remaining-files, a mixed-operand test proving non-refused operands still run, negative cases for dotfiles and ..., the / guard, and a spawned-subprocess test for the shell-cwd fix (process launched from /). All three of my earlier inline notes are resolved — two were declined with reasonable justification (cross-platform \\ handling is a shell-wide convention; --no-preserve-root is pre-existing and out of scope), and the Windows ... fixture was split out with skipIf(isWindows) in 165c878.
…serve-root against the shell cwd POSIX rm requires a diagnostic and no action when the last path component of an operand is '.' or '..'. Bun Shell's rm was recursing into them, so 'rm -rf .', 'rm -rf ./', and 'rm -rf d/.' emptied the directory before the final rmdir failed with EINVAL. The preserve-root guard also resolved relative operands against the JS process cwd instead of the shell instance's cwd, so it both misfired (spurious refusals when the process was launched from '/') and failed to fire when it should. The operand loop now runs once at the start of the Exec state: operands with a '.'/'..' basename get the GNU-style 'refusing to remove' message, the preserve-root check resolves against Builtin::shell().cwd(), refused operands are skipped while the remaining operands proceed, and the exit code is 1 if anything was refused.
Win32 path normalization strips trailing periods from path components, so a directory literally named '...' does not round-trip through the harness mkdirSync/readdirSync path. Keep dotfile coverage on all platforms and gate the triple-dot case on POSIX.
165c878 to
0e26ef3
Compare
0f9f271 to
4902e1c
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/shell/commands/rm.test.ts`:
- Around line 291-302: The rm command fixture must assert that the spawned child
launches with the configured root working directory. Update the fixture JSON
produced by the child to include process.cwd(), then extend the expected object
in the test around Bun.spawn to require that value to be "/"; preserve the
existing stderr, exitCode, and subExists assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8d83902c-3f97-49d9-8633-2052bfa0db3c
📒 Files selected for processing (2)
src/runtime/shell/builtin/rm.rstest/js/bun/shell/commands/rm.test.ts
There was a problem hiding this comment.
I didn't find any issues, but since this rewrites the rm builtin's per-operand validation and folds it into the Exec state (changing how refusal diagnostics interact with the output_count/output_done accounting and concurrent ShellRmTask scheduling), it's worth a maintainer glance at the state-machine flow.
What was reviewed:
- The
./..basename refusal and shell-cwd resolution — matches GNU/POSIX;Builtin::shell().cwd()is the same sourcecp/mkdir/touchuse. - The
needs_io()refused-msg enqueue path whentotal_tasks == 0—on_io_writer_chunkcorrectly reachesDoneviatasks_done (0) >= total_tasks (0) && output_drained(). output_count.fetch_addfor the refusal write happens after tasks are scheduled, but task completions post back to the main thread so no ordering hazard.- Removed
WaitingWriteErr/write_failing_error— dead after the guard moved out ofParseOpts; remainingwait_write_erruses still route through the catch-all arm ofon_io_writer_chunk.
Extended reasoning...
Overview
Two files: src/runtime/shell/builtin/rm.rs and test/js/bun/shell/commands/rm.test.ts. The runtime change moves the per-operand preserve-root check from the ParseOpts state into the Exec state, adds a POSIX-mandated refusal for operands whose basename is . or .., switches the guard's cwd source from bun_sys::getcwd_z (host process) to Builtin::shell().cwd() (shell instance), and lets refused operands be skipped while remaining operands still run. Supporting refactors: a new had_refusal flag and exit_code() helper on ExecState, and removal of the now-dead RmState::WaitingWriteErr variant and write_failing_error helper.
Security risks
The change is fail-safe in direction — it adds refusals, never removes them. The one prior nit (backslash-as-separator on POSIX via resolve_path::basename) fails toward a spurious refusal, not a wrong deletion, and the author declined it for consistency with other shell builtins. The pre-existing --no-preserve-root no-op was already flagged and deferred as out of scope.
Level of scrutiny
High. rm -rf is a destructive operation and the previous behavior was silent data loss (rm -rf . emptied the cwd). The fix itself is straightforward, but it's woven into a hand-rolled state machine that coordinates worker-thread ShellRmTasks with main-thread IO-writer callbacks via atomic output_count/output_done counters. I traced the four combinations (total_tasks == 0 × needs_io() Some/None) and they all reach Done with the right exit code, but this is exactly the kind of accounting a maintainer who owns the shell builtin dispatch should confirm.
Other factors
All prior review threads (mine and CodeRabbit's) are resolved: the ... test is now Windows-gated, long comments were trimmed per comment-cop, and the preserve-root subprocess test now asserts process.cwd() === "/". Test coverage is thorough — seven ./.. operand spellings, mixed refused+accepted operands, dotfiles and ... as negative cases, / still refused, and a spawned-from-/ subprocess for the cwd fix. The bug hunter found nothing this run.
|
CI status for a9438ff (build #85018): No |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-29, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Reproduction
Same for
rm -rf ./,rm -rf d/.,rm -rf d/.., andcd k/s; rm -rf ..(which deleted the parent's contents).Cause
POSIX requires
rmto write a diagnostic and do nothing more with an operand whose last path component is.or... The builtin had no such check, so the operand was passed to the recursiveShellRmTaskwhich opened it as a directory, deleted every entry, and only then hitEINVALfromunlinkat(cwd, ".", AT_REMOVEDIR).The preserve-root guard (
"<path>" may not be removed) also resolved relative operands against the host process cwd (bun_sys::getcwd_z) rather than the shell instance's cwd. With a process launched from/,$\rm -rf sub`.cwd(deepSandbox)` was spuriously refused; launched from elsewhere, the same guard could be side-stepped.Fix
The per-operand validation now runs once at the start of the
Execstate:basenameis.or..get the GNU-stylerefusing to remove '.' or '..' directory: skipping '<op>'diagnostic and are skipped.Builtin::shell().cwd()(the shell instance's cwd, same sourcecp/mkdir/touchalready use).Dotfiles and
...are unaffected, andrm -rf /still prints"/" may not be removed.Verification
The new
rm refuses '.' and '..' operandscases andrm preserve-root guard resolves against the shell cwdfail on the released binary and pass with this change.no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/shell/commands/rm.test.ts