fix(ida): refuse universal Mach-O targets in attached profiles (#4765)

Attached IDA binds the open database to the target only through the
recorded whole-input SHA-256, path and image base. Every slice of a
universal Mach-O shares that digest, so an attached database of another
slice passed the identity check and its results were attributed to the
host-selected architecture. Attached admission now reports the target
format as unsupported, with lipo -thin and headless recovery, before any
upstream contact. Headless mode keeps thinning the selected slice.

Fixes #3871

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822andClaude Opus 5.5 authored and GitHub committed 2026-10-17 04:27:46 -07:00
1 parent 8589b7e778
commit 83d2780008
3 files changed
+101 -1

No files matched your search

+1 -1
View File
@@ -41,7 +41,7 @@ rea function /absolute/path/to/program main --json
rea decompile /absolute/path/to/program main --json
```
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows.
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows. Attached profiles, including native mode, refuse universal Mach-O inputs at admission: every slice shares the recorded whole-file hash, so REA cannot verify which slice the GUI loaded. Open a thin file extracted with `lipo -thin` in both IDA and REA, or use headless mode, which analyzes the selected slice.
REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched.
+15
View File
@@ -141,6 +141,21 @@ export class IdaProvider implements AnalysisProviderCandidate {
"IDA does not admit headerless raw binaries. Open the target with Ghidra and supply architecture and entry_point.",
diagnostics,
};
// Attached identity is the recorded whole-input digest, which every slice
// of a universal file shares, so it cannot prove the GUI loaded the slice
// admission selected. Headless mode thins that slice into its own input.
const config = this.#configuration();
if (
target.format === "mach-o-universal" &&
config.ok &&
config.value.mode === "attached"
)
return {
status: "unsupported",
code: "target_format_unsupported",
reason: `Attached IDA records only the whole-file digest of a universal Mach-O, so REA cannot verify that the open database is its ${target.architecture} slice. Extract that slice with lipo -thin and open the thin file in both IDA and REA, or use headless mode.`,
diagnostics: { ...diagnostics, mode: "attached" },
};
return { status: "supported", code: null, reason: null, diagnostics };
}
async resolveAnalysisProfile(
+85
View File
@@ -16,6 +16,12 @@ import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js";
import { EnhancedTools } from "../../src/application/EnhancedTools.js";
import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderCapabilities.js";
import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js";
import { fat } from "../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { BinarySession } from "../../src/application/binary/BinarySession.js";
import { SessionProviderRouter } from "../../src/application/binary/SessionProviderRouter.js";
import { AnalysisProviderRegistry } from "../../src/application/binary/AnalysisProviderRegistry.js";
const roots: string[] = [];
afterEach(async () => {
@@ -185,6 +191,85 @@ describe("IDA provider composition", () => {
});
});
describe("IDA universal Mach-O target identity", () => {
const openUniversal = async (registration: Record<string, unknown>) => {
const root = await createTestTempDirectory("rea-ida-universal-");
roots.push(root);
const input = join(root, "universal");
// x86_64 and arm64 slices share one whole-file digest.
await writeFile(input, fat([0x01000007, 0x0100000c]));
const resolved = await parseBinaryTarget(input);
if (!resolved.ok) throw resolved.error;
const path = join(root, "registration.json");
await writeFile(
path,
JSON.stringify({ ...registration, workspaceRoot: root }),
);
const config = parseConfig({ REA_IDA_MCP_CONFIG: path });
if (!config.ok) throw config.error;
const producer = new RecordingIdaMcp(
resolved.value,
registration.mode === "headless" ? "headless" : "attached",
);
// The production registry consults IDA's own target admission.
const session = new BinarySession(
SessionProviderRouter.selectable(
new AnalysisProviderRegistry([
new IdaProvider(config.value, {}, () => producer),
]),
[],
),
);
return {
producer,
session,
opened: await session.open(input, { providerId: "ida" }),
};
};
it.each([
{ profile: "legacy attached", registration: { command: "fixture" } },
{
profile: "native attached",
registration: {
url: "http://127.0.0.1:13337/mcp",
mode: "attached",
protocol: "native",
},
},
])(
"refuses $profile analysis whose recorded whole-file digest cannot identify the loaded slice",
async ({ registration }) => {
const { producer, session, opened } = await openUniversal(registration);
expect(opened).toMatchObject({
ok: false,
error: {
_tag: "ProviderSelectionError",
reason: "target_unsupported",
rejections: [
{
providerId: "ida",
code: "target_format_unsupported",
reason: expect.stringContaining("lipo -thin"),
},
],
},
});
expect(producer.connects).toBe(0);
expect((await session.close()).ok).toBe(true);
},
);
it("keeps admitting headless analysis, which thins the selected slice", async () => {
const { session, opened } = await openUniversal({
command: "fixture",
mode: "headless",
});
expect(opened.ok).toBe(true);
expect((await session.close()).ok).toBe(true);
});
});
describe("IDA binary_overview execution", () => {
it("uses the bound IDA document and omits unsupported segment coverage", async () => {
const { root, target } = await createIdaTarget();