diff --git a/test/js/node/http2/node-http2-getter-rehash.fixture.js b/test/js/node/http2/node-http2-getter-rehash.fixture.js new file mode 100644 index 000000000000..14083cf7b91d --- /dev/null +++ b/test/js/node/http2/node-http2-getter-rehash.fixture.js @@ -0,0 +1,61 @@ +"use strict"; +// Re-entrant session.request() from an options getter. request() now shallow +// copies options in JS before any stream exists (#31323), so this is a +// re-entrancy/ordering smoke test rather than the original native UAF repro. + +const http2 = require("node:http2"); + +const server = http2.createServer(); +server.on("stream", stream => { + stream.respond({ ":status": 200 }); + stream.end(); +}); +server.on("error", () => {}); + +server.listen(0, "127.0.0.1", () => { + const port = server.address().port; + const client = http2.connect("http://127.0.0.1:" + port); + client.on("error", () => {}); + + client.on("connect", () => { + let triggered = false; + + const options = { + get paddingStrategy() { + if (!triggered) { + triggered = true; + // Insert enough new streams to force the HashMap to rehash while the + // outer request() is still on the stack. + for (let i = 0; i < 32; i++) { + const r = client.request({ ":path": "/", ":method": "GET" }); + r.on("error", () => {}); + r.on("response", () => {}); + r.resume(); + } + } + return 0; + }, + exclusive: true, + waitForTrailers: false, + endStream: true, + }; + + const req = client.request({ ":path": "/", ":method": "POST" }, options); + req.on("error", () => {}); + req.on("response", () => {}); + req.resume(); + req.on("close", () => { + client.close(() => { + server.close(() => { + if (!triggered) { + console.error("getter was never invoked"); + process.exit(1); + } + console.log("done"); + process.exit(0); + }); + }); + }); + req.end(); + }); +}); diff --git a/test/js/node/http2/node-http2-streams-rehash.test.ts b/test/js/node/http2/node-http2-streams-rehash.test.ts index 0edd5c05d848..84ce0e3fe4a0 100644 --- a/test/js/node/http2/node-http2-streams-rehash.test.ts +++ b/test/js/node/http2/node-http2-streams-rehash.test.ts @@ -21,73 +21,8 @@ test("session.request() from a stream 'timeout' listener during forEachStream do }); test("http2 client request() does not hold *Stream across user-controlled options getters", async () => { - const script = /* js */ ` - const http2 = require("node:http2"); - - const server = http2.createServer(); - server.on("stream", (stream) => { - stream.respond({ ":status": 200 }); - stream.end(); - }); - server.on("error", () => {}); - - server.listen(0, "127.0.0.1", () => { - const port = server.address().port; - const client = http2.connect("http://127.0.0.1:" + port); - client.on("error", () => {}); - - client.on("connect", () => { - let triggered = false; - - // Use a POST so the options object is passed through to the native - // parser without being shallow-copied. - const options = { - get paddingStrategy() { - if (!triggered) { - triggered = true; - // Insert enough new streams to force the HashMap to rehash, - // invalidating any *Stream pointer held by the outer request(). - for (let i = 0; i < 128; i++) { - const r = client.request({ ":path": "/", ":method": "GET" }); - r.on("error", () => {}); - r.on("response", () => {}); - r.resume(); - } - } - return 0; - }, - // Ensure the outer request writes through the (previously dangling) - // stream pointer after the getter returns. - exclusive: true, - parent: 1, - weight: 16, - waitForTrailers: false, - endStream: true, - }; - - const req = client.request({ ":path": "/", ":method": "POST" }, options); - req.on("error", () => {}); - req.on("response", () => {}); - req.resume(); - req.on("close", () => { - client.close(() => { - server.close(() => { - if (!triggered) { - console.error("getter was never invoked"); - process.exit(1); - } - console.log("done"); - process.exit(0); - }); - }); - }); - req.end(); - }); - }); - `; - await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", script], + cmd: [bunExe(), path.join(import.meta.dir, "node-http2-getter-rehash.fixture.js")], env: bunEnv, stdout: "pipe", stderr: "pipe",