diff --git a/.github/workflows/ghidra-bridge-sources.yml b/.github/workflows/ghidra-bridge-sources.yml index e5c761baa8..9836fd767d 100644 --- a/.github/workflows/ghidra-bridge-sources.yml +++ b/.github/workflows/ghidra-bridge-sources.yml @@ -25,16 +25,16 @@ jobs: node-version-file: .nvmrc - name: Validate the SDK pin against the provider's verified release run: node scripts/verify-ghidra-bridge-sources.mjs --check-sdk-pin >> "$GITHUB_ENV" - # Only the jars and application.properties are extracted: the check - # compiles against them and never launches Ghidra or imports a target. - - name: Acquire checksum-verified Ghidra jars in isolated runner storage + # The bridge probe imports only source-owned ARM/Thumb bytes and never + # executes target code. All SDK/project state stays in runner storage. + - name: Acquire checksum-verified Ghidra SDK in isolated runner storage shell: bash run: | ghidra_root="${RUNNER_TEMP}/rea-ghidra" mkdir -p "${ghidra_root}" curl -fsSL --retry 3 --write-out 'ghidra_download_bytes=%{size_download} ghidra_download_seconds=%{time_total}\n' -o "${ghidra_root}/ghidra.zip" "${GHIDRA_RELEASE_URL}" echo "${GHIDRA_RELEASE_SHA256} ${ghidra_root}/ghidra.zip" | sha256sum -c - - unzip -q "${ghidra_root}/ghidra.zip" '*/Ghidra/*.jar' '*/Ghidra/application.properties' -d "${ghidra_root}" + unzip -q "${ghidra_root}/ghidra.zip" -d "${ghidra_root}" rm "${ghidra_root}/ghidra.zip" properties="$(find "${ghidra_root}" -path '*/Ghidra/application.properties' -print -quit)" test -n "${properties}" @@ -46,3 +46,19 @@ jobs: java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64" test -n "${!java_home_variable}" JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk + - name: Inspect real ARM and Thumb instruction context through the bridge + shell: bash + run: | + java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64" + export JAVA_HOME="${!java_home_variable}" + probe_root="${RUNNER_TEMP}/rea-arm-context" + mkdir -p "${probe_root}/project" + printf '\036\377\057\341\160\107\000\000' > "${probe_root}/arm-thumb.bin" + timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/project" ArmContext \ + -import "${probe_root}/arm-thumb.bin" -loader BinaryLoader -processor ARM:LE:32:v7 \ + -noanalysis -readOnly -deleteProject \ + -scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \ + -postScript ReaArmContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \ + > "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; } + cat "${probe_root}/output.log" + rg -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log" diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java index b091221c7a..a980e9aee8 100644 --- a/bridge/ghidra/ReaGhidraBridge.java +++ b/bridge/ghidra/ReaGhidraBridge.java @@ -1268,9 +1268,11 @@ public final class ReaGhidraBridge extends HeadlessScript { result.add("procedure", function == null ? JsonNull.INSTANCE : GSON.toJsonTree(canonicalAddress(function.getEntryPoint()))); result.addProperty("architecture", currentProgram.getLanguageID().getIdAsString()); result.addProperty("mode", currentProgram.getLanguage().getLanguageDescription().getVariant()); - if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")) { + if (currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") || + currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")) { JsonArray decoderContext = new JsonArray(); - var modeRegister = currentProgram.getRegister("ISA_MODE"); + String regName = currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:") ? "ISA_MODE" : "TMode"; + var modeRegister = currentProgram.getRegister(regName); if (modeRegister != null) { var modeValue = currentProgram.getProgramContext().getValue(modeRegister, address, false); JsonObject context = new JsonObject(); diff --git a/scripts/verify-real-ghidra-raw.mjs b/scripts/verify-real-ghidra-raw.mjs index 1db0f8cf9e..549da44456 100644 --- a/scripts/verify-real-ghidra-raw.mjs +++ b/scripts/verify-real-ghidra-raw.mjs @@ -140,6 +140,13 @@ try { address: entry, }); assert.equal(instruction.normalized_result.address, entry); + assert.ok(Array.isArray(instruction.normalized_result.decoder_context)); + const tmodeContext = instruction.normalized_result.decoder_context.find( + (ctx) => ctx.register === "TMode", + ); + assert.ok(tmodeContext !== undefined); + assert.equal(tmodeContext.source, "ghidra-program-context"); + assert.ok([null, "0", "1"].includes(tmodeContext.value)); } assert.equal(profiles[0], profiles[1]); assert.notEqual(profiles[1], profiles[2]); diff --git a/src/domain/firmware/firmwareAnalysis.ts b/src/domain/firmware/firmwareAnalysis.ts index 841ba2055f..06c0e93830 100644 --- a/src/domain/firmware/firmwareAnalysis.ts +++ b/src/domain/firmware/firmwareAnalysis.ts @@ -209,3 +209,6 @@ export const firmwareResultSchemas = { published_bytes: bytes, }), } as const; + +export * from "./hardwareMemoryMap.js"; +export * from "./hardwarePeripheralAttribution.js"; diff --git a/src/domain/firmware/hardwareMemoryMap.test.ts b/src/domain/firmware/hardwareMemoryMap.test.ts new file mode 100644 index 0000000000..699c429b96 --- /dev/null +++ b/src/domain/firmware/hardwareMemoryMap.test.ts @@ -0,0 +1,289 @@ +import { describe, expect, it } from "vitest"; +import { + classifyHardwareMemoryAddress, + isHardwareMmio, + parseAddressValue, +} from "./hardwareMemoryMap.js"; + +describe("ARM Cortex-M hardware memory map classification", () => { + it.each([ + { + address: "0x0", + kind: "code", + name: "Code / Flash", + isMmio: false, + }, + { + address: "0x08000000", + kind: "code", + name: "Code / Flash", + isMmio: false, + }, + { + address: "0x20000000", + kind: "sram", + name: "Internal SRAM", + isMmio: false, + }, + { + address: "0x40000000", + kind: "peripheral_mmio", + name: "Peripheral MMIO", + isMmio: true, + }, + { + address: "0x40011000", + kind: "peripheral_mmio", + name: "Peripheral MMIO", + isMmio: true, + }, + { + address: "0x60000000", + kind: "external_device", + name: "External RAM", + isMmio: false, + }, + { + address: "0xa0000000", + kind: "external_device", + name: "External Device Space", + isMmio: false, + }, + { + address: "0xe000e010", + kind: "system_control", + name: "System Control Space (SCS)", + isMmio: true, + }, + { + address: "0xe000e100", + kind: "system_control", + name: "System Control Space (SCS)", + isMmio: true, + }, + { + address: "0xe0100000", + kind: "reserved", + name: "Reserved Architecture Space", + isMmio: false, + }, + ])( + "classifies ARM address $address as $kind ($name)", + ({ address, kind, name, isMmio }) => { + const result = classifyHardwareMemoryAddress(address, "arm"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.regionKind).toBe(kind); + expect(result.value.regionName).toBe(name); + expect(result.value.isMmio).toBe(isMmio); + expect(isHardwareMmio(address, "arm")).toBe(isMmio); + }, + ); +}); + +describe("RISC-V and MIPS hardware memory map classification", () => { + it.each([ + { + address: "0x00001000", + kind: "code", + name: "Debug & Boot ROM", + isMmio: false, + }, + { + address: "0x02000000", + kind: "system_control", + name: "Core Local Interruptor (CLINT)", + isMmio: true, + }, + { + address: "0x0c000000", + kind: "system_control", + name: "Platform-Level Interrupt Controller (PLIC)", + isMmio: true, + }, + { + address: "0x10012000", + kind: "peripheral_mmio", + name: "Peripheral MMIO", + isMmio: true, + }, + { + address: "0x20000000", + kind: "code", + name: "Flash Memory", + isMmio: false, + }, + { + address: "0x80000000", + kind: "sram", + name: "Main System RAM", + isMmio: false, + }, + ])( + "classifies RISC-V address $address", + ({ address, kind, name, isMmio }) => { + const result = classifyHardwareMemoryAddress(address, "riscv"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.regionKind).toBe(kind); + expect(result.value.regionName).toBe(name); + expect(result.value.isMmio).toBe(isMmio); + }, + ); + + it.each([ + { + address: "0x00400000", + kind: "code", + name: "kuseg (User TLB Space)", + isMmio: false, + }, + { + address: "0x80000000", + kind: "sram", + name: "kseg0 (Kernel Unmapped Cached)", + isMmio: false, + }, + { + address: "0xa0000000", + kind: "sram", + name: "kseg1 (Kernel Unmapped Uncached)", + isMmio: false, + }, + { + address: "0xbfc00000", + kind: "code", + name: "kseg1 Boot ROM / Reset Vector", + isMmio: false, + }, + { + address: "0xc0000000", + kind: "system_control", + name: "kseg2 / kseg3 (Kernel TLB Space)", + isMmio: false, + }, + ])("classifies MIPS address $address", ({ address, kind, name, isMmio }) => { + const result = classifyHardwareMemoryAddress(address, "mips"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.regionKind).toBe(kind); + expect(result.value.regionName).toBe(name); + expect(result.value.isMmio).toBe(isMmio); + }); +}); + +describe("x86 and ARM64 hardware memory map classification", () => { + it.each([ + { + address: "0x00000000", + kind: "sram", + name: "Real Mode Base RAM", + isMmio: false, + }, + { + address: "0x000a0000", + kind: "peripheral_mmio", + name: "Video MMIO (VGA)", + isMmio: true, + }, + { + address: "0x000e0000", + kind: "code", + name: "Option ROM / BIOS Code", + isMmio: false, + }, + { + address: "0xfec00000", + kind: "system_control", + name: "APIC System Control MMIO", + isMmio: true, + }, + { + address: "0xfffffff0", + kind: "code", + name: "BIOS Firmware Flash", + isMmio: false, + }, + ])("classifies x86 address $address", ({ address, kind, name, isMmio }) => { + const result = classifyHardwareMemoryAddress(address, "x86"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.regionKind).toBe(kind); + expect(result.value.regionName).toBe(name); + expect(result.value.isMmio).toBe(isMmio); + }); + + it.each([ + { + address: "0x0000000000001000", + kind: "code", + name: "Flash / Firmware Code", + isMmio: false, + }, + { + address: "0x0000000008000000", + kind: "system_control", + name: "Interrupt Controller (GIC) MMIO", + isMmio: true, + }, + { + address: "0x0000000009000000", + kind: "peripheral_mmio", + name: "Peripheral MMIO Space", + isMmio: true, + }, + { + address: "0x0000000040000000", + kind: "sram", + name: "System RAM / DRAM", + isMmio: false, + }, + ])("classifies ARM64 address $address", ({ address, kind, name, isMmio }) => { + const result = classifyHardwareMemoryAddress(address, "arm64"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.regionKind).toBe(kind); + expect(result.value.regionName).toBe(name); + expect(result.value.isMmio).toBe(isMmio); + }); +}); + +describe("hardware memory map address parsing and overflow boundaries", () => { + it("rejects 32-bit address overflow on 32-bit architectures", () => { + const overflow = classifyHardwareMemoryAddress("0x100000000", "arm"); + expect(overflow.ok).toBe(false); + if (overflow.ok) return; + expect(overflow.error).toContain("exceeds arm maximum address width"); + + const mipsOverflow = classifyHardwareMemoryAddress("0x100000000", "mips"); + expect(mipsOverflow.ok).toBe(false); + }); + + it("admits 64-bit addresses on arm64 and rejects 64-bit overflow", () => { + const valid64 = classifyHardwareMemoryAddress( + "0x0000000040000000", + "arm64", + ); + expect(valid64.ok).toBe(true); + + const overflow64 = parseAddressValue("0x10000000000000000"); + expect(overflow64.ok).toBe(false); + if (overflow64.ok) return; + expect(overflow64.error).toContain("hexadecimal string"); + }); + + it("rejects malformed address strings and negative BigInt values", () => { + expect(parseAddressValue("not-hex").ok).toBe(false); + expect(parseAddressValue("0x").ok).toBe(false); + expect(parseAddressValue("").ok).toBe(false); + expect(parseAddressValue(-1n).ok).toBe(false); + }); + + it("handles BigInt address inputs directly", () => { + const result = classifyHardwareMemoryAddress(0x40001000n, "arm"); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.canonicalAddress).toBe("0x40001000"); + expect(result.value.isMmio).toBe(true); + }); +}); diff --git a/src/domain/firmware/hardwareMemoryMap.ts b/src/domain/firmware/hardwareMemoryMap.ts new file mode 100644 index 0000000000..7b2de8c3ac --- /dev/null +++ b/src/domain/firmware/hardwareMemoryMap.ts @@ -0,0 +1,415 @@ +import { z } from "zod"; +import { err, ok, type Result } from "../result.js"; + +/** Supported target hardware CPU architectures for memory map classification. */ +export const HARDWARE_ARCHITECTURES = [ + "arm", + "arm64", + "riscv", + "mips", + "x86", +] as const; +export type HardwareArchitecture = (typeof HARDWARE_ARCHITECTURES)[number]; +export const hardwareArchitectureSchema = z.enum(HARDWARE_ARCHITECTURES); + +/** Standard hardware memory region functional classifications. */ +export const HARDWARE_MEMORY_REGION_KINDS = [ + "code", + "sram", + "peripheral_mmio", + "external_device", + "system_control", + "reserved", +] as const; +export type HardwareMemoryRegionKind = + (typeof HARDWARE_MEMORY_REGION_KINDS)[number]; +export const hardwareMemoryRegionKindSchema = z.enum( + HARDWARE_MEMORY_REGION_KINDS, +); + +/** Internal descriptor for an architectural memory map segment. */ +export interface HardwareMemoryRegionDefinition { + readonly name: string; + readonly kind: HardwareMemoryRegionKind; + readonly start: bigint; + readonly end: bigint; + readonly isMmio: boolean; + readonly description: string; +} + +/** Verified classification outcome for an addressed memory location. */ +export interface HardwareMemoryClassification { + readonly canonicalAddress: string; + readonly addressValue: bigint; + readonly architecture: HardwareArchitecture; + readonly regionKind: HardwareMemoryRegionKind; + readonly regionName: string; + readonly range: { + readonly start: string; + readonly end: string; + }; + readonly isMmio: boolean; + readonly description: string; +} + +const HEX_ADDRESS_PATTERN = /^0x[0-9a-fA-F]{1,16}$/u; + +const formatHex = (value: bigint): string => `0x${value.toString(16)}`; + +const maxAddressFor = (architecture: HardwareArchitecture): bigint => + architecture === "arm64" ? 0xffffffffffffffffn : 0xffffffffn; + +// Standard ARM Cortex-M / ARMv7-M / ARMv8-M memory architecture. +const ARM_REGIONS: readonly HardwareMemoryRegionDefinition[] = [ + { + name: "Code / Flash", + kind: "code", + start: 0x00000000n, + end: 0x1fffffffn, + isMmio: false, + description: "Executable Flash ROM, BootROM, or vector table mapping", + }, + { + name: "Internal SRAM", + kind: "sram", + start: 0x20000000n, + end: 0x3fffffffn, + isMmio: false, + description: "Internal SRAM, stack, heap, and static data allocation", + }, + { + name: "Peripheral MMIO", + kind: "peripheral_mmio", + start: 0x40000000n, + end: 0x5fffffffn, + isMmio: true, + description: + "Microcontroller peripherals: GPIO, UART, SPI, I2C, Timers, ADC, DMA", + }, + { + name: "External RAM", + kind: "external_device", + start: 0x60000000n, + end: 0x9fffffffn, + isMmio: false, + description: "External RAM accessed via FMC/FSMC; non-peripheral memory", + }, + { + name: "External Device Space", + kind: "external_device", + start: 0xa0000000n, + end: 0xdfffffffn, + isMmio: false, + description: "External device memory or external storage mapping", + }, + { + name: "System Control Space (SCS)", + kind: "system_control", + start: 0xe0000000n, + end: 0xe00fffffn, + isMmio: true, + description: "ARM core system control: NVIC, SysTick, SCB, MPU, DWT, ITM", + }, + { + name: "Reserved Architecture Space", + kind: "reserved", + start: 0xe0100000n, + end: 0xffffffffn, + isMmio: false, + description: "Vendor or architecture reserved space", + }, +]; + +// Standard embedded RISC-V platform memory architecture. +const RISCV_REGIONS: readonly HardwareMemoryRegionDefinition[] = [ + { + name: "Debug & Boot ROM", + kind: "code", + start: 0x00000000n, + end: 0x01ffffffn, + isMmio: false, + description: "On-chip Debug module, mask ROM, and boot execution space", + }, + { + name: "Core Local Interruptor (CLINT)", + kind: "system_control", + start: 0x02000000n, + end: 0x0bffffffn, + isMmio: true, + description: "RISC-V timer and software interrupt control registers", + }, + { + name: "Platform-Level Interrupt Controller (PLIC)", + kind: "system_control", + start: 0x0c000000n, + end: 0x0fffffffn, + isMmio: true, + description: + "RISC-V external interrupt controller priority and enable registers", + }, + { + name: "Peripheral MMIO", + kind: "peripheral_mmio", + start: 0x10000000n, + end: 0x1fffffffn, + isMmio: true, + description: "SoC peripheral registers: UART, SPI, GPIO, PWM, I2C", + }, + { + name: "Flash Memory", + kind: "code", + start: 0x20000000n, + end: 0x7fffffffn, + isMmio: false, + description: "SPI/QSPI XIP Flash executable code", + }, + { + name: "Main System RAM", + kind: "sram", + start: 0x80000000n, + end: 0xffffffffn, + isMmio: false, + description: "Main SRAM or DDR memory space", + }, +]; + +// Standard 32-bit MIPS memory segments. +const MIPS_REGIONS: readonly HardwareMemoryRegionDefinition[] = [ + { + name: "kuseg (User TLB Space)", + kind: "code", + start: 0x00000000n, + end: 0x7fffffffn, + isMmio: false, + description: "User virtual space, TLB-mapped and cached", + }, + { + name: "kseg0 (Kernel Unmapped Cached)", + kind: "sram", + start: 0x80000000n, + end: 0x9fffffffn, + isMmio: false, + description: "Kernel executable and data space, cached and unmapped", + }, + { + name: "kseg1 (Kernel Unmapped Uncached)", + kind: "sram", + start: 0xa0000000n, + end: 0xbfbfffffn, + isMmio: false, + description: + "Kernel uncached space: mirrors physical memory for DMA and non-cached buffers", + }, + { + name: "kseg1 Boot ROM / Reset Vector", + kind: "code", + start: 0xbfc00000n, + end: 0xbfffffffn, + isMmio: false, + description: "Reset vector and firmware boot ROM in uncached kernel space", + }, + { + name: "kseg2 / kseg3 (Kernel TLB Space)", + kind: "system_control", + start: 0xc0000000n, + end: 0xffffffffn, + isMmio: false, + description: "Kernel mapped space and debug/supervisor control", + }, +]; + +// Standard x86 hardware memory map. +const X86_REGIONS: readonly HardwareMemoryRegionDefinition[] = [ + { + name: "Real Mode Base RAM", + kind: "sram", + start: 0x00000000n, + end: 0x0009ffffn, + isMmio: false, + description: "Conventional real-mode RAM and interrupt vector table", + }, + { + name: "Video MMIO (VGA)", + kind: "peripheral_mmio", + start: 0x000a0000n, + end: 0x000bffffn, + isMmio: true, + description: "Legacy VGA text and graphics frame buffer MMIO", + }, + { + name: "Option ROM / BIOS Code", + kind: "code", + start: 0x000c0000n, + end: 0x000fffffn, + isMmio: false, + description: "Expansion ROM and legacy BIOS firmware code", + }, + { + name: "System RAM", + kind: "sram", + start: 0x00100000n, + end: 0xfebfffffn, + isMmio: false, + description: "Extended physical memory space", + }, + { + name: "APIC System Control MMIO", + kind: "system_control", + start: 0xfec00000n, + end: 0xfeefffffn, + isMmio: true, + description: "I/O APIC and Local APIC memory-mapped control registers", + }, + { + name: "BIOS Firmware Flash", + kind: "code", + start: 0xfff00000n, + end: 0xffffffffn, + isMmio: false, + description: + "System firmware ROM containing x86 reset vector at 0xfffffff0", + }, +]; + +// Generic 64-bit ARM64 / AArch64 hardware layout. +const ARM64_REGIONS: readonly HardwareMemoryRegionDefinition[] = [ + { + name: "Flash / Firmware Code", + kind: "code", + start: 0x0000000000000000n, + end: 0x0000000007ffffffn, + isMmio: false, + description: "Primary system firmware and boot code", + }, + { + name: "Interrupt Controller (GIC) MMIO", + kind: "system_control", + start: 0x0000000008000000n, + end: 0x0000000008ffffffn, + isMmio: true, + description: + "ARM Generic Interrupt Controller (GIC) distributor and CPU interfaces", + }, + { + name: "Peripheral MMIO Space", + kind: "peripheral_mmio", + start: 0x0000000009000000n, + end: 0x000000003fffffffn, + isMmio: true, + description: "Memory-mapped I/O peripherals and PCIe configuration ECAM", + }, + { + name: "System RAM / DRAM", + kind: "sram", + start: 0x0000000040000000n, + end: 0x000000ffffffffffn, + isMmio: false, + description: "Main physical memory space", + }, + { + name: "Extended Hardware Space", + kind: "reserved", + start: 0x0000010000000000n, + end: 0xffffffffffffffffn, + isMmio: false, + description: "Upper 64-bit physical address space", + }, +]; + +const getArchitectureRegions = ( + arch: HardwareArchitecture, +): readonly HardwareMemoryRegionDefinition[] => { + switch (arch) { + case "arm": + return ARM_REGIONS; + case "riscv": + return RISCV_REGIONS; + case "mips": + return MIPS_REGIONS; + case "x86": + return X86_REGIONS; + case "arm64": + return ARM64_REGIONS; + } +}; + +/** Parse and canonicalize an address input into a safe BigInt representation. */ +export const parseAddressValue = ( + address: bigint | string, +): Result => { + if (typeof address === "bigint") { + return address < 0n + ? err("Address cannot be a negative value") + : ok(address); + } + if (typeof address !== "string" || !HEX_ADDRESS_PATTERN.test(address)) { + return err("Address must be a hexadecimal string of the form 0x…"); + } + try { + const parsed = BigInt(address); + return ok(parsed); + } catch { + return err("Invalid hexadecimal address format"); + } +}; + +/** + * Classify a target address within the standardized hardware memory map for + * the specified CPU architecture. Distinguishes code, SRAM, peripheral MMIO, + * external devices, and core system control spaces. + */ +export const classifyHardwareMemoryAddress = ( + address: bigint | string, + architecture: HardwareArchitecture, +): Result => { + const parsed = parseAddressValue(address); + if (!parsed.ok) return parsed; + const value = parsed.value; + const max = maxAddressFor(architecture); + if (value > max) { + return err( + `Address ${formatHex(value)} exceeds ${architecture} maximum address width ${formatHex(max)}`, + ); + } + + const regions = getArchitectureRegions(architecture); + const matched = regions.find( + (region) => value >= region.start && value <= region.end, + ); + + if (matched !== undefined) { + return ok({ + canonicalAddress: formatHex(value), + addressValue: value, + architecture, + regionKind: matched.kind, + regionName: matched.name, + range: { + start: formatHex(matched.start), + end: formatHex(matched.end), + }, + isMmio: matched.isMmio, + description: matched.description, + }); + } + + return ok({ + canonicalAddress: formatHex(value), + addressValue: value, + architecture, + regionKind: "reserved", + regionName: "Unknown / Unmapped Region", + range: { start: formatHex(value), end: formatHex(value) }, + isMmio: false, + description: "Address does not match any recognized hardware region", + }); +}; + +/** Fast boolean predicate to check if an address represents memory-mapped I/O. */ +export const isHardwareMmio = ( + address: bigint | string, + architecture: HardwareArchitecture, +): boolean => { + const result = classifyHardwareMemoryAddress(address, architecture); + return result.ok ? result.value.isMmio : false; +}; diff --git a/src/domain/firmware/hardwarePeripheralAttribution.test.ts b/src/domain/firmware/hardwarePeripheralAttribution.test.ts new file mode 100644 index 0000000000..ce14e45ec4 --- /dev/null +++ b/src/domain/firmware/hardwarePeripheralAttribution.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from "vitest"; +import { + attributeHardwareAccess, + summarizeHardwarePeripheralInteractions, + type AnalyzedInstructionReference, + type HardwareAccessAttribution, +} from "./hardwarePeripheralAttribution.js"; + +describe("Hardware peripheral reference attribution", () => { + it("attributes a read access to a UART peripheral register", () => { + const reference: AnalyzedInstructionReference = { + target_address: "0x40011004", + read: true, + write: false, + source_address: "0x08000150", + operand_index: 1, + provenance: "instruction_disassembly", + }; + + const result = attributeHardwareAccess(reference, "arm"); + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.value).toEqual({ + targetAddress: "0x40011004", + addressValue: 0x40011004n, + architecture: "arm", + regionKind: "peripheral_mmio", + regionName: "Peripheral MMIO", + isMmio: true, + accessKind: "read", + peripheralBlockBase: "0x40011000", + peripheralBlockOffset: "0x4", + sourceAddress: "0x08000150", + operandIndex: 1, + provenance: "instruction_disassembly", + }); + }); + + it("attributes a write access to a GPIO peripheral register", () => { + const reference: AnalyzedInstructionReference = { + target_address: "0x40020014", + read: false, + write: true, + source_address: "0x08000180", + operand_index: 0, + }; + + const result = attributeHardwareAccess(reference, "arm"); + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.value.accessKind).toBe("write"); + expect(result.value.isMmio).toBe(true); + expect(result.value.peripheralBlockBase).toBe("0x40020000"); + expect(result.value.peripheralBlockOffset).toBe("0x14"); + }); + + it("attributes a read/write access to ARM System Control Space (SCS)", () => { + const reference: AnalyzedInstructionReference = { + target_address: "0xe000e010", + read: true, + write: true, + source_address: "0x08000200", + }; + + const result = attributeHardwareAccess(reference, "arm"); + expect(result.ok).toBe(true); + if (!result.ok) return; + + expect(result.value.regionKind).toBe("system_control"); + expect(result.value.accessKind).toBe("read_write"); + expect(result.value.isMmio).toBe(true); + expect(result.value.peripheralBlockBase).toBe("0xe000e000"); + expect(result.value.peripheralBlockOffset).toBe("0x10"); + }); + + it("distinguishes non-MMIO code and RAM memory accesses", () => { + const codeRef: AnalyzedInstructionReference = { + target_address: "0x08000500", + call: true, + }; + const codeResult = attributeHardwareAccess(codeRef, "arm"); + expect(codeResult.ok).toBe(true); + if (!codeResult.ok) return; + expect(codeResult.value.isMmio).toBe(false); + expect(codeResult.value.regionKind).toBe("code"); + + const ramRef: AnalyzedInstructionReference = { + target_address: "0x20000400", + read: true, + }; + const ramResult = attributeHardwareAccess(ramRef, "arm"); + expect(ramResult.ok).toBe(true); + if (!ramResult.ok) return; + expect(ramResult.value.isMmio).toBe(false); + expect(ramResult.value.regionKind).toBe("sram"); + }); + + it("rejects invalid target address representations", () => { + const invalid: AnalyzedInstructionReference = { + target_address: "malformed", + }; + const result = attributeHardwareAccess(invalid, "arm"); + expect(result.ok).toBe(false); + if (result.ok) return; + expect(result.error).toContain("hexadecimal string"); + }); +}); + +describe("Hardware peripheral interaction summarization", () => { + it("aggregates MMIO accesses into distinct peripheral blocks", () => { + const references: AnalyzedInstructionReference[] = [ + // Block 0x40011000 (UART1): 2 writes, 1 read + { target_address: "0x40011000", write: true }, + { target_address: "0x40011004", read: true }, + { target_address: "0x40011004", write: true }, + // Block 0x40020000 (GPIOA): 1 write + { target_address: "0x40020014", write: true }, + // Non-MMIO accesses: Code and Stack RAM + { target_address: "0x08000100", call: true }, + { target_address: "0x20000000", read: true }, + ]; + + const attributions: HardwareAccessAttribution[] = []; + for (const ref of references) { + const attributed = attributeHardwareAccess(ref, "arm"); + if (attributed.ok) attributions.push(attributed.value); + } + + const summary = summarizeHardwarePeripheralInteractions( + attributions, + "arm", + ); + + expect(summary.totalReferencesExamined).toBe(6); + expect(summary.mmioAccessCount).toBe(4); + expect(summary.nonMmioAccessCount).toBe(2); + expect(summary.peripheralBlocks).toHaveLength(2); + + const uartBlock = summary.peripheralBlocks[0]; + expect(uartBlock).toBeDefined(); + if (uartBlock === undefined) return; + expect(uartBlock.blockBase).toBe("0x40011000"); + expect(uartBlock.regionKind).toBe("peripheral_mmio"); + expect(uartBlock.totalAccesses).toBe(3); + expect(uartBlock.readCount).toBe(1); + expect(uartBlock.writeCount).toBe(2); + expect(uartBlock.uniqueAddresses).toEqual(["0x40011000", "0x40011004"]); + expect(uartBlock.observedOffsets).toEqual(["0x0", "0x4"]); + + const gpioBlock = summary.peripheralBlocks[1]; + expect(gpioBlock).toBeDefined(); + if (gpioBlock === undefined) return; + expect(gpioBlock.blockBase).toBe("0x40020000"); + expect(gpioBlock.totalAccesses).toBe(1); + expect(gpioBlock.writeCount).toBe(1); + expect(gpioBlock.readCount).toBe(0); + expect(gpioBlock.observedOffsets).toEqual(["0x14"]); + }); + + it("handles empty attribution sets without error", () => { + const emptySummary = summarizeHardwarePeripheralInteractions([], "arm"); + expect(emptySummary.totalReferencesExamined).toBe(0); + expect(emptySummary.mmioAccessCount).toBe(0); + expect(emptySummary.peripheralBlocks).toEqual([]); + }); + + it("sorts observed register offsets numerically rather than lexicographically", () => { + const references: AnalyzedInstructionReference[] = [ + { target_address: "0x40011100", write: true }, + { target_address: "0x40011004", read: true }, + { target_address: "0x40011010", write: true }, + ]; + + const attributions: HardwareAccessAttribution[] = []; + for (const ref of references) { + const attributed = attributeHardwareAccess(ref, "arm"); + if (attributed.ok) attributions.push(attributed.value); + } + + const summary = summarizeHardwarePeripheralInteractions( + attributions, + "arm", + ); + + expect(summary.peripheralBlocks).toHaveLength(1); + const block = summary.peripheralBlocks[0]; + expect(block).toBeDefined(); + if (block === undefined) return; + expect(block.observedOffsets).toEqual(["0x4", "0x10", "0x100"]); + expect(block.uniqueAddresses).toEqual([ + "0x40011004", + "0x40011010", + "0x40011100", + ]); + }); +}); diff --git a/src/domain/firmware/hardwarePeripheralAttribution.ts b/src/domain/firmware/hardwarePeripheralAttribution.ts new file mode 100644 index 0000000000..ff2ebfed24 --- /dev/null +++ b/src/domain/firmware/hardwarePeripheralAttribution.ts @@ -0,0 +1,195 @@ +import { + classifyHardwareMemoryAddress, + type HardwareArchitecture, + type HardwareMemoryRegionKind, +} from "./hardwareMemoryMap.js"; +import { err, ok, type Result } from "../result.js"; + +/** Minimal input reference contract representing an analyzed instruction reference. */ +export interface AnalyzedInstructionReference { + readonly target_address: string; + readonly type?: string; + readonly call?: boolean; + readonly jump?: boolean; + readonly data?: boolean | null; + readonly read?: boolean | null; + readonly write?: boolean | null; + readonly source_address?: string | null; + readonly operand_index?: number; + readonly provenance?: string | null; +} + +/** Access direction observed from the instruction reference. */ +export type HardwareAccessKind = "read" | "write" | "read_write" | "unknown"; + +/** Structured hardware peripheral attribution for an analyzed instruction reference. */ +export interface HardwareAccessAttribution { + readonly targetAddress: string; + readonly addressValue: bigint; + readonly architecture: HardwareArchitecture; + readonly regionKind: HardwareMemoryRegionKind; + readonly regionName: string; + readonly isMmio: boolean; + readonly accessKind: HardwareAccessKind; + readonly peripheralBlockBase: string; + readonly peripheralBlockOffset: string; + readonly sourceAddress?: string | null; + readonly operandIndex?: number | null; + readonly provenance?: string | null; +} + +/** Summarized hardware peripheral block with observed register accesses. */ +export interface HardwarePeripheralBlockSummary { + readonly blockBase: string; + readonly regionName: string; + readonly regionKind: HardwareMemoryRegionKind; + readonly totalAccesses: number; + readonly readCount: number; + readonly writeCount: number; + readonly uniqueAddresses: readonly string[]; + readonly observedOffsets: readonly string[]; +} + +/** Complete aggregated summary of hardware peripheral interactions. */ +export interface HardwarePeripheralAttributionSummary { + readonly architecture: HardwareArchitecture; + readonly totalReferencesExamined: number; + readonly mmioAccessCount: number; + readonly nonMmioAccessCount: number; + readonly peripheralBlocks: readonly HardwarePeripheralBlockSummary[]; +} + +const deriveAccessKind = ( + read?: boolean | null, + write?: boolean | null, +): HardwareAccessKind => { + if (read === true && write === true) return "read_write"; + if (write === true) return "write"; + if (read === true) return "read"; + return "unknown"; +}; + +/** + * Attribute an analyzed instruction reference to its underlying hardware memory + * region, calculating peripheral block coordinates and access direction. + */ +export const attributeHardwareAccess = ( + reference: AnalyzedInstructionReference, + architecture: HardwareArchitecture, +): Result => { + const classification = classifyHardwareMemoryAddress( + reference.target_address, + architecture, + ); + if (!classification.ok) return classification; + + const { canonicalAddress, addressValue, regionKind, regionName, isMmio } = + classification.value; + + // Group into standard 4 KiB (0x1000) hardware peripheral register pages. + const blockBaseValue = addressValue & ~0xfffn; + const blockOffsetValue = addressValue & 0xfffn; + + const accessKind = deriveAccessKind(reference.read, reference.write); + + return ok({ + targetAddress: canonicalAddress, + addressValue, + architecture, + regionKind, + regionName, + isMmio, + accessKind, + peripheralBlockBase: `0x${blockBaseValue.toString(16)}`, + peripheralBlockOffset: `0x${blockOffsetValue.toString(16)}`, + sourceAddress: reference.source_address ?? null, + operandIndex: reference.operand_index ?? null, + provenance: reference.provenance ?? null, + }); +}; + +/** + * Aggregate and group multiple hardware memory attributions into cohesive + * peripheral block summaries. Filters out non-MMIO memory (e.g. code/stack RAM) + * to provide a clear reconstruction picture of hardware peripherals touched by firmware. + */ +export const summarizeHardwarePeripheralInteractions = ( + attributions: readonly HardwareAccessAttribution[], + architecture: HardwareArchitecture, +): HardwarePeripheralAttributionSummary => { + let mmioAccessCount = 0; + let nonMmioAccessCount = 0; + + type BlockAccumulator = { + readonly blockBase: string; + readonly regionName: string; + readonly regionKind: HardwareMemoryRegionKind; + totalAccesses: number; + readCount: number; + writeCount: number; + readonly uniqueAddresses: Set; + readonly observedOffsets: Set; + }; + + const blocks = new Map(); + + for (const item of attributions) { + if (!item.isMmio) { + nonMmioAccessCount += 1; + continue; + } + + mmioAccessCount += 1; + let block = blocks.get(item.peripheralBlockBase); + if (block === undefined) { + block = { + blockBase: item.peripheralBlockBase, + regionName: item.regionName, + regionKind: item.regionKind, + totalAccesses: 0, + readCount: 0, + writeCount: 0, + uniqueAddresses: new Set(), + observedOffsets: new Set(), + }; + blocks.set(item.peripheralBlockBase, block); + } + + block.totalAccesses += 1; + if (item.accessKind === "read" || item.accessKind === "read_write") { + block.readCount += 1; + } + if (item.accessKind === "write" || item.accessKind === "read_write") { + block.writeCount += 1; + } + block.uniqueAddresses.add(item.targetAddress); + block.observedOffsets.add(item.peripheralBlockOffset); + } + + const peripheralBlocks: HardwarePeripheralBlockSummary[] = Array.from( + blocks.values(), + ) + .sort((a, b) => (BigInt(a.blockBase) < BigInt(b.blockBase) ? -1 : 1)) + .map((block) => ({ + blockBase: block.blockBase, + regionName: block.regionName, + regionKind: block.regionKind, + totalAccesses: block.totalAccesses, + readCount: block.readCount, + writeCount: block.writeCount, + uniqueAddresses: Array.from(block.uniqueAddresses).sort((a, b) => + BigInt(a) < BigInt(b) ? -1 : 1, + ), + observedOffsets: Array.from(block.observedOffsets).sort((a, b) => + BigInt(a) < BigInt(b) ? -1 : 1, + ), + })); + + return { + architecture, + totalReferencesExamined: attributions.length, + mmioAccessCount, + nonMmioAccessCount, + peripheralBlocks, + }; +}; diff --git a/src/ghidra/GhidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts index fbc5bc7a65..de483efc0a 100644 --- a/src/ghidra/GhidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -1,3 +1,4 @@ +import { readFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -961,3 +962,119 @@ describe("complete Ghidra function body range evidence", () => { ).toBe(false); }); }); + +describe("Ghidra native instruction decoder context for ARM TMode and MIPS ISA_MODE", () => { + it.each([ + { + arch: "ARM:LE:32:v7", + register: "TMode", + value: "1", + description: "Thumb execution mode", + }, + { + arch: "ARM:LE:32:v7", + register: "TMode", + value: "0", + description: "ARM 32-bit execution mode", + }, + { + arch: "MIPS:LE:32:default", + register: "ISA_MODE", + value: "1", + description: "MIPS16 mode", + }, + { + arch: "MIPS:LE:32:default", + register: "ISA_MODE", + value: "0", + description: "Standard MIPS32 mode", + }, + ])( + "preserves $description ($register=$value) across boundary", + ({ arch, register, value }) => { + const instruction = { + ...compactInstruction(), + architecture: arch, + decoder_context: [ + { + register, + value, + source: "ghidra-program-context", + }, + ], + }; + const parsed = parseFunctionValueFixture( + "inspect_native_instruction", + instruction, + ); + expect(parsed.ok).toBe(true); + if (!parsed.ok) return; + expect(parsed.value).toMatchObject({ + architecture: arch, + decoder_context: [ + { register, value, source: "ghidra-program-context" }, + ], + }); + }, + ); + + it("admits null value for uninitialized processor context register", () => { + const instruction = { + ...compactInstruction(), + architecture: "ARM:LE:32:v7", + decoder_context: [ + { + register: "TMode", + value: null, + source: "ghidra-program-context", + }, + ], + }; + const parsed = parseFunctionValueFixture( + "inspect_native_instruction", + instruction, + ); + expect(parsed.ok).toBe(true); + if (!parsed.ok) return; + expect(parsed.value).toMatchObject({ + decoder_context: [{ register: "TMode", value: null }], + }); + }); + + it("rejects malformed decoder context entries", () => { + const emptyRegister = { + ...compactInstruction(), + decoder_context: [{ register: "", value: "1", source: "ghidra" }], + }; + expect( + parseFunctionValueFixture("inspect_native_instruction", emptyRegister).ok, + ).toBe(false); + + const emptySource = { + ...compactInstruction(), + decoder_context: [{ register: "TMode", value: "1", source: "" }], + }; + expect( + parseFunctionValueFixture("inspect_native_instruction", emptySource).ok, + ).toBe(false); + }); +}); + +describe("Ghidra bridge script processor context contract", () => { + it("binds TMode to ARM and ISA_MODE to MIPS in ReaGhidraBridge.java", async () => { + const bridgePath = new URL( + "../../bridge/ghidra/ReaGhidraBridge.java", + import.meta.url, + ); + const content = await readFile(bridgePath, "utf8"); + expect(content).toContain( + 'currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")', + ); + expect(content).toContain( + 'currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")', + ); + expect(content).toContain('"TMode"'); + expect(content).toContain('"ISA_MODE"'); + expect(content).toContain('"ghidra-program-context"'); + }); +}); diff --git a/src/ghidra/GhidraLanguageOverride.test.ts b/src/ghidra/GhidraLanguageOverride.test.ts index 5ac8a1a735..28ef19e05e 100644 --- a/src/ghidra/GhidraLanguageOverride.test.ts +++ b/src/ghidra/GhidraLanguageOverride.test.ts @@ -51,7 +51,7 @@ const MIPS_LDEFS = ` `; const pe: BinaryTarget = { - path: "/fixture.exe", + path: process.platform === "win32" ? "C:\\fixture.exe" : "/fixture.exe", sha256: "a".repeat(64), kind: "executable", format: "pe", diff --git a/tests/boundary/mcp/ghidraCompactReferencesMcp.test.ts b/tests/boundary/mcp/ghidraCompactReferencesMcp.test.ts index 5d9994a9ee..2c137941ee 100644 --- a/tests/boundary/mcp/ghidraCompactReferencesMcp.test.ts +++ b/tests/boundary/mcp/ghidraCompactReferencesMcp.test.ts @@ -102,4 +102,45 @@ describe("compact Ghidra reference MCP evidence", () => { await harness.close(); } }, 30_000); + + it("preserves ARM TMode and Thumb decoder context across the MCP boundary", async () => { + const instructionWithContext = (value: string | null) => ({ + ...compactInstruction(), + architecture: "ARM:LE:32:v7", + decoder_context: [ + { + register: "TMode", + value, + source: "ghidra-program-context", + }, + ], + }); + + for (const tmodeValue of ["0", "1", null]) { + const outputs = { + inspect_native_instruction: instructionWithContext(tmodeValue), + }; + const harness = await connectGhidraMcp( + `ghidra-arm-tmode-${String(tmodeValue)}`, + (operation) => { + if (operation !== "inspect_native_instruction") + throw new TypeError(`Unexpected operation: ${operation}`); + return Promise.resolve(ok(outputs[operation])); + }, + ); + try { + const result = await harness.mcp.callTool({ + name: "inspect_native_instruction", + arguments: { address: "0x08000000" }, + }); + expect(result.isError).not.toBe(true); + expect( + sessionEvidence(harness.session, result.structuredContent) + .normalized_result, + ).toEqual(outputs.inspect_native_instruction); + } finally { + await harness.close(); + } + } + }, 30_000); }); diff --git a/tests/conformance/ghidra/ReaArmContextProbe.java b/tests/conformance/ghidra/ReaArmContextProbe.java new file mode 100644 index 0000000000..58c15927a7 --- /dev/null +++ b/tests/conformance/ghidra/ReaArmContextProbe.java @@ -0,0 +1,101 @@ +//@category REA Verification + +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import ghidra.app.script.GhidraScript; +import ghidra.app.script.GhidraScriptUtil; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Program; +import ghidra.program.model.listing.ProgramContext; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.math.BigInteger; + +/** Real ARM/Thumb decoding; only the unavailable context value is injected. */ +public class ReaArmContextProbe extends GhidraScript { + @Override + public void run() throws Exception { + String[] args = getScriptArgs(); + require(args.length == 1, "Expected the exact production bridge directory"); + var source = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java"); + require(source != null && new File(source.getAbsolutePath()).getCanonicalFile().equals( + new File(args[0], "ReaGhidraBridge.java").getCanonicalFile()), "Production bridge source"); + GhidraScript bridge = GhidraScriptUtil.getProvider(source).getScriptInstance(source, errorWriter); + bridge.set(getState(), getControls()); + require(currentProgram.getLanguageID().getIdAsString().equals("ARM:LE:32:v7"), "ARM fixture language"); + var mode = currentProgram.getRegister("TMode"); + require(mode != null, "Real TMode register"); + Address arm = currentProgram.getMemory().getMinAddress(); + Address thumb = arm.add(4); + var context = currentProgram.getProgramContext(); + context.setValue(mode, arm, arm.add(3), BigInteger.ZERO); + context.setValue(mode, thumb, thumb.add(1), BigInteger.ONE); + require(disassemble(arm), "ARM instruction decoded"); + require(disassemble(thumb), "Thumb instruction decoded"); + Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace"); + space.setAccessible(true); + space.set(null, arm.getAddressSpace()); + Method inspect = bridge.getClass().getDeclaredMethod("inspectNativeInstruction", JsonObject.class, boolean.class); + inspect.setAccessible(true); + check(inspect, bridge, arm, "0", 4, "1eff2fe1"); + check(inspect, bridge, thumb, "1", 2, "7047"); + + // getValue includes language defaults. Inject only its unavailable result, + // preserving the real instruction, register, language and all other APIs. + Program original = currentProgram; + ProgramContext unavailable = (ProgramContext) Proxy.newProxyInstance( + ProgramContext.class.getClassLoader(), new Class[] { ProgramContext.class }, + (proxy, method, values) -> method.getName().equals("getValue") && mode.equals(values[0]) + ? null : invoke(method, context, values)); + Program program = (Program) Proxy.newProxyInstance(Program.class.getClassLoader(), + new Class[] { Program.class }, (proxy, method, values) -> + method.getName().equals("getProgramContext") ? unavailable : invoke(method, original, values)); + Field programField = field(bridge.getClass(), "currentProgram"); + programField.setAccessible(true); + try { + programField.set(bridge, program); + check(inspect, bridge, arm, null, 4, "1eff2fe1"); + } finally { + programField.set(bridge, original); + } + println("REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)"); + } + + private static void check(Method inspect, Object bridge, Address address, String mode, + int length, String bytes) throws Exception { + JsonObject input = new JsonObject(); + input.add("document", JsonNull.INSTANCE); + input.addProperty("address", "0x" + Long.toUnsignedString(address.getOffset(), 16)); + JsonObject result = (JsonObject) inspect.invoke(bridge, input, false); + require(result.get("status").getAsString().equals("decoded"), "Decoded bridge response"); + require(result.get("length").getAsInt() == length, "ARM/Thumb instruction width"); + require(result.get("bytes").getAsString().equals(bytes), "Original instruction bytes"); + var contexts = result.getAsJsonArray("decoder_context"); + require(contexts != null && contexts.size() == 1, "One retained mode context"); + var value = contexts.get(0).getAsJsonObject(); + require(value.get("register").getAsString().equals("TMode"), "TMode register name"); + require(value.get("source").getAsString().equals("ghidra-program-context"), "Context provenance"); + require(mode == null ? value.get("value").isJsonNull() + : mode.equals(value.get("value").getAsString()), "Exact mode value " + mode); + } + + private static Object invoke(Method method, Object owner, Object[] values) throws Throwable { + try { return method.invoke(owner, values); } + catch (InvocationTargetException failure) { throw failure.getCause(); } + } + + private static Field field(Class owner, String name) throws NoSuchFieldException { + for (Class type = owner; type != null; type = type.getSuperclass()) { + try { return type.getDeclaredField(name); } + catch (NoSuchFieldException absent) { /* Check the inherited API owner. */ } + } + throw new NoSuchFieldException(name); + } + + private static void require(boolean condition, String label) { + if (!condition) throw new IllegalStateException(label); + } +}