From 09f65a0b1149d8414b9ad81d7f00947cc095cba9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:42:52 +0000 Subject: [PATCH] util.isError: propagate a throwing getPrototypeOf trap instead of crashing The VMInquiry PropertySlot used for the toStringTag check forbids entering the VM for as long as it is alive, so a Proxy getPrototypeOf trap reached by the getPrototype() call below it aborted, and the empty value returned by a throwing trap was used as a cell. Scope the slot to the tag check and check for an exception after getPrototype(). --- src/jsc/modules/NodeUtilTypesModule.cpp | 26 ++++++++++-------- test/js/node/util/util.test.js | 36 ++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/jsc/modules/NodeUtilTypesModule.cpp b/src/jsc/modules/NodeUtilTypesModule.cpp index 46050f5ed8d1..322abc0ce3b8 100644 --- a/src/jsc/modules/NodeUtilTypesModule.cpp +++ b/src/jsc/modules/NodeUtilTypesModule.cpp @@ -898,22 +898,26 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionIsError, // node util.isError relies on toString // https://github.com/nodejs/node/blob/cf8c6994e0f764af02da4fa70bc5962142181bf3/doc/api/util.md#L2923 // util.isError is deprecated and removed in node 23 - PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, &vm); - bool has = object->getPropertySlot(globalObject, vm.propertyNames->toStringTagSymbol, slot); - scope.assertNoException(); - if (has) { - if (slot.isValue()) { - JSValue value = slot.getValue(globalObject, vm.propertyNames->toStringTagSymbol); - if (value.isString()) { - String tag = asString(value)->value(globalObject); - CLEAR_IF_EXCEPTION(scope); - if (tag == "Error"_s) - return JSValue::encode(jsBoolean(true)); + { + PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, &vm); + bool has = object->getPropertySlot(globalObject, vm.propertyNames->toStringTagSymbol, slot); + scope.assertNoException(); + if (has) { + if (slot.isValue()) { + JSValue value = slot.getValue(globalObject, vm.propertyNames->toStringTagSymbol); + if (value.isString()) { + String tag = asString(value)->value(globalObject); + CLEAR_IF_EXCEPTION(scope); + if (tag == "Error"_s) + return JSValue::encode(jsBoolean(true)); + } } } + // The VMInquiry slot disallows VM entry while alive; the Proxy trap below needs it dead. } JSValue proto = object->getPrototype(globalObject); + RETURN_IF_EXCEPTION(scope, {}); if (proto.isCell() && (proto.inherits() || proto.asCell()->type() == ErrorInstanceType || proto.inherits())) return JSValue::encode(jsBoolean(true)); } diff --git a/test/js/node/util/util.test.js b/test/js/node/util/util.test.js index 7689c1230321..4624a98a174c 100644 --- a/test/js/node/util/util.test.js +++ b/test/js/node/util/util.test.js @@ -23,7 +23,7 @@ import assert from "assert"; import { describe, expect, it } from "bun:test"; -import "harness"; +import { bunEnv, bunExe } from "harness"; import util from "util"; // const context = require('vm').runInNewContext; // TODO: Use a vm polyfill @@ -152,6 +152,40 @@ describe("util", () => { let err8 = new Error3(); strictEqual(util.isError(err8), true); }); + + // Spawned: these inputs crashed the whole process before the fix (the + // VMInquiry slot outlived the toStringTag check, so any getPrototypeOf + // trap aborted), and a regression must not take the file down with it. + it.concurrent("handles Proxy getPrototypeOf traps", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const util = require("node:util"); + const expected = new Error("nope"); + let caught; + try { + util.isError(new Proxy({}, { getPrototypeOf() { throw expected; } })); + } catch (error) { + caught = error; + } + console.log("identity:" + (caught === expected)); + console.log("proto-error:" + util.isError(new Proxy({}, { getPrototypeOf: () => Error.prototype }))); + console.log("proto-null:" + util.isError(new Proxy({}, { getPrototypeOf: () => null })));`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect({ stdout, stderr: stderr.trim(), exitCode }).toEqual({ + stdout: "identity:true\nproto-error:true\nproto-null:false\n", + stderr: "", + exitCode: 0, + }); + }); }); describe("isObject", () => {