From d1185a71e33d197eee2f7cc3eebe3180e39c2a6a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 06:56:32 +0000 Subject: [PATCH] process.binding('uv'): check for exceptions while building getErrorMap() jsGetErrorMap built each [name, message] entry inside a void lambda that dereferenced the result of constructEmptyArray without checking for an exception (the check was left as a comment). constructEmptyArray returns null once an exception is pending, and RETURN_IF_EXCEPTION also services VM traps, so a worker.terminate() requested while a worker is inside getErrorMap() turned into a null dereference that took down the whole process ("Segmentation fault at address 0x4"). Build the map from a constexpr table in a plain loop instead, checking the scope after every allocation, putDirectIndex and JSMap::set, so the termination (or an out-of-memory error) propagates like any other exception. The map contents and order are unchanged. --- src/jsc/bindings/ProcessBindingUV.cpp | 30 +++++++++++------ test/js/node/process-binding.test.ts | 46 +++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/jsc/bindings/ProcessBindingUV.cpp b/src/jsc/bindings/ProcessBindingUV.cpp index 7488ef2b19ef..372fc8f3841a 100644 --- a/src/jsc/bindings/ProcessBindingUV.cpp +++ b/src/jsc/bindings/ProcessBindingUV.cpp @@ -153,20 +153,30 @@ JSC_DEFINE_HOST_FUNCTION(jsErrname, (JSGlobalObject * globalObject, JSC::CallFra JSC_DEFINE_HOST_FUNCTION(jsGetErrorMap, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(globalObject); - auto map = JSC::JSMap::create(vm, globalObject->mapStructure()); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* map = JSC::JSMap::create(vm, globalObject->mapStructure()); - // Inlining each of these via macros costs like 300 KB. - const auto putProperty = [](JSC::VM& vm, JSC::JSMap* map, JSC::JSGlobalObject* globalObject, ASCIILiteral name, int value, ASCIILiteral desc) -> void { - auto arr = JSC::constructEmptyArray(globalObject, static_cast(nullptr), 2); - // RETURN_IF_EXCEPTION + struct Entry { + ASCIILiteral name; + int value; + ASCIILiteral desc; + }; + static constexpr Entry entries[] = { +#define ENTRY(name, desc) { #name##_s, UV_##name, desc##_s }, + BUN_UV_ERRNO_MAP(ENTRY) +#undef ENTRY + }; + + for (const auto& [name, value, desc] : entries) { + auto* arr = JSC::constructEmptyArray(globalObject, static_cast(nullptr), 2); + RETURN_IF_EXCEPTION(scope, {}); arr->putDirectIndex(globalObject, 0, JSC::jsString(vm, String(name))); + RETURN_IF_EXCEPTION(scope, {}); arr->putDirectIndex(globalObject, 1, JSC::jsString(vm, String(desc))); + RETURN_IF_EXCEPTION(scope, {}); map->set(globalObject, JSC::jsNumber(value), arr); - }; - -#define PUT_PROPERTY(name, desc) putProperty(vm, map, globalObject, #name##_s, UV_##name, desc##_s); - BUN_UV_ERRNO_MAP(PUT_PROPERTY) -#undef PUT_PROPERTY + RETURN_IF_EXCEPTION(scope, {}); + } return JSValue::encode(map); } diff --git a/test/js/node/process-binding.test.ts b/test/js/node/process-binding.test.ts index 420aab936270..8a935821c93a 100644 --- a/test/js/node/process-binding.test.ts +++ b/test/js/node/process-binding.test.ts @@ -1,3 +1,6 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + describe("process.binding", () => { test("process.binding('constants')", () => { /* @ts-ignore */ @@ -28,4 +31,47 @@ describe("process.binding", () => { expect(map).toBeDefined(); expect(map.get(uv.UV_EISCONN)).toEqual(["EISCONN", "socket is already connected"]); }); + + // A pending worker.terminate() surfaces inside getErrorMap() at one of its ~85 array allocations. + // This used to segfault the whole process, hence the subprocess. The timeout covers booting four + // workers on debug and ASAN builds. + const workerTerminateTimeout = 20_000; + test( + "process.binding('uv').getErrorMap() survives worker.terminate() landing mid-call", + async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const { Worker } = require("node:worker_threads"); + const source = \` + const { parentPort } = require("node:worker_threads"); + const uv = process.binding("uv"); + parentPort.postMessage("busy"); + for (;;) uv.getErrorMap(); + \`; + const exitCodes = []; + for (let i = 0; i < 4; i++) { + const worker = new Worker(source, { eval: true }); + worker.on("message", () => worker.terminate()); + worker.on("exit", code => { + exitCodes.push(code); + if (exitCodes.length === 4) console.log(JSON.stringify(exitCodes)); + }); + } + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("[1,1,1,1]\n"); + expect(exitCode).toBe(0); + }, + workerTerminateTimeout, + ); });