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
39 changes: 39 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,40 @@ 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.
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: "inherit",
});

const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout).toBe("[1,1,1,1]\n");
expect(exitCode).toBe(0);
});
});
29 changes: 29 additions & 0 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,3 +1527,32 @@ test("node:vm Object.defineProperty on the context global when the sandbox is an
expect(stdout.trim()).toBe(JSON.stringify({ result: 1, sandboxArray: 1 }));
expect(exitCode).toBe(0);
});

// The watchdog's TerminationException can land while an ERR_* error is being built for the
// sandboxed code; building it must not dereference a half-made (null) error object.
test("timeout landing while node validation errors are being constructed does not crash", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const vm = require("node:vm"), tls = require("node:tls");
const ctx = vm.createContext({ tls, Buffer });
for (const code of [
"for(;;){ try { tls.checkServerIdentity('a.com', {subject:{CN:'b.com'}, subjectaltname:'DNS:c.com'}) } catch (e) {} }",
"for(;;){ try { Buffer.alloc(-1) } catch (e) {} }",
]) {
for (let i = 0; i < 60; i++) {
try { vm.runInContext(code, ctx, { timeout: 1 + (i % 6) }); }
catch (e) { if (e.code !== "ERR_SCRIPT_EXECUTION_TIMEOUT") throw e; }
}
}
console.log("survived");`,
],
env: bunEnv,
stdout: "pipe",
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout).toBe("survived\n");
expect(exitCode).toBe(0);
});
Loading