Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions src/jsc/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,9 @@ JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalO
uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, 0);
}

// only JSC::JSUint8Array::create can throw and we control the ArrayBuffer passed in.
scope.assertNoException();
// only JSC::JSUint8Array::create can throw and we control the ArrayBuffer passed in,
// but its allocation may service VMTraps and observe a pending TerminationException.
scope.assertNoExceptionExceptTermination();
ASSERT(uint8Array);

return JSC::JSValue::encode(uint8Array);
Expand Down
61 changes: 61 additions & 0 deletions test/js/web/workers/worker-terminate-lifetime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,64 @@ test.skipIf(!isDebug)(
},
120_000,
);

// Regression: JSBuffer__bufferFromPointerAndLengthAndDeinit finished with a
// plain scope.assertNoException(). JSUint8Array::create allocates, which
// services VMTraps; a concurrent worker.terminate() installs the sticky
// TerminationException there, and the assert SIGABRTs the whole process.
// Hit from async crypto.pbkdf2()'s completion (Pbkdf2Ctx::then ->
// JSValue::create_buffer) running on a worker that has just been terminated.
// Release WebKit compiles that ASSERT out, so debug-only.
test.skipIf(!isDebug)(
"terminate() while a worker's async crypto.pbkdf2() completion is creating its result Buffer does not trip assertNoException()",
async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { Worker } = require("node:worker_threads");
const src =
'const { parentPort } = require("node:worker_threads");' +
'const crypto = require("node:crypto");' +
// One iteration, large salt: EVP_PBKDF2_HMAC is dominated by the
// HMAC over the salt, so the threadpool job is short but nonzero
// and overlaps the parent's terminate(). The async completion then
// allocates a 64-byte Buffer via JSBuffer__bufferFromPointerAndLengthAndDeinit.
'const salt = Buffer.alloc(3 << 20, 0xaa);' +
'parentPort.postMessage("up");' +
'function lane() { crypto.pbkdf2(salt, salt, 1, 64, "sha256", lane); }' +
'for (let i = 0; i < 8; i++) lane();';
function ready(w) {
return new Promise((res, rej) => {
w.once("message", res);
w.once("error", rej);
w.once("exit", (c) => rej(new Error("worker exited " + c + " before ready")));
});
}
for (let r = 0; r < 15; r++) {
const ws = [];
for (let i = 0; i < 4; i++) {
const w = new Worker(src, { eval: true });
ws.push(w);
}
await Promise.all(ws.map(ready));
for (const w of ws) w.on("error", () => {});
await Bun.sleep(r % 7);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
await Promise.all(ws.map((w) => w.terminate()));
}
console.log("PASS");
`,
],
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("PASS\n");
expect(exitCode).toBe(0);
},
120_000,
);
Loading