Repository navigation
--hot: fix a use-after-free of the entry point's promise #44350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
853225e
c0dd158
df2d5ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -828,3 +828,103 @@ it( | |
| }, | ||
| timeout, | ||
| ); | ||
|
|
||
| it("holds the promise of the entry point itself, which it looks at on every tick", async () => { | ||
| const source = (comment: string) => ` | ||
| globalThis.loads = (globalThis.loads ?? 0) + 1; | ||
| const load = globalThis.loads; | ||
| setTimeout(() => { | ||
| Bun.gc(true); | ||
| const { nodes, nodeClassNames, edges } = require("bun:jsc").generateHeapSnapshotForDebugging(); | ||
| const className = new Map(); | ||
| for (let i = 0; i < nodes.length; i += 7) className.set(nodes[i], nodeClassNames[nodes[i + 2]]); | ||
| let held = 0; | ||
| for (let i = 0; i < edges.length; i += 4) | ||
| if (className.get(edges[i]) === "StrongRootBlock" && className.get(edges[i + 1]) === "Promise") held++; | ||
| console.log("load " + load + ": " + held + " held"); | ||
| }, 0); | ||
| // ${comment} | ||
| `; | ||
| using dir = tempDir("hot-entry-promise", { "main.js": source("first") }); | ||
| await using runner = spawn({ | ||
| cmd: [bunExe(), "--hot", "--no-clear-screen", "main.js"], | ||
| env: bunEnv, | ||
| cwd: String(dir), | ||
| stdout: "pipe", | ||
| stderr: "inherit", | ||
| stdin: "ignore", | ||
| }); | ||
| const reader = runner.stdout.getReader(); | ||
| let stdout = ""; | ||
| const line = async (prefix: string) => { | ||
| for (;;) { | ||
| const found = stdout | ||
| .split("\n") | ||
| .slice(0, -1) | ||
| .find(line => line.startsWith(prefix)); | ||
| if (found) return found; | ||
| const { value, done } = await reader.read(); | ||
| if (done) return stdout; | ||
| stdout += Buffer.from(value).toString(); | ||
| } | ||
| }; | ||
| expect(await line("load 1:")).toBe("load 1: 1 held"); | ||
| writeFileSync(join(String(dir), "main.js"), source("second")); | ||
| expect(await line("load 2:")).toBe("load 2: 1 held"); | ||
| }); | ||
|
|
||
| // The cell of a collected promise goes to the next promise that is made. | ||
| it.each([ | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| ["pending", `new Promise(() => {})`], | ||
| ["rejected and handled", `Promise.reject(new Error("not the entry point's"))`], | ||
| ])("does not take a promise of the program's, %s, for that of the entry point", async (_, promise) => { | ||
| using dir = tempDir("hot-entry-promise-reused", { "main.js": `console.log("first load");` }); | ||
| await using runner = spawn({ | ||
| cmd: [bunExe(), "--hot", "--no-clear-screen", "main.js"], | ||
| env: bunEnv, | ||
| cwd: String(dir), | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| stdin: "ignore", | ||
| }); | ||
| const reader = runner.stdout.getReader(); | ||
| let stdout = ""; | ||
| const line = async (expected: string) => { | ||
| while (!stdout.split("\n").slice(0, -1).includes(expected)) { | ||
| const { value, done } = await reader.read(); | ||
| if (done) break; | ||
| stdout += Buffer.from(value).toString(); | ||
| } | ||
| }; | ||
| await line("first load"); | ||
| // What is left on the stack can keep it alive, and differs with where a collection starts from. | ||
| writeFileSync( | ||
| join(String(dir), "main.js"), | ||
| ` | ||
| globalThis.kept = []; | ||
| require("fs").readFile(__filename, () => { | ||
| Bun.gc(true); | ||
| setImmediate(() => { | ||
| Bun.gc(true); | ||
| require("crypto").randomBytes(8, () => { | ||
| Bun.gc(true); | ||
| for (let i = 0; i < 20000; i++) { | ||
| const promise = ${promise}; | ||
| promise.catch(() => {}); | ||
| kept.push(promise); | ||
| } | ||
| console.log("collected"); | ||
| }); | ||
| }); | ||
| }); | ||
| `, | ||
| ); | ||
| await line("collected"); | ||
|
Comment on lines
+900
to
+922
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 nit (optional): maintainers get a regression test that can pass on the unfixed build, since nothing checks the entry-point promise was actually collected before the 20k promises are made. The fixture at test/cli/hot/hot.test.ts:900-919 relies on three Why this was flaggedThe Verification: nit. On the unfixed build, whenever one of the three |
||
| writeFileSync(join(String(dir), "main.js"), `console.log("third load"); process.exit(0);`); | ||
| await line("third load"); | ||
| const stderr = (await runner.stderr.text()).split("\n").filter(line => line && !line.startsWith("DEBUG: ")); | ||
| expect({ stdout: stdout.split("\n"), stderr }).toEqual({ | ||
| stdout: ["first load", "collected", "third load", ""], | ||
| stderr: [], | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.