Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
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 we may be entered with a worker's TerminationException already pending.
Comment thread
robobun marked this conversation as resolved.
Outdated
scope.assertNoExceptionExceptTermination();
ASSERT(uint8Array);

return JSC::JSValue::encode(uint8Array);
Expand Down
63 changes: 63 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,66 @@ test.skipIf(!isDebug)(
},
120_000,
);

// Regression: JSBuffer__bufferFromPointerAndLengthAndDeinit finished with a
// plain scope.assertNoException(). With several pbkdf2 lanes in flight, one
// lane's JS callback hits a bytecode trap checkpoint where a concurrent
// worker.terminate() installs the sticky TerminationException; the next
// lane's completion (Pbkdf2Ctx::then -> JSValue::create_buffer) then enters
// JSBuffer__bufferFromPointerAndLengthAndDeinit with that exception still
// pending and the assert SIGABRTs the whole process. The assert is under
// ENABLE(EXCEPTION_SCOPE_VERIFICATION) = ASSERT_ENABLED || ASAN_ENABLED, so
// it is compiled in for both debug and release+ASAN.
test.skipIf(!isDebug && !isASAN)(
"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