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.
This commit is contained in:
BaaRai authored and GitHub committed 2026-10-10 06:58:29 +08:00
1 parent 355a292198
commit 73a664bc78
2 files changed
+53 -20

No files matched your search

+22 -8
View File
@@ -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,
+31 -12
View File
@@ -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)