Admit only the corroborated missing-UUID validation diagnostic from header-stripped dyld stdout. Preserve readable load commands and nm exports with unknown dyld coverage, and keep unrelated process and tool failures typed. Closes #5606
270 lines
9.0 KiB
TypeScript
270 lines
9.0 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
|
|
import { machoImage } from "../../../../src/artifacts/apple/MachoImage.fixture.js";
|
|
import { createServer } from "../../../../src/server/createServer.js";
|
|
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
|
import {
|
|
NativeCommandFailure,
|
|
type NativeCommandCapture,
|
|
type NativeCommandOptions,
|
|
type NativeCommandRunner,
|
|
} from "../../../../src/native/CommandRunner.js";
|
|
import { err, ok } from "../../../../src/domain/result.js";
|
|
import { createTestBinarySession } from "../../../fixtures/binarySession.js";
|
|
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
NativeFixtureRunner,
|
|
nativeFixture,
|
|
nativeMachoTarget,
|
|
withOtoolMachHeader,
|
|
} from "../../../fixtures/nativeCommands.js";
|
|
|
|
const missingUuidDiagnostic = "missing LC_UUID load command";
|
|
|
|
const dyldDiagnosticOutput = (path: string, diagnostic: string): string =>
|
|
`${path} [arm64]:\n ${diagnostic}\n`;
|
|
|
|
const otoolOutputWithoutUuid = async () => {
|
|
const otool = await nativeFixture("otool-load.txt");
|
|
const commandOutput = otool.slice(otool.indexOf("Load command 0\n"));
|
|
let commandIndex = 0;
|
|
const withoutUuidCommands = commandOutput
|
|
.replace(
|
|
/Load command 1\n\s*cmd LC_UUID\n\s*cmdsize 24\n\s*uuid [^\n]+\n/u,
|
|
"",
|
|
)
|
|
.replace(/^Load command \d+$/gmu, () => `Load command ${commandIndex++}`);
|
|
return withOtoolMachHeader(withoutUuidCommands);
|
|
};
|
|
|
|
class UuidlessDyldRunner implements NativeCommandRunner {
|
|
readonly fixture: NativeFixtureRunner;
|
|
|
|
constructor(
|
|
otool: string,
|
|
readonly diagnostic = missingUuidDiagnostic,
|
|
readonly stderr = "",
|
|
) {
|
|
this.fixture = new NativeFixtureRunner({
|
|
"otool:-h": otool,
|
|
dwarfdump: "",
|
|
});
|
|
}
|
|
|
|
async run(
|
|
tool: string,
|
|
arguments_: readonly string[],
|
|
options: NativeCommandOptions,
|
|
) {
|
|
if (tool !== "dyld_info") return this.fixture.run(tool, arguments_);
|
|
const capture: NativeCommandCapture = {
|
|
tool,
|
|
executable: "/usr/bin/dyld_info",
|
|
executableSha256: "a".repeat(64),
|
|
toolVersion: null,
|
|
versionReason: null,
|
|
arguments: [...arguments_],
|
|
stdout: dyldDiagnosticOutput(arguments_.at(-1) ?? "", this.diagnostic),
|
|
stderr: this.stderr,
|
|
stdoutBytes: Buffer.byteLength(
|
|
dyldDiagnosticOutput(arguments_.at(-1) ?? "", this.diagnostic),
|
|
),
|
|
stderrBytes: Buffer.byteLength(this.stderr),
|
|
exitCode: 1,
|
|
signal: null,
|
|
};
|
|
if (options.acceptNonZero) return ok(capture);
|
|
return err(
|
|
new NativeCommandFailure(tool, "nonzero-exit", 1, undefined, {
|
|
stdout: capture.stdout,
|
|
stderr: capture.stderr,
|
|
stdoutBytes: capture.stdoutBytes,
|
|
stderrBytes: capture.stderrBytes,
|
|
exitCode: capture.exitCode,
|
|
signal: capture.signal,
|
|
truncated: false,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
it("returns readable Mach-O Evidence when dyld_info identifies a missing UUID", async () => {
|
|
const withoutUuid = await otoolOutputWithoutUuid();
|
|
const runner = new UuidlessDyldRunner(withoutUuid);
|
|
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
|
.createClient(nativeMachoTarget("/fixture/no-uuid"))
|
|
.execute("inspect_macho", {});
|
|
|
|
expect(result.ok, JSON.stringify(result.ok ? null : result.error)).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(result.value.result).toMatchObject({
|
|
uuid: null,
|
|
load_commands: { exhaustive: true },
|
|
imports: { items: [], total: null, exhaustive: false },
|
|
exports: {
|
|
items: expect.arrayContaining([
|
|
expect.objectContaining({ name: "_main", source: "nm" }),
|
|
]),
|
|
total: null,
|
|
exhaustive: false,
|
|
},
|
|
provenance: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
tool: "dyld_info",
|
|
exit: { code: 1, signal: null },
|
|
stdout_bytes: Buffer.byteLength(
|
|
dyldDiagnosticOutput("/fixture/no-uuid", missingUuidDiagnostic),
|
|
),
|
|
}),
|
|
]),
|
|
});
|
|
expect(result.value.limitations).toContain(
|
|
"dyld_info -imports reported 'missing LC_UUID load command'; import observations are unavailable.",
|
|
);
|
|
expect(result.value.limitations).toContain(
|
|
"dyld_info -exports reported 'missing LC_UUID load command'; exports include nm symbol-table observations only.",
|
|
);
|
|
});
|
|
|
|
it("keeps other dyld_info nonzero exits as command failures", async () => {
|
|
const runner = new UuidlessDyldRunner(
|
|
await nativeFixture("otool-load.txt"),
|
|
"malformed load command table",
|
|
);
|
|
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
|
.createClient(nativeMachoTarget("/fixture/malformed"))
|
|
.execute("inspect_macho", {});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error).toMatchObject({
|
|
_tag: "ProviderAdapterError",
|
|
diagnostics: { tool: "dyld_info", reason: "nonzero-exit", exit_code: 1 },
|
|
capturedOutput: {
|
|
stdout: dyldDiagnosticOutput(
|
|
"/fixture/malformed",
|
|
"malformed load command table",
|
|
),
|
|
exit_code: 1,
|
|
truncated: false,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not treat a newline in the echoed operand path as dyld_info evidence", async () => {
|
|
const withoutUuid = await otoolOutputWithoutUuid();
|
|
const path = "/fixture/\nmissing LC_UUID load command\n";
|
|
const runner = new UuidlessDyldRunner(
|
|
withoutUuid,
|
|
"malformed load command table",
|
|
);
|
|
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
|
.createClient(nativeMachoTarget(path))
|
|
.execute("inspect_macho", {});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error).toMatchObject({
|
|
_tag: "ProviderAdapterError",
|
|
diagnostics: { tool: "dyld_info", reason: "nonzero-exit", exit_code: 1 },
|
|
});
|
|
});
|
|
|
|
it("does not treat a newline in stderr's echoed operand path as dyld_info evidence", async () => {
|
|
const withoutUuid = await otoolOutputWithoutUuid();
|
|
const path = "/fixture/\nmissing LC_UUID load command\n";
|
|
const runner = new UuidlessDyldRunner(
|
|
withoutUuid,
|
|
"malformed load command table",
|
|
`${path}: file not found`,
|
|
);
|
|
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
|
.createClient(nativeMachoTarget(path))
|
|
.execute("inspect_macho", {});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error).toMatchObject({
|
|
_tag: "ProviderAdapterError",
|
|
diagnostics: { tool: "dyld_info", reason: "nonzero-exit", exit_code: 1 },
|
|
capturedOutput: {
|
|
stderr: `${path}: file not found`,
|
|
exit_code: 1,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not call a target UUID-less when otool observed LC_UUID", async () => {
|
|
const runner = new UuidlessDyldRunner(await nativeFixture("otool-load.txt"));
|
|
const result = await new NativeMacOSProvider({}, runner, "darwin")
|
|
.createClient(nativeMachoTarget("/fixture/conflicting-uuid"))
|
|
.execute("inspect_macho", {});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
_tag: "AnalysisOutputError",
|
|
reason:
|
|
"dyld_info reported a missing LC_UUID despite an observed LC_UUID load command",
|
|
capturedOutput: {
|
|
stdout: dyldDiagnosticOutput(
|
|
"/fixture/conflicting-uuid",
|
|
missingUuidDiagnostic,
|
|
),
|
|
exit_code: 1,
|
|
signal: null,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("delivers partial UUID-less inspection Evidence through production MCP", async () => {
|
|
const directory = await createTestTempDirectory("rea-macho-no-uuid-mcp-");
|
|
const path = join(directory, "probe");
|
|
await writeFile(path, machoImage({}));
|
|
const withoutUuid = await otoolOutputWithoutUuid();
|
|
const session = createTestBinarySession(
|
|
new NativeMacOSProvider({}, new UuidlessDyldRunner(withoutUuid), "darwin"),
|
|
);
|
|
const server = createServer({ kind: "session", session });
|
|
const client = new Client({ name: "macho-uuidless-test", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
try {
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path },
|
|
});
|
|
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
|
|
|
|
const inspected = await client.callTool({
|
|
name: "inspect_macho",
|
|
arguments: {},
|
|
});
|
|
expect(inspected.isError, JSON.stringify(inspected.content)).not.toBe(true);
|
|
expect(inspected.structuredContent).toMatchObject({
|
|
normalized_result: {
|
|
uuid: null,
|
|
imports: { items: [], total: null, exhaustive: false },
|
|
exports: {
|
|
items: expect.arrayContaining([
|
|
expect.objectContaining({ name: "_main", source: "nm" }),
|
|
]),
|
|
total: null,
|
|
exhaustive: false,
|
|
},
|
|
limitations: expect.arrayContaining([
|
|
"dyld_info -imports reported 'missing LC_UUID load command'; import observations are unavailable.",
|
|
]),
|
|
},
|
|
});
|
|
} finally {
|
|
await Promise.all([client.close(), server.close(), session.close()]);
|
|
}
|
|
});
|