Skip to content

shell(exit): propagate last status on no-arg, fix negative/invalid arg codes#34903

Open
robobun wants to merge 4 commits into
mainfrom
farm/b9e0b02e/shell-exit-status-mapping
Open

shell(exit): propagate last status on no-arg, fix negative/invalid arg codes#34903
robobun wants to merge 4 commits into
mainfrom
farm/b9e0b02e/shell-exit-status-mapping

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Problem

import { $ } from "bun";
$.nothrow();
(await $`${{ raw: "false; exit" }}`.quiet()).exitCode;  // bun: 0  bash: 1
(await $`${{ raw: "exit -1" }}`.quiet()).exitCode;       // bun: 1  bash: 255
(await $`${{ raw: "exit abc" }}`.quiet()).exitCode;      // bun: 1  bash: 2

POSIX specifies that exit with no argument returns the status of the last command executed. Bun returned 0 unconditionally, so the standard cmd; exit tail idiom always reported success to ShellPromise.exitCode, which is what CI/deploy wrappers branch on.

Cause

  • exit with no argument was hardcoded to 0 in builtin/exit.rs.
  • The argument was parsed as u64, so negatives failed to parse and fell into the "numeric argument required" path.
  • Both the "numeric argument required" and "too many arguments" errors returned 1; bash returns 2 for the former.

Fix

  • Track the last command's exit status on ShellExecEnv (last_exit_code), updated by Stmt::child_done and Binary::child_done. It is inherited when the env is duped for a subshell/command-substitution/pipeline child, so (exit 7); exit works and the child never writes back.
  • exit with no argument reads last_exit_code instead of returning 0.
  • Parse the argument as i64 and keep the low 8 bits: exit -1 → 255, exit -300 → 212, exit 300 → 44 (unchanged).
  • "numeric argument required" now returns 2; "too many arguments" still returns 1.

Verification

test/js/bun/shell/commands/exit.test.ts gains coverage for all of the above (16 cases, 8 of which fail on the current release). bunshell.test.ts (414 tests) and exec.test.ts pass unchanged apart from the expected 1→2 update for exit --help.

Related: #33274 adds the same last_exit_code field on ShellExecEnv to implement $?; whichever lands first, the other is a trivial merge. #33282 makes exit unwind the script, which is orthogonal.


[review] gate passed · iteration 2 · 6 files touched

fails on main (without fix)
ASAN without fix: 12 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/shell/commands/exit.test.ts test/js/bun/shell/exec.test.ts
bun test v1.4.0 (d8a9367dc)

test/js/bun/shell/exec.test.ts:
hi!
(pass) bun exec > it works [195.71ms]
bun: command not found: sldkfjslkdjflksdjflj
(pass) bun exec > it works on command fail [152.20ms]
Usage: bun exec <script>

Execute a shell script directly from Bun.

Note: If executing this from a shell, make sure to escape the string!

Examples:
  bun exec "echo hi"
  bun exec "echo \"hey friends\"!"
(pass) bun exec > no args prints help text [131.26ms]
hi "there bud"
(pass) bun exec > it works2 [142.10ms]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (058ab3c9a)

test/js/bun/shell/exec.test.ts:
hi!
(pass) bun exec > it works [4.67ms]
bun: command not found: sldkfjslkdjflksdjflj
(pass) bun exec > it works on command fail [2.35ms]
Usage: bun exec <script>

Execute a shell script directly from Bun.

Note: If executing this from a shell, make sure to escape the string!

Examples:
  bun exec "echo hi"
  bun exec "echo \"hey friends\"!"
(pass) bun exec > no args prints help text [1.51ms]
hi "there bud"
(pass) bun exec > it works2 [1.84ms]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/shell/commands/exit.test.ts test/js/bun/shell/exec.test.ts
bun test v1.4.0 (d8a9367dc)

test/js/bun/shell/exec.test.ts:
hi!
(pass) bun exec > it works [194.80ms]
bun: command not found: sldkfjslkdjflksdjflj
(pass) bun exec > it works on command fail [145.34ms]
Usage: bun exec <script>

Execute a shell script directly from Bun.

Note: If executing this from a shell, make sure to escape the string!

Examples:
  bun exec "echo hi"
  bun exec "echo \"hey friends\"!"
