mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
scr_fss_new takes ownership of the FileHandle owner record (and its
reference to the handle), but its first two refusal paths returned before
the shared cleanup, so a FileHandle stream refused by its options, such
as fh.createReadStream({ encoding: "nope" }), leaked both. Those paths
now release the owner as the later ones do.
Corpus 5554 refuses a FileHandle stream and then closes the handle, and
tests/harness/fs-stream-leaks.test.ts runs the fs stream programs under
LeakSanitizer in the sanitized lane: 5554 reports the leak without this
change.
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
/* fs read and write streams under LeakSanitizer. A stream owns its native
|
|
* state and, for FileHandle streams, a reference to the handle; every path
|
|
* that refuses or tears down a stream must release them. The sanitized
|
|
* lanes otherwise run with detect_leaks=0, so this runs the fs stream
|
|
* programs with LeakSanitizer explicitly on. 5554 includes a FileHandle
|
|
* stream refused by its options. Linux only (Apple's ASan has no
|
|
* LeakSanitizer); runs in the SCRIPTC_SAN=1 lane. */
|
|
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { describe, expect, test } from "vitest";
|
|
import { compile } from "@scriptc/compiler";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = join(import.meta.dirname, "../..");
|
|
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
|
|
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
|
|
|
const programs = [
|
|
"tests/corpus/5546-fs-read-stream.cjs",
|
|
"tests/corpus/5547-fs-write-stream.cjs",
|
|
"tests/corpus/5554-fs-streams-typed.ts",
|
|
];
|
|
|
|
describe.skipIf(!sanitize || process.platform !== "linux")("fs streams under LeakSanitizer", () => {
|
|
test.each(programs)("%s reports no leaks", { timeout: 300_000 }, async (program) => {
|
|
const file = join(repoRoot, program);
|
|
const key = createHash("sha256")
|
|
.update(file)
|
|
.update(readFileSync(file))
|
|
.update("san")
|
|
.digest("hex")
|
|
.slice(0, 16);
|
|
const outDir = join(cacheDir, `fs-stream-leaks-${key}`);
|
|
mkdirSync(outDir, { recursive: true });
|
|
const result = await compile(file, {
|
|
outPath: join(outDir, "program"),
|
|
outDir,
|
|
sanitize: true,
|
|
backend: "llvm",
|
|
});
|
|
if (!result.ok) {
|
|
throw new Error(
|
|
"program failed to compile:\n" +
|
|
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
|
|
);
|
|
}
|
|
const run = await execFileAsync(result.binaryPath, [], {
|
|
env: { ...process.env, ASAN_OPTIONS: "detect_leaks=1" },
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
}).catch((error: { code?: number; stdout?: string; stderr?: string }) => error);
|
|
expect(run.stderr).toBe("");
|
|
expect("code" in run ? run.code : 0).toBe(0);
|
|
const expected = await execFileAsync(process.execPath, ["--no-warnings", file], {
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
});
|
|
expect(run.stdout).toBe(expected.stdout);
|
|
});
|
|
});
|