Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/runtime/webcore/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,11 @@ fn encode_base64_to_bun_string(input: &[u8], url_safe: bool) -> BunString {
bun_base64::encode_len(input)
};

// Checked here so an over-MaxLength output fails before the allocate+encode, not after.
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,
);
});