(pass) bun exec > no args prints help text [110.20ms]
hi "there bud"
(pass) bun exec > it works2 [139.76ms]
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 652ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v
... (truncated)
diff hotspot
src/runtime/shell/builtin/exit.rs       | 39 +++++++++++++++++++--------------
 src/runtime/shell/interpreter.rs        |  8 +++++++
 src/runtime/shell/states/Binary.rs      |  1 +
 src/runtime/shell/states/Stmt.rs        |  1 +
 test/js/bun/shell/commands/exit.test.ts | 25 +++++++++++++++++++--
 test/js/bun/shell/exec.test.ts          |  2 +-
 6 files changed, 56 insertions(+), 20 deletions(-)

gate history · 4 passed · 0 rejected · iteration 2

evidence per changed file
file                                     reads  edits  tests
src/runtime/shell/builtin/exit.rs            2      2      0
src/runtime/shell/interpreter.rs             6      3      0
src/runtime/shell/states/Binary.rs           1      1      0
src/runtime/shell/states/Stmt.rs             1      1      0
test/js/bun/shell/commands/exit.test.ts      2      6      0
test/js/bun/shell/exec.test.ts               1      1      0

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:00 AM PT - Jul 21st, 2026

@robobun, your commit d8a9367 has 1 failures in Build #76822 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34903

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

bun-34903 --bun

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced on stock 1.4.0 with:

bun -e 'import {$} from "bun"; $.nothrow(); console.log((await $`${{raw:"false; exit"}}`.quiet()).exitCode)'
# prints 0, should print 1

Fix verified locally: bun bd test test/js/bun/shell/commands/exit.test.ts → 19 pass.

