Files
2026-08-27 21:09:14 +00:00

84 lines
3.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isDebug, tempDir } from "harness";
// Regression test for https://github.com/oven-sh/bun/issues/31636
//
// `bun run --parallel` (with `--filter` or `--workspaces`) parsed a standalone
// PackageJSON per workspace package and moved its `scripts` map into a
// collected struct. The map's values borrow the package.json source bytes the
// PackageJSON owns, so when that PackageJSON dropped at the end of each
// collection-loop iteration, the bytes were freed while the scripts map still
// pointed into them (heap-use-after-free). The freed bytes were later scanned
// while building the shell command, producing a corrupted command: the reporter
// saw `bash: line 1: $'\375\001': command not found` and exit 127, with the
// garbage varying between runs.
//
// Under ASAN the use-after-free is deterministic; on release builds the freed
// region is only sometimes reused before the read, which is why a tiny repro
// "may succeed" while heavier monorepos fail consistently. Release CI lanes
// skip this; the gate runs under `bun bd` which is debug+ASAN, so it's covered.
// The workspace scripts run `bun <file>` — `bun` goes through
// `replacePackageManagerRun` (which scans the freed script bytes) and the file
// is a real entrypoint that prints a marker and exits, so we can assert the
// children actually started with an uncorrupted command.
function workspace() {
const dev = (marker: string) => ({
[`packages/${marker}/package.json`]: JSON.stringify({
name: marker,
scripts: { dev: "bun index.ts" },
}),
[`packages/${marker}/index.ts`]: `console.log(${JSON.stringify(marker + "-started")});`,
});
return tempDir("issue-31636", {
"package.json": JSON.stringify({
name: "monorepo-root",
private: true,
workspaces: ["packages/*"],
}),
...dev("pdf-service"),
...dev("app-website"),
...dev("api"),
});
}
describe.concurrent.skipIf(!isDebug)("issue 31636", () => {
test("bun run --parallel --filter spawns an uncorrupted workspace script", async () => {
using dir = workspace();
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--parallel", "--filter=pdf-service", "dev"],
env: { ...bunEnv, NO_COLOR: "1" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("command not found");
expect(stderr).not.toContain("No such file or directory");
expect(stdout).toContain("pdf-service-started");
expect(exitCode).toBe(0);
});
test("bun run --parallel --workspaces spawns uncorrupted workspace scripts", async () => {
using dir = workspace();
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--parallel", "--workspaces", "--if-present", "dev"],
env: { ...bunEnv, NO_COLOR: "1" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).not.toContain("command not found");
expect(stderr).not.toContain("No such file or directory");
expect(stdout).toContain("pdf-service-started");
expect(stdout).toContain("app-website-started");
expect(stdout).toContain("api-started");
expect(exitCode).toBe(0);
});
});