Skip to content

fix(python): name the interpreter python3 rather than the worker's host path - #423

Open
mutewinter wants to merge 1 commit into
vercel-labs:mainfrom
mutewinter:fix/python3-pin-program-name
Open

mutewinter wants to merge 1 commit into
vercel-labs:mainfrom
mutewinter:fix/python3-pin-program-name

Conversation

@mutewinter

Copy link
Copy Markdown
Contributor

Problem

const bash = new Bash({ python: true });
await bash.exec(`python3 -c "import os, sys; print(os.environ['_']); print(sys.executable)"`);
// /Users/me/app/node_modules/just-bash/dist/bundle/chunks/worker.js
// /Users/me/app/node_modules/just-bash/dist/bundle/chunks/worker.js

The guest sees the host's absolute install path, which is the one class of string the rest of the worker goes to some length to keep out (sanitizeHostErrorMessage, the protocol-abuse tests).

The second consequence is the one that sent me here. In a pnpm workspace with a patched just-bash, the worker lives at node_modules/.pnpm/just-bash@3.4.1_patch_hash=<64 hex>/node_modules/just-bash/dist/bundle/chunks/worker.js, 200 characters, and every python3 invocation exited 1 with correct stdout:

$ python3 -c "print('hi')"
hi
Fatal Python error: gilstate_tss_clear: failed to clear current tstate (TSS)
Python runtime state: finalizing (tstate=0x00289198)

Aborted()
python3: Security violation: webassembly

An agent reads that as its script failing. The Security violation line is a consequence: Emscripten's abort() builds a WebAssembly.RuntimeError, and the worker defense blocks the WebAssembly global after CPython loads, so the abort's own error is what gets reported.

Cause

Emscripten's Node glue sets thisProgram = process.argv[1], and in a worker_threads worker that is the worker script's own path. It reaches CPython twice: callMain prepends it as argv[0], and getEnvStrings() sets _ to it. Nothing in worker.ts overrides it.

The length of that string shapes the initial heap, and at some lengths Py_FinalizeEx aborts in gilstate_tss_clear, meaning pthread_setspecific returned EINVAL, which under Emscripten's pthread stub means the key was no longer marked in use. Copying the identical worker.js to paths of different lengths, same script, same machine:

worker path length outcome
60 to 190 exit 0
194 to 220 abort at finalization, exit 1
240 to 289 exit 0

Holding the path fixed and setting thisProgram directly reproduces it: 7, 16, 50, 100, 150 characters pass; 200 aborts. The script does not matter (pass aborts too) and what the script allocates does not matter; only the early heap does. The underlying fault is in the CPython/Emscripten build, not in this repo, but this repo is where the install path becomes the program name.

The existing tests cannot see it because a checkout's src/commands/python3/worker.js path is short (82 characters here), as is a plain npm install's.

Fix

thisProgram: "python3" in the createPythonModule config. The guest sees python3 for _, and sys.executable becomes '', which is what CPython reports for a program name it cannot locate on PATH.

Scope

Unchanged: sys.argv, which the setup code already sets from scriptPath and the script args.

Not addressed, deliberately: the finalization abort itself, which is a CPython-on-Emscripten bug this pin only stops one input from triggering. I did not find any other input that lands in the window; the environment strings are otherwise fixed by the glue.

Tests

python3.env.test.ts: os.environ['_'] is python3 and sys.executable is ''. Fails before the change with the worker's host path in both. It proves the path no longer reaches the guest; it cannot prove the abort is gone, since that depends on the checkout's path length, which is what the table above is for.

Suite: 239 passed, 2 skipped across the 16 python3 and python-scripting files.


Authored with Claude Opus 5

…ost path

Emscripten's Node glue reads the program name off process.argv[1], which in a worker thread is worker.js's absolute path on the host. CPython received it as argv[0] and as the _ environment variable, so sys.executable and os.environ['_'] disclosed the host install path to the guest, and its length shaped the initial heap layout: at some lengths (200 characters, a pnpm store path with a patch hash) Py_FinalizeEx aborted with gilstate_tss_clear: failed to clear current tstate after the program had run, and every invocation exited 1 with the right output. Passing thisProgram pins what CPython sees.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

@mutewinter 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 11, 2026

Copy link
Copy Markdown

🤖 auto-maintain review

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

Check Result
Author's merged PRs (this repo) 10
Account established ✅ (age 5932d · 190 followers · 130 public repos)
Commits signed/verified ✅ 1/1
Changeset included ✅ (.changeset/python3-pin-program-name.md)

Review panel: 🟢 low highest severity

just-bash maintainer code review: 🟢 low

No actionable findings; the program-name override correctly prevents host-path exposure and is covered by a focused regression test.

General code review: 🟢 low

The change correctly overrides Emscripten's program name and adds focused regression coverage; no actionable issues found.

Adversarial security: 🟢 low

No actionable security issues found; the change replaces a leaked host path with a fixed interpreter name without expanding attacker-controlled behavior.

Adversarial security (second opinion): 🟢 low

Three-line, security-positive change: pinning Emscripten's `thisProgram` to "python3" stops the worker's absolute host path from reaching the guest via `argv[0]`/`_`/`sys.executable`. Verified the vendored glue applies `Module["thisProgram"]` after the `process.argv[1]` default, that stdlib resolution relies on the explicitly set PYTHONHOME/PYTHONPATH rather than argv[0], and that `sys.argv` is still set independently from `scriptPath`; no repo code depends on `sys.executable`. No backdoors, networking, dependency, or sandbox-boundary changes.

Standard Bash and host portability: 🟢 low

No actionable Bash or host-portability issues found.

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

mutewinter added a commit to instrument-org/instrument that referenced this pull request Sep 11, 2026
vercel-labs/just-bash#423 pins the program name, #424 corrects the errnos, #425 names the program in tracebacks; the register, the finding, the decision, and the plan each carry the numbers where they said the fixes were unoffered.
@mutewinter

Copy link
Copy Markdown
Contributor Author

#444 (opened 2026-09-20) reaches the same diagnosis independently: thisProgram defaulting to the worker's install path is what decides whether Py_FinalizeEx aborts, reproduced there with the vendored runtime alone. Its fix is the same one-line pin, to /usr/bin/python3 where this one uses python3; either name keeps the host path out of the guest, and CPython's sys.executable reads more naturally as an absolute path, so I have no objection to that spelling if this is the one that lands. It also adds a python3.finalization.test.ts covering atexit and virtual-file flushing that would be worth keeping whichever PR merges. Merging both as written yields two thisProgram keys in the same object literal, so one of them closes.

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