Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
f3f62a52b4 fix(fs): throw early for strings exceeding WebKit's 2GB limit
Fixes #2570

Previously, `readFileSync` with string encodings (utf8, ascii, etc.) only
checked against `synthetic_allocation_limit` (~4GB), but WebKit's
`WTF::String::MaxLength` is only ~2GB (`std::numeric_limits<int32_t>::max()`).

This mismatch caused large files (>2GB) to be silently truncated when
converted to strings, resulting in confusing "JSON Parse error: Unexpected EOF"
errors instead of a clear memory error.

The fix:
- Changes `string_allocation_limit` default from `maxInt(u32)` to `maxInt(i32)`
  to match WebKit's String::MaxLength
- Updates `shouldThrowOutOfMemoryEarlyForJavaScript` to use the appropriate
  limit based on encoding type (buffer encoding uses higher typed array limit,
  string encodings use the WebKit string limit)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 07:32:23 +00:00
3 changed files with 46 additions and 7 deletions

View File

@@ -10,7 +10,10 @@ pub export var isBunTest: bool = false;
// TODO: evaluate if this has any measurable performance impact.
pub var synthetic_allocation_limit: usize = std.math.maxInt(u32);
pub var string_allocation_limit: usize = std.math.maxInt(u32);
/// Max WTFStringImpl/JSString length, matches `WTF::String::MaxLength` / `JSString::MaxLength`.
/// This ensures readFileSync and other APIs that return strings will throw early
/// instead of allowing WebKit to silently truncate strings larger than ~2GB.
pub var string_allocation_limit: usize = std.math.maxInt(i32);
comptime {
_ = Bun__remapStackFramePositions;

View File

@@ -4870,9 +4870,10 @@ pub const NodeFS = struct {
}
fn shouldThrowOutOfMemoryEarlyForJavaScript(encoding: Encoding, size: usize, syscall: Syscall.Tag) ?Syscall.Error {
// Strings & typed arrays max out at 4.7 GB.
// But, it's **string length**
// So you can load an 8 GB hex string, for example, it should be fine.
// String length limits depend on the encoding:
// - For buffer encoding, we're limited by typed array size (~4.7 GB)
// - For string encodings, we're limited by WebKit's String::MaxLength (~2.15 GB)
// The adjusted_size accounts for encoding expansion/contraction.
const adjusted_size = switch (encoding) {
.utf16le, .ucs2, .utf8 => size / 4 -| 1,
.hex => size / 2 -| 1,
@@ -4880,9 +4881,15 @@ pub const NodeFS = struct {
.ascii, .latin1, .buffer => size,
};
if (
// Typed arrays in JavaScript are limited to 4.7 GB.
adjusted_size > jsc.VirtualMachine.synthetic_allocation_limit or
// Use the appropriate limit based on encoding type:
// - buffer returns a typed array (synthetic_allocation_limit ~4.7GB)
// - all other encodings return a string (string_allocation_limit ~2.15GB)
const allocation_limit = switch (encoding) {
.buffer => jsc.VirtualMachine.synthetic_allocation_limit,
else => jsc.VirtualMachine.string_allocation_limit,
};
if (adjusted_size > allocation_limit or
// If they do not have enough memory to open the file and they're on Linux, let's throw an error instead of dealing with the OOM killer.
(Environment.isLinux and size >= bun.getTotalMemorySize()))
{

View File

@@ -0,0 +1,29 @@
import { constants, kMaxLength, kStringMaxLength } from "buffer";
import { expect, test } from "bun:test";
// Issue #2570: JSON.parse silently truncates strings larger than ~2GB
// Root cause: Mismatch between Bun's string allocation limit and WebKit's String::MaxLength
//
// The fix ensures that readFileSync with string encodings (utf8, ascii, etc.)
// checks against WebKit's String::MaxLength (~2GB) instead of the higher
// typed array limit (~4GB). This prevents silent string truncation that
// caused confusing "JSON Parse error: Unexpected EOF" errors.
test("kStringMaxLength matches WebKit's String::MaxLength (2^31 - 1)", () => {
// WebKit's String::MaxLength is std::numeric_limits<int32_t>::max()
const maxInt32 = Math.pow(2, 31) - 1;
expect(kStringMaxLength).toBe(maxInt32);
expect(constants.MAX_STRING_LENGTH).toBe(maxInt32);
});
test("buffer encoding has higher limit than string encoding", () => {
// Buffer encoding uses synthetic_allocation_limit (~4.7GB)
// String encoding uses string_allocation_limit (~2.15GB)
// kMaxLength is for buffers/typed arrays
// kStringMaxLength is for strings
expect(kMaxLength).toBeGreaterThan(kStringMaxLength);
// kStringMaxLength should be 2^31 - 1 (maxInt32)
expect(kStringMaxLength).toBe(2147483647);
});