Files
reverse-engineering-anything/src/artifacts/MachOSliceArtifactReader.test.ts
T
morluto d4415444c4 fix(native): retain processor, hidden-slice and property type facts
Separate AMDGPU processors from feature-qualified note identities. Read Apple hidden ARM64 table rows once and preserve visibility through artifact, dylib and fuse consumers. Keep Objective-C++ template commas within type attributes and incomplete metadata unknown.

Validated provider/parser and production property regressions, Electron CLI/MCP consumers, and native Apple lipo -hideARM64 output through the production slice reader and public artifact inspection. LLVM execution remains unverified.

Fixes #6806

Fixes #6799

Fixes #6801
2026-10-24 01:00:14 +08:00

485 lines
16 KiB
TypeScript

import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream";
import { truncate, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it, onTestFinished, vi } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { CPU, fatImage, machoImage } from "./apple/MachoImage.fixture.js";
import { MachOSliceArtifactReader } from "./MachOSliceArtifactReader.js";
import { err, ok } from "../domain/result.js";
import { openRegularFile } from "../filesystem/RegularFile.js";
import { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
import type { NativeCommandRunner } from "../native/CommandRunner.js";
import { NativeCommandFailure } from "../native/CommandRunner.js";
const cleanupOwner = (runId: string): ProviderProcessSupervisor => {
const child = Object.assign(new EventEmitter(), {
stdout: new PassThrough(),
stderr: new PassThrough(),
exitCode: null as number | null,
signalCode: null as NodeJS.Signals | null,
kill: () => true,
});
return new ProviderProcessSupervisor({
process: child,
ownsProcessLifetime: true,
ownership: {
runId,
leaderPid: 45_000,
processGroupId: 45_000,
expectedParentPid: process.pid,
},
cleanup: async () => ({ cleaned: false, reason: "still running" }),
});
};
describe("Mach-O cleanup exceptions", () => {
it("retains an undefined cleanup rejection as an explicit failure", async () => {
const reader = new MachOSliceArtifactReader(
"/unused/macho",
{},
{
run: async () => {
throw new Error("Unexpected command");
},
retryCleanup: async () => {
throw undefined;
},
},
);
await expect(reader.close()).rejects.toMatchObject({
cleanup: {
reason: "undefined",
resources: ["native-command-process-cleanup"],
},
});
});
});
describe("Mach-O command cleanup lifecycle", () => {
it("waits for active lipo capture before cleanup and preserves its failure", async () => {
const root = await createTestTempDirectory("rea-slices-close-race-");
const binary = join(root, "input");
await writeFile(binary, Buffer.from("fixture"));
let finishCommand!: (
result: Awaited<ReturnType<NativeCommandRunner["run"]>>,
) => void;
let commandStarted!: () => void;
const started = new Promise<void>((resolve) => {
commandStarted = resolve;
});
const command = new Promise<
Awaited<ReturnType<NativeCommandRunner["run"]>>
>((resolve) => {
finishCommand = resolve;
});
let cleanupRetries = 0;
const runner: NativeCommandRunner = {
run: () => {
commandStarted();
return command;
},
retryCleanup: async () => {
cleanupRetries += 1;
if (cleanupRetries > 1) return [];
return [
{
owner: cleanupOwner("run:first"),
reason: "first process group remains live",
resources: ["run:first", "process-group:1234"],
},
{
owner: cleanupOwner("run:second"),
reason: "second process group remains live",
resources: ["run:second", "process-group:5678"],
},
];
},
};
const reader = new MachOSliceArtifactReader(binary, {}, runner);
const iteration = reader.entries();
const firstEntry = iteration[Symbol.asyncIterator]().next();
await started;
const closing = reader.close();
await Promise.resolve();
expect(cleanupRetries).toBe(0);
finishCommand(
err(
new NativeCommandFailure(
"lipo",
"timeout",
null,
undefined,
{
stdout: "partial lipo output",
stderr: "partial lipo diagnostic",
stdoutBytes: 19,
stderrBytes: 23,
exitCode: null,
signal: null,
truncated: false,
},
"capture process group remains unverified",
["capture-group:9999"],
),
),
);
await expect(firstEntry).rejects.toMatchObject({
reason: "unavailable",
cause: expect.objectContaining({
cleanupFailure: "capture process group remains unverified",
}),
cleanup: {
reason: "capture process group remains unverified",
resources: ["capture-group:9999"],
},
});
await expect(closing).rejects.toMatchObject({
cleanup: {
reason:
"run:first, process-group:1234: first process group remains live; run:second, process-group:5678: second process group remains live",
resources: [
"run:first",
"process-group:1234",
"run:second",
"process-group:5678",
],
},
});
await reader.close();
expect(cleanupRetries).toBe(2);
});
});
describe("MachOSliceArtifactReader hidden architectures", () => {
it("retains Apple's hidden ARM64 row separately from the declared FAT count", async () => {
const root = await createTestTempDirectory("rea-hidden-arm64-slices-");
const binary = join(root, "fat");
const armv7 = { type: 12, subtype: 9 };
const armv7Image = machoImage({ cpu: armv7, wide: false });
const arm64Image = machoImage({ cpu: CPU.arm64 });
const bytes = Buffer.from(
fatImage([
{ cpu: armv7, bytes: armv7Image },
{ cpu: CPU.arm64, bytes: arm64Image },
]),
);
bytes.writeUInt32BE(1, 4);
await writeFile(binary, bytes);
const stdout = `fat_magic 0xcafebabe\nnfat_arch 1 (+1 hidden)\narchitecture armv7\n cputype 12\n cpusubtype 9\n offset 4096\n size ${String(armv7Image.length)}\n align 2^12 (4096)\narchitecture arm64 (hidden)\n cputype 16777228\n cpusubtype 0\n offset 8192\n size ${String(arm64Image.length)}\n align 2^12 (4096)\n`;
const reader = new MachOSliceArtifactReader(
binary,
{},
{
run: async () =>
ok({
tool: "lipo",
executable: "/usr/bin/lipo",
executableSha256: "1".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: [],
stdout,
stderr: "",
stdoutBytes: Buffer.byteLength(stdout),
stderrBytes: 0,
exitCode: 0,
signal: null,
}),
},
);
onTestFinished(() => reader.close());
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
expect(entries.map(({ path }) => path)).toEqual([
"slices/armv7",
"slices/arm64",
]);
});
it("rejects a lipo hidden ARM64 claim when its physical header disagrees", async () => {
const root = await createTestTempDirectory("rea-hidden-arm64-mismatch-");
const binary = join(root, "fat");
const armv7 = { type: 12, subtype: 9 };
const armv7Image = machoImage({ cpu: armv7, wide: false });
const arm64Image = machoImage({ cpu: CPU.arm64 });
const bytes = Buffer.from(
fatImage([
{ cpu: armv7, bytes: armv7Image },
{ cpu: CPU.arm64, bytes: arm64Image },
]),
);
bytes.writeUInt32BE(1, 4);
bytes.writeUInt32LE(7, 8196); // Hidden FAT row claims ARM64; thin header says x86.
await writeFile(binary, bytes);
const stdout = `fat_magic 0xcafebabe\nnfat_arch 1 (+1 hidden)\narchitecture armv7\n cputype 12\n cpusubtype 9\n offset 4096\n size ${String(armv7Image.length)}\n align 2^12 (4096)\narchitecture arm64 (hidden)\n cputype 16777228\n cpusubtype 0\n offset 8192\n size ${String(arm64Image.length)}\n align 2^12 (4096)\n`;
const reader = new MachOSliceArtifactReader(
binary,
{},
{
run: async () =>
ok({
tool: "lipo",
executable: "/usr/bin/lipo",
executableSha256: "1".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: [],
stdout,
stderr: "",
stdoutBytes: Buffer.byteLength(stdout),
stderrBytes: 0,
exitCode: 0,
signal: null,
}),
},
);
onTestFinished(() => reader.close());
const enumerate = async (): Promise<void> => {
for await (const _entry of reader.entries()) {
/* enumerate */
}
};
await expect(enumerate()).rejects.toMatchObject({ reason: "integrity" });
});
});
describe("MachOSliceArtifactReader", () => {
it("keeps a borrowed source usable after idle and active cancelled slice reads", async () => {
const root = await createTestTempDirectory("rea-slices-cancelled-");
const binary = join(root, "fat");
const x86 = machoImage({ cpu: CPU.x86_64 });
const arm = Buffer.concat([
Buffer.from(machoImage({ cpu: CPU.arm64 })),
Buffer.alloc(160_000, 0x5a),
]);
await writeFile(
binary,
fatImage([
{ cpu: CPU.x86_64, bytes: x86 },
{ cpu: CPU.arm64, bytes: arm },
]),
);
const handle = await openRegularFile(binary, { symlinks: "reject" });
const source = { handle, initial: await handle.stat() };
const runner: NativeCommandRunner = {
run: () =>
Promise.resolve(
ok({
tool: "lipo",
executable: "/usr/bin/lipo",
executableSha256: "1".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: ["-detailed_info", binary],
stdout: `architecture x86_64\n cputype 16777223\n cpusubtype 3\n offset 4096\n size ${String(x86.length)}\n align 2^12 (4096)\narchitecture arm64\n cputype 16777228\n cpusubtype 0\n offset 8192\n size ${String(arm.length)}\n align 2^12 (4096)\n`,
stderr: "",
stdoutBytes: 1,
stderrBytes: 0,
exitCode: 0,
signal: null,
}),
),
};
const reader = new MachOSliceArtifactReader(binary, {}, runner, source);
onTestFinished(async () => {
await reader.close();
await handle.close();
});
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
const armEntry = entries.find((entry) => entry.path === "slices/arm64");
if (armEntry === undefined) throw new Error("Expected an arm64 slice");
const idleController = new AbortController();
const idle = await reader.open(armEntry, idleController.signal);
const idleFailure = new Promise<Error>((resolve) =>
idle.once("error", resolve),
);
idleController.abort(null);
await expect(idleFailure).resolves.toMatchObject({
reason: "cancelled",
cancellation: { reason: null },
});
expect((await handle.stat()).isFile()).toBe(true);
const controller = new AbortController();
const cancelledRead = await reader.open(armEntry, controller.signal);
const iterator = cancelledRead[Symbol.asyncIterator]();
const firstChunk = await iterator.next();
expect(firstChunk.value).toHaveLength(64 * 1024);
controller.abort();
await expect(iterator.next()).rejects.toMatchObject({
reason: "cancelled",
});
const verifiedEntries = [];
for await (const entry of reader.entries()) verifiedEntries.push(entry);
const verifiedArmEntry = verifiedEntries.find(
(entry) => entry.path === "slices/arm64",
);
if (verifiedArmEntry === undefined)
throw new Error("Expected arm64 after re-verifying the borrowed source");
const verifiedChunks: Buffer[] = [];
const verifiedRead = await reader.open(verifiedArmEntry);
for await (const chunk of verifiedRead)
verifiedChunks.push(Buffer.from(chunk));
expect(Buffer.concat(verifiedChunks)).toEqual(arm);
const truncatedController = new AbortController();
const truncated = await reader.open(
verifiedArmEntry,
truncatedController.signal,
);
await truncate(binary, 8193);
let readEOF = false;
const read = handle.read.bind(handle);
vi.spyOn(handle, "read").mockImplementation((async (...args: unknown[]) => {
const result = (await Reflect.apply(read, handle, args)) as {
bytesRead: number;
buffer: Buffer;
};
if (result.bytesRead === 0) readEOF = true;
return result;
}) as typeof handle.read);
const failed = new Promise<Error>((resolve) =>
truncated.once("error", resolve),
);
truncated.read(0);
await expect.poll(() => readEOF).toBe(true);
await new Promise<void>((resolve) => setImmediate(resolve));
expect(truncated.readableLength).toBeGreaterThan(0);
truncatedController.abort(null);
await expect(failed).resolves.toMatchObject({ reason: "integrity" });
});
});
describe("universal slices outside the structural index", () => {
const PPC = { type: 18, subtype: 0 } as const;
/** A minimal big-endian 32-bit Mach-O header, as a PowerPC slice stores it. */
const bigEndianImage = (): Uint8Array => {
const bytes = Buffer.alloc(32);
bytes.writeUInt32BE(0xfeed_face, 0);
bytes.writeUInt32BE(PPC.type, 4);
bytes.writeUInt32BE(1, 12);
return bytes;
};
/** An arm64 image whose load commands claim more bytes than the slice holds. */
const malformedImage = (): Uint8Array => {
const bytes = machoImage({ cpu: CPU.arm64 });
new DataView(bytes.buffer).setUint32(20, 4096, true);
return bytes;
};
const lipoRunner = (
binary: string,
slices: readonly {
readonly name: string;
readonly cpu: { readonly type: number; readonly subtype: number };
readonly offset: number;
readonly size: number;
}[],
withRanges = true,
): NativeCommandRunner => ({
run: () =>
Promise.resolve(
ok({
tool: "lipo",
executable: "/usr/bin/lipo",
executableSha256: "1".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: ["-detailed_info", binary],
stdout: slices
.map(
({ name, cpu, offset, size }) =>
`architecture ${name}\n cputype ${String(cpu.type)}\n cpusubtype ${String(cpu.subtype)}\n${withRanges ? ` offset ${String(offset)}\n size ${String(size)}\n` : ""} align 2^12 (4096)\n`,
)
.join(""),
stderr: "",
stdoutBytes: 1,
stderrBytes: 0,
exitCode: 0,
signal: null,
}),
),
});
const readAll = async (
slices: readonly {
readonly name: string;
readonly cpu: { readonly type: number; readonly subtype: number };
readonly bytes: Uint8Array;
}[],
withRanges = true,
) => {
const root = await createTestTempDirectory("rea-slices-unsupported-");
const binary = join(root, "fat");
await writeFile(binary, fatImage(slices));
const reader = new MachOSliceArtifactReader(
binary,
{},
lipoRunner(
binary,
slices.map(({ name, cpu, bytes }, index) => ({
name,
cpu,
offset: 4096 * (index + 1),
size: bytes.length,
})),
withRanges,
),
);
onTestFinished(() => reader.close());
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
return entries;
};
it("inventories every lipo range beside a big-endian slice", async () => {
const entries = await readAll([
{ name: "ppc", cpu: PPC, bytes: bigEndianImage() },
{ name: "arm64", cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) },
]);
expect(entries.map(({ path, byteOffset }) => [path, byteOffset])).toEqual([
["slices/ppc", 4096],
["slices/arm64", 8192],
]);
expect(entries[0]?.limitations).toContain(
"Structural Mach-O facts were unsupported (big-endian Mach-O slices are not supported); slice range is based on lipo output.",
);
});
it("reports missing lipo ranges for an unindexed slice as unavailable support", async () => {
await expect(
readAll(
[
{ name: "ppc", cpu: PPC, bytes: bigEndianImage() },
{
name: "arm64",
cpu: CPU.arm64,
bytes: machoImage({ cpu: CPU.arm64 }),
},
],
false,
),
).rejects.toMatchObject({ reason: "unavailable" });
});
it.each([
["before", true],
["after", false],
])(
"rejects a malformed slice %s a big-endian slice",
async (_order, malformedFirst) => {
const ppc = { name: "ppc", cpu: PPC, bytes: bigEndianImage() };
const broken = { name: "arm64", cpu: CPU.arm64, bytes: malformedImage() };
await expect(
readAll(malformedFirst ? [broken, ppc] : [ppc, broken]),
).rejects.toMatchObject({ reason: "integrity" });
},
);
});