Skip to content

fix(interpreter): restore working directory after pipeline stages - #430

Open
josephbajor wants to merge 1 commit into
vercel-labs:mainfrom
josephbajor:codex/fix-pipeline-working-directory
Open

josephbajor wants to merge 1 commit into
vercel-labs:mainfrom
josephbajor:codex/fix-pipeline-working-directory

Conversation

@josephbajor

Copy link
Copy Markdown

cd in a pipeline currently restores $PWD but leaves the interpreter's actual working directory changed. Later pipeline stages and subsequent relative reads/writes therefore use the wrong directory, even though $PWD still reports the parent.

Reproduced against upstream main at 062ce005c0a7676163852fb6f0c8590cbdaa1d45 (just-bash 3.4.2).

Reproduce

From the repository root, after pnpm install --frozen-lockfile, run this on the base branch and then this branch. dev:exec runs the commands in the virtual filesystem; it does not create /work on the host.

printf '%s\n' \
  'mkdir -p /work/child' \
  'cd /work' \
  'echo parent > marker.txt' \
  'echo child > child/marker.txt' \
  'cd child | cat' \
  'echo "$PWD"' \
  'pwd' \
  'cat marker.txt' | pnpm --silent dev:exec

Current upstream reports:

exitCode: 0
stderr: ""
stdout: "/work\n/work/child\nchild\n"

With this fix (and in real Bash), stdout is:

/work
/work
parent

cd child | cat marker.txt also currently reads the child's file in the second stage; it should read the parent's file. A relative write after the pipeline is similarly redirected to the child directory.

Change

Save ctx.state.cwd before each pipeline stage and restore it with the existing environment/array snapshots in finally. This also restores cwd when a stage throws, and removes the duplicated error-path restoration. The existing subshell decision remains unchanged: a single command or brace group can still change cwd, and the final pipeline stage can still do so with shopt -s lastpipe.

Only TypeScript source, tests, recorded Bash fixtures, and a changeset are changed. No compiled bundles are edited or committed.

Verify

pnpm --filter just-bash test:unit src/interpreter/pipeline-cwd.test.ts
pnpm --filter just-bash test:comparison src/comparison-tests/pipeline-cwd.comparison.test.ts
# Optional: record the comparison cases against the machine's real Bash
RECORD_FIXTURES=1 pnpm --filter just-bash test:comparison src/comparison-tests/pipeline-cwd.comparison.test.ts

The 12 unit cases cover first/middle/final stages, relative reads/writes, nested pipelines, explicit exit, unhandled errors, single commands, and lastpipe. Nine fail on the unmodified source. Seven recorded comparisons establish real Bash behavior.

Validation on Node 24.10.0 / macOS:

  • Interpreter/related pipeline tests: 1,178 passed, 2 skipped.
  • All comparison tests: 879 passed.
  • WASM tests: 711 passed, 2 skipped.
  • Distribution tests: 17 passed; build, both package typechecks, lint, and knip passed.
  • Full test:run: 15,629 passed, 98 skipped, 6 failed. The same six filesystem symlink/special-mode tests fail on unmodified 062ce005 in this environment (four existing files under src/fs/); they are unrelated to pipeline execution.

@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

@josephbajor is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@auto-maintain

auto-maintain Bot commented Sep 14, 2026

Copy link
Copy Markdown

🤖 auto-maintain review

Automated, advisory triage for @josephbajor's PR. Facts below are read from the GitHub API.

Check Result
Author's merged PRs (this repo) 0 — ⚠️ first-time contributor
Account established ✅ (age 3404d · 2 followers · 17 public repos)
Commits signed/verified ⚠️ 0/1
Changeset included ✅ (.changeset/tidy-pipelines-return.md)

Review panel: 🟡 medium highest severity

just-bash maintainer code review: 🟡 medium

The cwd fix handles relative path resolution but still leaks related directory state from pipeline subshells.

  • packages/just-bash/src/interpreter/pipeline-execution.ts:146 — Subshell restoration remains incomplete: `cd`, `pushd`, and `popd` also mutate `previousDir` and `directoryStack`. After `cd child | cat`, `cwd`/`OLDPWD` are restored but `cd -` reads the leaked `previousDir`; similarly, `pushd child | cat` leaks a stack entry into later `dirs`/`popd`. Snapshot and restore these directory-state fields with `cwd`.

General code review: 🟡 medium

The cwd fix is incomplete because pipeline-stage `cd` still leaks the internal previous-directory state.

  • packages/just-bash/src/interpreter/pipeline-execution.ts:146 — Subshell restoration leaves `previousDir` mutated by `cd`. After `cd /old; cd /work; cd child | cat; cd -`, this interpreter stays in `/work` instead of returning to `/old`, despite restoring `OLDPWD`. Save and restore `previousDir` alongside `cwd`.

Adversarial security: 🟡 medium

Pipeline cwd isolation is incomplete and permits subshell directory-state changes to redirect later operations.

  • packages/just-bash/src/interpreter/pipeline-execution.ts:146 — Restoring only `cwd` leaves related directory state mutated by subshell stages. `cd child | cat` overwrites `state.previousDir`, while `pushd`/`popd` mutate `directoryStack`; afterward `cd -` or stack operations can follow attacker-influenced paths despite PWD/OLDPWD being restored. Save and restore `previousDir` and a cloned directory stack alongside cwd.

Adversarial security (second opinion): 🟡 medium

The cwd restore is a genuine, narrowly scoped correctness/isolation fix with no suspicious behavior (no deps, network, process execution, or build artifacts touched, and the added fixtures match real bash semantics); the only gap is that sibling directory state (previousDir, directoryStack) is still left leaking out of pipeline subshell stages.

  • packages/just-bash/src/interpreter/pipeline-execution.ts:146 — The new subshell restore block restores env/arrays/cwd but not the other directory state a `cd`/`pushd` stage mutates: `ctx.state.previousDir` and the in-place-mutated `ctx.state.directoryStack`. So `pushd /etc | cat; popd` still leaks a stack entry and moves the parent shell (bash would report "directory stack empty"), `dirs` shows a phantom entry, and `cd /x | cat; cd -` uses the subshell-set `previousDir` instead of the restored `OLDPWD`. Note the real subshell path (`beginStateTransaction` in src/interpreter/state-transaction.ts saves `previousDir` and clones `directoryStack`) already handles both, so the hand-rolled pipeline snapshot is now the odd one out.

Standard Bash and host portability: 🟡 medium

Pipeline cwd restoration remains incomplete because related directory state still leaks from subshell stages.

  • packages/just-bash/src/interpreter/pipeline-execution.ts:146 — The rollback restores cwd/env but leaves `previousDir` and `directoryStack` mutated by `cd`/`pushd`. Consequently, `cd child | cat; cd -` uses the pipeline stage's stale previous directory, and `pushd /tmp | cat; dirs` leaks a stack entry, unlike Bash.

Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call.

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.

1 participant