Files
rea/tests/boundary/cli/foreignUniversalMachoCli.test.ts
T
Muhammad Hamza 6b8f4f7528 fix(native): admit universal Mach-O files without a host slice (#5501)
* fix(native): admit universal Mach-O files without a host slice

FAT Mach-O admission selected only the host's slice and rejected a valid
container whose slices are all foreign, so list-architectures returned
target_unavailable instead of the container's inventory. A thin foreign
image was already admitted.

Prefer the host's slice and otherwise select the first declared supported
slice. Remove the Ghidra installation probe's re-parse under other host
architectures, which existed only to work around the rejection.

Fixes #5404

* fix(native): select the first valid foreign FAT slice

Without a host slice, FAT selection took the first declared slice even when
it was too short for its Mach-O header, rejecting a container whose later
foreign slice is valid. The Ghidra decompiler probe then reported such a
foreign-only decompiler as unknown rather than incompatible. Select the first
foreign slice that can hold its header; a malformed host slice still fails.

* fix(native): select foreign FAT slices from every declaration

Foreign-slice fallback chose among architecture families after keeping only
each family's first declaration, so an undersized first arm64 subtype hid a
valid later arm64 subtype. Select from all declarations, and report the
deduplicated families separately. The host-slice rule is unchanged. Move the
FAT selection tests into their own block.

* fix(native): admit only unambiguous foreign FAT families

With no host slice, a family declared more than once, such as arm64 and
arm64e, has no provider-neutral choice: IDA analyzes the recorded slice while
Hopper prefers its own subtype. Select only families declared exactly once and
keep ambiguous foreign-only containers rejected, as on main.

* fix(ghidra): decide FAT decompiler compatibility from validated families

Slice selection now refuses a foreign-only FAT table that declares one family
more than once, such as arm64 and arm64e, and the decompiler probe reported that
refusal as unknown, so an x64 host accepted an arm64-only decompiler. When
selection fails, read the table's validated CPU families and report the
decompiler incompatible when they exclude the host. Selection and host
exclusion now have separate owners in binaryTarget.ts.
2026-10-23 05:23:34 +08:00

91 lines
2.9 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { fat } from "../../../src/domain/binaryTarget.fixture.js";
import { listArchitecturesSchema } from "../../../src/domain/native/nativeInspection.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const environment = { REA_LOG_LEVEL: "silent" } as const;
// The one slice of each container below is never the host's architecture.
const foreign =
process.arch === "arm64"
? { name: "x86_64", cpuType: 0x01000007 }
: { name: "arm64", cpuType: 0x0100000c };
cliTest.skipIf(process.platform !== "darwin")(
"inventories a universal Mach-O whose only slice is foreign to the host",
async ({ cli, processes }) => {
const directory = await createTestTempDirectory("rea-foreign-fat-cli-");
const thin = join(directory, "thin");
const universal = join(directory, "universal");
const compiled = await processes.run(
"/usr/bin/xcrun",
[
"clang",
"-arch",
foreign.name,
resolve("tests/conformance/c/fixture.c"),
"-o",
thin,
],
{ timeoutMs: 30_000 },
);
expect(compiled.exitCode, compiled.stderr).toBe(0);
const created = await processes.run(
"/usr/bin/xcrun",
["lipo", "-create", thin, "-output", universal],
{ timeoutMs: 30_000 },
);
expect(created.exitCode, created.stderr).toBe(0);
const result = await cli.run({
arguments: ["list-architectures", universal, "--json"],
environment,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = z
.object({
subject: z.object({ format: z.string() }),
normalized_result: listArchitecturesSchema,
})
.parse(result.json);
expect(evidence.subject.format).toBe("mach-o-universal");
expect(evidence.normalized_result.architectures).toMatchObject({
total: 1,
exhaustive: true,
items: [{ name: foreign.name }],
});
expect(
evidence.normalized_result.architectures.items[0]?.file_offset,
).toBeGreaterThan(0);
},
60_000,
);
cliTest.skipIf(process.platform === "darwin")(
"admits a universal Mach-O whose only slice is foreign to the host",
async ({ cli }) => {
const directory = await createTestTempDirectory("rea-foreign-fat-cli-");
const universal = join(directory, "universal");
await writeFile(universal, fat([foreign.cpuType]));
const result = await cli.run({
arguments: ["list-architectures", universal, "--json"],
environment,
});
// The target is admitted; only the macOS-only lipo inventory is refused.
expect(result.exitCode).not.toBe(0);
expect(result.json).toMatchObject({
code: "capability_unavailable",
details: { provider_id: "native-macos", operation: "list_architectures" },
});
},
);