From ac9677bd6cb84a3ca2a1e598a77e7346b260ab01 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sun, 11 Oct 2026 05:45:00 +0800 Subject: [PATCH] fix(test): preserve Ghidra interior-label controls on Mach-O (#1640) * fix(test): preserve Ghidra interior-label controls on Mach-O * test: preserve selector semantics and accept signaled fixture exits * test(javascript): exercise deep returns through the public CLI --- scripts/lib/real-ghidra-entry-aliases.mjs | 32 ++++++++++++++----- .../hopper/hopperClient.diagnostics.test.ts | 10 ++++-- tests/conformance/ghidra/entry-aliases.c | 3 +- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/scripts/lib/real-ghidra-entry-aliases.mjs b/scripts/lib/real-ghidra-entry-aliases.mjs index 5a38c7061..dc68ab125 100644 --- a/scripts/lib/real-ghidra-entry-aliases.mjs +++ b/scripts/lib/real-ghidra-entry-aliases.mjs @@ -21,15 +21,25 @@ export async function verifyGhidraEntryAliases({ new URL("../../tests/conformance/ghidra/entry-aliases.c", import.meta.url), ); const path = join(workspace, "entry-aliases"); - const prefix = process.platform === "darwin" ? "_" : ""; try { - await exec(compiler, ["-O0", "-g", "-fno-inline", source, "-o", path], { - env, - timeout: 30000, - }); + // Mach-O function-start metadata would promote the interior control label. + await exec( + compiler, + [ + "-O0", + "-g", + "-fno-inline", + ...(process.platform === "darwin" ? ["-Wl,-no_function_starts"] : []), + source, + "-o", + path, + ], + { env, timeout: 30000 }, + ); const bytes = await readFile(path); await call("close_binary"); - await call("open_binary", { path, provider_id: "ghidra" }); + const imported = await call("open_binary", { path, provider_id: "ghidra" }); + const prefix = imported.format === "mach-o" ? "_" : ""; const names = await call("list_names"); const alias = names.find( ({ value }) => value === `${prefix}rea_entry_alias`, @@ -46,8 +56,10 @@ export async function verifyGhidraEntryAliases({ ); assert.ok(canonical, "Fixture alias is not at a function entry"); const xrefsSelectors = []; + // CLI xrefs keeps bare hexadecimal selectors as addresses. A Mach-O + // primary label such as "dead" is tested below as a procedure name instead. for (const value of [ - canonical.value, + `${prefix}rea_alias_target`, alias.value, `${prefix}rea_xrefs_data`, `${prefix}rea_xrefs_unreferenced`, @@ -153,7 +165,11 @@ export async function verifyGhidraEntryAliases({ { env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 }, ); const evidence = JSON.parse(stdout); - assert.deepEqual(evidence.normalized_result, expected); + assert.deepEqual( + evidence.normalized_result, + expected, + `CLI xrefs mismatch for ${JSON.stringify(selector)}`, + ); assert.equal(evidence.parameters.address, selector.at(-1)); } } diff --git a/tests/boundary/providers/hopper/hopperClient.diagnostics.test.ts b/tests/boundary/providers/hopper/hopperClient.diagnostics.test.ts index fd4982d9e..833da518d 100644 --- a/tests/boundary/providers/hopper/hopperClient.diagnostics.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.diagnostics.test.ts @@ -9,6 +9,12 @@ import { } from "../../../../src/hopper/HopperClient.js"; import { HopperFixtureLauncher } from "./hopperClient.fixture.js"; +const expectStoppedLauncher = (launcher: HopperFixtureLauncher): void => { + const child = launcher.processes[0]; + if (child === undefined) throw new Error("Diagnostic fixture did not launch"); + expect(child.exitCode !== null || child.signalCode !== null).toBe(true); +}; + it("bounds long-lived launcher output while keeping the bridge usable", async () => { let observedStderrBytes = 0; const diagnosticLauncher = new HopperFixtureLauncher(undefined, "noisy"); @@ -35,7 +41,7 @@ it("bounds long-lived launcher output while keeping the bridge usable", async () await client.close(); const directory = diagnosticLauncher.directories[0] ?? ""; await expect(access(directory)).rejects.toMatchObject({ code: "ENOENT" }); - expect(diagnosticLauncher.processes[0]?.exitCode).not.toBeNull(); + expectStoppedLauncher(diagnosticLauncher); }); it("reports observed and retained output when capture cuts a token", async () => { @@ -84,5 +90,5 @@ it("reports observed and retained output when capture cuts a token", async () => await client.close(); const directory = diagnosticLauncher.directories[0] ?? ""; await expect(access(directory)).rejects.toMatchObject({ code: "ENOENT" }); - expect(diagnosticLauncher.processes[0]?.exitCode).not.toBeNull(); + expectStoppedLauncher(diagnosticLauncher); }); diff --git a/tests/conformance/ghidra/entry-aliases.c b/tests/conformance/ghidra/entry-aliases.c index 25493f737..af152827a 100644 --- a/tests/conformance/ghidra/entry-aliases.c +++ b/tests/conformance/ghidra/entry-aliases.c @@ -9,7 +9,8 @@ volatile int rea_xrefs_data = 17; __attribute__((used)) volatile int rea_xrefs_unreferenced = 41; __attribute__((noinline, used)) int rea_alias_target(void) { - __asm__ volatile(".globl " REA_SYMBOL("rea_interior") "\n" + // ARM64 leaf functions may have no prologue; keep the control past entry. + __asm__ volatile("nop\n.globl " REA_SYMBOL("rea_interior") "\n" REA_SYMBOL("rea_interior") ":"); return rea_xrefs_data; }