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
10 changes: 10 additions & 0 deletions src/jsc/bindings/JSCTaskScheduler.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#include "config.h"
#include <JavaScriptCore/VM.h>
#include <JavaScriptCore/DeferredWorkTimerInlines.h>
#include <JavaScriptCore/TopExceptionScope.h>
#include "JSCTaskScheduler.h"
#include "BunClientData.h"
#include "ZigGlobalObject.h"

using Ticket = JSC::DeferredWorkTimer::Ticket;
using Task = JSC::DeferredWorkTimer::Task;
Expand Down Expand Up @@ -87,7 +90,14 @@ static void runPendingWork(void* bunVM, Bun::JSCTaskScheduler& scheduler, JSCDef
holder.unlockEarly();

if (pendingTicket && !pendingTicket->isCancelled()) {
auto& vm = job->vm();
auto* globalObject = defaultGlobalObject(job->ticket->target()->realm());
auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
job->task(job->ticket.ptr());
if (auto* exception = scope.exception()) {
if (scope.clearExceptionExceptTermination())
Zig::GlobalObject::reportUncaughtExceptionAtEventLoop(globalObject, exception);
}
}

delete job;
Expand Down
117 changes: 117 additions & 0 deletions test/js/bun/util/finalization-registry-throw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

test.concurrent("FinalizationRegistry callback that throws is reported as uncaughtException", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
let caught = null;
process.on("uncaughtException", (err) => {
caught = err;
});
const registry = new FinalizationRegistry(() => {
throw new Error("boom");
});
for (let i = 0; i < 1000; i++) {
registry.register({}, i);
}
(async () => {
for (let i = 0; i < 20; i++) {
Bun.gc(true);
await new Promise(r => setImmediate(r));
if (caught) break;
}
console.log(JSON.stringify({ caught: caught?.message ?? null }));
})();
`,
],
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(JSON.parse(stdout.trim())).toEqual({ caught: "boom" });
expect(exitCode).toBe(0);
});

test.concurrent(
"FinalizationRegistry callback that throws does not crash when triggered by generateHeapSnapshot",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
let caught = null;
process.on("uncaughtException", (err) => {
caught = err;
});
const registry = new FinalizationRegistry(() => {
ArrayBuffer();
});
for (let i = 0; i < 1000; i++) {
registry.register({}, i);
}
(async () => {
for (let i = 0; i < 20 && !caught; i++) {
Bun.gc(true);
Bun.generateHeapSnapshot();
Bun.gc(true);
await new Promise(r => setImmediate(r));
}
console.log(JSON.stringify({ caught: caught?.message ?? null }));
})();
`,
],
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(JSON.parse(stdout.trim()).caught).toContain("ArrayBuffer");
expect(exitCode).toBe(0);
},
);

test.concurrent(
"FinalizationRegistry callback that throws inside a node:vm context is reported as uncaughtException",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const vm = require("node:vm");
let caught = null;
process.on("uncaughtException", (err) => {
caught = err;
});
const ctx = vm.createContext({});
vm.runInContext(\`
globalThis.r = new FinalizationRegistry(() => { throw new Error("boom-in-vm"); });
for (let i = 0; i < 1000; i++) globalThis.r.register({}, i);
\`, ctx);
(async () => {
for (let i = 0; i < 20; i++) {
Bun.gc(true);
await new Promise(r => setImmediate(r));
if (caught) break;
}
console.log(JSON.stringify({ caught: caught?.message ?? null }));
})();
`,
],
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(JSON.parse(stdout.trim())).toEqual({ caught: "boom-in-vm" });
expect(exitCode).toBe(0);
},
);
Loading