diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 3b6eb16dc9a2..e05f3de3b5eb 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -259,6 +259,21 @@ pub(crate) fn with_text_format_source( _str_hold.slice() }; + // Every parser reached from here records source positions as an `i32` + // (`ast::Loc` via `usize2loc` for JSONC/TOML, JSON5's token locs, YAML's + // `Pos`), so an input those offsets cannot represent panics inside the + // lexer instead of reporting an error. Reject it before parsing. + if bytes.len() > i32::MAX as usize { + return Err(global.throw_range_error( + bytes.len() as i64, + bun_jsc::RangeErrorOptions { + field_name: b"input.byteLength", + max: i64::from(i32::MAX), + ..Default::default() + }, + )); + } + let mut log = bun_ast::Log::init(); let source = bun_ast::Source::init_path_string(path, bytes); diff --git a/test/js/bun/json5/json5.test.ts b/test/js/bun/json5/json5.test.ts index f1ea1ec31e8a..98a4f41b3ebd 100644 --- a/test/js/bun/json5/json5.test.ts +++ b/test/js/bun/json5/json5.test.ts @@ -1699,3 +1699,31 @@ describe("stringify memory", () => { expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); }); }); + +// The JSON5 lexer records every source position as an i32, so an input of +// 2**31 bytes or more used to abort the process with +// `panic: int cast: TryFromIntError(PosOverflow)` instead of throwing. It is +// rejected before parsing, so the Uint8Array below is virtual pages that are +// never read. The runtime accepts a TypedArray here (the binding takes a +// Blob, Buffer or string); the declared `string` type is narrower. +test("parse rejects an input of 2**31 bytes or more instead of panicking", () => { + let input: Uint8Array; + try { + input = new Uint8Array(2 ** 31 + 2); + } catch { + // The 2 GiB reservation itself can fail on a memory-pressured runner; + // there is nothing to test then. + return; + } + let err: any; + try { + JSON5.parse(input as unknown as string); + } catch (e) { + err = e; + } + expect(err?.constructor?.name).toBe("RangeError"); + expect(err?.code).toBe("ERR_OUT_OF_RANGE"); + expect(err?.message).toBe( + 'The value of "input.byteLength" is out of range. It must be <= 2147483647. Received 2147483650', + ); +}); diff --git a/test/js/bun/yaml/yaml.test.ts b/test/js/bun/yaml/yaml.test.ts index cbd9ae14c28a..00b41d14814d 100644 --- a/test/js/bun/yaml/yaml.test.ts +++ b/test/js/bun/yaml/yaml.test.ts @@ -4743,3 +4743,31 @@ describe("plain scalar whitespace handling", () => { expect(() => YAML.parse("g: -")).toThrow(SyntaxError); }); }); + +// The YAML scanner records every source position as an i32, so an input of +// 2**31 bytes or more used to abort the process with +// `panic: int cast: TryFromIntError(PosOverflow)` instead of throwing. It is +// rejected before parsing, so the Uint8Array below is virtual pages that are +// never read. The runtime accepts a TypedArray here (the binding takes a +// Blob, Buffer or string); the declared `string` type is narrower. +test("parse rejects an input of 2**31 bytes or more instead of panicking", () => { + let input: Uint8Array; + try { + input = new Uint8Array(2 ** 31 + 2); + } catch { + // The 2 GiB reservation itself can fail on a memory-pressured runner; + // there is nothing to test then. + return; + } + let err: any; + try { + YAML.parse(input as unknown as string); + } catch (e) { + err = e; + } + expect(err?.constructor?.name).toBe("RangeError"); + expect(err?.code).toBe("ERR_OUT_OF_RANGE"); + expect(err?.message).toBe( + 'The value of "input.byteLength" is out of range. It must be <= 2147483647. Received 2147483650', + ); +});