feat(firmware): attribute hardware MMIO peripheral memory regions in native disassembly (#5348)

* feat(firmware): attribute hardware MMIO peripheral memory regions in native disassembly

- introduce hardwareMemoryMap domain model supporting standard ARM Cortex-M, RISC-V, MIPS, x86, and ARM64 physical memory region classifications (Code, SRAM, Peripheral MMIO, System Control Space, External Device)
- distinguish RAM/ROM and address-space segments from MMIO (isolate ARM External RAM / Device and MIPS kseg0/kseg1 uncached mirrors and boot ROM as non-MMIO)
- add hardwarePeripheralAttribution to map analyzed instruction references to hardware peripheral blocks and summarize peripheral register interaction profiles with numerical offset ordering
- expand ReaGhidraBridge.java decoder_context to capture ARM TMode execution register alongside MIPS ISA_MODE for native instruction inspection
- add focused unit and boundary tests across hardwareMemoryMap.test.ts, hardwarePeripheralAttribution.test.ts, GhidraFunctionValues.test.ts, and ghidraCompactReferencesMcp.test.ts verifying region bounds, MMIO isolation, numerical sorting, 32-bit/64-bit overflow rejection, ARM TMode MCP preservation, and bridge contract bindings
- make fake Ghidra installation host platform-aware on Windows in GhidraLanguageOverride.test.ts

* test(ghidra): inspect ARM and Thumb context in the Java bridge

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
CyberSprout
2026-10-23 05:37:58 +08:00
committed by GitHub
parent b1ff86832c
commit ee0ec027db
12 changed files with 1391 additions and 7 deletions
+20 -4
View File
@@ -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"
+4 -2
View File
@@ -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();
+7
View File
@@ -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]);
+3
View File
@@ -209,3 +209,6 @@ export const firmwareResultSchemas = {
published_bytes: bytes,
}),
} as const;
export * from "./hardwareMemoryMap.js";
export * from "./hardwarePeripheralAttribution.js";
@@ -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);
});
});
+415
View File
@@ -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<bigint, string> => {
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<HardwareMemoryClassification, string> => {
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;
};
@@ -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",
]);
});
});
@@ -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<HardwareAccessAttribution, string> => {
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<string>;
readonly observedOffsets: Set<string>;
};
const blocks = new Map<string, BlockAccumulator>();
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<string>(),
observedOffsets: new Set<string>(),
};
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,
};
};
+117
View File
@@ -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"');
});
});
+1 -1
View File
@@ -51,7 +51,7 @@ const MIPS_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
`;
const pe: BinaryTarget = {
path: "/fixture.exe",
path: process.platform === "win32" ? "C:\\fixture.exe" : "/fixture.exe",
sha256: "a".repeat(64),
kind: "executable",
format: "pe",
@@ -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);
});
@@ -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);
}
}