Files
scriptc/tests/harness/fs-stream-leaks.test.ts
T
Chris Tate 4a56c17be7 Release a FileHandle stream's owner when its options are refused
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.
2026-10-16 08:16:46 -05:00

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);
});
});