Skip to content

shell: export NAME without a value no longer clobbers the variable - #33549

Closed
robobun wants to merge 3 commits into
mainfrom
farm/5c76e6ef/shell-export-no-value
Closed

robobun wants to merge 3 commits into
mainfrom
farm/5c76e6ef/shell-export-no-value

Conversation

@robobun

@robobun robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

In Bun Shell, the export builtin treated every operand as NAME=<value-after-equals>. With no = present the value parsed as the empty string and was then assigned into the exported env table. This broke the canonical POSIX two-step export idiom and, worse, destroyed existing values.

Repro

import { $ } from "bun";
$.nothrow();
const E = { PATH: "/usr/bin:/bin", OUTER: "fromenv" };
const run = async s => (await $`${{ raw: s }}`.env(E).quiet()).stdout.toString();

// 1) canonical idiom: the child should see "5"
await run("T=5; export T; /usr/bin/printenv T; echo shellvar=[$T]");
//   bun:  "\nshellvar=[5]\n"   (child sees T="")   bash: "5\nshellvar=[5]\n"

// 2) export of an inherited var erased its value
await run("export OUTER; /usr/bin/printenv OUTER; echo shellvar=[$OUTER]");
//   bun:  "\nshellvar=[]\n"    bash: "fromenv\nshellvar=[fromenv]\n"

// 3) export of an unset name invented an empty entry
await run("export NOSUCH; /usr/bin/printenv NOSUCH || echo NOT-IN-ENV");
//   bun:  "\n" (printenv succeeds)    bash: "NOT-IN-ENV\n"

Cause

src/runtime/shell/builtin/export.rs split each operand on =; the None arm fell through to (name, "") and inserted that empty value into export_env, overwriting any existing binding and materializing entries for unset names.

Fix

export NAME with no = now only sets the export attribute: it looks up the current value (shell variable first, then an already-exported/inherited one) and exports that, and does nothing when the name is unset. export NAME=value is unchanged.

Verification

Added three regression tests to test/js/bun/shell/bunshell.test.ts (the export idiom, export of an inherited var, export of an unset name). All fail on main and pass with this change.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3dd39429-d331-4b60-9369-9b51a4466459

📥 Commits

Reviewing files that changed from the base of the PR and between 48ff9eb and 74e3329.

📒 Files selected for processing (2)
  • src/runtime/shell/builtin/export.rs
  • test/js/bun/shell/bunshell.test.ts

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

@github-actions github-actions Bot added the claude label Jul 6, 2026
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:13 PM PT - Jul 6th, 2026

@robobun, your commit 353838e is building: #69298

@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:41 PM PT - Jul 6th, 2026

❌ @robobun, your commit 74e3329 has 1 failures in Build #69406 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33549

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

bun-33549 --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.

LGTM — strict improvement over the previous clobbering behavior, refcounting is balanced on all paths, and the follow-up noted inline is a pre-existing gap.

Extended reasoning...

Overview

This PR fixes the export shell builtin so that export NAME (no =) exports the variable's current value instead of overwriting it with an empty string. It touches ~30 lines in src/runtime/shell/builtin/export.rs (splitting the None match arm out to look up the existing value in shell_env/export_env before inserting) and adds 3 regression tests to test/js/bun/shell/bunshell.test.ts covering the three repro cases (shell var, inherited env var, unset name).

Security risks

None. This is shell-builtin bookkeeping over the interpreter's own env maps; no new untrusted-input parsing, no path handling, no network/auth surface. The unsafe blocks are the same raw-pointer-to-ShellState pattern already used in the unchanged Some(eq) arm.

Level of scrutiny

Moderate — it touches manual refcounting via EnvStr/EnvMap, which is memory-safety-sensitive. I traced the refcounts against EnvMap::get (bumps the returned value, caller must deref) and EnvMap::insert (refs the key only if new, derefs the old value, refs the new value):

  • found in shell_env: get +1 on val → insert refs val again (+1, stored) and refs/keeps label → val.deref() balances the get → label.deref() balances the dupe_ref_counted. Net: map holds one ref each. ✓
  • found in export_env (re-inserting same value at same key): get +1 → insert derefs the stored copy (-1, still alive because of the get bump) then refs val (+1, stored) → val.deref() and label.deref() balance. ✓
  • unset: only label is duped and immediately deref'd; nothing inserted. ✓

No leaks or use-after-free on any branch.

Other factors

  • The inline nit (later T=6 after export T doesn't propagate to export_env) is a pre-existing limitation of assign_var's AssignCtx::Shell arm and also affects export T=5; T=6 today — not introduced or worsened here. The PR is a strict improvement.
  • Moving let shell = ... above the match is safe: arg_bytes returns an unbounded-lifetime slice into separately-allocated argv storage per its doc comment (Builtin.rs:478-487).
  • Tests spawn a child bun -e to observe process.env, which correctly exercises the child-env-construction path (export_env → child), and use TestBuilder consistent with neighboring export tests.

Comment thread src/runtime/shell/builtin/export.rs

@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 — small, well-scoped fix; refcounts balance against the documented EnvMap get/insert contract, and the three tests cover the behavioral matrix.

Extended reasoning...

Overview

This PR changes one function in src/runtime/shell/builtin/export.rs (the export builtin's argument loop) and adds three regression tests to test/js/bun/shell/bunshell.test.ts. The Some(eq) arm is byte-identical to the pre-existing code, just moved into a match. The only new logic is the ~12-line None arm, which looks up the current value via shell_env.get() / export_env.get() and re-inserts it into export_env instead of writing an empty string.

Security risks

None. This is a shell builtin's variable-bookkeeping path; no untrusted input parsing, no filesystem/network I/O, no auth or crypto surface.

Level of scrutiny

Moderate — it's unsafe Rust with manual EnvStr refcounting, which is the most-blocked review category per repo guidance. I traced the refcounts on every terminal path against the actual EnvMap implementation:

  • EnvMap::get returns a +1 ref (line 107: val.ref_()) and its doc says "deref the string when done" — the new code does val.deref() after insert. ✓
  • EnvMap::insert takes its own +1 on both key and value (lines 79/83) — the caller's local +1 on label is released via label.deref() on all three sub-paths (found in shell_env / found in export_env / unset). ✓
  • The re-insert-same-value case (export of an already-exported name) can't UAF because get()'s +1 keeps the value alive across insert's internal deref-old-value step. ✓
  • The unset case skips insert entirely and just releases label. ✓

Other factors

The three added tests exercise each branch (shell var → exported, inherited var preserved, unset name stays unset) and follow the existing TestBuilder pattern in the same describe block. My earlier inline note about the pre-existing AssignCtx::Shell routing gap was acknowledged as out-of-scope and resolved; it's a strict improvement over current behavior with no regression. No CODEOWNERS on this path. The bug-hunting pass found nothing.

@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. The two failing darwin aarch64 - test-bun jobs are infrastructure, not test failures: both exit before running any tests with buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun' (the macOS build artifact never finished downloading). This reproduced across two separate builds (69301, 69406) on the same lane; a re-run was already attempted. The change only touches the shell export builtin and its tests, which don't run on that timed-out step. Ready for a maintainer to re-run the darwin lane or merge.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-06, 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.

@robobun robobun closed this Sep 13, 2026
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