[GH-ISSUE #7490] [Bug] inspect_native_instruction reports bytes Ghidra never disassembled as undecodable #4460

Closed
opened 2026-10-31 00:50:01 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @Entrpi on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7490

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

With the Ghidra provider, inspect_native_instruction returns status: "undecodable" for an in-memory address when Ghidra's listing has no instruction and no defined data there. bytes, length, mnemonic and raw_disassembly are all null. This also happens when the bytes are a valid instruction that analysis never reached.

Observed once, with real Ghidra over MCP: in an armv6-m raw image at base 0x0, address 0x5a holds c0 46. That is the Thumb mov r8, r8 alignment fill after bx lr. read_bytes returned these bytes, but inspect_native_instruction at 0x5a returned undecodable (result below). The same result reports Thumb decoder context (TMode = 1) at that address.

Reproduction

Ran once, on an unpublished local commit (1837c10f1, based on main d6f88becd) that adds ARM M-profile raw loading. The commit does not change inspectNativeInstruction; the method is byte-identical on main 570abe95f.

  1. Open a self-built 100-byte armv6-m raw image at base 0x0 with the Ghidra provider. At 0x56 it holds adds r0,#3; bx lr, then the 2-byte fill c0 46 at 0x5a. In the fixture layout, nothing branches to or references 0x5a.
  2. Call read_bytes over the whole image. The result matches the image, including c0 46 at 0x5a.
  3. Call inspect_native_instruction with {"address":"0x5a"}. status is undecodable.

Reproduced 1 of 1. A reproduction on main was not run. Proposed check (not run): on main, query a valid instruction that follows a return and that no flow, symbol or reference reaches.

Expected behavior

No document defines undecodable, so this expectation is unverified. The existing wording describes the bytes or a decode, not analysis coverage:

  • The MCP tool description (src/contracts/officialToolContracts.ts:69) says "Data, interior instruction addresses and undecodable bytes are distinct outcomes."
  • #491, the feature request for this tool, lists "undecodable instructions" as a distinct outcome.
  • The Ghidra seed script uses the word only after a decode attempt: bridge/ghidra/ReaGhidraApplySeeds.java:139 reports undecodable when disassemble(address) (:148) produced no instruction.

Proposed: a result should not call bytes undecodable when no decode was attempted. It should either separate "not decoded" from "decode failed", or define the status as a fact about the provider's listing. See the options below.

Impact and scope

Observed: one valid Thumb instruction was reported as undecodable in a verifier run. That unpublished verifier now declares the fill as data, so it no longer queries 0x5a. REA's behavior is unchanged.

Source-derived, not run:

  • On main, only the Ghidra provider implements inspect_native_instruction (src/ghidra/GhidraFunctionValues.ts:62,318). No other provider reports this status. REA passes the status to MCP and CLI callers unchanged, and no TypeScript consumer branches on it.

  • Every in-memory address with no instruction and no defined data gets undecodable. This includes:

    • (a) bytes that no analysis flow reached (the observed case);
    • (b) bytes whose disassembly failed;
    • (c) addresses in uninitialized blocks such as .bss. Ghidra's Memory.contains includes every block, initialized or not.

    The result cannot tell (a), (b) and (c) apart, and bytes is null in all three.

  • resolve_native_call_targets reuses this result and returns unavailable for any status other than decoded (ReaGhidraBridge.java:1394-1400,1421). That outcome stays accurate.

  • No test or real-Ghidra verifier on main asserts undecodable. The verifiers cover decoded, not-instruction-boundary and outside-memory (for example scripts/lib/real-ghidra-boundaries.mjs:341-349).

Inference: an analyst or agent who follows the description can conclude that the bytes are not valid code, for example data, packed bytes or the wrong decoder mode. The result only shows that Ghidra's analysis did not disassemble them. Whether this has misled an agent: NOT_ENOUGH_INFO, no such report was found. Frequency on real targets and severity were not assessed.

REA version

Observed on unpublished local commit 1837c10f1 (base: main d6f88becd). Source re-checked on main 570abe95f. inspectNativeInstruction is byte-identical on all three. No main commit since d6f88becd changes ReaGhidraBridge.java, the schema, the tool description or docs/native-investigation.md.

Runtime and platform

Ghidra 12.1.4 PUBLIC; OpenJDK 21.0.12; Node 24.22.1; Debian 13, Linux x86_64 (container). macOS and Windows not run.

Target and artifact context

Self-built 100-byte raw armv6-m image (Thumb code assembled by LLVM), base 0x0, Ghidra language ARM:LE:32:Cortex.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

MCP request run through the verifier's stdio MCP client: inspect_native_instruction with {"address":"0x5a"}. Result, trimmed to the relevant fields:

{"address":"0x5a","status":"undecodable","procedure":null,"architecture":"ARM:LE:32:Cortex","mode":"Cortex","decoder_context":[{"register":"TMode","value":"1","source":"ghidra-program-context"}],"bytes":null,"length":null,"mnemonic":null,"raw_disassembly":null}

Derivation: little-endian c0 46 is the halfword 0x46c0. That is the Thumb high-register MOV with Rd = Rm = r8, which is mov r8, r8. The verifier's llvm-objdump oracle also listed 0x5a as an instruction.

Source at main 570abe95f:

  • bridge/ghidra/ReaGhidraBridge.java:1284 only looks up getListing().getInstructionAt(address). Nothing decodes the bytes.
  • bridge/ghidra/ReaGhidraBridge.java:1314-1317: with no instruction, the status is outside-memory if memory does not contain the address, else not-instruction-boundary if an instruction contains it, else data if defined data contains it, else undecodable. The four byte and decode fields are set to null. github.com/morluto/rea@570abe95f2/bridge/ghidra/ReaGhidraBridge.java (L1314-L1317)
  • src/domain/native/nativeInstruction.ts:47-53: the status enum has no description, so the advertised output schema does not define its values.
  • docs/native-investigation.md:128-131 and :140-141 name "undecodable addresses" but do not define them.

Missing evidence: the full verifier log is not attached. Cases (b) and (c) are source-derived and were not run.

Typed outcome or recovery expected

Options, for a maintainer decision:

  1. Distinct status. Add a value such as undisassembled for in-memory bytes with no code unit, and keep undecodable for a failed decode. Uninitialized memory (c) needs its own value or a documented mapping. For (b), Ghidra's disassembler can record a failure as a "Bad Instruction" bookmark, which the bridge does not read now. That marking is optional, so a missing bookmark does not prove that the bytes decode.
  2. Decode on demand without changing the program. Ghidra's PseudoDisassembler is documented for decoding "when you don't want the program to be changed". Its disassemble(Address) throws InsufficientBytesException, UnknownInstructionException or UnknownContextException, which could separate (a), (b) and (c). Its result is not an analysis fact: it has no analysis references, and it depends on the program context at the address. It must not be reported as decoded, because resolve_native_call_targets treats decoded as an instruction in the listing. It needs its own status or provenance. Not tried in REA.
  3. Document the current meaning only. Describe each status in the schema, the tool description and docs/native-investigation.md. For example: "undecodable: in memory, but the provider's listing has no instruction or defined data here. REA does not try to decode the bytes, so they can still be valid code." There is no enum change, but callers still cannot tell (a),
    (b) and (c) apart.

Options 1 and 2 add values to, or change the meaning of, the public MCP output enum nativeInstructionSchema.status. That is a public contract change, so it needs a maintainer scope decision before implementation (contribution scope). Older captures that hold undecodable keep the ambiguous meaning. Option 3 fits the current contract.

Analysis or suspected cause

Source-derived: the status describes Ghidra's listing, not the bytes. undecodable is the fallback for an in-memory address with no code unit, and no decode is attempted (ReaGhidraBridge.java:1284,1314-1317). In the observed image, 0x5a follows bx lr and nothing references it, so analysis never created an instruction there.

Hypothesis, not checked: a non-committing decode at 0x5a with the reported program context (TMode = 1) would return mov r8, r8.

Additional context

Originally created by @Entrpi on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7490 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior With the Ghidra provider, `inspect_native_instruction` returns `status: "undecodable"` for an in-memory address when Ghidra's listing has no instruction and no defined data there. `bytes`, `length`, `mnemonic` and `raw_disassembly` are all `null`. This also happens when the bytes are a valid instruction that analysis never reached. Observed once, with real Ghidra over MCP: in an armv6-m raw image at base `0x0`, address `0x5a` holds `c0 46`. That is the Thumb `mov r8, r8` alignment fill after `bx lr`. `read_bytes` returned these bytes, but `inspect_native_instruction` at `0x5a` returned `undecodable` (result below). The same result reports Thumb decoder context (`TMode` = 1) at that address. ### Reproduction Ran once, on an unpublished local commit (`1837c10f1`, based on main `d6f88becd`) that adds ARM M-profile raw loading. The commit does not change `inspectNativeInstruction`; the method is byte-identical on main `570abe95f`. 1. Open a self-built 100-byte armv6-m raw image at base `0x0` with the Ghidra provider. At `0x56` it holds `adds r0,#3; bx lr`, then the 2-byte fill `c0 46` at `0x5a`. In the fixture layout, nothing branches to or references `0x5a`. 2. Call `read_bytes` over the whole image. The result matches the image, including `c0 46` at `0x5a`. 3. Call `inspect_native_instruction` with `{"address":"0x5a"}`. `status` is `undecodable`. Reproduced 1 of 1. A reproduction on main was not run. Proposed check (not run): on main, query a valid instruction that follows a return and that no flow, symbol or reference reaches. ### Expected behavior No document defines `undecodable`, so this expectation is unverified. The existing wording describes the bytes or a decode, not analysis coverage: - The MCP tool description (`src/contracts/officialToolContracts.ts:69`) says "Data, interior instruction addresses and undecodable bytes are distinct outcomes." - #491, the feature request for this tool, lists "undecodable instructions" as a distinct outcome. - The Ghidra seed script uses the word only after a decode attempt: `bridge/ghidra/ReaGhidraApplySeeds.java:139` reports `undecodable` when `disassemble(address)` (`:148`) produced no instruction. Proposed: a result should not call bytes undecodable when no decode was attempted. It should either separate "not decoded" from "decode failed", or define the status as a fact about the provider's listing. See the options below. ### Impact and scope Observed: one valid Thumb instruction was reported as `undecodable` in a verifier run. That unpublished verifier now declares the fill as data, so it no longer queries `0x5a`. REA's behavior is unchanged. Source-derived, not run: - On main, only the Ghidra provider implements `inspect_native_instruction` (`src/ghidra/GhidraFunctionValues.ts:62,318`). No other provider reports this status. REA passes the status to MCP and CLI callers unchanged, and no TypeScript consumer branches on it. - Every in-memory address with no instruction and no defined data gets `undecodable`. This includes: - (a) bytes that no analysis flow reached (the observed case); - (b) bytes whose disassembly failed; - (c) addresses in uninitialized blocks such as `.bss`. Ghidra's `Memory.contains` includes every block, initialized or not. The result cannot tell (a), (b) and (c) apart, and `bytes` is null in all three. - `resolve_native_call_targets` reuses this result and returns `unavailable` for any status other than `decoded` (`ReaGhidraBridge.java:1394-1400,1421`). That outcome stays accurate. - No test or real-Ghidra verifier on main asserts `undecodable`. The verifiers cover `decoded`, `not-instruction-boundary` and `outside-memory` (for example `scripts/lib/real-ghidra-boundaries.mjs:341-349`). Inference: an analyst or agent who follows the description can conclude that the bytes are not valid code, for example data, packed bytes or the wrong decoder mode. The result only shows that Ghidra's analysis did not disassemble them. Whether this has misled an agent: NOT_ENOUGH_INFO, no such report was found. Frequency on real targets and severity were not assessed. ### REA version Observed on unpublished local commit `1837c10f1` (base: main `d6f88becd`). Source re-checked on main `570abe95f`. `inspectNativeInstruction` is byte-identical on all three. No main commit since `d6f88becd` changes `ReaGhidraBridge.java`, the schema, the tool description or `docs/native-investigation.md`. ### Runtime and platform Ghidra 12.1.4 PUBLIC; OpenJDK 21.0.12; Node 24.22.1; Debian 13, Linux x86_64 (container). macOS and Windows not run. ### Target and artifact context Self-built 100-byte raw armv6-m image (Thumb code assembled by LLVM), base `0x0`, Ghidra language `ARM:LE:32:Cortex`. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence MCP request run through the verifier's stdio MCP client: `inspect_native_instruction` with `{"address":"0x5a"}`. Result, trimmed to the relevant fields: ```json {"address":"0x5a","status":"undecodable","procedure":null,"architecture":"ARM:LE:32:Cortex","mode":"Cortex","decoder_context":[{"register":"TMode","value":"1","source":"ghidra-program-context"}],"bytes":null,"length":null,"mnemonic":null,"raw_disassembly":null} ``` Derivation: little-endian `c0 46` is the halfword `0x46c0`. That is the Thumb high-register `MOV` with Rd = Rm = r8, which is `mov r8, r8`. The verifier's `llvm-objdump` oracle also listed `0x5a` as an instruction. Source at main `570abe95f`: - `bridge/ghidra/ReaGhidraBridge.java:1284` only looks up `getListing().getInstructionAt(address)`. Nothing decodes the bytes. - `bridge/ghidra/ReaGhidraBridge.java:1314-1317`: with no instruction, the status is `outside-memory` if memory does not contain the address, else `not-instruction-boundary` if an instruction contains it, else `data` if defined data contains it, else `undecodable`. The four byte and decode fields are set to null. https://github.com/morluto/rea/blob/570abe95f2a35b5c580a5ec2fca90c33d4114bdc/bridge/ghidra/ReaGhidraBridge.java#L1314-L1317 - `src/domain/native/nativeInstruction.ts:47-53`: the `status` enum has no description, so the advertised output schema does not define its values. - `docs/native-investigation.md:128-131` and `:140-141` name "undecodable addresses" but do not define them. Missing evidence: the full verifier log is not attached. Cases (b) and (c) are source-derived and were not run. ### Typed outcome or recovery expected Options, for a maintainer decision: 1. **Distinct status.** Add a value such as `undisassembled` for in-memory bytes with no code unit, and keep `undecodable` for a failed decode. Uninitialized memory (c) needs its own value or a documented mapping. For (b), Ghidra's disassembler can record a failure as a "Bad Instruction" bookmark, which the bridge does not read now. That marking is optional, so a missing bookmark does not prove that the bytes decode. 2. **Decode on demand without changing the program.** Ghidra's `PseudoDisassembler` is documented for decoding "when you don't want the program to be changed". Its `disassemble(Address)` throws `InsufficientBytesException`, `UnknownInstructionException` or `UnknownContextException`, which could separate (a), (b) and (c). Its result is not an analysis fact: it has no analysis references, and it depends on the program context at the address. It must not be reported as `decoded`, because `resolve_native_call_targets` treats `decoded` as an instruction in the listing. It needs its own status or provenance. Not tried in REA. 3. **Document the current meaning only.** Describe each status in the schema, the tool description and `docs/native-investigation.md`. For example: "`undecodable`: in memory, but the provider's listing has no instruction or defined data here. REA does not try to decode the bytes, so they can still be valid code." There is no enum change, but callers still cannot tell (a), (b) and (c) apart. Options 1 and 2 add values to, or change the meaning of, the public MCP output enum `nativeInstructionSchema.status`. That is a public contract change, so it needs a maintainer scope decision before implementation ([contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope)). Older captures that hold `undecodable` keep the ambiguous meaning. Option 3 fits the current contract. ### Analysis or suspected cause Source-derived: the status describes Ghidra's listing, not the bytes. `undecodable` is the fallback for an in-memory address with no code unit, and no decode is attempted (`ReaGhidraBridge.java:1284,1314-1317`). In the observed image, `0x5a` follows `bx lr` and nothing references it, so analysis never created an instruction there. Hypothesis, not checked: a non-committing decode at `0x5a` with the reported program context (`TMode` = 1) would return `mov r8, r8`. ### Additional context - #491 (closed): the feature request for this tool. - #5339 (open): proposes a tool that marks an address as code, data or undefined in a Ghidra session. It would let an analyst disassemble such bytes. It does not change what this status means. - Ghidra 12.1.4 sources cited above: - `Memory.contains` includes every block: https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/mem/MemoryMapDB.java#L1767-L1768 (blocks are added at `#L208-L213`) - `PseudoDisassembler`: https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/app/util/PseudoDisassembler.java#L34-L52 - "Bad Instruction" bookmarks, written only when bad-instruction marking is on: https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/disassemble/Disassembler.java#L1470-L1494
Author
Owner

@Entrpi commented on GitHub (Oct 28, 2026):

The ARM M-profile raw loading this report ran on is now open as #7951.

Its real-Ghidra lane reproduces case (a) a second way. In the Cortex-M55 case of npm run verify:ghidra:cortex-m, Ghidra 12.1.4 gets the flow of the wls/le loop branches wrong, so the pop that LLVM lists at 0x5c, after the loop, is never disassembled. The lane asserts that inspect_native_instruction at 0x5c returns undecodable, and it passes on Ghidra 12.1.4 (Linux x86_64). Whichever option is chosen here, that one assertion (notDisassembled in scripts/verify-real-ghidra-cortex-m.mjs) changes with it.

<!-- gh-comment-id:6483184463 --> @Entrpi commented on GitHub (Oct 28, 2026): The ARM M-profile raw loading this report ran on is now open as #7951. Its real-Ghidra lane reproduces case (a) a second way. In the Cortex-M55 case of `npm run verify:ghidra:cortex-m`, Ghidra 12.1.4 gets the flow of the `wls`/`le` loop branches wrong, so the `pop` that LLVM lists at `0x5c`, after the loop, is never disassembled. The lane asserts that `inspect_native_instruction` at `0x5c` returns `undecodable`, and it passes on Ghidra 12.1.4 (Linux x86_64). Whichever option is chosen here, that one assertion (`notDisassembled` in `scripts/verify-real-ghidra-cortex-m.mjs`) changes with it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4460
No description provided.