Files
reverse-engineering-anything/src/native/SignatureTargetBinding.test.ts
T
morluto 9e259f87c8 fix(native): retain signature roots until descriptor cleanup completes
Give each signature snapshot a private child-resource scope. Its composite
root lease closes those children before removing the directory and retains
both stages across failed retries. Bind local cleanup failures to their
exact owner so child release cannot clear a failed root removal.

Replace the mixed-outcome regression that expected premature root removal.
Cover real descriptor denial, primary/cancellation preservation, MCP close
and same-connection recovery. Expose underlying real codesign test errors
before asserting result fields.

Fixes #5052
2026-10-19 01:24:40 +08:00

596 lines
19 KiB
TypeScript

import { createHash } from "node:crypto";
import {
mkdtemp,
rm,
stat,
writeFile,
open,
type FileHandle,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it, vi, onTestFinished } from "vitest";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ok, err } from "../domain/result.js";
import type { NativeCommandRunner } from "./CommandRunner.js";
import type { NativeCallTracer } from "./LldbCallTracer.js";
import { NativeMacOSProvider } from "./NativeMacOSProvider.js";
import { withSignatureTarget } from "./SignatureTargetBinding.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { resolvedAnalysisError } from "../domain/analysisErrorBase.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { signatureInspectionFixture } from "../../tests/fixtures/nativeCommands.js";
const unusedInspection = async (): Promise<never> => {
throw new Error("Inspection must not run after failed target binding");
};
const roots: string[] = [];
const targets: string[] = [];
const unusedRunner: NativeCommandRunner = {
async run() {
throw new Error("Changed target must be rejected before a command runs");
},
};
const unusedTracer: NativeCallTracer = {
async trace() {
throw new Error("Signature inspection must not start a trace");
},
async close() {
return ok(null);
},
};
afterEach(async () => {
await Promise.all(
[...roots, ...targets].map((path) =>
rm(path, { recursive: true, force: true }),
),
);
roots.length = 0;
targets.length = 0;
});
const createTarget = async (
bytes: string,
digest = bytes,
): Promise<BinaryTarget> => {
const directory = await mkdtemp(join(tmpdir(), "rea-signature-target-"));
targets.push(directory);
const path = join(directory, "target.bin");
await writeFile(path, bytes);
return {
path,
sha256: createHash("sha256").update(digest).digest("hex"),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
};
};
const createRoot = async (failedAttempts = 0) => {
const path = await mkdtemp(join(tmpdir(), "rea-signature-root-"));
roots.push(path);
let attempts = 0;
return {
path,
async close() {
if (attempts++ < failedAttempts) {
throw new Error("injected root cleanup failure");
}
await rm(path, { recursive: true, force: true });
},
};
};
it("retains a snapshot root after cleanup fails alongside a digest mismatch", async () => {
const target = await createTarget("observed", "expected");
const resources = new ArtifactResourceScope();
const result = await withSignatureTarget(
target,
resources,
unusedInspection,
undefined,
() => createRoot(1),
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupResources).toEqual(expect.arrayContaining(roots));
expect(projectAnalysisError(result.error)).toMatchObject({
details: { diagnostics: { primary_error: { code: "artifact_changed" } } },
});
await resources.close();
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.cleanupResources).toEqual([]);
expect(resolvedAnalysisError(result.error)._tag).toBe(
"AnalysisArtifactChangedError",
);
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
});
it("returns a retained cleanup failure when the next call cannot retry its root", async () => {
const target = await createTarget("observed", "expected");
const resources = new ArtifactResourceScope();
const createFailingRoot = async () => {
const path = await mkdtemp(join(tmpdir(), "rea-signature-root-sticky-"));
roots.push(path);
return {
path,
close: async () => {
throw new Error("still blocked");
},
};
};
const first = await withSignatureTarget(
target,
resources,
unusedInspection,
undefined,
createFailingRoot,
);
expect(first.ok).toBe(false);
const second = await withSignatureTarget(
target,
resources,
unusedInspection,
undefined,
createRoot,
);
expect(second.ok).toBe(false);
if (second.ok) return;
expect(second.error.cleanupIncomplete).toBe(true);
expect(second.error.cleanupResources).toEqual(expect.arrayContaining(roots));
});
it("retries a retained signature root when the provider client closes", async () => {
const target = await createTarget("observed", "expected");
const provider = new NativeMacOSProvider(
{},
unusedRunner,
"darwin",
() => unusedTracer,
() => createRoot(2),
);
const client = provider.createClient(target);
const result = await client.execute("inspect_signature", {});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.cleanupResources).toEqual(expect.arrayContaining(roots));
const firstClose = await client.close();
expect(firstClose).toMatchObject({
ok: false,
error: { cleanupIncomplete: true, cleanupResources: roots },
});
expect((await stat(roots[0]!)).isDirectory()).toBe(true);
await expect(client.close()).resolves.toMatchObject({
ok: true,
value: null,
});
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.cleanupResources).toEqual([]);
if (!firstClose.ok) {
expect(firstClose.error.cleanupIncomplete).toBe(false);
expect(firstClose.error.cleanupResources).toEqual([]);
}
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
});
it("waits for an in-flight signature binding before provider close", async () => {
const target = await createTarget("observed", "expected");
let rootFactoryEntered!: () => void;
let releaseRootFactory!: () => void;
const entered = new Promise<void>(
(resolve) => (rootFactoryEntered = resolve),
);
const gate = new Promise<void>((resolve) => (releaseRootFactory = resolve));
const provider = new NativeMacOSProvider(
{},
unusedRunner,
"darwin",
() => unusedTracer,
async () => {
rootFactoryEntered();
await gate;
return createRoot();
},
);
const client = provider.createClient(target);
const operation = client.execute("inspect_signature", {});
await entered;
let closeSettled = false;
const closing = client.close().finally(() => {
closeSettled = true;
});
await Promise.resolve();
expect(closeSettled).toBe(false);
releaseRootFactory();
await expect(operation).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisArtifactChangedError" },
});
await expect(closing).resolves.toMatchObject({ ok: true, value: null });
expect(closeSettled).toBe(true);
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
});
it("releases retained descriptors before retrying their containing root", async () => {
const target = await createTarget("observed", "expected");
const resources = new ArtifactResourceScope();
const original = OwnedFileHandle.prototype.close;
let blocked: OwnedFileHandle | undefined;
let attempts = 0;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(function (this: OwnedFileHandle) {
blocked ??= this;
if (this === blocked && ++attempts <= 2)
return Promise.reject(
new Error("controlled snapshot descriptor close failure"),
);
return original.call(this);
});
onTestFinished(async () => {
close.mockRestore();
await resources.close();
});
const inspect = vi.fn(unusedInspection);
const first = await withSignatureTarget(
target,
resources,
inspect,
undefined,
() => createRoot(1),
);
expect(first.ok).toBe(false);
if (first.ok) throw new Error("Expected both retained owners");
const snapshot = join(roots[0]!, "target.bin");
expect(new Set(first.error.cleanupResources)).toEqual(
new Set([snapshot, roots[0]!]),
);
const retried = await withSignatureTarget(
target,
resources,
inspect,
undefined,
createRoot,
);
expect(retried.ok).toBe(false);
if (retried.ok) throw new Error("Expected retained root retry failure");
expect(first.error.cleanupIncomplete).toBe(true);
expect(first.error.cleanupResources).toEqual([roots[0]!]);
expect(retried.error.cleanupResources).toEqual([roots[0]!]);
expect(roots).toHaveLength(1);
expect(inspect).not.toHaveBeenCalled();
expect((await stat(roots[0]!)).isDirectory()).toBe(true);
await resources.close();
expect(attempts).toBe(3);
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
for (const failure of [first.error, retried.error]) {
expect(failure.cleanupIncomplete).toBe(false);
expect(failure.cleanupResources).toEqual([]);
}
expect(resolvedAnalysisError(first.error)._tag).toBe(
"AnalysisArtifactChangedError",
);
});
it("retains the snapshot root until its failed descriptor owner confirms release", async () => {
const target = await createTarget("observed", "expected");
const resources = new ArtifactResourceScope();
const original = OwnedFileHandle.prototype.close;
let blocked: OwnedFileHandle | undefined;
let allowClose = false;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(function (this: OwnedFileHandle) {
blocked ??= this;
if (this === blocked && !allowClose)
return Promise.reject(new Error("snapshot descriptor remains open"));
return original.call(this);
});
const root = await createRoot();
const removeRoot = vi.fn(root.close);
onTestFinished(async () => {
allowClose = true;
await resources.close();
close.mockRestore();
});
const result = await withSignatureTarget(
target,
resources,
unusedInspection,
undefined,
async () => ({ path: root.path, close: removeRoot }),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected retained snapshot descriptor");
expect(removeRoot).not.toHaveBeenCalled();
expect((await stat(root.path)).isDirectory()).toBe(true);
const retry = await resources.close().then(
() => undefined,
(cause: unknown) => cause,
);
expect(retry).toBeDefined();
expect(removeRoot).not.toHaveBeenCalled();
expect((await stat(root.path)).isDirectory()).toBe(true);
expect(result.error.cleanupIncomplete).toBe(true);
allowClose = true;
await resources.close();
expect(removeRoot).toHaveBeenCalledTimes(1);
expect(result.error.cleanupIncomplete).toBe(false);
expect(result.error.cleanupResources).toEqual([]);
expect(resolvedAnalysisError(result.error)._tag).toBe(
"AnalysisArtifactChangedError",
);
await expect(stat(root.path)).rejects.toMatchObject({ code: "ENOENT" });
});
const opaqueReasons = [
null,
0,
new ProviderCleanupError("foreign", ["unowned"], {}),
];
for (const cleanupFails of [false, true])
it.each(opaqueReasons)(
`retains completed signature facts and exact reason %s during root cleanup (fails: ${cleanupFails})`,
async (reason) => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const facts = signatureInspectionFixture();
const root = await createRoot(cleanupFails ? 1 : 0);
const close = root.close;
const result = await withSignatureTarget(
target,
resources,
async () => ok(facts),
controller.signal,
async () => ({
path: root.path,
async close() {
try {
await close();
} finally {
controller.abort(reason);
}
},
}),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected finalization cancellation");
expect(result.error.partialObservation).toEqual(facts);
expect(result.error.cleanupIncomplete).toBe(cleanupFails);
expect(result.error.cleanupResources).not.toContain("unowned");
await resources.close();
const primary = resolvedAnalysisError(result.error);
expect(primary._tag).toBe("AnalysisCancelledError");
expect(primary.cause).toBe(reason);
expect(projectAnalysisError(primary)).toMatchObject({
code: "cancelled",
details: { partial_observation: facts },
});
},
);
for (const ownerIndex of [0, 1])
for (const cleanupFails of [false, true])
it(`does not inspect after cancellation during descriptor ${ownerIndex} cleanup (fails: ${cleanupFails})`, async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const reason = { stage: "descriptor-close" };
const original = OwnedFileHandle.prototype.close;
let calls = 0;
let blocked: OwnedFileHandle | undefined;
let allowClose = false;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
const cancels = calls++ === ownerIndex;
if (cancels && cleanupFails) blocked = this;
try {
if (this === blocked && !allowClose)
throw new Error("controlled close failure");
await original.call(this);
} finally {
if (cancels) controller.abort(reason);
}
});
onTestFinished(async () => {
close.mockRestore();
await resources.close();
});
const inspect = vi.fn(async () => ok(signatureInspectionFixture()));
const result = await withSignatureTarget(
target,
resources,
inspect,
controller.signal,
createRoot,
);
expect(result.ok).toBe(false);
if (result.ok)
throw new Error("Expected descriptor finalization cancellation");
expect(inspect).not.toHaveBeenCalled();
expect(result.error.cleanupIncomplete).toBe(cleanupFails);
allowClose = true;
await resources.close();
expect(resolvedAnalysisError(result.error)).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
});
it("keeps a physical source read failure primary when the caller also aborts", async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const probe = await open(target.path, "r");
const prototype = Object.getPrototypeOf(probe) as FileHandle;
await probe.close();
const failure = new Error("controlled physical read failure");
const read = vi.spyOn(prototype, "read").mockImplementationOnce(async () => {
controller.abort({ stage: "source-read" });
throw failure;
});
onTestFinished(async () => {
read.mockRestore();
await resources.close();
});
const inspect = vi.fn(async () => ok(signatureInspectionFixture()));
const result = await withSignatureTarget(
target,
resources,
inspect,
controller.signal,
createRoot,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected physical failure");
expect(result.error._tag).toBe("ProviderAdapterError");
expect(result.error.cause).toBe(failure);
expect(inspect).not.toHaveBeenCalled();
});
for (const cleanupFails of [false, true])
it(`keeps a failed inspection primary when the caller aborts during root close (fails: ${cleanupFails})`, async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const primary = new AnalysisOutputError(
"inspect_signature",
"controlled inspection failure",
);
const root = await createRoot(cleanupFails ? 1 : 0);
const result = await withSignatureTarget(
target,
resources,
async () => err(primary),
controller.signal,
async () => ({
path: root.path,
async close() {
controller.abort(null);
await root.close();
},
}),
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected failed inspection");
expect(result.error.cleanupIncomplete).toBe(cleanupFails);
await resources.close();
expect(resolvedAnalysisError(result.error)).toBe(primary);
});
it.each(opaqueReasons)(
"preserves pre-entry reason %s without allocating a root or adopting foreign ownership",
async (reason) => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const makeRoot = vi.fn(createRoot);
const result = await withSignatureTarget(
target,
resources,
unusedInspection,
AbortSignal.abort(reason),
makeRoot,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected pre-entry cancellation");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
cleanupIncomplete: false,
});
expect(result.error.cleanupResources).toEqual([]);
expect(makeRoot).not.toHaveBeenCalled();
await resources.close();
},
);
it("does not replace a failed root creation with a simultaneous abort", async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const failure = new Error("controlled root creation failure");
const result = await withSignatureTarget(
target,
resources,
unusedInspection,
controller.signal,
async () => {
controller.abort(null);
throw failure;
},
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected root creation failure");
expect(result.error).toMatchObject({
_tag: "ProviderAdapterError",
cause: failure,
cleanupIncomplete: false,
});
await resources.close();
});
it("releases a late root when cancellation arrives during creation", async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const reason = { stage: "root-creation" };
const result = await withSignatureTarget(
target,
resources,
unusedInspection,
controller.signal,
async () => {
const root = await createRoot();
controller.abort(reason);
return root;
},
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected root handoff cancellation");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
cleanupIncomplete: false,
});
await expect(stat(roots[0]!)).rejects.toMatchObject({ code: "ENOENT" });
await resources.close();
});
it("does not adopt an inspection's arbitrary thrown cleanup marker", async () => {
const target = await createTarget("same");
const resources = new ArtifactResourceScope();
const marker = new ProviderCleanupError("foreign", ["unowned"], {});
const result = await withSignatureTarget(
target,
resources,
async () => {
throw marker;
},
undefined,
createRoot,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected opaque producer failure");
expect(result.error).toMatchObject({
_tag: "AnalysisOutputError",
cause: marker,
cleanupIncomplete: false,
});
expect(result.error.cleanupResources).toEqual([]);
await resources.close();
});