Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/runtime/webcore/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,13 @@ fn encode_base64_to_bun_string(input: &[u8], url_safe: bool) -> BunString {
bun_base64::encode_len(input)
};

// An over-MaxLength output can never become a string; fail here instead of
// allocating and encoding `to_len` bytes first (the create_* constructors
// re-check, and callers surface Dead as ERR_STRING_TOO_LONG).
Comment thread
robobun marked this conversation as resolved.
Outdated
if to_len > BunString::max_length() {
return BunString::dead();
}

if to_len < EXTERNAL_MIN_LEN {
let (str, chars) = BunString::create_uninitialized_latin1(to_len);
if str.is_dead() {
Expand Down
45 changes: 45 additions & 0 deletions test/js/node/string_decoder/string-decoder.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, isASAN, isDebug, withoutAggressiveGC } from "harness";
import os from "node:os";

const RealStringDecoder = require("string_decoder").StringDecoder;

Expand Down Expand Up @@ -421,3 +422,47 @@ it(
// Allocating a 2 GiB buffer under debug/ASAN is slow even when lazily committed.
isDebug || isASAN ? 60_000 : undefined,
);

// Output lengths above WTF::StringImpl::MaxLength (2^31 - 1) used to trip a
// RELEASE_ASSERT and abort the process instead of throwing. Runs in a
// subprocess because of the multi-GiB peak; skips on small machines (same
// gate as blob-oom.test.ts).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
describe.skipIf(os.totalmem() < 10 * 1024 ** 3)("write() at the 2 GiB string limit", () => {
it(
"throws ERR_STRING_TOO_LONG for base64 and hex instead of aborting",
async () => {
const src = `
const { StringDecoder } = require("string_decoder");
const report = e => ({ name: e.name, code: e.code, message: e.message });
const results = [];
// 1610612736 = 3 * 2^29: base64 output is (len / 3) * 4 = 2147483648 = 2^31,
// hex output is len * 2 = 3221225472; both exceed 2^31 - 1.
const buf = Buffer.alloc(1610612736);
for (const encoding of ["base64", "hex"]) {
try {
results.push({ unexpectedLength: new StringDecoder(encoding).write(buf).length });
} catch (e) {
results.push(report(e));
}
}
console.log(JSON.stringify(results));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const tooLong = {
name: "Error",
code: "ERR_STRING_TOO_LONG",
message: "Cannot create a string longer than 2147483647 characters",
};
expect(JSON.parse(stdout.trim() || JSON.stringify({ stdout, stderr, exitCode }))).toEqual([tooLong, tooLong]);
expect(exitCode).toBe(0);
},
// Allocating multi-GiB buffers under debug/ASAN is slow.
isDebug || isASAN ? 60_000 : undefined,
);
});
69 changes: 68 additions & 1 deletion test/js/web/util/atob.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { expect, it } from "bun:test";
import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe } from "harness";
import os from "node:os";

function expectInvalidCharacters(val) {
expect(() => atob(val)).toThrow("The string contains invalid characters.");
Expand Down Expand Up @@ -67,3 +69,68 @@
expect(btoa("\u0080\u0081")).toBe("gIE=");
expect(btoa(Bun)).toBe(btoa("[object Bun]"));
});

// btoa output lengths above WTF::StringImpl::MaxLength (2^31 - 1) used to trip
// a RELEASE_ASSERT and abort the process instead of throwing. These need real
// multi-GiB peaks, so each case runs in a subprocess and the block skips on
// small machines (same gate as blob-oom.test.ts).
describe.skipIf(os.totalmem() < 10 * 1024 ** 3)("btoa at the 2 GiB string limit", () => {
// Building and encoding multi-GiB strings is slow under debug/ASAN.
const timeout = 90_000;

Check warning on line 79 in test/js/web/util/atob.test.js

View check run for this annotation

Claude / Claude Code Review

Unconditional 90s timeout diverges from sibling test and cited pattern

nit: The comment says "slow under debug/ASAN" but `timeout = 90_000` is applied unconditionally, while the sibling change in `string-decoder.test.js` in this same PR gates it as `isDebug || isASAN ? 60_000 : undefined` and the cited `blob-oom.test.ts` pattern sets no timeout at all. Consider importing `isDebug`/`isASAN` from harness and gating this the same way (a higher value than 60s is fine given the second test actually completes a full 2 GiB encode).
Comment thread
robobun marked this conversation as resolved.
Outdated

it(
"throws ERR_STRING_TOO_LONG when the output would exceed 2^31 - 1 characters",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
// base64 output = ceil(1610612734 / 3) * 4 = 2147483648 = 2^31
const input = Buffer.alloc(1610612734, 0x61).toString();
try {
console.log(JSON.stringify({ unexpectedLength: btoa(input).length }));
} catch (e) {
console.log(JSON.stringify({ name: e.name, code: e.code, message: e.message }));
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(JSON.parse(stdout.trim() || JSON.stringify({ stdout, stderr, exitCode }))).toEqual({
name: "Error",
code: "ERR_STRING_TOO_LONG",
message: "Cannot create a string longer than 2147483647 characters",
});
expect(exitCode).toBe(0);
},
timeout,
);

it(
"still encodes the largest input whose output fits",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
// base64 output = (1610612733 / 3) * 4 = 2147483644 <= 2^31 - 1
const input = Buffer.alloc(1610612733, 0x61).toString();
console.log(JSON.stringify({ length: btoa(input).length }));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(JSON.parse(stdout.trim() || JSON.stringify({ stdout, stderr, exitCode }))).toEqual({ length: 2147483644 });
expect(exitCode).toBe(0);
},
timeout,
);
});