Skip to content

fix(python): report an oversize file as EFBIG and a read-only mount as EROFS - #424

Open
mutewinter wants to merge 3 commits into
vercel-labs:mainfrom
mutewinter:fix/python3-hostfs-errnos
Open

mutewinter wants to merge 3 commits into
vercel-labs:mainfrom
mutewinter:fix/python3-hostfs-errnos

Conversation

@mutewinter

Copy link
Copy Markdown
Contributor

Problem

const bash = new Bash({ python: true, executionLimits: { maxStringLength: 128 } });
await bash.fs.writeFile("/tmp/big.bin", "x".repeat(256));
await bash.exec(`python3 -c "open('/tmp/big.bin').read()"`);
// OSError: [Errno 33] No file descriptors available: '/host/tmp/big.bin'
// OSError: [Errno 33] No file descriptors available: '/lib/python313.zip'

Any file over maxStringLength (8 MB by default through the bridge buffer) does this, after a noticeable pause, and the second line is the stdlib zip: the interpreter cannot import traceback to print the first error. Two smaller ones: a file the bridge refuses as too large for its buffer is reported as FileNotFoundError for a file that exists, and a write into a read-only mount is OSError: [Errno 29] I/O error rather than the EROFS the mount actually raised.

Cause

The errno table in createHOSTFS has

EFBIG: 27,

Under Emscripten's numbering 27 is EINTR; EFBIG is 22 (import errno; errno.EFBIG in the vendored build says so, and os.strerror(22) is File too large). CPython retries an open() that fails with EINTR (PEP 475), and Emscripten's FS.open has already allocated the stream when stream_ops.open throws, so every retry leaks a descriptor until the table is exhausted and the retry fails with the real EMFILE, 33. ENODATA: 42 is wrong the same way (116).

The two mappings: stream_ops.open catches every backend.readFile failure as ENOENT, so the bridge's Result too large: N > 8388608 reads as a missing file; and tryFSOperation has no branch for read-only/erofs, so the mount's EROFS: read-only file system, ... falls through to EIO.

python3.security.test.ts asserted [Errno 27] for the truncate case, which is how the wrong number stayed in place: the test was checking the message the bug produced.

Fix

EFBIG is 22 and ENODATA 116; tryFSOperation maps read-only/erofs to EROFS and too large to EFBIG; stream_ops.open throws EFBIG rather than ENOENT when the read failed as too large.

Scope

Unchanged: maxFileSize and where it is enforced; a missing file is still ENOENT.

Not addressed, deliberately: the 8 MB per-operation bridge buffer (Size.DATA_BUFFER), which is what makes a 9 MB file unreadable at any maxStringLength. Chunked reads across the bridge would lift it; that is a protocol change and a separate PR if you want it.

Tests

python3.files.test.ts: a 256-byte file under maxStringLength: 128 raises OSError: [Errno 22] File too large once, with no No file descriptors available; a write into an OverlayFs({ readOnly: true }) mount raises [Errno 69] Read-only file system and creates nothing, while a read of the same mount works. python3.security.test.ts: the truncate assertion now names [Errno 22] File too large. Both new tests fail before the change with the output quoted above.

Suite: 240 passed, 2 skipped across the 16 files.


Authored with Claude Opus 5

…as EROFS

The worker's errno table had EFBIG as 27, which is EINTR under Emscripten's numbering. CPython retries an open() that fails with EINTR, and each retry leaked the stream Emscripten allocated for the failed open, so reading a file over maxStringLength looped until the descriptor table ran out and the script died of EMFILE, taking any later import with it. A read the bridge refused as too large for its buffer surfaced as ENOENT, and a write into a read-only mount as EIO. EFBIG is 22, ENODATA 116, and the two failures carry their own errno.
@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-hostfs-errnos.md)

Review panel: 🔴 high highest severity

just-bash maintainer code review: 🔴 high

The fix leaves a silent data-loss path when appending to oversized existing files.

  • packages/just-bash/src/commands/python3/worker.ts:560 — The oversize-error handling is bypassed whenever O_CREAT is present. Emscripten retains O_CREAT for existing files, so appending to an existing file larger than the bridge buffer treats the failed read as empty; after a write, close() replaces the entire file with only the appended bytes instead of raising EFBIG. Classify `too large` before the create fallback.

General code review: 🟡 medium

The errno fix leaves a data-loss path when appending to files larger than the bridge buffer.

  • packages/just-bash/src/commands/python3/worker.ts:560 — Handle “too large” before the O_CREAT fallback. Python append mode sets O_CREAT, so opening a bridge-oversized existing file swallows the read failure, treats it as empty, and can overwrite the file with only newly appended data.

Adversarial security: 🟢 low

No actionable security issues found in the complete diff.

Adversarial security (second opinion): 🟡 medium

No security regression, backdoor, or sandbox weakening in the diff — the errno values and new mappings are correct and fix a genuine descriptor-leak DoS; one pre-existing data-loss path in the same modified open() handler (too-large read treated as empty content in create+write mode, truncating the file on close) is left unaddressed and should be fixed or consciously accepted.

  • packages/just-bash/src/commands/python3/worker.ts:560 — The errno corrections (EFBIG 22, ENODATA 116, EROFS/EFBIG mapping in tryFSOperation) are correct for Emscripten's alphabetical errno numbering and remove a real fd-exhaustion DoS, but the fix is incomplete in the handler it touches: the sibling `isCreate && isWrite` branch still swallows every readFile failure — including the `Result too large` / `EFBIG: file too large` case this PR is about — and substitutes an empty buffer. Opening an existing over-limit file in append mode (`open(p,'a')` → O_WRONLY|O_CREAT|O_APPEND) therefore yields content=[] and position 0; the first write sets hostModified, and close() writes the truncated buffer back through backend.writeFile, silently destroying the original file's contents instead of raising EFBIG. Same repro as the PR description (256-byte file under maxStringLength: 128), just with 'a' instead of 'r'.

Standard Bash and host portability: 🟡 medium

The errno fix misses O_CREAT append mode and can destructively overwrite oversized existing files.

  • packages/just-bash/src/commands/python3/worker.ts:560 — Oversize errors remain swallowed for append opens: Python's open(existing, 'a') sets O_CREAT, so the create branch initializes empty content before the new EFBIG mapping runs. Writing then replaces the oversized file, causing data loss instead of EFBIG.

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

An append opens with O_CREAT, so a read the bridge refused as too large fell into the create branch, was treated as an empty file, and close() wrote the appended bytes back over the whole file. The size failure is now recognized before the fallback and raises EFBIG, leaving the file as it was.
@mutewinter

Copy link
Copy Markdown
Contributor Author

Good catch on the append path, and it was real: with the first commit, open('/tmp/big.log', 'a') on a 9 MB file wrote tail back over the whole file with no error, because the bridge's Result too large fell into the isCreate && isWrite branch as an empty file. The size failure is now classified before the create fallback (72b6d8bfHEAD), and python3.files.test.ts gained a test that appends to a 9 MB file and asserts [Errno 22] File too large with the file left intact; it fails against the previous commit with an empty stderr. Note the earlier 256-byte case never reached this branch, since a read under the bridge buffer succeeds and is caught by the maxFileSize check after it; only a file the bridge itself refuses did.

mutewinter added a commit to instrument-org/instrument that referenced this pull request Sep 11, 2026
…ews asked for

An append opens with O_CREAT, so a read the bridge refused as too large fell into the create fallback and close() wrote the appended bytes back over the whole file; the size failure is classified first now and raises EFBIG (vercel-labs/just-bash#424 review). The program runs in a types.ModuleType('__main__') registered in sys.modules rather than a bare dict, so pickle, unittest.main() and doctest find what it defines; a compile-time SyntaxError prints from the exception alone the way CPython prints it rather than naming the wrapper; and every non-None, non-integer sys.exit value prints and exits 1 (#425 review). Guarded by two more cases in create-bash-env-python.test.ts.

This branch has not been deployed

No deployments
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