fix(filesystem): finish executable and project span reads

Reuse the positional range owner for target metadata, Hopper Mach-O selection and Kuna spans. Preserve physical failures, completed span evidence and cleanup ownership across cancellation and short reads.

Validated 147 focused file and lifecycle tests; ten new cases reproduced failures against the prior implementation.
This commit is contained in:
morluto committed 2026-10-27 03:14:14 +08:00
1 parent f68064e46c
commit a7004c7df1
8 files changed
+280 -134

No files matched your search

+65 -115
View File
@@ -1,3 +1,4 @@
import { readFileHandleRange } from "../filesystem/readFileHandleRange.js";
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
import {
regularFileCancellation,
@@ -750,23 +751,12 @@ const readHandleRange = async (
position: number,
length: number,
signal?: AbortSignal,
): Promise<Uint8Array> => {
): Promise<Buffer> => {
const bytes = await readFileHandleRange(handle, position, length, () =>
throwIfTargetResolutionCancelled(signal),
);
throwIfTargetResolutionCancelled(signal);
const bytes = Buffer.alloc(length);
let filled = 0;
while (filled < length) {
if (filled > 0) throwIfTargetResolutionCancelled(signal);
const observed = await handle.read(
bytes,
filled,
length - filled,
position + filled,
);
throwIfTargetResolutionCancelled(signal);
if (observed.bytesRead === 0) break;
filled += observed.bytesRead;
}
return bytes.subarray(0, filled);
return bytes;
};
/** Read exactly `size` bytes by offset; a short read means the file changed or ended early. */
@@ -853,10 +843,7 @@ const readExecutableMetadata = async (
): Promise<Result<ExecutableMetadata, ExecutableMetadataFailure>> => {
const { handle, initial } = source;
throwIfTargetResolutionCancelled(signal);
const prefix = Buffer.alloc(4096);
const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
throwIfTargetResolutionCancelled(signal);
const bytes = prefix.subarray(0, prefixRead.bytesRead);
const bytes = await readHandleRange(handle, 0, 4096, signal);
// Header commitments such as Mach-O load commands and FAT slices are
// checked against the whole file, not the probed prefix.
const fileSize = initial.size;
@@ -867,17 +854,14 @@ const readExecutableMetadata = async (
| undefined;
if (evidence.windowsHeader.state === "signature-needed") {
const { offset } = evidence.windowsHeader;
const signature = Buffer.alloc(Math.min(4, fileSize - offset));
const observedSignature = await handle.read(
signature,
0,
signature.length,
offset,
);
throwIfTargetResolutionCancelled(signal);
signatureEvidence = {
offset,
bytes: signature.subarray(0, observedSignature.bytesRead),
bytes: await readHandleRange(
handle,
offset,
Math.min(4, fileSize - offset),
signal,
),
};
evidence = withMzWindowsHeaderSignature(
bytes,
@@ -908,15 +892,12 @@ const readExecutableMetadata = async (
// bounds are at most 65535 records inside a 65535-paragraph header.
const { relocationTableRead } = evidence;
if (relocationTableRead !== undefined) {
const table = Buffer.alloc(relocationTableRead.byteLength);
const observed = await handle.read(
table,
0,
table.length,
const observedBytes = await readHandleRange(
handle,
relocationTableRead.offset,
relocationTableRead.byteLength,
signal,
);
throwIfTargetResolutionCancelled(signal);
const observedBytes = table.subarray(0, observed.bytesRead);
evidence = parseMzHeaderEvidence(
observedBytes,
fileSize,
@@ -1150,16 +1131,13 @@ const readMachoCommandFrames = async (
offset + 8 <= input.fileSize &&
(offset < windowOffset || offset + 8 > windowOffset + window.length)
) {
window = Buffer.alloc(Math.min(4096, input.fileSize - offset));
windowOffset = offset;
const observed = await handle.read(
window,
0,
window.length,
window = await readHandleRange(
handle,
(input.fileOffset ?? 0) + offset,
Math.min(4096, input.fileSize - offset),
input.signal,
);
throwIfTargetResolutionCancelled(input.signal);
window = window.subarray(0, observed.bytesRead);
}
const relative = offset - windowOffset;
const frame = commands.read(
@@ -1176,10 +1154,12 @@ const readMachoSliceCommandFrames = async (
slice: UniversalMachOSlice,
signal?: AbortSignal,
): Promise<Result<null, string>> => {
const prefix = Buffer.alloc(Math.min(4096, slice.size));
const observed = await handle.read(prefix, 0, prefix.length, slice.offset);
throwIfTargetResolutionCancelled(signal);
const bytes = prefix.subarray(0, observed.bytesRead);
const bytes = await readHandleRange(
handle,
slice.offset,
Math.min(4096, slice.size),
signal,
);
const table = readMachoLoadCommandTableHeader(bytes, slice.size);
if (!table.ok) return table;
if (table.value === null)
@@ -1203,21 +1183,13 @@ const readFatSliceHeaderEvidence = async (
if (header.state === "needs-data") {
// readFatContainerHeader enforces the 128-entry format bound before this
// allocation; this is only the table, never a synthesized file image.
const table = Buffer.alloc(header.requiredBytes);
let bytesRead = 0;
while (bytesRead < table.length) {
throwIfTargetResolutionCancelled(signal);
const read = await handle.read(
table,
bytesRead,
table.length - bytesRead,
bytesRead,
);
throwIfTargetResolutionCancelled(signal);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
}
header = readFatContainerHeader(table.subarray(0, bytesRead), fileSize);
const table = await readHandleRange(
handle,
0,
header.requiredBytes,
signal,
);
header = readFatContainerHeader(table, fileSize);
}
if (header.state !== "parsed") return undefined;
@@ -1233,23 +1205,15 @@ const readFatSliceHeaderEvidence = async (
continue;
}
const observedLength = Math.min(12, slice.size);
const thinHeader = Buffer.alloc(observedLength);
let bytesRead = 0;
while (bytesRead < thinHeader.length) {
throwIfTargetResolutionCancelled(signal);
const read = await handle.read(
thinHeader,
bytesRead,
thinHeader.length - bytesRead,
slice.offset + bytesRead,
);
throwIfTargetResolutionCancelled(signal);
if (read.bytesRead === 0) break;
bytesRead += read.bytesRead;
}
const thinHeader = await readHandleRange(
handle,
slice.offset,
observedLength,
signal,
);
sliceHeadersByOffset.set(
slice.offset,
readMachoHeaderPrefixFacts(thinHeader.subarray(0, bytesRead)),
readMachoHeaderPrefixFacts(thinHeader),
);
}
return { header, sliceHeadersByOffset };
@@ -1271,16 +1235,8 @@ const readPeMetadata = async (
},
): Promise<Result<ExecutableMetadata, string>> => {
throwIfTargetResolutionCancelled(signal);
const fileHeader = Buffer.alloc(24);
const fileHeaderRead = await handle.read(
fileHeader,
0,
fileHeader.length,
offset,
);
throwIfTargetResolutionCancelled(signal);
if (fileHeaderRead.bytesRead !== fileHeader.length)
return err("invalid or truncated PE header");
const fileHeader = await readHandleRange(handle, offset, 24, signal);
if (fileHeader.length !== 24) return err("invalid or truncated PE header");
const { windowsHeader } = withMzWindowsHeaderSignature(
mzPrefix,
fileSize,
@@ -1296,10 +1252,13 @@ const readPeMetadata = async (
const optionalHeaderSize = coff.value.optionalHeaderSize;
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
const numberOfSections = coff.value.sectionCount;
const record = Buffer.alloc(24 + optionalHeaderSize);
const recordRead = await handle.read(record, 0, record.length, offset);
throwIfTargetResolutionCancelled(signal);
if (recordRead.bytesRead !== record.length)
const record = await readHandleRange(
handle,
offset,
24 + optionalHeaderSize,
signal,
);
if (record.length !== 24 + optionalHeaderSize)
return err("invalid or truncated PE header");
const codeView = await readPeCodeView(
handle,
@@ -1326,15 +1285,13 @@ const readPeCodeView = async (
if (debugDir === undefined || numberOfSections > 96) return undefined;
const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
const sectionTableSize = numberOfSections * 40;
const sectionTable = Buffer.alloc(sectionTableSize);
const sectionRead = await handle.read(
sectionTable,
0,
sectionTableSize,
const sectionTable = await readHandleRange(
handle,
sectionTableOffset,
sectionTableSize,
signal,
);
throwIfTargetResolutionCancelled(signal);
if (sectionRead.bytesRead !== sectionTableSize) return undefined;
if (sectionTable.length !== sectionTableSize) return undefined;
const debugFileOffset = peRvaToFileOffset(
debugDir.debugRva,
sectionTable,
@@ -1344,36 +1301,29 @@ const readPeCodeView = async (
);
if (debugFileOffset === undefined) return undefined;
const maxDebugSize = Math.min(debugDir.debugSize, 4096);
const debugDirectoryBuffer = Buffer.alloc(maxDebugSize);
const debugDirRead = await handle.read(
debugDirectoryBuffer,
0,
maxDebugSize,
const debugDirectoryBuffer = await readHandleRange(
handle,
debugFileOffset,
maxDebugSize,
signal,
);
throwIfTargetResolutionCancelled(signal);
for (const {
fileOffset: pointerToRawData,
declaredSize: sizeOfData,
} of peCodeViewDataRanges(
debugDirectoryBuffer.subarray(0, debugDirRead.bytesRead),
debugDirectoryBuffer,
sectionTable,
numberOfSections,
debugDir.sizeOfHeaders,
)) {
const maxCodeViewRead = Math.min(sizeOfData, 4096);
const codeViewBuffer = Buffer.alloc(maxCodeViewRead);
const codeViewRead = await handle.read(
codeViewBuffer,
0,
maxCodeViewRead,
const codeViewBuffer = await readHandleRange(
handle,
pointerToRawData,
maxCodeViewRead,
signal,
);
throwIfTargetResolutionCancelled(signal);
const parsed = parseCodeViewData(
codeViewBuffer.subarray(0, codeViewRead.bytesRead),
sizeOfData,
);
const parsed = parseCodeViewData(codeViewBuffer, sizeOfData);
if (parsed !== undefined) return parsed;
}
return undefined;
+9 -5
View File
@@ -1,11 +1,15 @@
import type { FileHandle } from "node:fs/promises";
/** Fill a positional window until its end or explicit EOF, without owning the descriptor. */
/**
* Fill a positional window until its end or explicit EOF, without owning the descriptor.
* Checkpoints receive cumulative acquired bytes so an owner can defer cancellation
* of a completed range to its finalization without losing the completed observation.
*/
export const readFileHandleRange = async (
handle: Pick<FileHandle, "read">,
position: number,
length: number,
assertActive: () => void = () => {},
assertActive: (bytesRead: number) => void = () => {},
): Promise<Buffer> => {
if (
!Number.isSafeInteger(position) ||
@@ -15,11 +19,11 @@ export const readFileHandleRange = async (
!Number.isSafeInteger(position + length)
)
throw new RangeError("Invalid file read range");
assertActive();
assertActive(0);
const bytes = Buffer.alloc(length);
let filled = 0;
while (filled < length) {
assertActive();
assertActive(filled);
const { bytesRead } = await handle.read(
bytes,
filled,
@@ -29,8 +33,8 @@ export const readFileHandleRange = async (
// Let the caller interpret physical EOF before checking cancellation.
// A native I/O rejection also remains primary if cancellation arrived in flight.
if (bytesRead === 0) break;
assertActive();
filled += bytesRead;
assertActive(filled);
}
return bytes.subarray(0, filled);
};
+38
View File
@@ -4,9 +4,11 @@ import {
access,
chmod,
mkdir,
open,
readFile,
readdir,
writeFile,
type FileHandle,
} from "node:fs/promises";
import { join } from "node:path";
@@ -130,6 +132,42 @@ const resolveHopperMachOImage = (
resourceScope: inspectionResources.owner,
});
it("retains Hopper FAT selection across short header, table and slice reads", async () => {
const root = await createTestTempDirectory("rea-hopper-short-reads-");
const fixture = buildFat32Container([
{ cpuType: CPU_TYPES.arm64, cpuSubtype: 0, little: true },
]);
const path = await writeFixture(root, "fat", fixture);
const target = fat32Target(path, fixture, "arm64");
const expected = await resolveHopperMachOImage(target);
expect(expected.ok).toBe(true);
let shortReads = 0;
const handles: FileHandle[] = [];
const actual = await resolveHopperMachOImageWithOwner(target, {
resourceScope: inspectionResources.owner,
fileSystem: {
open: (async (...args: Parameters<typeof open>) => {
const handle = await open(...args);
handles.push(handle);
const read = handle.read.bind(handle);
vi.spyOn(handle, "read").mockImplementation((async (
...readArgs: unknown[]
) => {
if (typeof readArgs[2] === "number" && readArgs[2] > 3) {
readArgs[2] = 3;
shortReads += 1;
}
return Reflect.apply(read, handle, readArgs);
}) as typeof handle.read);
return handle;
}) as typeof open,
},
});
expect(actual).toEqual(expected);
expect(shortReads).toBeGreaterThan(5);
expect(handles.every((handle) => handle.fd === -1)).toBe(true);
});
it.skipIf(process.platform === "win32")(
"reports a FAT64 source that became unreadable as a host read denial",
async (context) => {
+8 -5
View File
@@ -33,6 +33,7 @@ import {
import { err, ok, type Result } from "../domain/result.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
import { readFileHandleRange } from "../filesystem/readFileHandleRange.js";
import {
copyHopperSource,
@@ -280,11 +281,13 @@ const inspectHopperMachOImage = async (
cause === undefined ? undefined : { cause },
),
);
const read = async (offset: number, length: number): Promise<Buffer> => {
const bytes = Buffer.alloc(length);
const { bytesRead } = await file.read(bytes, 0, length, offset);
return bytes.subarray(0, bytesRead);
};
const read = (offset: number, length: number): Promise<Buffer> =>
readFileHandleRange(file, offset, length, () => {
if (signal?.aborted)
throw new AnalysisCancelledError("open_binary", {
cause: signal.reason,
});
});
const header = await read(0, 8);
const fatPrefix = readFatHeaderPrefix(header);
const thinPrefix = readMachoHeaderPrefixFacts(header);
+25 -4
View File
@@ -2,6 +2,7 @@ import type { RegularFileState } from "../filesystem/RegularFile.js";
import {
RegularFileChangedError,
sameRegularFileState,
RegularFileOperationCancelled,
} from "../filesystem/RegularFile.js";
import { assertKunaFileState } from "./KunaProjectState.js";
import { createReadStream } from "node:fs";
@@ -18,13 +19,13 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok, type Result } from "../domain/result.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleRange } from "../filesystem/readFileHandleRange.js";
import {
RegularFileCancelled,
RegularFileCleanupFailure,
OwnedRegularFileOperationFailure,
withOwnedFileHandle,
} from "../filesystem/OwnedRegularFile.js";
import { RegularFileOperationCancelled } from "../filesystem/RegularFile.js";
import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js";
import type { KunaProject } from "./KunaProjectCache.js";
import {
@@ -107,12 +108,24 @@ const readSpan = async (
if (!opened.ok) return opened;
const owner = new OwnedFileHandle(opened.value);
let read: Result<string, unknown>;
let spanCancellation: AnalysisCancelledError | undefined;
try {
if (!sameRegularFileState(expected, await owner.handle.stat()))
throw new RegularFileChangedError(path);
const buffer = Buffer.alloc(length);
const { bytesRead } = await owner.handle.read(buffer, 0, length, offset);
if (bytesRead !== length)
const buffer = await readFileHandleRange(
owner.handle,
offset,
length,
(bytesRead) => {
// Preserve a fully acquired span for owned finalization, which records
// cancellation alongside its text if descriptor cleanup fails.
if (bytesRead < length && signal?.aborted)
throw (spanCancellation = new AnalysisCancelledError(operation, {
cause: signal.reason,
}));
},
);
if (buffer.length !== length)
throw new Error(`${path} is shorter than its recorded span`);
if (!sameRegularFileState(expected, await owner.handle.stat()))
throw new RegularFileChangedError(path);
@@ -137,6 +150,12 @@ const readSpan = async (
}),
},
async () => {
if (
!read.ok &&
spanCancellation !== undefined &&
read.error === spanCancellation
)
throw read.error;
if (!read.ok) throw new OwnedRegularFileOperationFailure(read.error);
return read.value;
},
@@ -148,6 +167,8 @@ const readSpan = async (
: await ownership.finishSpan(finish);
return ok(value);
} catch (cause: unknown) {
if (spanCancellation !== undefined && cause === spanCancellation)
return err(spanCancellation);
if (cause instanceof OwnedRegularFileOperationFailure) {
if (cause.operationCause instanceof RegularFileChangedError)
throw cause.operationCause;
@@ -75,3 +75,15 @@ it("returns explicit EOF for the caller to interpret before late cancellation",
}),
).toHaveLength(0);
});
it("reports complete acquisition so the owner can finalize bytes after a late abort", async () => {
const { handle, spy } = await source();
const checkpoints: number[] = [];
const bytes = await readFileHandleRange(handle, 0, 4, (acquired) => {
checkpoints.push(acquired);
if (spy.mock.calls.length >= 2 && acquired < 4)
throw new Error("cancelled before full acquisition");
});
expect(bytes.toString()).toBe("0123");
expect(checkpoints).toEqual([0, 0, 2, 2, 4]);
});
@@ -18,6 +18,7 @@ const fault = vi.hoisted(() => ({
fail: false,
span: "",
short: false,
maximum: Number.POSITIVE_INFINITY,
reads: 0,
entered: 0,
closeEntered: 0,
@@ -51,6 +52,8 @@ vi.mock("node:fs/promises", async (importOriginal) => {
if (key === "read")
return async (...readArgs: unknown[]) => {
fault.entered += 1;
if (typeof readArgs[2] === "number")
readArgs[2] = Math.min(readArgs[2], fault.maximum);
const read = await Reflect.apply(target.read, target, readArgs);
fault.reads += 1;
fault.afterRead();
@@ -79,6 +82,7 @@ afterEach(() => {
fault.fail = false;
fault.span = "";
fault.short = false;
fault.maximum = Number.POSITIVE_INFINITY;
fault.reads = 0;
fault.entered = 0;
fault.closeEntered = 0;
@@ -92,9 +96,13 @@ afterEach(() => {
fault.handles = [];
});
it.each([false, true])(
"preserves the span outcome and retries the exact handle when close fails (short=%s)",
async (short) => {
it.each([
{ short: false, maximum: Number.POSITIVE_INFINITY },
{ short: false, maximum: 3 },
{ short: true, maximum: 3 },
])(
"preserves the span outcome and retries the exact handle when close fails ($short, $maximum)",
async ({ short, maximum }) => {
const { cacheRoot, open: createClient } = await session(
"rea-kuna-span-close-",
);
@@ -113,6 +121,7 @@ it.each([false, true])(
"program.asm",
);
fault.short = short;
fault.maximum = maximum;
fault.fail = true;
const result = await client.execute(
"procedure_assembly",
@@ -126,7 +135,9 @@ it.each([false, true])(
);
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupResources).toContain(fault.span);
expect(fault.reads).toBe(1);
const reads = fault.reads;
if (!short && maximum === 3) expect(reads).toBeGreaterThan(1);
else expect(reads).toBe(1);
expect(fault.closes).toBe(1);
expect(fault.opens).toBe(1);
const handle = fault.handles[0];
@@ -155,7 +166,7 @@ it.each([false, true])(
expect(blocked.ok).toBe(false);
if (blocked.ok) return;
expect(blocked.error.cleanupIncomplete).toBe(true);
expect(fault.reads).toBe(1);
expect(fault.reads).toBe(reads);
expect(fault.opens).toBe(1);
expect(fault.closes).toBeGreaterThan(1);
expect(handle.fd).toBeGreaterThanOrEqual(0);
@@ -284,6 +295,63 @@ it.each(
},
);
it.each([false, true])(
"cancels continued positive short span reads and releases the exact owner (closeFails=%s)",
async (closeFails) => {
const { cacheRoot, open: createClient } = await session(
"rea-kuna-span-short-cancel-",
);
const client = createClient();
try {
await run(client, "procedure_assembly", { procedure: "main" });
const [key] = await readdir(join(cacheRoot, "projects"));
fault.span = join(
cacheRoot,
"projects",
key ?? "",
"project",
"program.asm",
);
fault.maximum = 3;
fault.fail = closeFails;
const controller = new AbortController();
const reason = { requested: "continued span read" };
fault.afterRead = () => {
if (fault.reads === 2) controller.abort(reason);
};
const result = await client.execute(
"procedure_assembly",
{ procedure: "main" },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected continued-read cancellation");
const primary = closeFails ? result.error.cleanupPrimary : result.error;
expect(primary).toBeInstanceOf(AnalysisCancelledError);
expect(primary?.cause).toBe(reason);
expect(fault.reads).toBe(2);
const handle = fault.handles[0];
if (handle === undefined)
throw new Error("Missing cancelled span descriptor");
if (closeFails) {
expect(result.error.cleanupIncomplete).toBe(true);
expect(handle.fd).toBeGreaterThanOrEqual(0);
fault.fail = false;
expect(await client.close()).toEqual({ ok: true, value: null });
expect(result.error.cleanupIncomplete).toBe(false);
}
expect(handle.fd).toBe(-1);
} finally {
fault.fail = false;
fault.afterRead = () => undefined;
fault.span = "";
await client.close();
for (const handle of fault.handles)
if (handle.fd >= 0) await handle.close();
}
},
);
it("does not open a second span while a peer still holds a failed close", async () => {
const { cacheRoot, open: createClient } = await session(
"rea-kuna-span-close-gate-",
@@ -55,6 +55,8 @@ import {
machoImage,
dylibCommand,
LC,
CPU,
fatImage,
} from "../../src/artifacts/apple/MachoImage.fixture.js";
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import { readElectronFuseImage } from "../../src/electron/ElectronFuseReader.js";
@@ -66,6 +68,54 @@ import {
OBSIDIAN_FUSE_BYTES,
} from "../fixtures/electron/fuseImage.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js";
import { elf, pe, dosMz } from "../../src/domain/binaryTarget.fixture.js";
const distantCommands = () => {
const bytes = Buffer.alloc(32 + 4080 + 16);
Buffer.from(machoImage({})).copy(bytes);
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(4096, 20);
bytes.writeUInt32LE(0x7fffffff, 32);
bytes.writeUInt32LE(4080, 36);
bytes.writeUInt32LE(0x7fffffff, 4112);
bytes.writeUInt32LE(16, 4116);
return bytes;
};
it.each([
["ELF", () => elf(2, 1, 62)],
["DOS MZ", () => dosMz()],
[
"PE CodeView",
() => pe(0x8664, 64, 2, false, { pdbPath: "short.pdb", age: 3 }),
],
["distant PE", () => pe(0x8664, 8192)],
["thin command windows", distantCommands],
[
"FAT slice command windows",
() => fatImage([{ cpu: CPU.arm64, bytes: distantCommands() }]),
],
] as const)(
"retains complete %s target admission under positive short reads",
async (_label, fixture) => {
const root = await createTestTempDirectory("rea-target-short-reads-");
const path = join(root, "selected");
await writeFile(path, fixture());
const expected = await parseBinaryTarget(path, {
cwd: root,
hostArchitecture: "arm64",
});
expect(expected.ok).toBe(true);
fault.path = path;
fault.maximum = 3;
expect(
await parseBinaryTarget(path, { cwd: root, hostArchitecture: "arm64" }),
).toEqual(expected);
expect(fault.reads).toBeGreaterThan(5);
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
},
);
beforeEach(() => {
fault.path = "";