CI (build #76822): robobun/evidence pass (test fails without the fix and passes with it on ASAN and release), package-binary-size pass. All test failures are unrelated: six known-flaky, and test/js/bun/http/bun-server.test.ts on darwin-14-aarch64 (websocket idle-CPU sampling, no relation to the shell; reported separately). Ready for review.

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Shell exit status behavior

Layer / File(s) Summary
Track completed command status
src/runtime/shell/interpreter.rs, src/runtime/shell/states/{Stmt,Binary}.rs
Shell environments initialize and inherit last_exit_code, while completed statements and binary expressions update it.
Apply exit code semantics
src/runtime/shell/builtin/exit.rs
exit uses the previous status without an argument, returns code 2 for invalid numeric arguments, and wraps signed values to 8 bits.
Validate exit behavior
test/js/bun/shell/commands/exit.test.ts, test/js/bun/shell/exec.test.ts
Tests cover status propagation, invalid arguments, negative values, and updated expected exit codes.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main shell exit-status changes in the PR.
Description check ✅ Passed The description covers the problem, fix, and verification, including status inheritance and exit-code behavior.

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Bun shell ignores 'exit' command #20368 - Reports that the exit builtin is ignored in Bun shell scripts (execution continues past exit, exit 0, exit 1), which is directly addressed by this PR's fixes to exit code propagation and argument handling.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #20368

🤖 Generated with Claude Code

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

This does not fix #20368. That issue is about exit not stopping the script (execution continues past it), which #33282 addresses. This PR only fixes the status that exit itself reports: no-argument propagation, negative-argument wrapping, and the non-numeric error code.

@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.

LGTM — focused POSIX-alignment fix for the exit builtin with good test coverage.

What was reviewed:

  • last_exit_code on ShellExecEnv: both construction sites (root init + dupe) set it, writes happen only in Stmt::child_done/Binary::child_done, and duped envs inherit one-way as intended.
  • Async error-write path: write_failing_error stashes the code on Builtin.exit_code, so on_io_writer_chunk reading it back with .unwrap_or(1) is correct.
  • parse_decimal::<i64> handles leading - and n as u8u16::from gives the documented 255/212/44 wrapping.
Extended reasoning...

Overview

The PR fixes three POSIX/bash divergences in Bun shell's exit builtin: (1) no-arg exit now propagates the last command's status instead of hardcoding 0, (2) negative arguments parse as signed and wrap mod 256, and (3) the "numeric argument required" error returns 2 instead of 1. To support (1), a last_exit_code: ExitCode field is added to ShellExecEnv and updated at the two sequencing points (Stmt::child_done, Binary::child_done). Six files touched: ~25 lines of Rust across four files, plus 16 new test cases and one updated assertion in exec.test.ts.

Security risks

None. No untrusted-input parsing beyond what already existed (the arg was already parsed as an integer; only the target type changed from u64 to i64). No new syscalls, no allocation changes, no pointer/lifetime changes — the new field is a plain u16 copied by value.

Level of scrutiny

Moderate. Shell semantics are user-visible and the cmd; exit idiom is common in CI scripts, so correctness matters — but the change is small, mechanical, and well-aligned with a spec (POSIX) and reference implementation (bash). The state addition on ShellExecEnv is the same shape as the pending $? work in #33274, so the placement has already been vetted. I verified: ShellExecEnv is only constructed at the root and in dupe(), both updated; Builtin::write_failing_error stashes exit_code so the async on_io_writer_chunk path correctly recovers 2 vs 1; parse_int (behind parse_decimal) accepts a leading sign; i64 as u8u16::from yields the expected low-8-bit values for -1/-300/300.

Other factors

Test coverage is thorough for the change (;, ||, subshell, subprocess, newline-separated statements, explicit-arg-wins, both negative-wrap cases) and follows local conventions (TestBuilder, process.argv0 matching sibling files like rm.test.ts/ls.test.ts). The 1→2 change is reflected in the only other assertion on that message (exec.test.ts). PR body confirms USE_SYSTEM_BUN=1 fails 8 of the new cases and bunshell.test.ts passes unchanged. No outstanding reviewer comments.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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/exit.test.ts`:
- Around line 22-30: Extend the “no argument propagates the last command’s
status” test in the exit command suite with cases covering command substitution
and pipeline-child execution. Assert the expected inherited exit codes and
retain the existing direct, compound, subshell, subprocess, statement, and
explicit-argument cases.
- Line 4: Replace the process.argv0-based executable selection in exit.test.ts
with the existing bunExe() helper for the nested -e subprocess, ensuring it
invokes the Bun binary under test.
🪄 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: cc802ae2-5841-4ded-b5c3-ebf83dfe4ff3

📥 Commits

Reviewing files that changed from the base of the PR and between 5b98630 and f282700.

📒 Files selected for processing (6)
  • src/runtime/shell/builtin/exit.rs
  • src/runtime/shell/interpreter.rs
  • src/runtime/shell/states/Binary.rs
  • src/runtime/shell/states/Stmt.rs
  • test/js/bun/shell/commands/exit.test.ts
  • test/js/bun/shell/exec.test.ts

Comment thread test/js/bun/shell/commands/exit.test.ts Outdated
Comment thread test/js/bun/shell/commands/exit.test.ts

@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.

LGTM — focused POSIX-compat fix to the exit builtin with thorough test coverage.

What was reviewed:

  • last_exit_code field: plain u16 on ShellExecEnv, initialized at both construction sites (root env + dupe_for_subshell), written from Stmt/Binary::child_done via the existing shell_mut() helper — no ownership/lifetime concerns.
  • on_io_writer_chunk now reads back the code stashed by Builtin::write_failing_error (verified at Builtin.rs:1132), so the async-stderr path returns 2 for non-numeric args as intended.
  • i64 as u8 truncation gives the documented wrap values (-1→255, -300→212, 300→44); ExitCode is u16 so ::from(u8) is the standard widening impl.
  • All CodeRabbit threads resolved; the declined pipeline-child test case was withdrawn after verifying dupe_for_subshell has no per-kind branching.
Extended reasoning...

Overview

This PR fixes three bash-compat gaps in Bun shell's exit builtin: no-argument exit now propagates the last command's status (was hardcoded 0), negative arguments wrap mod 256 instead of failing to parse, and non-numeric arguments return 2 instead of 1. Runtime changes span four files (~30 lines): a new last_exit_code: u16 field on ShellExecEnv, two one-line writes in Stmt::child_done/Binary::child_done, and the reworked exit.rs builtin. Tests add 13 new cases and update two existing exit-code assertions from 1→2.

Security risks

None. The change touches no user-controlled parsing beyond what already existed (parse_decimal on the arg bytes), adds no allocations, and the new field is plain-data u16 with no ownership semantics.

Level of scrutiny

Medium-low. The shell interpreter state machine is touched, but only additively: two writes to a new field via an existing shell_mut() accessor, and a read in one builtin. No control-flow changes to Stmt/Binary. The struct initializer additions are exhaustive (Rust would fail to compile otherwise). The one intentional user-visible behavior change (exit code 1→2 for non-numeric args) is a bash-alignment fix and both existing assertions on the old value were updated in the same PR.

Other factors

  • Test coverage is strong: 16 cases across statements, &&/||, pipelines, subshells, subprocess exits, negative wraps, and explicit-arg-wins; the PR body confirms 8 fail on the current release and bunshell.test.ts (414 tests) passes unchanged.
  • I verified Builtin::write_failing_error stores the code on the Builtin struct so the async on_io_writer_chunk completion correctly recovers 2 vs 1 rather than the old hardcoded 1.
  • All review-bot threads are resolved; the one declined suggestion (extra pipeline-child inheritance test) was withdrawn by the reviewer after confirming dupe_for_subshell has a single unbranched last_exit_code copy.
  • PR description explicitly coordinates with the two adjacent open PRs (#33274 $?, #33282 script-unwind) — no hidden merge conflict risk.

robobun added 3 commits July 21, 2026 10:23
…g codes

POSIX specifies that exit with no argument returns the status of the
last command executed. Bun returned 0 unconditionally, so the standard
'cmd; exit' tail idiom always reported success to ShellPromise.exitCode.

Also:
- exit -1 now wraps to 255 (was: 'numeric argument required', exit 1).
  The argument is now parsed as a signed integer and the low 8 bits are
  kept, matching bash.
- exit abc now returns 2 (was: 1), matching bash's 'numeric argument
  required' status.
- exit 1 2 (too many arguments) still returns 1.

Tracking for the last command's status lives on ShellExecEnv so it is
scoped per subshell/command-substitution (inherited on dupe, never
written back), and is updated by Stmt::child_done and
Binary::child_done.

@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.

Beyond the inline nit, I checked the async stderr path: Builtin::write_failing_error stashes the code on Builtin.exit_code (Builtin.rs:1132), so on_io_writer_chunk's new .exit_code.unwrap_or(1) correctly returns 2 for the numeric-arg error even when stderr needs I/O. Also confirmed ExitCode is u16 and i64 as u8 gives the documented wrap (-300 → 212).

Extended reasoning...

The change is small and correct; the one finding is a pre-existing check-order edge that only became observable because this PR split the two error codes. Verified the non-obvious bit — that the async write-then-done path picks up the new per-call code rather than the old hardcoded 1 — by reading Builtin::write_failing_error which sets self.exit_code = Some(exit_code) before enqueueing. Not approving because the inline nit is part of the same bash-compat class this PR targets and a maintainer may want it folded in, and because the new ShellExecEnv.last_exit_code field overlaps with in-flight #33274.

Comment thread src/runtime/shell/builtin/exit.rs Outdated
bash validates the first argument before counting: `exit abc def` is
'numeric argument required' (2), not 'too many arguments' (1). The order
was previously unobservable in the exit code because both paths returned
1; splitting them exposed it.

@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.

LGTM — my earlier nit (exit abc def check order) is addressed in d8a9367.

What was reviewed:

  • last_exit_code field: plain u16 on ShellExecEnv, initialized to 0, copied one-way in dupe_for_subshell, written only in Stmt/Binary::child_done — no lifetime or write-back concerns.
  • on_io_writer_chunk reads Builtin.exit_code, which Builtin::write_failing_error stashes before the async stderr enqueue, so the 2-vs-1 split survives the async path.
  • i64 as u8 truncation matches the -1→255 / -300→212 / 300→44 cases; parse_decimal::<i64> handles the leading sign.
  • Test matrix covers Stmt/Binary/pipeline/subshell/subprocess/negative/non-numeric/argc-order; fails-on-main evidence confirmed.
Extended reasoning...

Overview

Aligns the shell exit builtin with POSIX/bash: no-arg exit propagates the last command's status instead of hardcoding 0, negative arguments wrap mod 256 instead of failing to parse, "numeric argument required" now returns 2 instead of 1, and (per my earlier nit) the numeric check on arg[0] runs before the argc check. The state is a new last_exit_code: u16 on ShellExecEnv, updated by the two child_done sites and copied forward in dupe_for_subshell. ~43 net lines across 4 source files plus tests.

Security risks

None. No user-controlled allocation, no pointer/lifetime changes; the new field is a plain u16 on an existing struct. i64 as u8 truncation is well-defined in Rust and cannot panic or overflow.

Level of scrutiny

Medium — it touches shared interpreter state (ShellExecEnv), but the field is inert data with no ownership implications. I traced the read site (exit.rsbase.shell().last_exit_code), both write sites, both init sites, and confirmed Builtin::write_failing_error stashes exit_code on the Builtin before the async stderr path so on_io_writer_chunk's .exit_code.unwrap_or(1) picks up the correct 2 vs 1. The subshell/pipeline copy is one-way because dupe_for_subshell boxes a fresh env; child writes never reach the parent.

Other factors

Test coverage is thorough (16+ cases across the execution-variant matrix), the PR body includes fails-on-main / passes-on-PR evidence for both ASAN-debug and release, and all prior review threads (CodeRabbit's bunExe() and pipeline-coverage asks, my exit abc def ordering nit) are resolved. The exec.test.ts 1→2 update is the expected consequence of the error-code change. The author flagged the trivial merge with #33274 (which adds the same field for $?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant