Skip to content

Throw ERR_STRING_TOO_LONG instead of aborting for 2 GiB to 4 GiB strings - #37215

Merged
Jarred-Sumner merged 6 commits into
mainfrom
farm/2fc05a7d/fix-2gib-string-abort
Aug 8, 2026
Merged

Jarred-Sumner merged 6 commits into
mainfrom
farm/2fc05a7d/fix-2gib-string-abort

Conversation

@robobun

@robobun robobun commented Aug 8, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

await new Blob([new Uint8Array(2 ** 31)]).text() aborts the process. Same for Bun.file(path).text() and fs.readFileSync(path, "utf8") when the file is between 2 GiB and 4 GiB - 1 bytes (a sparse truncate -s 2G big.txt reproduces it). Release builds die with a silent SIGABRT; debug builds fail with:

ASSERTION FAILED: data.size() <= MaxLength
WTF/wtf/text/StringImpl.h(891) StringImplShape(uint32_t, std::span<const Latin1Character>, unsigned)

Sizes of 4 GiB and above already threw catchable errors, and 2^31 - 1 worked, so only the [2^31, 2^32) range crashed.

Cause

The Rust-side guards in front of WTF string construction (bun_core::String::max_length()) only consult Bun__stringSyntheticAllocationLimit, which defaults to 2^32 - 1. The hard cap enforced by RELEASE_ASSERT in the StringImplShape constructors is WTF::StringImpl::MaxLength = 2^31 - 1. The C++ conversions in helpers.h already clamp (len > Bun__stringSyntheticAllocationLimit || len > WTF::String::MaxLength); the Rust side missed the second half, so external-string creation for lengths in [2^31, 2^32) passed the guard and tripped the assert.

Fix

  • String::max_length() clamps the synthetic limit to WTF::StringImpl::MaxLength (2^31 - 1).
  • The create_external* guards compare with > instead of >=, so 2^31 - 1, the largest valid WTF length, still works.
  • ZigString__toJSONObject also checks WTF::String::MaxLength, so Blob.json() at these sizes reports ERR_STRING_TOO_LONG instead of falling through to JSONParse on a null string.
  • BunString__createUTF8ForJS rejects oversized all-ASCII input with ERR_STRING_TOO_LONG instead of asserting.
  • Error messages report the real limit (2147483647, matching the C++ ErrorCode.cpp text) instead of "2^32-1".

Blob.text() / Bun.file().text() now throw ERR_STRING_TOO_LONG; fs.readFileSync(path, "utf8") reports ENOMEM like the existing >= 4 GiB and /dev/zero paths (the fs layer speaks errno). Buffer.prototype.toString already threw ERR_STRING_TOO_LONG for these sizes via its own WTF::String::MaxLength check in JSBuffer.cpp.

Verification

  • test/js/web/fetch/blob-oom.test.ts: subprocess tests for Blob.text(), Blob.json() and Bun.file().text() at 2^31 bytes (abort before, catchable error after), plus the existing synthetic-limit assertions updated to the corrected message.
  • test/js/node/fs/fs-oom.test.ts: readFileSync(file, "utf8") at 2^31 bytes throws ENOMEM; at 2^31 - 1 bytes still decodes to a 2147483647-length string. The fixture files are sparse so only the in-memory read costs 2 GiB, and each case runs in a subprocess.
  • Manually verified 4 GiB inputs still produce the same catchable errors as before, and 2^31 - 1 still succeeds on all three faces.

no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/fs/fs-oom.test.ts

Blob.text(), Bun.file().text(), fs.readFileSync(path, "utf8") and
Blob.json() on 2^31..2^32-1 bytes aborted the process: the Rust-side
guards in front of WTF string construction only checked
Bun__stringSyntheticAllocationLimit (2^32-1 by default) and missed
WTF::StringImpl::MaxLength (2^31-1), which StringImplShape enforces
with a RELEASE_ASSERT. Lengths >= 2^32 were already caught.

