From 73a664bc78ee618045a2733b8714821281f77403 Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:58:29 +0200 Subject: [PATCH] fix(ghidra): verify dense jump tables on an optimized fixture (#1374) The dense jump-table oracles (inspect-native-api CLI and the analyze_function dossier) ran against the -O0 debug fixture, which assumes the host compiler lowers dense switches to tables at -O0. GCC 16 and later expand them as compare chains instead, so on those hosts the lane rejected a correct provider and Ghidra build with empty jump_tables. Build the same inventory source once more at -O2 as a dedicated jump-table variant, run both strict dense oracles there, and keep the debug dossier at its complete generic shape. The dedicated switch lane already compiles at -O2 for the same reason. --- scripts/verify-real-ghidra-function.mjs | 30 ++++++++++++----- scripts/verify-real-ghidra.mjs | 43 ++++++++++++++++++------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/scripts/verify-real-ghidra-function.mjs b/scripts/verify-real-ghidra-function.mjs index 8200f3f2ee..b9d9c299e8 100644 --- a/scripts/verify-real-ghidra-function.mjs +++ b/scripts/verify-real-ghidra-function.mjs @@ -153,14 +153,15 @@ export async function verifyDebugFunctionOperations( const denseSwitchDossier = await functionCall(client, "analyze_function", { procedure: denseSwitchProcedure.address, }); - if (process.platform === "linux" && process.arch === "x64") - assertDenseSwitchDossier(denseSwitchDossier, denseSwitchProcedure.address); - else - assertDossier(denseSwitchDossier, { - address: denseSwitchProcedure.address, - requireAssembly: true, - requireMultiBlock: true, - }); + // The debug fixture is compiled -O0, and GCC 16 and later lower dense + // switches to compare chains at -O0, so this dossier asserts only its + // complete shape; the optimized jump-table variant proves dense jump-table + // completeness through assertDenseSwitchDossier. + assertDossier(denseSwitchDossier, { + address: denseSwitchProcedure.address, + requireAssembly: true, + requireMultiBlock: true, + }); assertIndirectDossier(indirectDossier, leaf.address); const { cancellation, timeout } = await verifyRequestCancellationAndTimeout( @@ -372,6 +373,19 @@ async function verifyConcurrentRequests(client, entry, leaf) { return "serialized-two-results"; } +/** Prove dense jump-table completeness on the optimized jump-table fixture. */ +export async function verifyJumpTableDenseSwitch(client, procedures) { + const denseSwitchProcedure = requireProcedure( + procedures, + "rea_ghidra_inventory_dense_switch", + ); + const dossier = await functionCall(client, "analyze_function", { + procedure: denseSwitchProcedure.address, + }); + assertDenseSwitchDossier(dossier, denseSwitchProcedure.address); + return { dense_switch: dossierSummary(dossier) }; +} + export async function verifyStrippedFunctionOperations(client, entryString) { const stringXrefs = await functionCall(client, "xrefs", { document: null, diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs index 59698f9c2d..2ed9b0013e 100644 --- a/scripts/verify-real-ghidra.mjs +++ b/scripts/verify-real-ghidra.mjs @@ -35,6 +35,7 @@ import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.m import { verifyDenseDefaultReturn, + verifyJumpTableDenseSwitch, verifyNativeTypeLayout, verifyRelativeSwitch, verifyNativeValueTrace, @@ -80,6 +81,7 @@ const crossFormatSourcePath = fileURLToPath( const debugPath = join(fixtureRoot, "rea-ghidra-inventory-debug"); const layoutPath = join(fixtureRoot, "rea-ghidra-layout.o"); const strippedPath = join(fixtureRoot, "rea-ghidra-inventory-stripped"); +const jumpTablePath = join(fixtureRoot, "rea-ghidra-inventory-jump"); const arm64ElfPath = join(fixtureRoot, "rea-ghidra-cross-arm64"); const peObjectPath = join(fixtureRoot, "rea-ghidra-cross-x86_64.obj"); const pePath = join(fixtureRoot, "rea-ghidra-cross-x86_64.exe"); @@ -134,6 +136,14 @@ try { if (expectedNativeTarget.format === "mach-o") strippedFlags.push("-fvisibility=hidden"); await exec(compiler, [...strippedFlags, sourcePath, "-o", strippedPath]); + // The dense jump-table oracle needs the switch lowered to a table: GCC 16 and + // later expand dense switches as compare chains at -O0, so the CLI oracle + // runs against an optimized build while the debug fixture keeps its -O0 + // stack-local and DWARF shape. + const jumpTableFlags = ["-O2", "-g", "-fno-inline"]; + if (expectedNativeTarget.format === "elf") + jumpTableFlags.push("-fno-pie", "-no-pie"); + await exec(compiler, [...jumpTableFlags, sourcePath, "-o", jumpTablePath]); const crossTargets = []; if (aarch64JumpTableOnly) { const relativeSource = fileURLToPath( @@ -253,6 +263,11 @@ try { expectedNativeTarget, ); assertStrippedFixture(stripped); + const jumpTable = await verifyTarget( + jumpTablePath, + "jump-table", + expectedNativeTarget, + ); const crossResults = []; for (const [targetPath, variant, expectedTarget] of crossTargets) { const result = await verifyTarget(targetPath, variant, expectedTarget); @@ -277,11 +292,13 @@ try { crossFormat || aarch64JumpTableOnly ? [sourcePath, crossFormatSourcePath] : [sourcePath], - fixtures: [debug, stripped, layout, ...crossResults].map(summary), + fixtures: [debug, stripped, layout, jumpTable, ...crossResults].map( + summary, + ), malformed_target: "rejected-before-provider-start", native_api_cli: aarch64JumpTableOnly ? (crossResults[0]?.native_api_cli ?? null) - : debug.native_api_cli, + : jumpTable.native_api_cli, native_value_e2e: debug.native_values, custom_target: custom === null ? null : summary(custom), cleanup: "complete", @@ -493,7 +510,7 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) { const crossArm64Elf = variant === "cross-arm64-elf" || variant === "aarch64-jump-table"; const nativeApiCli = - variant === "debug" || crossArm64Elf + variant === "jump-table" || crossArm64Elf ? await verifyNativeApiCli(parsedTarget.value.path, procedures, { requireDenseJumpTable: crossArm64Elf || @@ -513,15 +530,17 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) { ) : variant === "type-layout" ? await verifyNativeTypeLayout(client, names) - : variant === "custom" || variant === "aarch64-jump-table" - ? null - : await verifyInventoryOperations({ - client, - variant, - procedures, - names, - strings, - }); + : variant === "jump-table" + ? await verifyJumpTableDenseSwitch(client, procedures) + : variant === "custom" || variant === "aarch64-jump-table" + ? null + : await verifyInventoryOperations({ + client, + variant, + procedures, + names, + strings, + }); const nativeValues = variant === "debug" ? await verifyNativeValueTrace(client, procedures, parsedTarget.value)