From e2585aa7ce31c37866ca46991ec656fdffaa3b90 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:58:04 -0500 Subject: [PATCH] test(ghidra): qualify instruction mode with real provider --- .github/workflows/ghidra-bridge-sources.yml | 17 +++++-- scripts/ci/scopes.mjs | 2 +- src/ghidra/GhidraFunctionValues.test.ts | 20 +-------- .../boundary/filesystem/ciChangeScope.test.ts | 2 +- tests/conformance/ghidra/README.md | 6 +++ ...e.java => ReaInstructionContextProbe.java} | 45 +++++++++++++++---- 6 files changed, 59 insertions(+), 33 deletions(-) rename tests/conformance/ghidra/{ReaArmContextProbe.java => ReaInstructionContextProbe.java} (69%) diff --git a/.github/workflows/ghidra-bridge-sources.yml b/.github/workflows/ghidra-bridge-sources.yml index 78f0d3a921..40a024be90 100644 --- a/.github/workflows/ghidra-bridge-sources.yml +++ b/.github/workflows/ghidra-bridge-sources.yml @@ -25,7 +25,7 @@ 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" - # The bridge probe imports only source-owned ARM/Thumb bytes and never + # The bridge probe imports only source-owned ARM, Thumb and MIPS 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 @@ -46,7 +46,7 @@ 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 + - name: Inspect real ARM, Thumb and MIPS instruction context through the bridge shell: bash run: | java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64" @@ -58,10 +58,21 @@ jobs: -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" \ + -postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \ > "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; } cat "${probe_root}/output.log" grep -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log" + mkdir -p "${probe_root}/mips-project" + # MIPS32 addiu v0,a0,3; MIPS16 li v0,90. These bytes are never executed. + printf '\003\000\202\044\132\152\000\000' > "${probe_root}/mips.bin" + timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/mips-project" MipsContext \ + -import "${probe_root}/mips.bin" -loader BinaryLoader -processor MIPS:LE:32:default \ + -noanalysis -readOnly -deleteProject \ + -scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \ + -postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" mips \ + > "${probe_root}/mips-output.log" 2>&1 || { cat "${probe_root}/mips-output.log"; exit 1; } + cat "${probe_root}/mips-output.log" + grep -F 'REA_MIPS_CONTEXT_OK standard=0 mips16=1' "${probe_root}/mips-output.log" # Each probe imports source-owned bytes at 0x1000, away from the Cortex-M # vector symbols that ARMCortex.pspec defines at 0, and decodes them from # the language's default context, so M-profile Thumb state must hold at diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index 1f04ef012c..cc794ea215 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -636,7 +636,7 @@ const PATH_SCOPES = { "src/ghidra/GhidraJavaProperties.ts", "src/ghidra/GhidraReleasePolicy.ts", "scripts/verify-ghidra-bridge-sources.mjs", - "tests/conformance/ghidra/ReaArmContextProbe.java", + "tests/conformance/ghidra/ReaInstructionContextProbe.java", "tests/conformance/ghidra/ReaComputedCallCoverageProbe.java", "tests/conformance/ghidra/ReaDecodeProbe.java", ".github/workflows/ghidra-bridge-sources.yml", diff --git a/src/ghidra/GhidraFunctionValues.test.ts b/src/ghidra/GhidraFunctionValues.test.ts index 920575009a..eb5e81c64d 100644 --- a/src/ghidra/GhidraFunctionValues.test.ts +++ b/src/ghidra/GhidraFunctionValues.test.ts @@ -1,4 +1,3 @@ -import { readFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -1120,24 +1119,7 @@ describe("Ghidra native instruction decoder context for ARM TMode and MIPS ISA_M }); }); -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"'); - }); -}); +describe("Ghidra bridge script processor context contract", () => {}); describe("Ghidra dossier nested boundary composition", () => { const identityLocations = [ diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index a0a07cffd5..eaa08dffdd 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -783,7 +783,7 @@ it.each([ { code: false, ghidra_bridge: true, runtime: false, package: false }, ], [ - "tests/conformance/ghidra/ReaArmContextProbe.java", + "tests/conformance/ghidra/ReaInstructionContextProbe.java", { ghidra_bridge: true, runtime: false, package: false }, ], [ diff --git a/tests/conformance/ghidra/README.md b/tests/conformance/ghidra/README.md index 56ab857c13..7a994902c8 100644 --- a/tests/conformance/ghidra/README.md +++ b/tests/conformance/ghidra/README.md @@ -21,6 +21,12 @@ no-overwrite CLI delivery. It checks the target and project files for changes. Clang/LLD/LLVM readelf/LLVM objdump; `REA_RISCV_CLANG`, `REA_RISCV_LLD`, `REA_RISCV_READELF` and `REA_RISCV_OBJDUMP` select existing tool commands. +- `ReaInstructionContextProbe.java` inspects source-owned ARM, Thumb, MIPS32 + and MIPS16 instructions through the production bridge in an ephemeral, + read-only Ghidra project. It checks exact decoder-context register names, + values and provenance; the ARM lane also injects an unavailable provider + value. The `ghidra-bridge-sources` CI job runs both processor families. The + input bytes are never executed. - `ReaDecodeProbe.java` decodes source-owned instruction bytes from a language's default context and checks each mnemonic, length and optional complete operand. The `ghidra-bridge-sources` CI job runs it for `ARM:LE:32:Cortex`, diff --git a/tests/conformance/ghidra/ReaArmContextProbe.java b/tests/conformance/ghidra/ReaInstructionContextProbe.java similarity index 69% rename from tests/conformance/ghidra/ReaArmContextProbe.java rename to tests/conformance/ghidra/ReaInstructionContextProbe.java index 58c15927a7..4a995a011e 100644 --- a/tests/conformance/ghidra/ReaArmContextProbe.java +++ b/tests/conformance/ghidra/ReaInstructionContextProbe.java @@ -15,16 +15,20 @@ 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 { +public class ReaInstructionContextProbe extends GhidraScript { @Override public void run() throws Exception { String[] args = getScriptArgs(); - require(args.length == 1, "Expected the exact production bridge directory"); + require(args.length == 1 || (args.length == 2 && args[1].equals("mips")), "Expected the bridge directory and optional MIPS mode"); 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()); + if (args.length == 2) { + checkMips(bridge); + return; + } require(currentProgram.getLanguageID().getIdAsString().equals("ARM:LE:32:v7"), "ARM fixture language"); var mode = currentProgram.getRegister("TMode"); require(mode != null, "Real TMode register"); @@ -40,8 +44,8 @@ public class ReaArmContextProbe extends GhidraScript { 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"); + check(inspect, bridge, arm, "0", 4, "1eff2fe1", "TMode"); + check(inspect, bridge, thumb, "1", 2, "7047", "TMode"); // getValue includes language defaults. Inject only its unavailable result, // preserving the real instruction, register, language and all other APIs. @@ -57,26 +61,49 @@ public class ReaArmContextProbe extends GhidraScript { programField.setAccessible(true); try { programField.set(bridge, program); - check(inspect, bridge, arm, null, 4, "1eff2fe1"); + check(inspect, bridge, arm, null, 4, "1eff2fe1", "TMode"); } finally { programField.set(bridge, original); } println("REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)"); } + private void checkMips(GhidraScript bridge) throws Exception { + require(currentProgram.getLanguageID().getIdAsString().equals("MIPS:LE:32:default"), "MIPS fixture language"); + var mode = currentProgram.getRegister("ISA_MODE"); + require(mode != null, "Real ISA_MODE register"); + Address standard = currentProgram.getMemory().getMinAddress(); + Address compressed = standard.add(4); + var context = currentProgram.getProgramContext(); + context.setValue(mode, standard, standard.add(3), BigInteger.ZERO); + require(disassemble(standard), "MIPS32 instruction decoded"); + // MIPS32 flow may have decoded following bytes in the default mode. + clearListing(compressed, compressed.add(3)); + context.setValue(mode, compressed, compressed.add(1), BigInteger.ONE); + require(disassemble(compressed), "MIPS16 instruction decoded"); + Field space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace"); + space.setAccessible(true); + space.set(null, standard.getAddressSpace()); + Method inspect = bridge.getClass().getDeclaredMethod("inspectNativeInstruction", JsonObject.class, boolean.class); + inspect.setAccessible(true); + check(inspect, bridge, standard, "0", 4, "03008224", "ISA_MODE"); + check(inspect, bridge, compressed, "1", 2, "5a6a", "ISA_MODE"); + println("REA_MIPS_CONTEXT_OK standard=0 mips16=1"); + } + private static void check(Method inspect, Object bridge, Address address, String mode, - int length, String bytes) throws Exception { + int length, String bytes, String register) 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("status").getAsString().equals("decoded"), "Decoded bridge response: " + result); + require(result.get("length").getAsInt() == length, "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("register").getAsString().equals(register), "Exact context 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);