- bun_core::String::max_length() now clamps the synthetic limit to
  WTF::StringImpl::MaxLength, matching the C++ helpers.h checks
- the create_external* guards use > instead of >=, so 2^31-1 (the
  largest valid WTF length) keeps working
- ZigString__toJSONObject checks MaxLength too instead of falling
  through to JSONParse on a null string
- BunString__createUTF8ForJS rejects oversized ASCII input instead of
  asserting
- error messages report the real limit (2147483647, same as the C++
  message) instead of 2^32-1
@coderabbitai

coderabbitai Bot commented Aug 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c6cab61c-2d03-421c-b4cc-d0ee25b3c283

📥 Commits

Reviewing files that changed from the base of the PR and between fb170de and ad378c8.

📒 Files selected for processing (2)
  • test/js/node/fs/fs-oom.test.ts
  • test/js/web/fetch/blob-oom.test.ts

Walkthrough

The change aligns string limits with the signed 32-bit WTF maximum, updates oversized-string guards and error messages, and adds subprocess tests for 2 GiB filesystem and web string conversions.

Changes

String length limits

Layer / File(s) Summary
Core string limit contract
src/bun_core/string/mod.rs
Adds WTF_STRING_MAX_LENGTH, clamps String::max_length(), and allows external strings whose length equals the maximum.
Conversion and binding guards
src/jsc/ZigString.rs, src/jsc/bindings/BunString.cpp, src/jsc/bindings/bindings.cpp, src/jsc/lib.rs
Adds oversized ASCII validation, applies the WTF maximum to JSON validation, and updates error messages to 2147483647 characters.
Boundary regression tests
test/js/node/fs/fs-oom.test.ts, test/js/web/fetch/blob-oom.test.ts
Adds memory-gated subprocess tests for 2 GiB inputs and updates Blob, Response, and Request expectations.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: catchable ERR_STRING_TOO_LONG errors for strings from 2 GiB through 4 GiB minus 1.
Description check ✅ Passed The description explains the problem, cause, fix, affected APIs, and verification with relevant tests.
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.

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

@github-actions github-actions Bot added the claude label Aug 8, 2026
Comment thread src/bun_core/string/mod.rs Outdated
Comment thread src/bun_core/string/mod.rs Outdated
Comment thread src/bun_core/string/mod.rs
Comment thread src/bun_core/string/mod.rs
Comment thread test/js/node/fs/fs-oom.test.ts Outdated
Comment thread src/bun_core/string/mod.rs

@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: 1

