mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
Refs #4495. Normalize dylib file access once at the admitted descriptor boundary. Preserve full file identity, physical read outcomes, cancellation provenance and cleanup retry through artifact analysis.
384 lines
14 KiB
TypeScript
384 lines
14 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { execFile } from "node:child_process";
|
|
import { mkdir, rename, writeFile, type FileHandle } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
|
|
const fault = vi.hoisted(() => ({
|
|
path: "",
|
|
handles: [] as FileHandle[],
|
|
blocked: new Set<FileHandle>(),
|
|
blockClose: false,
|
|
readFailure: undefined as Error | undefined,
|
|
controller: undefined as AbortController | undefined,
|
|
abortOnRead: false,
|
|
abortClose: 0,
|
|
closeCount: 0,
|
|
onClose: undefined as (() => Promise<void>) | undefined,
|
|
beforeOpen: undefined as (() => Promise<void>) | undefined,
|
|
onRead: undefined as (() => Promise<void>) | undefined,
|
|
}));
|
|
vi.mock("node:fs/promises", async (importOriginal) => {
|
|
const original = await importOriginal<typeof import("node:fs/promises")>();
|
|
return {
|
|
...original,
|
|
open: async (...args: Parameters<typeof original.open>) => {
|
|
if (args[0] === fault.path) await fault.beforeOpen?.();
|
|
const handle = await original.open(...args);
|
|
if (args[0] !== fault.path) return handle;
|
|
fault.handles.push(handle);
|
|
if (fault.blockClose) fault.blocked.add(handle);
|
|
const read = handle.read.bind(handle);
|
|
handle.read = (async (...readArgs: unknown[]) => {
|
|
if (fault.abortOnRead) fault.controller?.abort(null);
|
|
if (fault.readFailure !== undefined) throw fault.readFailure;
|
|
const result = await Reflect.apply(read, handle, readArgs);
|
|
await fault.onRead?.();
|
|
return result;
|
|
}) as FileHandle["read"];
|
|
const close = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
fault.closeCount += 1;
|
|
await fault.onClose?.();
|
|
if (fault.closeCount === fault.abortClose)
|
|
fault.controller?.abort(null);
|
|
if (fault.blocked.has(handle))
|
|
throw new Error("controlled dyld close failure");
|
|
await close();
|
|
};
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { traceDylibResolution } from "../../src/artifacts/apple/DylibResolutionReader.js";
|
|
import { machoImage } from "../../src/artifacts/apple/MachoImage.fixture.js";
|
|
import { ArtifactFileCleanupFailure } from "../../src/artifacts/ArtifactFileOperation.js";
|
|
import { ArtifactReaderFailure } from "../../src/artifacts/ArtifactReader.js";
|
|
import { ArtifactProvider } from "../../src/artifacts/ArtifactProvider.js";
|
|
import type { AnalysisClient } from "../../src/application/AnalysisProvider.js";
|
|
import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
|
|
const clients: AnalysisClient[] = [];
|
|
|
|
beforeEach(() => {
|
|
fault.path = "";
|
|
fault.handles = [];
|
|
fault.blocked.clear();
|
|
fault.blockClose = false;
|
|
fault.readFailure = undefined;
|
|
fault.controller = undefined;
|
|
fault.abortOnRead = false;
|
|
fault.abortClose = 0;
|
|
fault.closeCount = 0;
|
|
fault.onClose = undefined;
|
|
fault.beforeOpen = undefined;
|
|
fault.onRead = undefined;
|
|
});
|
|
afterEach(async () => {
|
|
fault.blocked.clear();
|
|
fault.onClose = undefined;
|
|
for (const client of clients.splice(0)) await client.close();
|
|
for (const handle of fault.handles)
|
|
if (handle.fd !== -1) await handle.close();
|
|
});
|
|
|
|
const fixture = async () => {
|
|
const root = await createTestTempDirectory("rea-dyld-lifetime-");
|
|
const path = join(root, "Main");
|
|
const bytes = machoImage({});
|
|
await writeFile(path, bytes);
|
|
const target = await parseBinaryTarget(path);
|
|
if (!target.ok) throw target.error;
|
|
const controller = new AbortController();
|
|
fault.path = path;
|
|
fault.controller = controller;
|
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
const client = new ArtifactProvider({}).createClient(target.value);
|
|
clients.push(client);
|
|
return {
|
|
controller,
|
|
client,
|
|
trace: () =>
|
|
traceDylibResolution({
|
|
rootPath: root,
|
|
targetPath: path,
|
|
targetSha256: sha256,
|
|
enumerateRoots: false,
|
|
parameters: { roots: ["Main"] },
|
|
signal: controller.signal,
|
|
}),
|
|
};
|
|
};
|
|
|
|
it("preserves a dyld read failure when the same read observes cancellation", async () => {
|
|
const { trace } = await fixture();
|
|
const cause = Object.assign(new Error("controlled dyld EIO"), {
|
|
code: "EIO",
|
|
});
|
|
fault.readFailure = cause;
|
|
fault.abortOnRead = true;
|
|
await expect(trace()).rejects.toBe(cause);
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it("observes cancellation during the final dyld digest close", async () => {
|
|
const { trace } = await fixture();
|
|
fault.abortClose = 2;
|
|
await expect(trace()).rejects.toMatchObject({
|
|
reason: "cancelled",
|
|
cancellation: { reason: null },
|
|
});
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it.each(["completed", "failed", "cancelled"] as const)(
|
|
"retains the exact dyld descriptor and %s outcome when close fails",
|
|
async (kind) => {
|
|
const { trace } = await fixture();
|
|
fault.blockClose = true;
|
|
const cause = new ArtifactReaderFailure("io", "controlled primary");
|
|
if (kind === "failed") fault.readFailure = cause;
|
|
if (kind === "cancelled") fault.abortClose = 1;
|
|
const failure = await trace().catch((error: unknown) => error);
|
|
expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
if (!(failure instanceof ArtifactFileCleanupFailure))
|
|
throw new Error("Expected exact descriptor cleanup capability");
|
|
expect(failure.fileCleanupFailure.owner.handle).toBe(fault.handles[0]);
|
|
if (kind === "cancelled")
|
|
expect(failure).toMatchObject({
|
|
reason: "cancelled",
|
|
cancellation: { reason: null },
|
|
});
|
|
expect(failure.fileCleanupFailure.outcome.kind).toBe(
|
|
kind === "failed" ? "failed" : "completed",
|
|
);
|
|
if (kind === "failed")
|
|
expect(failure.fileCleanupFailure.outcome).toEqual({
|
|
kind: "failed",
|
|
cause,
|
|
});
|
|
fault.blocked.clear();
|
|
await failure.cleanupLease.release();
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
},
|
|
);
|
|
|
|
it("projects cancellation with cleanup uncertainty when dyld final close fails", async () => {
|
|
const { client, controller } = await fixture();
|
|
fault.abortClose = 1;
|
|
fault.blockClose = true;
|
|
const result = await client.execute(
|
|
"trace_dylib_resolution",
|
|
{ roots: ["Main"] },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected interruption during finalization");
|
|
expect(result.error).toMatchObject({
|
|
reason: "cancelled",
|
|
cause: null,
|
|
cleanupIncomplete: true,
|
|
});
|
|
fault.blocked.clear();
|
|
await expect(client.close()).resolves.toMatchObject({ ok: true });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"retains nonregular admission classification and its exact failed close",
|
|
async () => {
|
|
const { client } = await fixture();
|
|
fault.beforeOpen = async () => {
|
|
fault.beforeOpen = undefined;
|
|
await rename(fault.path, `${fault.path}.old`);
|
|
await mkdir(fault.path);
|
|
};
|
|
fault.blockClose = true;
|
|
const result = await client.execute("trace_dylib_resolution", {
|
|
roots: ["Main"],
|
|
});
|
|
if (result.ok) throw new Error("Expected nonregular admission failure");
|
|
expect(result.error).toMatchObject({
|
|
reason: "format",
|
|
cleanupIncomplete: true,
|
|
});
|
|
expect(result.error.cause).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
expect(fault.handles).toHaveLength(1);
|
|
fault.blocked.clear();
|
|
await expect(client.close()).resolves.toMatchObject({ ok: true });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
},
|
|
);
|
|
|
|
it.each(["EACCES", "EPERM"])(
|
|
"retains %s semantics alongside failed cleanup",
|
|
async (code) => {
|
|
const { client } = await fixture();
|
|
fault.readFailure = Object.assign(
|
|
new Error("controlled permission failure"),
|
|
{ code },
|
|
);
|
|
fault.blockClose = true;
|
|
const result = await client.execute("trace_dylib_resolution", {
|
|
roots: ["Main"],
|
|
});
|
|
if (result.ok) throw new Error("Expected permission failure");
|
|
expect(result.error).toMatchObject({
|
|
reason: "unavailable",
|
|
cleanupIncomplete: true,
|
|
});
|
|
expect(result.error.cause).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
fault.blocked.clear();
|
|
await expect(client.close()).resolves.toMatchObject({ ok: true });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("rejects a substituted pathname after parsing the original descriptor", async () => {
|
|
const { trace } = await fixture();
|
|
fault.onRead = async () => {
|
|
fault.onRead = undefined;
|
|
await rename(fault.path, `${fault.path}.old`);
|
|
await writeFile(fault.path, machoImage({}));
|
|
};
|
|
await expect(trace()).rejects.toMatchObject({ reason: "integrity" });
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
});
|
|
|
|
it.skipIf(process.platform === "win32").each(["magic", "image", "digest"])(
|
|
"rejects a FIFO substituted at the compiled dyld %s open without a writer",
|
|
async (stage) => {
|
|
await fixture();
|
|
const path = fault.path;
|
|
fault.path = "";
|
|
const module = pathToFileURL(
|
|
join(process.cwd(), "dist/artifacts/apple/DylibResolutionReader.js"),
|
|
).href;
|
|
const script = `
|
|
import fs from 'node:fs';
|
|
import { syncBuiltinESMExports } from 'node:module';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import { dirname } from 'node:path';
|
|
const [stage, path] = process.argv.slice(1);
|
|
const sha256 = createHash('sha256').update(await fs.promises.readFile(path)).digest('hex');
|
|
const nativeOpen = fs.promises.open;
|
|
let opens = 0;
|
|
let substituted = false;
|
|
fs.promises.open = async (selected, ...args) => {
|
|
if (selected === path && ++opens === (stage === 'digest' ? 2 : 1)) {
|
|
await fs.promises.rename(path, path + '.old');
|
|
execFileSync('mkfifo', [path]);
|
|
substituted = true;
|
|
}
|
|
return await nativeOpen(selected, ...args);
|
|
};
|
|
syncBuiltinESMExports();
|
|
const { traceDylibResolution } = await import(${JSON.stringify(module)});
|
|
try {
|
|
await traceDylibResolution({ rootPath: dirname(path), targetPath: path,
|
|
targetSha256: sha256, enumerateRoots: stage === 'magic',
|
|
parameters: stage === 'magic' ? {} : { roots: ['Main'] } });
|
|
throw new Error('Unexpected FIFO success');
|
|
} catch (error) {
|
|
if (error.reason !== 'format') throw error;
|
|
process.stdout.write(JSON.stringify({ stage, opens, substituted, reason: error.reason }));
|
|
}
|
|
`;
|
|
const result = await promisify(execFile)(
|
|
process.execPath,
|
|
["--input-type=module", "--eval", script, stage, path],
|
|
{ timeout: 5000 },
|
|
);
|
|
expect(JSON.parse(result.stdout)).toEqual({
|
|
stage,
|
|
opens: stage === "digest" ? 2 : 1,
|
|
substituted: true,
|
|
reason: "format",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("binds both dyld failure and failed provider-close projections to the same descriptor", async () => {
|
|
const { client } = await fixture();
|
|
fault.blockClose = true;
|
|
fault.readFailure = new ArtifactReaderFailure("io", "controlled primary");
|
|
const result = await client.execute("trace_dylib_resolution", {
|
|
roots: ["Main"],
|
|
});
|
|
if (result.ok) throw new Error("Expected failed dyld read");
|
|
expect(result.error).toMatchObject({ reason: "io", cleanupIncomplete: true });
|
|
expect(result.error.cause).toBeInstanceOf(ArtifactFileCleanupFailure);
|
|
const close = await client.close();
|
|
if (close.ok) throw new Error("Expected retained descriptor");
|
|
expect(close.error.cleanupIncomplete).toBe(true);
|
|
expect(fault.handles).toHaveLength(1);
|
|
fault.blocked.clear();
|
|
await expect(client.close()).resolves.toMatchObject({ ok: true });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(close.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
});
|
|
|
|
it("preserves the caller's explicit null cancellation reason through dyld provider projection", async () => {
|
|
const { client, controller } = await fixture();
|
|
fault.abortOnRead = true;
|
|
fault.blockClose = true;
|
|
const result = await client.execute(
|
|
"trace_dylib_resolution",
|
|
{ roots: ["Main"] },
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected cancelled dyld read");
|
|
expect(result.error).toMatchObject({
|
|
reason: "cancelled",
|
|
cleanupIncomplete: true,
|
|
});
|
|
expect(result.error.cause).toBeNull();
|
|
fault.blocked.clear();
|
|
await expect(client.close()).resolves.toMatchObject({ ok: true });
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
expect(fault.handles[0]?.fd).toBe(-1);
|
|
});
|
|
|
|
it("waits for an in-flight final dyld close before completing provider close", async () => {
|
|
const { client, controller } = await fixture();
|
|
let release: () => void = () => {};
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
fault.onClose = async () => {
|
|
if (fault.closeCount === 2) await gate;
|
|
};
|
|
const running = client.execute(
|
|
"trace_dylib_resolution",
|
|
{ roots: ["Main"] },
|
|
{ signal: controller.signal },
|
|
);
|
|
try {
|
|
await expect.poll(() => fault.closeCount).toBe(2);
|
|
let closed = false;
|
|
const closing = client.close().then((result) => {
|
|
closed = true;
|
|
return result;
|
|
});
|
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
expect(closed).toBe(false);
|
|
controller.abort(null);
|
|
release();
|
|
await expect(running).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { reason: "cancelled", cause: null },
|
|
});
|
|
await expect(closing).resolves.toMatchObject({ ok: true });
|
|
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
|
|
} finally {
|
|
release();
|
|
await running;
|
|
}
|
|
});
|