Files
rea/tests/fixtures/nativeCommands.ts
T
Lewis Wang 861e17a3a7 fix(native): admit thin arm64_32 Mach-O targets for static inspection (#5788)
* fix(native): admit thin arm64_32 Mach-O targets for static inspection

Target admission mapped Mach-O CPU types only to x86, x86_64, arm and
arm64, so a thin arm64_32 object was rejected as "unsupported Mach-O
architecture" before list-architectures, inspect-macho or
inspect-signature could run, although the same slice inside a FAT file
was inventoried.

Recognize CPU_TYPE_ARM64_32 as its own Mach-O-only architecture with a
dedicated thin target variant, so ELF, PE and raw-binary paths cannot
carry it. Add its slice policy so 32-bit dispatch metadata refuses it
like other 32-bit Mach-O images instead of reporting a CPU mismatch.
Ghidra, Hopper and IDA have no verified arm64_32 lane and decline it
explicitly; Hopper's declined families now share one predicate.

Fixes #5713

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci(apple): run the thin arm64_32 CLI regression on macOS

The regression compiles an arm64_32 object with Apple clang, so it skips
on the Ubuntu jobs. List it in the macOS Apple artifact job and its
change scope so the job runs it and changes to it select that job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:14:42 +08:00

171 lines
5.3 KiB
TypeScript

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import type {
BinaryTarget,
HeaderArchitecture,
} from "../../src/domain/binaryTargetTypes.js";
import type { InspectSignature } from "../../src/domain/native/nativeInspection.js";
import { ok } from "../../src/domain/result.js";
import type {
NativeCommandCapture,
NativeCommandOptions,
NativeCommandRunner,
} from "../../src/native/CommandRunner.js";
/** Replay captured native command outputs through the production runner seam. */
export class NativeFixtureRunner implements NativeCommandRunner {
constructor(
private readonly overrides: Readonly<Record<string, string>> = {},
private readonly exitCode = 0,
) {}
async run(
tool: string,
arguments_: readonly string[],
_options?: NativeCommandOptions,
) {
const output =
this.overrides[`${tool}:${arguments_[0] ?? ""}`] ??
this.overrides[tool] ??
(await outputFor(tool, arguments_));
return ok({
...capture(tool, arguments_, output),
exitCode: this.exitCode,
});
}
}
const outputFor = async (
tool: string,
arguments_: readonly string[],
): Promise<string> => {
if (tool === "lipo") return nativeFixture("lipo-fat.txt");
if (tool === "otool") return nativeFixture("otool-load.txt");
if (tool === "nm") return "_main\n_$s4Test3fooyyF\n";
if (tool === "dyld_info")
return nativeFixture(
arguments_[0] === "-imports" ? "dyld-imports.txt" : "dyld-exports.txt",
);
if (tool === "dwarfdump")
return `UUID: 01234567-89AB-CDEF-0123-456789ABCDEF (arm64) ${arguments_.at(-1)}\n`;
if (tool === "file")
return arguments_.at(-1)?.endsWith(".plist") === true
? "XML 1.0 document text\n"
: "Mach-O 64-bit executable arm64\n";
if (tool === "vtool") return "Load command 3 LC_BUILD_VERSION\n";
if (tool === "swift-demangle") return nativeFixture("demangle.txt");
if (tool === "plutil") return nativeFixture("plist.json");
if (tool === "codesign") {
if (arguments_.includes("--entitlements"))
return nativeFixture("entitlements.xml");
if (arguments_.includes("-r-"))
return "designated => identifier com.example.fixture\n";
return nativeFixture("codesign.txt");
}
throw new Error(`Unexpected fixture tool ${tool}`);
};
/**
* codesign prints display diagnostics to stderr and requirements or
* entitlements to stdout.
*/
const fixtureStreams = (
tool: string,
arguments_: readonly string[],
output: string,
) => {
const stderr =
tool === "codesign" &&
!arguments_.includes("-r-") &&
!arguments_.includes("--entitlements");
return {
stdout: stderr ? "" : output,
stderr: stderr ? output : "",
stdoutBytes: Buffer.byteLength(stderr ? "" : output),
stderrBytes: Buffer.byteLength(stderr ? output : ""),
};
};
const capture = (
tool: string,
arguments_: readonly string[],
output: string,
): NativeCommandCapture => ({
tool,
executable: `/usr/bin/${tool}`,
executableSha256: "a".repeat(64),
toolVersion: null,
versionReason: "fixture",
arguments: [...arguments_],
...fixtureStreams(tool, arguments_, output),
exitCode: 0,
signal: null,
});
/** Build the Mach-O target identity used by native boundary fixtures. */
export const nativeMachoTarget = (
path: string,
sourcePath?: string,
): BinaryTarget & {
readonly kind: "executable";
readonly format: "mach-o";
readonly architecture: HeaderArchitecture;
} => ({
path,
...(sourcePath === undefined ? {} : { sourcePath }),
sha256: "0".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["x86_64", "arm64"],
});
/** Register the actual bytes of a filesystem-backed native test target. */
export const nativeMachoTargetForFile = async (
path: string,
sourcePath?: string,
): Promise<ReturnType<typeof nativeMachoTarget>> => ({
...nativeMachoTarget(path, sourcePath),
sha256: createHash("sha256")
.update(await readFile(path))
.digest("hex"),
});
/** Load one captured native tool output. */
export const nativeFixture = (name: string): Promise<string> =>
readFile(new URL(`./native-macos/${name}`, import.meta.url), "utf8");
/** Add consistent Mach command-count metadata to a command-only test capture. */
export const withOtoolMachHeader = (output: string): string => {
const count = [...output.matchAll(/^Load command (\d+)$/gmu)].length;
const sizes = [...output.matchAll(/^\s*cmdsize\s+(\d+)\s*$/gmu)].map(
([, size]) => Number(size),
);
if (sizes.length !== count)
throw new TypeError("command-only fixture must provide every cmdsize");
const bytes = sizes.reduce((sum, size) => sum + size, 0);
return [
"Mach header",
" magic cputype cpusubtype caps filetype ncmds sizeofcmds flags",
` 0xfeedfacf 16777228 0 0x00 2 ${count} ${bytes} 0x00000000`,
output,
].join("\n");
};
/** Parsed signature facts for lifecycle controls that do not invoke native tools. */
export const signatureInspectionFixture = (): InspectSignature => ({
signed: true,
identifier: "rea.lifecycle-fixture",
team_identifier: null,
format: "Mach-O thin (arm64)",
cdhashes: ["0123456789abcdef"],
hash_algorithms: ["sha256"],
authorities: [],
designated_requirement: null,
entitlements: null,
timestamp: null,
hardened_runtime: null,
provenance: [],
limitations: ["Controlled parsed facts; no native signing tool invoked."],
});