🤖 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/node/fs/fs-oom.test.ts`:
- Around line 89-93: Remove the explicit 180_000 per-test timeout arguments from
the boundary tests in test/js/node/fs/fs-oom.test.ts lines 89-93 and 95-99, and
test/js/web/fetch/blob-oom.test.ts lines 159-191 and 193-223. Leave each test’s
assertions and behavior unchanged so the repository-wide timeout policy applies.
🪄 Autofix

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: 28c3c175-3b74-4327-97a6-242a4d3e46ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9d519e8 and fb170de.

📒 Files selected for processing (7)
  • src/bun_core/string/mod.rs
  • src/jsc/ZigString.rs
  • src/jsc/bindings/BunString.cpp
  • src/jsc/bindings/bindings.cpp
  • src/jsc/lib.rs
  • test/js/node/fs/fs-oom.test.ts
  • test/js/web/fetch/blob-oom.test.ts

Comment thread test/js/node/fs/fs-oom.test.ts Outdated

@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 — both earlier findings (memory gating on the 2 GiB tests, stale SAFETY bound) are addressed, and the per-test timeouts are gone.

What was reviewed:

  • max_length() clamp + >=→> guards: verified all Rust callers of max_length() use >, and the C++ helpers.h already uses the same min(limit, WTF::String::MaxLength) pattern this now mirrors.
  • Error-message text matches ErrorCode.cpp's makeString(..., WTF::String::MaxLength, ...) (2147483647).
  • ZigString__toJSONObject / BunString__createUTF8ForJS guards follow the existing helpers.h shape; checked the null-string fallthrough in the JSON path.
  • Tests: subprocess isolation, os.totalmem() gate matches buffer.test.js precedent, both boundary sides (2^31 throws, 2^31-1 succeeds) covered.
Extended reasoning...

Overview

This PR fixes a process abort when converting byte sources in the [2^31, 2^32) range to JS strings. Seven files touched: src/bun_core/string/mod.rs (clamp max_length() to i32::MAX, adjust three create_external* guards from >= to >, add WTF_STRING_MAX_LENGTH constant), src/jsc/bindings/BunString.cpp (guard the all-ASCII fast path in BunString__createUTF8ForJS), src/jsc/bindings/bindings.cpp (add || WTF::String::MaxLength to the JSON null-string check), three error-message string updates in ZigString.rs/lib.rs, and two test files with new subprocess-isolated 2 GiB boundary tests plus updated message assertions.

Security risks

None. The change converts an unrecoverable RELEASE_ASSERT abort (a DoS on user-controllable input size) into a catchable ERR_STRING_TOO_LONG, which is strictly safer. No new untrusted input reaches native code; the guards are tightened, not loosened. The >=→> change admits exactly len == 2^31-1, which StringImplShape's data.size() <= MaxLength assert accepts.

Level of scrutiny

Medium-high — this is core string infrastructure at the Rust/C++ FFI boundary, and the guards front unsafe external-string construction. However, the change is a strict tightening: any input that previously reached WTF now still does (the only newly-admitted length is exactly i32::MAX, which WTF accepts), and inputs in [2^31, 2^32) that previously crashed now return String::DEAD / throw. I verified against helpers.h that the C++ side already uses the identical min(syntheticLimit, MaxLength) pattern this PR brings the Rust side into agreement with, and grepped all max_length() callers to confirm they consistently use >.

Other factors

I left two prior rounds of feedback on this PR (memory-gating the 2 GiB tests; a stale SAFETY comment bound), both addressed in 6f49733 and fb170de. CodeRabbit's per-test-timeout feedback was addressed in ad378c8. The comment-cop doc-comment length warnings were trimmed in edd9c46. Tests cover both sides of the boundary (2^31 throws, 2^31-1 succeeds), run in subprocesses to isolate the 2 GiB peak, and gate on os.totalmem() < 10 GiB matching the buffer.test.js 4 GiB precedent. No outstanding reviewer comments.

@robobun

robobun commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for build 90684: this PR's own tests pass on every lane that ran them, including the new 2 GiB cases in blob-oom.test.ts and fs-oom.test.ts on the linux, asan, macOS, and Windows lanes that completed. The two failing jobs are unrelated to the change:

  • darwin 14 aarch64: the tart runner failed during environment setup (SSH into the guest VM was refused) before any test ran. The same infra failure hit the darwin 26 lane in build 90682 on a different host, so it is a runner-fleet issue, reported separately.
  • debian 13 x64-asan: test/js/node/worker_threads/worker-transfer-terminate-stress.test.ts, an unchecked-exception assertion in the module loader that also fails on main (flagged for triage separately).

Every other annotation entry is a known-flaky test that passed on retry or when run alone. No open review comments; the diff is ready for review.

@Jarred-Sumner
Jarred-Sumner merged commit ab5b3f2 into main Aug 8, 2026
52 of 54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/2fc05a7d/fix-2gib-string-abort branch August 8, 2026 22:04
springmin pushed a commit to springmin/bun that referenced this pull request Aug 8, 2026
…ngs (oven-sh#37215)

### Problem

`await new Blob([new Uint8Array(2 ** 31)]).text()` aborts the process.
Same for `Bun.file(path).text()` and `fs.readFileSync(path, "utf8")`
when the file is between 2 GiB and 4 GiB - 1 bytes (a sparse `truncate
-s 2G big.txt` reproduces it). Release builds die with a silent SIGABRT;
debug builds fail with:

```
ASSERTION FAILED: data.size() <= MaxLength
WTF/wtf/text/StringImpl.h(891) StringImplShape(uint32_t, std::span<const Latin1Character>, unsigned)
```

Sizes of 4 GiB and above already threw catchable errors, and 2^31 - 1
worked, so only the [2^31, 2^32) range crashed.

### Cause

The Rust-side guards in front of WTF string construction
(`bun_core::String::max_length()`) only consult
`Bun__stringSyntheticAllocationLimit`, which defaults to 2^32 - 1. The
hard cap enforced by `RELEASE_ASSERT` in the `StringImplShape`
constructors is `WTF::StringImpl::MaxLength` = 2^31 - 1. The C++
conversions in `helpers.h` already clamp (`len >
Bun__stringSyntheticAllocationLimit || len > WTF::String::MaxLength`);
the Rust side missed the second half, so external-string creation for
lengths in [2^31, 2^32) passed the guard and tripped the assert.

### Fix

- `String::max_length()` clamps the synthetic limit to
`WTF::StringImpl::MaxLength` (2^31 - 1).
- The `create_external*` guards compare with `>` instead of `>=`, so
2^31 - 1, the largest valid WTF length, still works.
- `ZigString__toJSONObject` also checks `WTF::String::MaxLength`, so
`Blob.json()` at these sizes reports `ERR_STRING_TOO_LONG` instead of
falling through to `JSONParse` on a null string.
- `BunString__createUTF8ForJS` rejects oversized all-ASCII input with
`ERR_STRING_TOO_LONG` instead of asserting.
- Error messages report the real limit (2147483647, matching the C++
`ErrorCode.cpp` text) instead of "2^32-1".

`Blob.text()` / `Bun.file().text()` now throw `ERR_STRING_TOO_LONG`;
`fs.readFileSync(path, "utf8")` reports `ENOMEM` like the existing >= 4
GiB and `/dev/zero` paths (the fs layer speaks errno).
`Buffer.prototype.toString` already threw `ERR_STRING_TOO_LONG` for
these sizes via its own `WTF::String::MaxLength` check in
`JSBuffer.cpp`.

### Verification

- `test/js/web/fetch/blob-oom.test.ts`: subprocess tests for
`Blob.text()`, `Blob.json()` and `Bun.file().text()` at 2^31 bytes
(abort before, catchable error after), plus the existing synthetic-limit
assertions updated to the corrected message.
- `test/js/node/fs/fs-oom.test.ts`: `readFileSync(file, "utf8")` at 2^31
bytes throws `ENOMEM`; at 2^31 - 1 bytes still decodes to a
2147483647-length string. The fixture files are sparse so only the
in-memory read costs 2 GiB, and each case runs in a subprocess.
- Manually verified 4 GiB inputs still produce the same catchable errors
as before, and 2^31 - 1 still succeeds on all three faces.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/node/fs/fs-oom.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
robobun added a commit that referenced this pull request Aug 9, 2026
…string

Decoding 2^31 .. 2^32-1 bytes of ASCII with TextDecoder.decode() returned
the empty string with no error: Zig::toStringCopy maps a failed string
creation (length over WTF::StringImpl::MaxLength, or allocation failure)
to a null WTF::String, and jsString() turns that into the empty string.

Zig::toJSStringGC (and with it ZigString__toValueGC, i.e. ZigString.toJS)
now throws ERR_STRING_TOO_LONG when the input is over the length limit
and an out-of-memory error when allocation fails, instead of silently
returning "". The non-UTF-8 branches of the single-argument
Zig::toStringCopy now check the synthetic allocation limit like the
other helpers, and JSC__JSValue__fromEntries checks for the exception
before putDirect.

The sibling guards on the external-string paths (2 GiB to 4 GiB aborts)
are fixed separately in #37215.
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.

2 participants