From 006520c384da4da839a2767ac57a0c55507d5194 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:47:12 +0000 Subject: [PATCH 1/8] console: guard throwing Event property getters in Bun.inspect The native Event formatter in ConsoleObject reads .type through the user-visible accessor before deciding whether to render a MessageEvent / ErrorEvent specially or fall through to the generic object printer. If an Event subclass overrides type with a getter that throws, the exception propagates out of console.log and Bun.inspect. The generic object printer (forEachProperty in bindings.cpp) already swallows getter exceptions for every other Event property, so only the four reads done ahead of that path were unguarded: type, and the message/data/error reads on the MessageEvent/ErrorEvent arm. Catch and clear those four, matching the existing print_to_json pattern. --- src/jsc/ConsoleObject.rs | 42 ++++++++++++++++++++++++-------- test/js/bun/util/inspect.test.js | 30 +++++++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index e9f3b7261673..7bb1f3d9b93e 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -4940,8 +4940,13 @@ pub mod formatter { } let event_type_value: JSValue = 'brk: { - let Some(value_) = value.get(self.global_this, "type")? else { - break 'brk JSValue::UNDEFINED; + let value_ = match value.get(self.global_this, "type") { + Ok(Some(v)) => v, + Ok(None) => break 'brk JSValue::UNDEFINED, + Err(_) => { + self.global_this.clear_exception(); + break 'brk JSValue::UNDEFINED; + } }; if value_.is_string() { break 'brk value_; @@ -5015,9 +5020,15 @@ pub mod formatter { ); } - if let Some(message_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Message)? + let message_value = match value.fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(message_value) = message_value { if message_value.is_string() { if !self.single_line { self.write_indent(writer_).expect("unreachable"); @@ -5054,9 +5065,14 @@ pub mod formatter { pf!(""), pf!("") ); - let data: JSValue = value - .fast_get(self.global_this, jsc::BuiltinName::Data)? - .unwrap_or(JSValue::UNDEFINED); + let data: JSValue = + match value.fast_get(self.global_this, jsc::BuiltinName::Data) { + Ok(v) => v.unwrap_or(JSValue::UNDEFINED), + Err(_) => { + self.global_this.clear_exception(); + JSValue::UNDEFINED + } + }; let tag = Tag::get_advanced(data, self.global_this, self.tag_opts())?; self.format::(tag, writer_, data, self.global_this)?; if self.failed { @@ -5068,9 +5084,15 @@ pub mod formatter { } } EventType::ErrorEvent => { - if let Some(error_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Error)? - { + let error_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Error) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(error_value) = error_value { if !self.single_line { self.write_indent(writer_).expect("unreachable"); } diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index e76766e7f42f..584871124042 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -205,6 +205,36 @@ it("MessageEvent with deleted data", () => { ); }); +it("Event subclass with a throwing getter does not make Bun.inspect throw", () => { + class ThrowType extends Event { + get type() { + throw new Error("type-getter-boom"); + } + } + expect(() => Bun.inspect(new ThrowType("t"))).not.toThrow(); + expect(() => Bun.inspect({ payload: [new ThrowType("t")] })).not.toThrow(); + expect(Bun.inspect(new ThrowType("t"))).toContain("type"); + + class ThrowData extends Event { + get data() { + throw new Error("data-getter-boom"); + } + } + expect(() => Bun.inspect(new ThrowData("message"))).not.toThrow(); + expect(Bun.inspect(new ThrowData("message"))).toContain(`type: "message"`); + + class ThrowError extends Event { + get error() { + throw new Error("error-getter-boom"); + } + get message() { + throw new Error("message-getter-boom"); + } + } + expect(() => Bun.inspect(new ThrowError("error"))).not.toThrow(); + expect(Bun.inspect(new ThrowError("error"))).toContain(`type: "error"`); +}); + // https://github.com/oven-sh/bun/issues/561 it("TypedArray prints", () => { for (let TypedArray of [ From b71120b86d368942b863ceba52d967f2c7330a84 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:50:25 +0000 Subject: [PATCH 2/8] [autofix.ci] apply automated fixes --- src/jsc/ConsoleObject.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 7bb1f3d9b93e..2717a214f0ce 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -5020,14 +5020,14 @@ pub mod formatter { ); } - let message_value = match value.fast_get(self.global_this, jsc::BuiltinName::Message) - { - Ok(v) => v, - Err(_) => { - self.global_this.clear_exception(); - None - } - }; + let message_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; if let Some(message_value) = message_value { if message_value.is_string() { if !self.single_line { From fb1009bc6966440df0db8aea12ad14f45be602c2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:55:15 +0000 Subject: [PATCH 3/8] test: assert fallback rendering for guarded Event getters --- test/js/bun/util/inspect.test.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 584871124042..298a55b713cd 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -211,17 +211,19 @@ it("Event subclass with a throwing getter does not make Bun.inspect throw", () = throw new Error("type-getter-boom"); } } - expect(() => Bun.inspect(new ThrowType("t"))).not.toThrow(); + const typeOut = Bun.inspect(new ThrowType("t")); + expect(typeOut).toContain("type: [Getter]"); + expect(typeOut).not.toContain("type-getter-boom"); expect(() => Bun.inspect({ payload: [new ThrowType("t")] })).not.toThrow(); - expect(Bun.inspect(new ThrowType("t"))).toContain("type"); class ThrowData extends Event { get data() { throw new Error("data-getter-boom"); } } - expect(() => Bun.inspect(new ThrowData("message"))).not.toThrow(); - expect(Bun.inspect(new ThrowData("message"))).toContain(`type: "message"`); + const dataOut = Bun.inspect(new ThrowData("message")); + expect(dataOut).toContain(`type: "message"`); + expect(dataOut).toContain("data: undefined"); class ThrowError extends Event { get error() { @@ -231,8 +233,10 @@ it("Event subclass with a throwing getter does not make Bun.inspect throw", () = throw new Error("message-getter-boom"); } } - expect(() => Bun.inspect(new ThrowError("error"))).not.toThrow(); - expect(Bun.inspect(new ThrowError("error"))).toContain(`type: "error"`); + const errorOut = Bun.inspect(new ThrowError("error")); + expect(errorOut).toContain(`type: "error"`); + expect(errorOut).not.toContain("message:"); + expect(errorOut).not.toContain("error:"); }); // https://github.com/oven-sh/bun/issues/561 From 1e88697f968733f3a06631608d6f7c6840358c09 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:14:47 +0000 Subject: [PATCH 4/8] test_runner: guard the same Event getter reads in JestPrettyFormat The snapshot/diff pretty-format in src/runtime/test_runner/pretty_format.rs has a near-identical Tag::Event arm with the same four unguarded reads. Apply the same clear-on-Err treatment so toMatchSnapshot on an Event subclass with a throwing type/data/error/message getter no longer fails with 'Failed to pretty format value'. --- src/runtime/test_runner/pretty_format.rs | 44 ++++++++++++++++++------ test/js/bun/util/inspect.test.js | 36 ++++++++++++++++++- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index b4a68cca5c07..f88a8a8e5566 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1898,9 +1898,13 @@ impl<'a> Formatter<'a> { } Tag::Event => { let event_type_value: JSValue = 'brk: { - let value_: JSValue = match value.get(self.global_this, "type")? { - Some(v) => v, - None => break 'brk JSValue::UNDEFINED, + let value_: JSValue = match value.get(self.global_this, "type") { + Ok(Some(v)) => v, + Ok(None) => break 'brk JSValue::UNDEFINED, + Err(_) => { + self.global_this.clear_exception(); + break 'brk JSValue::UNDEFINED; + } }; if value_.is_string() { break 'brk value_; @@ -1947,9 +1951,16 @@ impl<'a> Formatter<'a> { pretty_fmt_const::(""), )); - if let Some(message_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Message)? + let message_value = match value + .fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(message_value) = message_value { if message_value.is_string() { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( @@ -1976,9 +1987,15 @@ impl<'a> Formatter<'a> { pretty_fmt_const::(""), pretty_fmt_const::(""), )); - let data: JSValue = value - .fast_get(self.global_this, jsc::BuiltinName::Data)? - .unwrap_or(JSValue::UNDEFINED); + let data: JSValue = match value + .fast_get(self.global_this, jsc::BuiltinName::Data) + { + Ok(v) => v.unwrap_or(JSValue::UNDEFINED), + Err(_) => { + self.global_this.clear_exception(); + JSValue::UNDEFINED + } + }; let tag = Tag::get(data, self.global_this)?; self.format::( @@ -1987,9 +2004,16 @@ impl<'a> Formatter<'a> { writer.write_all(b", \n"); } EventType::ErrorEvent => { - if let Some(data) = - value.fast_get(self.global_this, jsc::BuiltinName::Error)? + let error_value = match value + .fast_get(self.global_this, jsc::BuiltinName::Error) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(data) = error_value { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( "{}error{}:{} ", diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 298a55b713cd..d5e0ae554017 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, normalizeBunSnapshot, tmpdirSync } from "harness"; +import { bunEnv, bunExe, normalizeBunSnapshot, tempDir, tmpdirSync } from "harness"; import { join } from "path"; import util from "util"; it("prototype", () => { @@ -239,6 +239,40 @@ it("Event subclass with a throwing getter does not make Bun.inspect throw", () = expect(errorOut).not.toContain("error:"); }); +it("Event subclass with a throwing getter does not make toMatchSnapshot fail", async () => { + using dir = tempDir("inspect-event-snapshot", { + "snap.test.js": ` + import { test, expect } from "bun:test"; + test("type", () => { + class E extends Event { get type() { throw new Error("type-getter-boom"); } } + expect(new E("t")).toMatchSnapshot(); + }); + test("data", () => { + class M extends Event { get data() { throw new Error("data-getter-boom"); } } + expect(new M("message")).toMatchSnapshot(); + }); + test("error", () => { + class R extends Event { + get error() { throw new Error("error-getter-boom"); } + get message() { throw new Error("message-getter-boom"); } + } + expect(new R("error")).toMatchSnapshot(); + }); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "--update-snapshots", "snap.test.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const all = stdout + stderr; + expect(all).not.toContain("Failed to pretty format"); + expect(all).toContain("3 pass"); + expect(exitCode).toBe(0); +}); + // https://github.com/oven-sh/bun/issues/561 it("TypedArray prints", () => { for (let TypedArray of [ From b7e8fcd18b9d30c08f9b91227b4047b2172396b6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:35:24 +0000 Subject: [PATCH 5/8] console: guard AggregateError.errors and snapshot-formatter Event reads Extends the Event-getter guards to the remaining unguarded reads: - print_errorlike_object: getDirect on AggregateError.errors can return empty (deleted), a GetterSetter cell (defineProperty accessor), or a non-iterable object; for_each on any of those crashed or threw out of console.log/Bun.inspect. Gate on is_object() and clear the iteration exception. - test_runner/pretty_format.rs: apply the same type/message/data/error guards as ConsoleObject so toMatchSnapshot on a hostile Event subclass renders instead of failing with 'Failed to pretty format value'. Tests cover subclass getters, own-instance accessors, nested objects, and the three AggregateError.errors shapes. --- src/jsc/VirtualMachine.rs | 17 ++++++-- src/runtime/test_runner/pretty_format.rs | 45 +++++++++++++++------ test/js/bun/util/inspect.test.js | 51 +++++++++++++++++++----- 3 files changed, 88 insertions(+), 25 deletions(-) diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 191c40190c98..452c9d877b0a 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -4839,11 +4839,20 @@ impl VirtualMachine { allow_ansi_color, allow_side_effects, }; - // `getErrorsProperty` is - // `getDirect` (own data prop, nothrow); `for_each` may throw, in - // which case the error is swallowed. + // `getErrorsProperty` is `getDirect` (own slot, nothrow). User code + // can delete/replace `errors`, so it may be empty, an accessor + // (GetterSetter cell), or a non-iterable object; feeding any of + // those to `for_each` asserts or throws. Guard with `is_object()` + // (excludes empty/GetterSetter/primitives) and swallow iteration + // errors so the console formatter never propagates. let errors = value.get_errors_property(global_ref); - let _ = errors.for_each(global_ref, (&raw mut ctx).cast(), agg_iter); + if errors.is_object() + && errors + .for_each(global_ref, (&raw mut ctx).cast(), agg_iter) + .is_err() + { + global_ref.clear_exception(); + } return; } diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index b4a68cca5c07..9f0b9482479c 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1898,9 +1898,13 @@ impl<'a> Formatter<'a> { } Tag::Event => { let event_type_value: JSValue = 'brk: { - let value_: JSValue = match value.get(self.global_this, "type")? { - Some(v) => v, - None => break 'brk JSValue::UNDEFINED, + let value_: JSValue = match value.get(self.global_this, "type") { + Ok(Some(v)) => v, + Ok(None) => break 'brk JSValue::UNDEFINED, + Err(_) => { + self.global_this.clear_exception(); + break 'brk JSValue::UNDEFINED; + } }; if value_.is_string() { break 'brk value_; @@ -1947,9 +1951,15 @@ impl<'a> Formatter<'a> { pretty_fmt_const::(""), )); - if let Some(message_value) = - value.fast_get(self.global_this, jsc::BuiltinName::Message)? - { + let message_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Message) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(message_value) = message_value { if message_value.is_string() { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( @@ -1976,9 +1986,14 @@ impl<'a> Formatter<'a> { pretty_fmt_const::(""), pretty_fmt_const::(""), )); - let data: JSValue = value - .fast_get(self.global_this, jsc::BuiltinName::Data)? - .unwrap_or(JSValue::UNDEFINED); + let data: JSValue = + match value.fast_get(self.global_this, jsc::BuiltinName::Data) { + Ok(v) => v.unwrap_or(JSValue::UNDEFINED), + Err(_) => { + self.global_this.clear_exception(); + JSValue::UNDEFINED + } + }; let tag = Tag::get(data, self.global_this)?; self.format::( @@ -1987,9 +2002,15 @@ impl<'a> Formatter<'a> { writer.write_all(b", \n"); } EventType::ErrorEvent => { - if let Some(data) = - value.fast_get(self.global_this, jsc::BuiltinName::Error)? - { + let error_value = + match value.fast_get(self.global_this, jsc::BuiltinName::Error) { + Ok(v) => v, + Err(_) => { + self.global_this.clear_exception(); + None + } + }; + if let Some(data) = error_value { self.write_indent(writer.ctx).expect("unreachable"); writer.print(format_args!( "{}error{}:{} ", diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 298a55b713cd..88ea1e5efbdb 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -216,16 +216,16 @@ it("Event subclass with a throwing getter does not make Bun.inspect throw", () = expect(typeOut).not.toContain("type-getter-boom"); expect(() => Bun.inspect({ payload: [new ThrowType("t")] })).not.toThrow(); - class ThrowData extends Event { + class ThrowData extends MessageEvent { get data() { throw new Error("data-getter-boom"); } } - const dataOut = Bun.inspect(new ThrowData("message")); - expect(dataOut).toContain(`type: "message"`); - expect(dataOut).toContain("data: undefined"); + expect(Bun.inspect(new ThrowData("message", { data: "p" }))).toBe( + `MessageEvent {\n type: "message",\n data: undefined,\n}`, + ); - class ThrowError extends Event { + class ThrowError extends ErrorEvent { get error() { throw new Error("error-getter-boom"); } @@ -233,10 +233,43 @@ it("Event subclass with a throwing getter does not make Bun.inspect throw", () = throw new Error("message-getter-boom"); } } - const errorOut = Bun.inspect(new ThrowError("error")); - expect(errorOut).toContain(`type: "error"`); - expect(errorOut).not.toContain("message:"); - expect(errorOut).not.toContain("error:"); + expect(Bun.inspect(new ThrowError("error", { message: "m", error: new Error("i") }))).toBe( + `ErrorEvent {\n type: "error",\n}`, + ); + + // Own-instance accessors (not subclass) on the Event branch reads. + const me = new MessageEvent("message", { data: "p" }); + Object.defineProperty(me, "data", { + get() { + throw new Error("own-data-boom"); + }, + configurable: true, + }); + expect(Bun.inspect(me)).toBe(`MessageEvent {\n type: "message",\n data: undefined,\n}`); + expect(Bun.inspect({ nested: me })).toContain("data: undefined"); +}); + +it("AggregateError with a hostile 'errors' property does not make Bun.inspect throw", () => { + // Accessor own prop: getDirect returns the GetterSetter cell, for_each throws. + const a = new AggregateError([new Error("x")], "agg"); + Object.defineProperty(a, "errors", { + get() { + throw new Error("errors-getter-boom"); + }, + configurable: true, + }); + expect(() => Bun.inspect(a)).not.toThrow(); + expect(() => Bun.inspect({ nested: a })).not.toThrow(); + + // Non-iterable data prop: for_each throws TypeError. + const b = new AggregateError([new Error("x")], "agg"); + b.errors = { not: "iterable" }; + expect(() => Bun.inspect(b)).not.toThrow(); + + // Deleted own prop: getDirect returns empty; used to segfault in for_each. + const c = new AggregateError([new Error("x")], "agg"); + delete c.errors; + expect(() => Bun.inspect(c)).not.toThrow(); }); // https://github.com/oven-sh/bun/issues/561 From baf58ec327d9ece89f013a538d73fb758d82b36e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:39:57 +0000 Subject: [PATCH 6/8] trim VirtualMachine.rs comment per comment-cop --- src/jsc/VirtualMachine.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 452c9d877b0a..106890af437a 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -4839,12 +4839,8 @@ impl VirtualMachine { allow_ansi_color, allow_side_effects, }; - // `getErrorsProperty` is `getDirect` (own slot, nothrow). User code - // can delete/replace `errors`, so it may be empty, an accessor - // (GetterSetter cell), or a non-iterable object; feeding any of - // those to `for_each` asserts or throws. Guard with `is_object()` - // (excludes empty/GetterSetter/primitives) and swallow iteration - // errors so the console formatter never propagates. + // `getDirect` can hand back empty (deleted prop) or a GetterSetter + // cell (accessor); `is_object()` rejects both before `for_each`. let errors = value.get_errors_property(global_ref); if errors.is_object() && errors From e015b8a26a4159f4da3e9821cbc651b1638fecb4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:41:51 +0000 Subject: [PATCH 7/8] VirtualMachine: single-line comment for errors guard --- src/jsc/VirtualMachine.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 106890af437a..c5a8978e24d0 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -4839,8 +4839,7 @@ impl VirtualMachine { allow_ansi_color, allow_side_effects, }; - // `getDirect` can hand back empty (deleted prop) or a GetterSetter - // cell (accessor); `is_object()` rejects both before `for_each`. + // `getDirect` may hand back empty or a GetterSetter; `is_object()` rejects both. let errors = value.get_errors_property(global_ref); if errors.is_object() && errors From 939f3844e5ce0ededbdf15762fdd71e52468dd06 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:50:12 +0000 Subject: [PATCH 8/8] ci: retrigger