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
16 changes: 4 additions & 12 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,6 @@ impl<'a> TablePrinter<'a> {
f.single_line = true;
f.max_depth = 5;
f.can_throw_stack_overflow = true;
f.stack_check = StackCheck::init();
f
},
values_col_width: None,
Expand Down Expand Up @@ -1436,7 +1435,6 @@ pub fn format2(
fmt.max_depth = options.max_depth;
fmt.single_line = options.single_line;
fmt.indent = u32::from(options.default_indent);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
fmt.error_display_level = options.error_display_level;
let tag = formatter::Tag::get(vals[0], global)?;
Expand Down Expand Up @@ -1499,7 +1497,6 @@ pub fn format2(
fmt.max_depth = options.max_depth;
fmt.single_line = options.single_line;
fmt.indent = u32::from(options.default_indent);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
fmt.error_display_level = options.error_display_level;
let mut tag: formatter::TagResult;
Expand Down Expand Up @@ -1729,10 +1726,7 @@ pub mod formatter {
ordered_properties: false,
custom_formatted_object: CustomFormattedObject::default(),
disable_inspect_custom: false,
// `StackCheck::default()` has `cached_stack_end = 0` ⇒ the
// check always passes; callers that want a real bound
// overwrite with `StackCheck::init()` explicitly.
stack_check: StackCheck::default(),
stack_check: StackCheck::init(),
Comment thread
robobun marked this conversation as resolved.
can_throw_stack_overflow: false,
error_display_level: ErrorDisplayLevel::Full,
format_buffer_as_text: false,
Expand Down Expand Up @@ -3387,17 +3381,16 @@ pub mod formatter {
if self.global_this.has_exception() {
return Err(jsc::JsError::Thrown);
}
if !can_circ {
return Ok(true);
}

if !self.stack_check.is_safe_to_recurse() {
self.failed = true;
if self.can_throw_stack_overflow {
return Err(self.global_this.throw_stack_overflow());
}
return Ok(false);
}
if !can_circ {
return Ok(true);
}

if self.map_node.is_none() {
let mut node = core::ptr::NonNull::new(visited::Pool::get_node())
Expand Down Expand Up @@ -6015,7 +6008,6 @@ pub extern "C" fn Bun__ConsoleObject__timeLog(
fmt.max_depth = bun_options_types::context::try_get()
.and_then(|ctx| ctx.runtime_options.console_depth)
.unwrap_or(DEFAULT_CONSOLE_LOG_DEPTH);
fmt.stack_check = StackCheck::init();
fmt.can_throw_stack_overflow = true;
let console = vm_console(global);
// SAFETY: see [`vm_console`] — points at the live boxed `ConsoleObject` for
Expand Down
1 change: 0 additions & 1 deletion src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2678,7 +2678,6 @@ impl JSValue {
formatter: &'a mut crate::console_object::Formatter<'b>,
) -> crate::console_object::formatter::ZigFormatter<'a, 'b> {
formatter.remaining_values = bun_ptr::RawSlice::EMPTY;
formatter.stack_check.update();
crate::console_object::formatter::ZigFormatter::new(formatter, self)
}

Expand Down
3 changes: 1 addition & 2 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5600,8 +5600,7 @@ impl VirtualMachine {
// re-check here with an extra `MAX_PATH_BYTES * 3` of slack on Windows
// to cover the transpiler's nested path buffers — same parity-level
// protection the Object path gets from C++ `forEachProperty`'s
// `vm.isSafeToRecurse()`. The formatter's `stack_check` was seated by
// the caller (`format2` / `Bun.inspect`).
// `vm.isSafeToRecurse()`.
let extra_headroom: usize = if cfg!(windows) {
// 3× PathBuffer ≈ 288 KB — empirically enough for the
// `remap_zig_exception` → `transpile_source_code` chain on the
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::test_runner::expect::JSValueTestExt;
use core::ffi::c_void;

use bun_collections::HashMap;
use bun_core::{fmt as bun_fmt, Output};
use bun_core::{fmt as bun_fmt, Output, StackCheck};
use bun_jsc::{
self as jsc, ComptimeStringMapExt as _, JSGlobalObject, JSObject,
JSPropertyIterator, JSType, JSValue, JsError, JsResult, VM,
Expand Down Expand Up @@ -342,6 +342,7 @@ pub struct Formatter<'a> {
pub failed: bool,
pub estimated_line_length: usize,
pub always_newline_scope: bool,
pub stack_check: StackCheck,
}

impl<'a> Formatter<'a> {
Expand All @@ -357,6 +358,7 @@ impl<'a> Formatter<'a> {
failed: false,
estimated_line_length: 0,
always_newline_scope: false,
stack_check: StackCheck::init(),
}
}

Expand Down Expand Up @@ -1156,6 +1158,10 @@ impl<'a> Formatter<'a> {
if self.failed {
return Ok(());
}
if !self.stack_check.is_safe_to_recurse() {
self.failed = true;
return Ok(());
}
// reshaped for borrowck — `WrappedWriter` borrows both writer_
// and &mut self.estimated_line_length; we use a local wrapper and sync
// `failed` at scope exit. estimated_line_length is unused by WrappedWriter
Expand Down
23 changes: 23 additions & 0 deletions test/js/bun/test/pretty-format-overflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,27 @@ test("deep nesting", () => {
// Verify it actually formatted and showed the diff (not just crashed)
expect(stderr).toContain("expect(received).toEqual(expected)");
}, 30000);

test.concurrent("deeply nested array via toEqual", async () => {
const dir = tempDirWithFiles("pretty-format-stack", {
"deep.test.ts": `
import { test, expect } from "bun:test";
test("deep", () => {
let a: unknown = [];
for (let i = 0; i < 50000; i++) a = [a];
expect(a).toEqual([]);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "deep.test.ts"],
env: bunEnv,
cwd: dir,
stdout: "ignore",
stderr: "ignore",
});
await proc.exited;
expect(proc.signalCode).toBeNull();
expect(proc.exitCode).toBe(1);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
25 changes: 24 additions & 1 deletion test/js/bun/util/reportError.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { spawnSync } from "bun";
import { expect, test } from "bun:test";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { join } from "path";

Expand Down Expand Up @@ -122,3 +122,26 @@ test("native error printer handles lone surrogates in message and stack frame na
expect(proc.signalCode).toBeNull();
expect(exitCode).toBe(1);
});

describe.each([
["array", "[a]", "throw a;"],
["array", "[a]", "Promise.reject(a);"],
["Proxy", "new Proxy(a,{})", "throw a;"],
])("%s / %s / %s", (_, wrap, stmt) => {
test.concurrent("native error printer survives a deeply nested thrown value", async () => {
const src = `
let a = {};
for (let i = 0; i < 50000; i++) a = ${wrap};
${stmt}
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdout: "ignore",
stderr: "ignore",
});
await proc.exited;
expect(proc.signalCode).toBeNull();
Comment thread
robobun marked this conversation as resolved.
expect(proc.exitCode).toBe(1);
});
});
Loading