From f1c58cc53fbd83b968e11b3e2eccebca51299c54 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:36:39 +0800 Subject: [PATCH] fix(reference): reject FIFO replacements without blocking Use the shared nonblocking, no-follow regular-file opener at the source-read boundary and report a nonregular replacement as changed. Keep descriptor identity and extent checks. The compiled subprocess regression timed out on the baseline and completes without a FIFO writer after this change. Reference reader failure, capacity, cancellation, and import checks pass. --- src/reference/ReferenceSourceReaderFile.ts | 18 +++++++++-- .../referenceSourceFifoRace.test.ts | 29 +++++++++++++++++ tests/fixtures/referenceReaderFifoProbe.mjs | 31 +++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/boundary/filesystem/referenceSourceFifoRace.test.ts create mode 100644 tests/fixtures/referenceReaderFifoProbe.mjs diff --git a/src/reference/ReferenceSourceReaderFile.ts b/src/reference/ReferenceSourceReaderFile.ts index 11ee5f287..6f61c0253 100644 --- a/src/reference/ReferenceSourceReaderFile.ts +++ b/src/reference/ReferenceSourceReaderFile.ts @@ -1,8 +1,12 @@ import type { BigIntStats } from "node:fs"; -import { constants } from "node:fs"; -import { open, type FileHandle } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; import { dirname } from "node:path"; +import { + NonRegularFileReadError, + openRegularFile, +} from "../filesystem/RegularFile.js"; + import { entryFailure, filesystemFailureDetail, @@ -209,7 +213,7 @@ export const readStableFile = async ( parentBefore.message, safeSize(expected.size), ); - handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW); + handle = await openRegularFile(absolute, { symlinks: "reject", signal }); const prepared = await prepareFileRead(request, handle); if (prepared.status === "failed") return prepared.entry; const contents = await readFileContents({ @@ -240,6 +244,14 @@ export const readStableFile = async ( "File read cancelled", safeSize(expected.size), ); + if (cause instanceof NonRegularFileReadError) + return entryFailure( + path, + "file", + "changed", + "File is no longer a regular file", + safeSize(expected.size), + ); const message = filesystemFailureDetail( cause, "File could not be read safely", diff --git a/tests/boundary/filesystem/referenceSourceFifoRace.test.ts b/tests/boundary/filesystem/referenceSourceFifoRace.test.ts new file mode 100644 index 000000000..eb6d1be1e --- /dev/null +++ b/tests/boundary/filesystem/referenceSourceFifoRace.test.ts @@ -0,0 +1,29 @@ +import { fileURLToPath } from "node:url"; + +import { expect } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { processTest } from "../../support/process/processFixture.js"; + +processTest.skipIf(process.platform === "win32")( + "rejects a FIFO replacing an admitted source file without waiting for a writer", + async ({ processes }) => { + const root = await createTestTempDirectory("rea-reference-fifo-race-"); + const probe = fileURLToPath( + new URL("../../fixtures/referenceReaderFifoProbe.mjs", import.meta.url), + ); + const readerModule = fileURLToPath( + new URL( + "../../../dist/reference/ReferenceSourceReaderFile.js", + import.meta.url, + ), + ); + const result = await processes.run( + process.execPath, + [probe, readerModule, root], + { timeoutMs: 3_000 }, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout.trim()).toBe("changed"); + }, +); diff --git a/tests/fixtures/referenceReaderFifoProbe.mjs b/tests/fixtures/referenceReaderFifoProbe.mjs new file mode 100644 index 000000000..a8f785648 --- /dev/null +++ b/tests/fixtures/referenceReaderFifoProbe.mjs @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { lstat, unlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +const [modulePath, root] = process.argv.slice(2); +if (modulePath === undefined || root === undefined) + throw new Error("Reference reader FIFO probe requires module and root paths"); + +const { readStableFile } = await import(pathToFileURL(modulePath)); +const absolute = join(root, "source.txt"); +await writeFile(absolute, "observed regular file\n"); +const expected = await lstat(absolute, { bigint: true }); +await unlink(absolute); +execFileSync("mkfifo", [absolute]); + +// Capture the directory after replacement to reach the file-open boundary. +// The owning test bounds this child if an O_RDONLY FIFO open blocks again. +const rootIdentity = await lstat(root, { bigint: true }); +const result = await readStableFile({ + root, + rootIdentity, + absolute, + path: "source.txt", + expected, +}); +assert.equal(result.status, "failed"); +assert.equal(result.kind, "file"); +assert.equal(result.code, "changed"); +process.stdout.write("changed\n");