Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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, isASAN, isDebug } 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 @@ it("btoa", () => {
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 = isDebug || isASAN ? 90_000 : undefined;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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,
);
});
Loading