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
30 changes: 20 additions & 10 deletions src/jsc/bindings/ProcessBindingUV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSC::ArrayAllocationProfile*>(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<JSC::ArrayAllocationProfile*>(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);
}
Expand Down
46 changes: 46 additions & 0 deletions test/js/node/process-binding.test.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand Down Expand Up @@ -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,
);
});