mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626c7b422a | ||
|
|
678cd82042 |
No files matched your search
@@ -799,6 +799,7 @@ jobs:
|
|||||||
tests/boundary/filesystem/installScript.test.ts
|
tests/boundary/filesystem/installScript.test.ts
|
||||||
tests/boundary/filesystem/nibNativeHierarchy.test.ts
|
tests/boundary/filesystem/nibNativeHierarchy.test.ts
|
||||||
tests/boundary/filesystem/interfaceBuilderBundle.test.ts
|
tests/boundary/filesystem/interfaceBuilderBundle.test.ts
|
||||||
|
tests/acceptance/nativeUiHandlerLocation.test.ts
|
||||||
tests/boundary/cli/nativePlistCli.test.ts
|
tests/boundary/cli/nativePlistCli.test.ts
|
||||||
tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts
|
tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts
|
||||||
tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||||
|
|||||||
@@ -378,7 +378,9 @@ bounded typed call references. Direct references, resolved indirect references,
|
|||||||
ambiguous candidates, inferred untyped provider callees and targetless call
|
ambiguous candidates, inferred untyped provider callees and targetless call
|
||||||
sites remain distinct. A static route does not establish runtime reachability.
|
sites remain distinct. A static route does not establish runtime reachability.
|
||||||
Swift/closure dispatch requires resolved static references; unavailable ABI
|
Swift/closure dispatch requires resolved static references; unavailable ABI
|
||||||
forms remain unknown.
|
forms remain unknown. A handler node's `location` pairs the implementation
|
||||||
|
address with its `implementation_file_offset`, or an unknown offset; the method
|
||||||
|
record's own location remains in the node's Evidence.
|
||||||
|
|
||||||
Incomplete reference kinds do not discard known typed calls or partial
|
Incomplete reference kinds do not discard known typed calls or partial
|
||||||
provider call references. A `provider_call` with `observed` resolution retains
|
provider call references. A `provider_call` with `observed` resolution retains
|
||||||
@@ -835,6 +837,11 @@ complete observation.
|
|||||||
non-generic, non-resilient class vtable descriptors. Records include exact virtual addresses/file offsets and
|
non-generic, non-resilient class vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||||
start after the conformance header. Names unavailable in metadata remain null.
|
start after the conformance header. Names unavailable in metadata remain null.
|
||||||
|
An Objective-C implementation's `location` is its method-list entry; the
|
||||||
|
reader maps `implementation_address` separately into
|
||||||
|
`implementation_file_offset`, which uses the same file coordinates as
|
||||||
|
`location.file_offset`, including a universal slice's container offset.
|
||||||
|
Unmapped implementations and symbol-only records keep it null.
|
||||||
The selected Mach-O header must match the requested CPU family for both thin
|
The selected Mach-O header must match the requested CPU family for both thin
|
||||||
files and universal slices before metadata decoding. CPU subtypes and
|
files and universal slices before metadata decoding. CPU subtypes and
|
||||||
capability bits remain source facts; an `arm64` request may select an
|
capability bits remain source facts; an `arm64` request may select an
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ const PATH_SCOPES = {
|
|||||||
"tests/boundary/filesystem/installScript.test.ts",
|
"tests/boundary/filesystem/installScript.test.ts",
|
||||||
"tests/boundary/filesystem/nibNativeHierarchy.test.ts",
|
"tests/boundary/filesystem/nibNativeHierarchy.test.ts",
|
||||||
"tests/boundary/filesystem/interfaceBuilderBundle.test.ts",
|
"tests/boundary/filesystem/interfaceBuilderBundle.test.ts",
|
||||||
|
// Compiled UI handler joins: metadata reader, graph and trace workflow.
|
||||||
|
"src/domain/native/objcSwiftMetadata.ts",
|
||||||
|
"src/domain/native/nativeInvestigationGraph.ts",
|
||||||
|
"src/application/native/NativeUiActionTrace.ts",
|
||||||
|
"tests/acceptance/nativeUiHandlerLocation.test.ts",
|
||||||
"tests/boundary/cli/nativePlistCli.test.ts",
|
"tests/boundary/cli/nativePlistCli.test.ts",
|
||||||
"tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts",
|
"tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts",
|
||||||
"tests/boundary/cli/nativePlistSelectionFailureCli.test.ts",
|
"tests/boundary/cli/nativePlistSelectionFailureCli.test.ts",
|
||||||
|
|||||||
@@ -257,6 +257,90 @@ describe("Interface Builder dispatch joins", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Interface Builder handler locations", () => {
|
||||||
|
const record = { address: "0x100000798", file_offset: 34712 };
|
||||||
|
const handlerFor = (implementationFileOffset?: number | null) => {
|
||||||
|
const ui = buildInterfaceBuilderAnalysis({
|
||||||
|
targetSha256: "b".repeat(64),
|
||||||
|
toolVersion: "test",
|
||||||
|
documents: [
|
||||||
|
{
|
||||||
|
relativePath: "Contents/Resources/Main.nib",
|
||||||
|
archiveSha256: "c".repeat(64),
|
||||||
|
documentKind: "nib",
|
||||||
|
raw: {
|
||||||
|
"com.apple.ibtool.document.objects": {
|
||||||
|
controller: { customClass: "AuditController" },
|
||||||
|
button: { class: "NSButton", title: "Run" },
|
||||||
|
},
|
||||||
|
"com.apple.ibtool.document.connections": {
|
||||||
|
button: [
|
||||||
|
{
|
||||||
|
type: "action",
|
||||||
|
label: "runAction:",
|
||||||
|
destinationId: "controller",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||||
|
});
|
||||||
|
const metadata = objcSwiftMetadataSchema.parse({
|
||||||
|
objc_dispatch_implementations: [
|
||||||
|
{
|
||||||
|
class_name: "AuditController",
|
||||||
|
selector: "runAction:",
|
||||||
|
method_type: "instance",
|
||||||
|
implementation_address: "0x10000077c",
|
||||||
|
...(implementationFileOffset === undefined
|
||||||
|
? {}
|
||||||
|
: { implementation_file_offset: implementationFileOffset }),
|
||||||
|
location: record,
|
||||||
|
decode: { status: "decoded", reason: null },
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
kind: "binary_metadata",
|
||||||
|
description: "Encoded relative Objective-C method entry",
|
||||||
|
location: record,
|
||||||
|
artifact_path: "/fixture/Audit.app/Contents/MacOS/Audit",
|
||||||
|
artifact_sha256: "b".repeat(64),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
db_save_result: null,
|
||||||
|
});
|
||||||
|
return joinInterfaceBuilderDispatch(ui.graph, metadata).nodes.find(
|
||||||
|
({ id }) => id === "native:function:0x10000077c",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ label: "mapped selected-slice", offset: 34684, expected: 34684 },
|
||||||
|
{ label: "unmapped", offset: null, expected: null },
|
||||||
|
{
|
||||||
|
label: "older Evidence without a mapped",
|
||||||
|
offset: undefined,
|
||||||
|
expected: null,
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"pairs the handler address only with its $label implementation offset",
|
||||||
|
({ offset, expected }) => {
|
||||||
|
const handler = handlerFor(offset);
|
||||||
|
expect(handler?.location).toEqual({
|
||||||
|
address: "0x10000077c",
|
||||||
|
file_offset: expected,
|
||||||
|
});
|
||||||
|
// The method record keeps its own coordinates as Evidence.
|
||||||
|
expect(handler?.evidence.map(({ location }) => location)).toEqual([
|
||||||
|
record,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Objective-C category ownership joins", () => {
|
describe("Objective-C category ownership joins", () => {
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -389,9 +389,11 @@ export const joinInterfaceBuilderDispatch = (
|
|||||||
id: functionId,
|
id: functionId,
|
||||||
kind: "function",
|
kind: "function",
|
||||||
name: `${implementation.class_name} ${selector}`,
|
name: `${implementation.class_name} ${selector}`,
|
||||||
|
// `implementation.location` is the method record, which stays in its
|
||||||
|
// Evidence. Only the reader's mapping of the code address locates it.
|
||||||
location: {
|
location: {
|
||||||
address: implementation.implementation_address,
|
address: implementation.implementation_address,
|
||||||
file_offset: implementation.location.file_offset,
|
file_offset: implementation.implementation_file_offset ?? null,
|
||||||
},
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
class_name: implementation.class_name,
|
class_name: implementation.class_name,
|
||||||
|
|||||||
@@ -139,10 +139,12 @@ describe("symbol-only Objective-C implementation addresses", () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric);
|
expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric);
|
||||||
|
// A provider symbol names an address; it does not map it into the file.
|
||||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||||
implementation_address: canonical,
|
implementation_address: canonical,
|
||||||
location: { address: canonical },
|
implementation_file_offset: null,
|
||||||
evidence: [{ location: { address: source } }],
|
location: { address: canonical, file_offset: null },
|
||||||
|
evidence: [{ location: { address: source, file_offset: null } }],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ export const objcDispatchImplementationSchema = z.strictObject({
|
|||||||
selector: z.string().min(1),
|
selector: z.string().min(1),
|
||||||
method_type: z.enum(["instance", "class"]),
|
method_type: z.enum(["instance", "class"]),
|
||||||
implementation_address: z.string().nullable(),
|
implementation_address: z.string().nullable(),
|
||||||
|
/**
|
||||||
|
* File offset of `implementation_address`, in the same artifact coordinates
|
||||||
|
* as `location.file_offset`. Null when no reader mapped the implementation;
|
||||||
|
* absent in older Evidence. `location` is the method record, not the code.
|
||||||
|
*/
|
||||||
|
implementation_file_offset: z.int().nonnegative().nullable().optional(),
|
||||||
/** Category that adds this method, when it is not defined by the class itself. */
|
/** Category that adds this method, when it is not defined by the class itself. */
|
||||||
category: z.string().nullable().optional(),
|
category: z.string().nullable().optional(),
|
||||||
location: nativeMetadataLocationSchema,
|
location: nativeMetadataLocationSchema,
|
||||||
@@ -598,6 +604,7 @@ export const inspectNativeDispatchMetadata = (
|
|||||||
selector,
|
selector,
|
||||||
method_type: methodType,
|
method_type: methodType,
|
||||||
implementation_address: address,
|
implementation_address: address,
|
||||||
|
implementation_file_offset: null,
|
||||||
location: { address, file_offset: null },
|
location: { address, file_offset: null },
|
||||||
decode: { status: "partial", reason: "symbol_name_only" },
|
decode: { status: "partial", reason: "symbol_name_only" },
|
||||||
evidence,
|
evidence,
|
||||||
|
|||||||
@@ -1212,11 +1212,13 @@ describe("Apple dispatch binary metadata", () => {
|
|||||||
},
|
},
|
||||||
{ name: "Fixture", is_meta_class: true },
|
{ name: "Fixture", is_meta_class: true },
|
||||||
]);
|
]);
|
||||||
|
// The method entry and its code are distinct locations in one image.
|
||||||
expect(result.objc_dispatch_implementations).toMatchObject([
|
expect(result.objc_dispatch_implementations).toMatchObject([
|
||||||
{
|
{
|
||||||
selector: "performAction:",
|
selector: "performAction:",
|
||||||
implementation_address: "0x100000700",
|
implementation_address: "0x100000700",
|
||||||
location: { file_offset: 0x488 },
|
implementation_file_offset: 0x700,
|
||||||
|
location: { address: "0x100000488", file_offset: 0x488 },
|
||||||
decode: { status: "decoded" },
|
decode: { status: "decoded" },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -1315,6 +1317,8 @@ describe("Apple dispatch coverage ownership", () => {
|
|||||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||||
implementation_address: null,
|
implementation_address: null,
|
||||||
|
implementation_file_offset: null,
|
||||||
|
location: { address: "0x100000488", file_offset: 0x488 },
|
||||||
decode: { status: "partial" },
|
decode: { status: "partial" },
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
@@ -1387,6 +1391,8 @@ describe("Apple dispatch coverage ownership", () => {
|
|||||||
category: "FixtureCategory",
|
category: "FixtureCategory",
|
||||||
class_name: "(FixtureCategory)",
|
class_name: "(FixtureCategory)",
|
||||||
implementation_address: "0x100000700",
|
implementation_address: "0x100000700",
|
||||||
|
implementation_file_offset: 0x700,
|
||||||
|
location: { address: "0x100000b88", file_offset: 0xb88 },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
@@ -1951,6 +1957,7 @@ describe("universal Apple dispatch metadata", () => {
|
|||||||
});
|
});
|
||||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||||
implementation_address: "0x100000700",
|
implementation_address: "0x100000700",
|
||||||
|
implementation_file_offset: offset + 0x700,
|
||||||
location: { file_offset: offset + 0x488 },
|
location: { file_offset: offset + 0x488 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2035,6 +2042,7 @@ describe("byte-swapped universal Apple dispatch metadata", () => {
|
|||||||
});
|
});
|
||||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||||
implementation_address: "0x100000700",
|
implementation_address: "0x100000700",
|
||||||
|
implementation_file_offset: 256 + 0x700,
|
||||||
location: { file_offset: 256 + 0x488 },
|
location: { file_offset: 256 + 0x488 },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export const decodeObjcDispatchFacets = (input: {
|
|||||||
method_type: methodType,
|
method_type: methodType,
|
||||||
...(category === undefined ? {} : { category }),
|
...(category === undefined ? {} : { category }),
|
||||||
implementation_address: implementationLocation?.address ?? null,
|
implementation_address: implementationLocation?.address ?? null,
|
||||||
|
implementation_file_offset: implementationLocation?.file_offset ?? null,
|
||||||
location: read.location(entry),
|
location: read.location(entry),
|
||||||
decode:
|
decode:
|
||||||
implementationLocation === null
|
implementationLocation === null
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { buildBinary } from "plist";
|
||||||
|
import { describe, expect } from "vitest";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { nativeInvestigationTraceSchema } from "../../src/domain/native/nativeInvestigationGraph.js";
|
||||||
|
import { nativeDispatchMetadataResultSchema } from "../../src/domain/native/objcSwiftMetadata.js";
|
||||||
|
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||||
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||||
|
import { cliTest } from "../support/cli/cliFixture.js";
|
||||||
|
import type { TestProcesses } from "../support/process/processFixture.js";
|
||||||
|
|
||||||
|
const HANDLER = "-[AuditController runAction:]";
|
||||||
|
const SOURCE = `#import <Cocoa/Cocoa.h>
|
||||||
|
__attribute__((noinline)) void audit_tail_sink(id value) { __asm__ volatile("" : : "r"(value) : "memory"); }
|
||||||
|
@interface AuditController : NSViewController
|
||||||
|
- (IBAction)runAction:(id)sender;
|
||||||
|
@end
|
||||||
|
@implementation AuditController
|
||||||
|
- (IBAction)runAction:(id)sender { audit_tail_sink(sender); }
|
||||||
|
@end
|
||||||
|
int main(void) { return 0; }
|
||||||
|
`;
|
||||||
|
/** Keep provider selection on REA's own Interface Builder and Mach-O readers. */
|
||||||
|
const LOCAL_READERS = {
|
||||||
|
REA_LOG_LEVEL: "silent",
|
||||||
|
REA_ANALYSIS_PROVIDER: "auto",
|
||||||
|
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||||
|
GHIDRA_INSTALL_DIR: "/rea-unconfigured-deep-provider/ghidra",
|
||||||
|
JAVA_HOME: "/rea-unconfigured-deep-provider/java",
|
||||||
|
REA_IDA_MCP_CONFIG: "/rea-unconfigured-deep-provider/ida-mcp.json",
|
||||||
|
REA_KUNA_COMMAND: "/rea-unconfigured-deep-provider/kuna",
|
||||||
|
REA_KUNA_CACHE_DIR: "/rea-unconfigured-deep-provider/kuna-cache",
|
||||||
|
};
|
||||||
|
const evidenceOf = <Schema extends z.ZodType>(schema: Schema) =>
|
||||||
|
z.object({
|
||||||
|
subject: z.object({ architecture: z.string() }),
|
||||||
|
normalized_result: schema,
|
||||||
|
});
|
||||||
|
const traceEvidence = evidenceOf(nativeInvestigationTraceSchema);
|
||||||
|
const metadataEvidence = evidenceOf(nativeDispatchMetadataResultSchema);
|
||||||
|
|
||||||
|
/** Bundle a compiled handler with a keyed-archive NIB action wired to it. */
|
||||||
|
const writeApp = async (root: string, executable: string) => {
|
||||||
|
const app = join(root, "Audit.app");
|
||||||
|
const contents = join(app, "Contents");
|
||||||
|
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||||
|
await mkdir(join(contents, "Resources"));
|
||||||
|
await copyFile(executable, join(contents, "MacOS", "Audit"));
|
||||||
|
await writeFile(
|
||||||
|
join(contents, "Info.plist"),
|
||||||
|
"<plist><dict><key>CFBundleExecutable</key><string>Audit</string></dict></plist>",
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
join(contents, "Resources", "Main.nib"),
|
||||||
|
buildBinary({
|
||||||
|
$archiver: "NSKeyedArchiver",
|
||||||
|
$version: 100000,
|
||||||
|
$objects: [
|
||||||
|
"$null",
|
||||||
|
{ $class: { UID: 4 } },
|
||||||
|
{ $class: { UID: 5 }, NSClassName: "AuditController" },
|
||||||
|
{
|
||||||
|
$class: { UID: 6 },
|
||||||
|
NSSource: { UID: 1 },
|
||||||
|
NSDestination: { UID: 2 },
|
||||||
|
NSLabel: "runAction:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$classname: "NSButton",
|
||||||
|
$classes: ["NSButton", "NSControl", "NSView", "NSObject"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$classname: "NSCustomObject",
|
||||||
|
$classes: ["NSCustomObject", "NSObject"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$classname: "NSNibControlConnector",
|
||||||
|
$classes: ["NSNibControlConnector", "NSNibConnector", "NSObject"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
$top: { root: { UID: 1 } },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return { app, executable: join(contents, "MacOS", "Audit") };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Map the handler symbol through Apple's own symbol, load-command and FAT readers. */
|
||||||
|
const independentHandlerLocation = async (
|
||||||
|
processes: TestProcesses,
|
||||||
|
executable: string,
|
||||||
|
architecture: string,
|
||||||
|
) => {
|
||||||
|
const tool = async (...arguments_: string[]) => {
|
||||||
|
const result = await processes.run("/usr/bin/xcrun", arguments_, {
|
||||||
|
timeoutMs: 30_000,
|
||||||
|
});
|
||||||
|
expect(result.exitCode, result.stderr).toBe(0);
|
||||||
|
return result.stdout;
|
||||||
|
};
|
||||||
|
const symbol = (await tool("nm", "-arch", architecture, executable))
|
||||||
|
.split("\n")
|
||||||
|
.find((line) => line.endsWith(` ${HANDLER}`));
|
||||||
|
expect(symbol, `nm lacks ${HANDLER}`).toBeDefined();
|
||||||
|
const address = BigInt(`0x${symbol!.trim().split(/\s+/u)[0]!}`);
|
||||||
|
const segment = [
|
||||||
|
...(await tool("otool", "-arch", architecture, "-l", executable)).matchAll(
|
||||||
|
/cmd LC_SEGMENT_64\n\s+cmdsize \d+\n\s+segname \S+\n\s+vmaddr (0x[0-9a-f]+)\n\s+vmsize 0x[0-9a-f]+\n\s+fileoff (\d+)\n\s+filesize (\d+)/gu,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.map(([, vmaddr, fileoff, filesize]) => ({
|
||||||
|
address: BigInt(vmaddr!),
|
||||||
|
offset: BigInt(fileoff!),
|
||||||
|
size: BigInt(filesize!),
|
||||||
|
}))
|
||||||
|
.find(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.address <= address &&
|
||||||
|
address < candidate.address + candidate.size,
|
||||||
|
);
|
||||||
|
expect(segment, `no file-backed segment maps ${HANDLER}`).toBeDefined();
|
||||||
|
const fat = await tool("otool", "-f", "-v", executable);
|
||||||
|
const slice = !fat.startsWith("Fat headers")
|
||||||
|
? "0"
|
||||||
|
: /^\s+offset (\d+)$/mu.exec(
|
||||||
|
fat
|
||||||
|
.split(/^architecture /mu)
|
||||||
|
.find((block) => block.startsWith(`${architecture}\n`)) ?? "",
|
||||||
|
)?.[1];
|
||||||
|
expect(slice, `otool lacks the ${architecture} slice`).toBeDefined();
|
||||||
|
return {
|
||||||
|
address: `0x${address.toString(16)}`,
|
||||||
|
file_offset: Number(
|
||||||
|
BigInt(slice!) + segment!.offset + address - segment!.address,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe.skipIf(process.platform !== "darwin")(
|
||||||
|
"compiled Objective-C UI handler locations",
|
||||||
|
() => {
|
||||||
|
cliTest(
|
||||||
|
"locates the handler's code, not its method record, in thin and universal images",
|
||||||
|
async ({ cli, processes }) => {
|
||||||
|
const root = await createTestTempDirectory("rea-ui-handler-location-");
|
||||||
|
const source = join(root, "handler.m");
|
||||||
|
await writeFile(source, SOURCE);
|
||||||
|
const { call } = await connectLocalToolsMcp(LOCAL_READERS);
|
||||||
|
const good = async (name: string, arguments_: object) => {
|
||||||
|
const result = await call(name, { ...arguments_ });
|
||||||
|
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
|
||||||
|
return result.structuredContent;
|
||||||
|
};
|
||||||
|
for (const variant of [
|
||||||
|
// arm64 always uses relative method lists.
|
||||||
|
{ name: "arm64", flags: ["-arch", "arm64"], encoding: "relative" },
|
||||||
|
// A pre-macOS 11 x86_64 image keeps absolute method lists.
|
||||||
|
{
|
||||||
|
name: "x86_64",
|
||||||
|
flags: ["-arch", "x86_64", "-mmacosx-version-min=10.15"],
|
||||||
|
encoding: "absolute",
|
||||||
|
},
|
||||||
|
// The selected slice's offsets are relative to the container.
|
||||||
|
{
|
||||||
|
name: "universal",
|
||||||
|
flags: [
|
||||||
|
"-arch",
|
||||||
|
"x86_64",
|
||||||
|
"-arch",
|
||||||
|
"arm64",
|
||||||
|
"-mmacosx-version-min=10.15",
|
||||||
|
],
|
||||||
|
encoding: undefined,
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const directory = join(root, variant.name);
|
||||||
|
await mkdir(directory);
|
||||||
|
const compiled = await processes.run(
|
||||||
|
"/usr/bin/xcrun",
|
||||||
|
[
|
||||||
|
"clang",
|
||||||
|
...variant.flags,
|
||||||
|
"-O2",
|
||||||
|
"-framework",
|
||||||
|
"Cocoa",
|
||||||
|
source,
|
||||||
|
"-o",
|
||||||
|
join(directory, "Audit"),
|
||||||
|
],
|
||||||
|
{ timeoutMs: 60_000 },
|
||||||
|
);
|
||||||
|
expect(compiled.exitCode, compiled.stderr).toBe(0);
|
||||||
|
const { app, executable } = await writeApp(
|
||||||
|
directory,
|
||||||
|
join(directory, "Audit"),
|
||||||
|
);
|
||||||
|
const before = await readFile(executable);
|
||||||
|
|
||||||
|
await good("open_binary", { path: app });
|
||||||
|
const traced = traceEvidence.parse(
|
||||||
|
await good("trace_native_ui_action", { action: "runAction:" }),
|
||||||
|
);
|
||||||
|
const metadata = metadataEvidence.parse(
|
||||||
|
await good("inspect_native_dispatch_metadata", {}),
|
||||||
|
);
|
||||||
|
await good("close_binary", {});
|
||||||
|
|
||||||
|
const expected = await independentHandlerLocation(
|
||||||
|
processes,
|
||||||
|
executable,
|
||||||
|
traced.subject.architecture,
|
||||||
|
);
|
||||||
|
const functionId = `native:function:${expected.address}`;
|
||||||
|
const trace = traced.normalized_result;
|
||||||
|
expect(trace.edges, variant.name).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
relation: "objc_dispatch",
|
||||||
|
resolution: "resolved",
|
||||||
|
to: functionId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const handler = trace.nodes.find(({ id }) => id === functionId);
|
||||||
|
expect(handler?.location, variant.name).toEqual(expected);
|
||||||
|
|
||||||
|
const record =
|
||||||
|
metadata.normalized_result.result.objc_dispatch_implementations.find(
|
||||||
|
({ class_name, selector }) =>
|
||||||
|
class_name === "AuditController" && selector === "runAction:",
|
||||||
|
);
|
||||||
|
expect(record, variant.name).toMatchObject({
|
||||||
|
implementation_address: expected.address,
|
||||||
|
implementation_file_offset: expected.file_offset,
|
||||||
|
});
|
||||||
|
// The fixture is discriminating only while the record lies elsewhere.
|
||||||
|
expect(record?.location.file_offset).not.toBe(expected.file_offset);
|
||||||
|
expect(handler?.evidence, variant.name).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: "binary_metadata",
|
||||||
|
description:
|
||||||
|
variant.encoding === undefined
|
||||||
|
? expect.any(String)
|
||||||
|
: `Encoded ${variant.encoding} Objective-C method entry`,
|
||||||
|
location: record?.location,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (variant.name === "universal") {
|
||||||
|
const output = await cli.run({
|
||||||
|
arguments: [
|
||||||
|
"trace-native-ui-action",
|
||||||
|
app,
|
||||||
|
"runAction:",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
environment: LOCAL_READERS,
|
||||||
|
});
|
||||||
|
expect(output.exitCode, output.stderr).toBe(0);
|
||||||
|
const cliTrace = traceEvidence.parse(output.json).normalized_result;
|
||||||
|
expect(cliTrace.nodes.find(({ id }) => id === functionId)).toEqual(
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(Buffer.compare(await readFile(executable), before)).toBe(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
120_000,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -533,6 +533,11 @@ const assertSuccessfulRoute = (
|
|||||||
to: `native:function:${handlerAddress}`,
|
to: `native:function:${handlerAddress}`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
// A provider symbol names the handler address but maps no file bytes.
|
||||||
|
expect(
|
||||||
|
trace.nodes.find(({ id }) => id === `native:function:${handlerAddress}`)
|
||||||
|
?.location,
|
||||||
|
).toEqual({ address: handlerAddress, file_offset: null });
|
||||||
expect(calls).toContain("decode_interface_builder");
|
expect(calls).toContain("decode_interface_builder");
|
||||||
expect(calls).toContain(
|
expect(calls).toContain(
|
||||||
scenario.name === "authored-ui-direct"
|
scenario.name === "authored-ui-direct"
|
||||||
|
|||||||
Reference in new issue
Block a user