diff --git a/src/bun.js/bindings/UtilInspect.cpp b/src/bun.js/bindings/UtilInspect.cpp index ae09d5b11cd..6b3b3395634 100644 --- a/src/bun.js/bindings/UtilInspect.cpp +++ b/src/bun.js/bindings/UtilInspect.cpp @@ -57,8 +57,8 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__callCustomInspectFunction( auto callData = JSC::getCallData(functionToCall); MarkedArgumentBuffer arguments; arguments.append(jsNumber(depth)); - arguments.append(options); - arguments.append(inspectFn); + arguments.append(options ? JSValue(options) : jsUndefined()); + arguments.append(inspectFn ? JSValue(inspectFn) : jsUndefined()); auto inspectRet = JSC::profiledCall(globalObject, ProfilingReason::API, functionToCall, callData, thisValue, arguments); RETURN_IF_EXCEPTION(scope, {}); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index a0fc9605df3..820685b38dc 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1901,10 +1901,10 @@ void GlobalObject::finishCreation(VM& vm) [](const Initializer& init) { auto scope = DECLARE_THROW_SCOPE(init.vm); JSValue nodeUtilValue = jsCast(init.owner)->internalModuleRegistry()->requireId(init.owner, init.vm, Bun::InternalModuleRegistry::Field::NodeUtil); - RETURN_IF_EXCEPTION(scope, ); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); RELEASE_ASSERT(nodeUtilValue.isObject()); auto prop = nodeUtilValue.getObject()->getIfPropertyExists(init.owner, Identifier::fromString(init.vm, "inspect"_s)); - RETURN_IF_EXCEPTION(scope, ); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); ASSERT(prop); init.set(jsCast(prop)); }); @@ -1926,22 +1926,25 @@ void GlobalObject::finishCreation(VM& vm) m_utilInspectStylizeColorFunction.initLater( [](const Initializer& init) { auto scope = DECLARE_THROW_SCOPE(init.vm); + JSFunction* inspectFn = jsCast(init.owner)->utilInspectFunction(); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); + if (!inspectFn) [[unlikely]] + return init.property.setMayBeNull(init.vm, init.owner, nullptr); JSC::MarkedArgumentBuffer args; - args.append(jsCast(init.owner)->utilInspectFunction()); - RETURN_IF_EXCEPTION(scope, ); + args.append(inspectFn); JSC::JSFunction* getStylize = JSC::JSFunction::create(init.vm, init.owner, utilInspectGetStylizeWithColorCodeGenerator(init.vm), init.owner); - RETURN_IF_EXCEPTION(scope, ); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); JSC::CallData callData = JSC::getCallData(getStylize); NakedPtr returnedException = nullptr; auto result = JSC::profiledCall(init.owner, ProfilingReason::API, getStylize, callData, jsNull(), args, returnedException); - RETURN_IF_EXCEPTION(scope, ); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); if (returnedException) { throwException(init.owner, scope, returnedException.get()); } - RETURN_IF_EXCEPTION(scope, ); + RETURN_IF_EXCEPTION(scope, init.property.setMayBeNull(init.vm, init.owner, nullptr)); init.set(jsCast(result)); }); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index c957153cafc..8c19f850bd6 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6421,6 +6421,7 @@ extern "C" JSC::EncodedJSValue Bun__REPL__formatValue( // Get the util.inspect function from the global object auto* bunGlobal = jsCast(globalObject); JSC::JSValue inspectFn = bunGlobal->utilInspectFunction(); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); if (!inspectFn || !inspectFn.isCallable()) { // Fallback to toString if util.inspect is not available diff --git a/src/bun.js/bindings/webcore/JSBroadcastChannel.cpp b/src/bun.js/bindings/webcore/JSBroadcastChannel.cpp index a53e47a2936..acf9c1ae704 100644 --- a/src/bun.js/bindings/webcore/JSBroadcastChannel.cpp +++ b/src/bun.js/bindings/webcore/JSBroadcastChannel.cpp @@ -186,6 +186,11 @@ JSC_DEFINE_HOST_FUNCTION(jsBroadcastChannelPrototype_inspectCustom, (JSC::JSGlob depthValue = jsNumber(depth - 1); } + JSFunction* utilInspect = globalObject->utilInspectFunction(); + RETURN_IF_EXCEPTION(throwScope, {}); + if (!utilInspect) [[unlikely]] + return JSValue::encode(jsNontrivialString(vm, "BroadcastChannel"_s)); + JSObject* options = optionsValue.toObject(lexicalGlobalObject); RETURN_IF_EXCEPTION(throwScope, {}); PropertyNameArrayBuilder optionsArray(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude); @@ -211,8 +216,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBroadcastChannelPrototype_inspectCustom, (JSC::JSGlob inputObj->putDirect(vm, vm.propertyNames->name, jsString(vm, channel->name()), 0); inputObj->putDirect(vm, Identifier::fromString(vm, "active"_s), jsBoolean(!channel->isClosed()), 0); - JSFunction* utilInspect = globalObject->utilInspectFunction(); - RETURN_IF_EXCEPTION(throwScope, {}); auto callData = JSC::getCallData(utilInspect); MarkedArgumentBuffer arguments; arguments.append(inputObj); diff --git a/test/js/bun/util/inspect-custom-lazy-init-exception.test.ts b/test/js/bun/util/inspect-custom-lazy-init-exception.test.ts new file mode 100644 index 00000000000..77f8a34b081 --- /dev/null +++ b/test/js/bun/util/inspect-custom-lazy-init-exception.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +test("Bun.inspect on object with custom inspect does not crash when node:util fails to load", async () => { + const src = ` + const { writeSync } = require("node:fs"); + const print = (s) => writeSync(1, s + "\\n"); + Object.defineProperty(Array.prototype, "forEach", { + get() { throw new Error("poisoned forEach"); }, + }); + const obj = { [Symbol.for("nodejs.util.inspect.custom")]() { return "custom-result"; } }; + for (const colors of [true, false, true, false]) { + try { + Bun.inspect(obj, { colors }); + print("ok"); + } catch (e) { + print("caught: " + e.message); + } + } + const bc = new BroadcastChannel("test"); + try { + Bun.inspect(bc); + print("bc ok"); + } catch (e) { + print("bc caught: " + e.message); + } + bc.close(); + process.exit(0); + `; + + 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]); + + expect(stdout).toBe("caught: poisoned forEach\nok\nok\nok\nbc ok\n"); + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +}); + +test("Bun.inspect with colors does not crash when utilInspectFunction was previously nulled", async () => { + const src = ` + const { writeSync } = require("node:fs"); + const print = (s) => writeSync(1, s + "\\n"); + Object.defineProperty(Array.prototype, "forEach", { + get() { throw new Error("poisoned forEach"); }, + }); + const obj = { [Symbol.for("nodejs.util.inspect.custom")]() { return "custom-result"; } }; + try { + Bun.inspect(obj, { colors: false }); + print("nocolors ok"); + } catch (e) { + print("nocolors caught: " + e.message); + } + try { + Bun.inspect(obj, { colors: true }); + print("colors ok"); + } catch (e) { + print("colors caught: " + e.message); + } + process.exit(0); + `; + + 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]); + + expect(stdout).toBe("nocolors caught: poisoned forEach\ncolors ok\n"); + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +});