[GH-ISSUE #1620] [Feature] Admit legacy MIPS32 ELFs with an unspecified ABI, pinning the 32-bit Ghidra language (auto-selection picks MIPS:LE:64:64-32R6addr) #547

Closed
opened 2026-10-15 18:26:03 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @bqw371 on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1620

Before submitting

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

Area

Native analysis or decompilation

Current limitation

The Ghidra MIPS lane from #1362 admits only ELF32 files that declare EF_MIPS_ARCH_32R2, set the o32 bit (0x00001000) in e_flags, and carry an ABI flags record. Firmware built with older GNU toolchains can have none of these. One such header is e_flags 0x50000001 (MIPS32, noreorder, ABI field 0) with no PT_MIPS_ABIFLAGS / .MIPS.abiflags (their toolchains predate the record). REA refuses these files before provider selection ("This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2 …").

Ghidra's own header auto-selection picks a wrong language for exactly these files, so admitting them is not simply a matter of relaxing the check. I measured Ghidra 12.1.3 with plain analyzeHeadless, no -processor, on variants of the repository's own conformance fixture:

Variant of tests/conformance/c/mips.c + mips-probe.S e_flags ABI flags record Language Ghidra selects
as built by mips-fixture.mjs (both byte orders, via npm run verify:ghidra:mips) 0x70001001 (32r2, o32) yes MIPS:LE:32:default / MIPS:BE:32:default
built with -march=mips32 0x50001001 (r1, o32) yes MIPS:LE:32:default
same, record removed 0x50001001 no MIPS:LE:32:default
same, ABI bits cleared 0x50000001 yes MIPS:LE:64:64-32R6addr
ABI bits cleared and record removed 0x50000001 no MIPS:LE:64:64-32R6addr

The trigger is the o32 bit in e_flags; the ABI flags record makes no difference. The lane as merged is not exposed, because it requires that bit. On a real mid-2000s MIPS32 LE firmware image with e_flags 0x50000001, the wrong language silently loses 400 of 5,250 functions (7.6%): 4,850 vs 5,250 with -processor MIPS:LE:32:default. Spot-checked instructions decode identically under both languages, so nothing visibly fails.

To build the variants: compile with the mips-fixture.mjs arguments but -march=mips32, then set e_flags to 0x50000001 and/or retype the PT_MIPS_ABIFLAGS program header to PT_NULL and the SHT_MIPS_ABIFLAGS section to SHT_PROGBITS. Code bytes are unchanged.

Use cases

Static analysis of MIPS32 firmware and other executables built before ABI flags existed, or by toolchains that leave the ABI field at 0, such as embedded MIPS consumer-device firmware from the 2000s.

Desired outcome and success criteria

  • An ELF32 ET_EXEC MIPS file with EF_MIPS_ARCH_32 or EF_MIPS_ARCH_32R2, the e_flags ABI field 0, EF_MIPS_ABI2 clear and no ABI flags record is admitted, with o32 as an explicit, recorded analysis assumption, not a declared fact.
  • For that admission REA passes -processor MIPS:<LE|BE>:32:default and checks the bridge's reported language_id against it after import, as it already does for DOS targets (expectedLanguageId).
  • The evidence records the ABI as inferred, not declared, and lists floating-point ABI semantics as unverified. The lane's existing refusals (FP64, NAN2008, ASEs, machine variants, n32/n64, ELF64) stay as they are.
  • A conformance variant like the table above runs in verify:ghidra:mips and asserts the pinned language.

Authority boundaries and non-goals

No execution or rehosting. No change to Hopper/IDA support, MIPS64, MIPS16/microMIPS, PSP/Allegrex (#1330), or files that declare their ABI.

Alternatives considered

  • Keep refusing. That leaves this class of firmware with no REA path.
  • Admit without pinning. Ghidra then analyses these files as 64-bit R6, as the table shows, while the evidence labels them MIPS32.
  • Pin -processor for every ELF32 MIPS target. That is what we run locally, and it is simpler. But it would change the language for files the lane already handles correctly, so scoping the pin to the new admission is the more conservative option.

Possible direction

We maintain a local patch on d1e6fe2e that does this. It admits EF_MIPS_ARCH_32 with ABI field 0 and no record, pins -processor in GhidraLauncher, and versions the lane identity. We gate it on four real MIPS32 LE firmware ELFs: each function entry set matches a separately produced Ghidra headless export with the 32-bit language (0 extra, 0 missing), and a negative control with the pin removed fails. If the direction is acceptable, I can open a PR with the conformance variant and tests.

References

  • #1362 (the lane), #718 (firmware roadmap), #656 (an analogous case where the analysed object differed from its label)

  • verify:ghidra:mips passes unchanged at d1e6fe2e on macOS 27 arm64 with Homebrew Ghidra 12.1.3, OpenJDK 21, clang 23.1.0 and GNU readelf 2.47, both byte orders.

  • Related: #1619 (a carved MIPS ELF whose section table lies past EOF is refused before ABI resolution; the same firmware class needs both)

Originally created by @bqw371 on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1620 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Current limitation The Ghidra MIPS lane from #1362 admits only ELF32 files that declare `EF_MIPS_ARCH_32R2`, set the o32 bit (`0x00001000`) in `e_flags`, and carry an ABI flags record. Firmware built with older GNU toolchains can have none of these. One such header is `e_flags 0x50000001` (MIPS32, noreorder, ABI field 0) with no `PT_MIPS_ABIFLAGS` / `.MIPS.abiflags` (their toolchains predate the record). REA refuses these files before provider selection ("This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2 …"). Ghidra's own header auto-selection picks a wrong language for exactly these files, so admitting them is not simply a matter of relaxing the check. I measured Ghidra 12.1.3 with plain `analyzeHeadless`, no `-processor`, on variants of the repository's own conformance fixture: | Variant of `tests/conformance/c/mips.c` + `mips-probe.S` | `e_flags` | ABI flags record | Language Ghidra selects | | --- | --- | --- | --- | | as built by `mips-fixture.mjs` (both byte orders, via `npm run verify:ghidra:mips`) | `0x70001001` (32r2, o32) | yes | `MIPS:LE:32:default` / `MIPS:BE:32:default` | | built with `-march=mips32` | `0x50001001` (r1, o32) | yes | `MIPS:LE:32:default` | | same, record removed | `0x50001001` | no | `MIPS:LE:32:default` | | same, ABI bits cleared | `0x50000001` | yes | **`MIPS:LE:64:64-32R6addr`** | | ABI bits cleared and record removed | `0x50000001` | no | **`MIPS:LE:64:64-32R6addr`** | The trigger is the o32 bit in `e_flags`; the ABI flags record makes no difference. The lane as merged is not exposed, because it requires that bit. On a real mid-2000s MIPS32 LE firmware image with `e_flags 0x50000001`, the wrong language silently loses 400 of 5,250 functions (7.6%): 4,850 vs 5,250 with `-processor MIPS:LE:32:default`. Spot-checked instructions decode identically under both languages, so nothing visibly fails. To build the variants: compile with the `mips-fixture.mjs` arguments but `-march=mips32`, then set `e_flags` to `0x50000001` and/or retype the `PT_MIPS_ABIFLAGS` program header to `PT_NULL` and the `SHT_MIPS_ABIFLAGS` section to `SHT_PROGBITS`. Code bytes are unchanged. ### Use cases Static analysis of MIPS32 firmware and other executables built before ABI flags existed, or by toolchains that leave the ABI field at 0, such as embedded MIPS consumer-device firmware from the 2000s. ### Desired outcome and success criteria - An ELF32 `ET_EXEC` MIPS file with `EF_MIPS_ARCH_32` or `EF_MIPS_ARCH_32R2`, the `e_flags` ABI field 0, `EF_MIPS_ABI2` clear and no ABI flags record is admitted, with o32 as an explicit, recorded analysis assumption, not a declared fact. - For that admission REA passes `-processor MIPS:<LE|BE>:32:default` and checks the bridge's reported `language_id` against it after import, as it already does for DOS targets (`expectedLanguageId`). - The evidence records the ABI as inferred, not declared, and lists floating-point ABI semantics as unverified. The lane's existing refusals (FP64, NAN2008, ASEs, machine variants, n32/n64, ELF64) stay as they are. - A conformance variant like the table above runs in `verify:ghidra:mips` and asserts the pinned language. ### Authority boundaries and non-goals No execution or rehosting. No change to Hopper/IDA support, MIPS64, MIPS16/microMIPS, PSP/Allegrex (#1330), or files that declare their ABI. ### Alternatives considered - **Keep refusing.** That leaves this class of firmware with no REA path. - **Admit without pinning.** Ghidra then analyses these files as 64-bit R6, as the table shows, while the evidence labels them MIPS32. - **Pin `-processor` for every ELF32 MIPS target.** That is what we run locally, and it is simpler. But it would change the language for files the lane already handles correctly, so scoping the pin to the new admission is the more conservative option. ### Possible direction We maintain a local patch on `d1e6fe2e` that does this. It admits `EF_MIPS_ARCH_32` with ABI field 0 and no record, pins `-processor` in `GhidraLauncher`, and versions the lane identity. We gate it on four real MIPS32 LE firmware ELFs: each function entry set matches a separately produced Ghidra headless export with the 32-bit language (0 extra, 0 missing), and a negative control with the pin removed fails. If the direction is acceptable, I can open a PR with the conformance variant and tests. ### References - #1362 (the lane), #718 (firmware roadmap), #656 (an analogous case where the analysed object differed from its label) - `verify:ghidra:mips` passes unchanged at `d1e6fe2e` on macOS 27 arm64 with Homebrew Ghidra 12.1.3, OpenJDK 21, clang 23.1.0 and GNU readelf 2.47, both byte orders. - Related: #1619 (a carved MIPS ELF whose section table lies past EOF is refused before ABI resolution; the same firmware class needs both)
Author
Owner

@CassieFox23 commented on GitHub (Oct 11, 2026):

TL;DR: On Linux with Ghidra 12.1.4, I get the same split the issue reports. REA main refuses these files. Ghidra, left to choose, picks a 64-bit language for them and fails to decode the return instruction.

Thanks, bqw371, for the detailed table. I rebuilt it from the repo's own fixture on Linux. Every row below is one little-endian file.

Results

Fixture: tests/conformance/c/mips.c plus mips-probe.S at a11fbc0df, built with the mips-fixture.mjs arguments. Only -march changed. I patched e_flags at byte 36. Where the record is gone, I retyped PT_MIPS_ABIFLAGS to PT_NULL and the section to SHT_PROGBITS.

In the last two columns, "32-bit" means MIPS:LE:32:default. "64-bit R6" means MIPS:LE:64:64-32R6addr. "Entry OK" means the entry function decompiles. "Entry bad" means its decompile ends in bad instruction data.

File e_flags Record REA main Ghidra, no -processor Ghidra, pinned 32-bit
mips32r2 0x70001001 yes opens 32-bit, entry OK 32-bit, entry OK
mips32 0x50001001 yes refused, ISA message 32-bit, entry OK 32-bit, entry OK
mips32, ABI cleared 0x50000001 yes refused, ISA message 64-bit R6, entry bad 32-bit, entry OK
mips32, ABI cleared 0x50000001 no refused, ISA message 64-bit R6, entry bad 32-bit, entry OK
mips32, o32 kept 0x50001001 no refused, ISA message 32-bit, entry OK 32-bit, entry OK
mips32r2, ABI cleared 0x70000001 yes refused, o32 message 64-bit R6, entry bad 32-bit, entry OK
  • Check order: in GhidraMipsProfile.ts the ISA check comes before the ABI check. So every -march=mips32 file gets the ISA message, even with the o32 bit set. Only the mips32r2 file with the ABI bits cleared reaches the o32 message.
  • Same as the issue: the o32 bit in e_flags decides the language. The ABI flags record changes nothing. My rows match the issue's rows.
  • What breaks: the 64-bit language fails to decode two words, at 0x201a8 and 0x20230. Both words are 0x03e00008, which is jr ra. Ghidra logs "Unable to resolve constructor" for each. The entry function then ends in halt_baddata(). Pinned to 32-bit, the same bytes decompile to uVar1 + (byte)rea_mips_marker[0]. The issue saw no such failures. Its spot checks decoded the same in both languages. I don't know if the cause is Ghidra 12.1.4 or this fixture.
  • Function count: all 3 functions appear in every run, with the same names and addresses. The clang files carry debug symbols. So I also stripped two copies: one with e_flags 0x50001001, one with 0x50000001 and no record. Both give 2 functions with and without the pin. Ghidra does not find the unreferenced probe function. The language and entry results match the unstripped rows.
  • Language record: on the file REA opens, the analysis profile says language_id: "auto-from-header". GhidraLauncher.ts passes -processor only for DOS targets.

Not run

  • Big-endian files.
  • npm run verify:ghidra:mips on a11fbc0df.
  • Real firmware. I did not reproduce the 400 of 5,250 figure. I did not look for an open-source legacy MIPS32 ELF with e_flags 0x50000001. I used none.
  • Any change to REA, such as relaxing the check or pinning -processor.
  • Hopper and IDA.
  • Floating-point code. The fixture is integer only.
  • A file with no .reginfo section. The clang files keep it.
Refusal text

The error has code capability_unavailable and category unsupported_provider. The Ghidra rejection code is architecture_unsupported. The two reasons are below.

  • ISA message: "This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2; other ISA revisions need separate verification."
  • o32 message: "This Ghidra MIPS lane requires an explicit o32 ABI; unspecified, n32 and EABI targets remain unsupported."
Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • RAM: 32 GB DDR4-3600
  • Node: 24.18.0
  • Main: a11fbc0df. Main has moved since, to 66c344e28. It has no changes in src/ghidra or the MIPS fixture.
  • Ghidra: 12.1.4 (Apache-2.0) with JDK 21.0.12.1
  • Compiler: clang 22.1.8 (Apache-2.0 WITH LLVM-exception OR NCSA)
  • Linker: LLD 22.1.8 (Apache-2.0 WITH LLVM-exception OR NCSA)
  • Other: GNU readelf 2.46.1 (GPL-3.0-or-later)

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6103799561 --> @CassieFox23 commented on GitHub (Oct 11, 2026): **TL;DR:** On Linux with Ghidra 12.1.4, I get the same split the issue reports. REA main refuses these files. Ghidra, left to choose, picks a 64-bit language for them and fails to decode the return instruction. Thanks, bqw371, for the detailed table. I rebuilt it from the repo's own fixture on Linux. Every row below is one little-endian file. ### Results Fixture: `tests/conformance/c/mips.c` plus `mips-probe.S` at a11fbc0df, built with the `mips-fixture.mjs` arguments. Only `-march` changed. I patched `e_flags` at byte 36. Where the record is gone, I retyped `PT_MIPS_ABIFLAGS` to `PT_NULL` and the section to `SHT_PROGBITS`. In the last two columns, "32-bit" means `MIPS:LE:32:default`. "64-bit R6" means `MIPS:LE:64:64-32R6addr`. "Entry OK" means the entry function decompiles. "Entry bad" means its decompile ends in bad instruction data. | File | `e_flags` | Record | REA main | Ghidra, no `-processor` | Ghidra, pinned 32-bit | | --- | --- | --- | --- | --- | --- | | mips32r2 | 0x70001001 | yes | opens | 32-bit, entry OK | 32-bit, entry OK | | mips32 | 0x50001001 | yes | refused, ISA message | 32-bit, entry OK | 32-bit, entry OK | | mips32, ABI cleared | 0x50000001 | yes | refused, ISA message | 64-bit R6, entry bad | 32-bit, entry OK | | mips32, ABI cleared | 0x50000001 | no | refused, ISA message | 64-bit R6, entry bad | 32-bit, entry OK | | mips32, o32 kept | 0x50001001 | no | refused, ISA message | 32-bit, entry OK | 32-bit, entry OK | | mips32r2, ABI cleared | 0x70000001 | yes | refused, o32 message | 64-bit R6, entry bad | 32-bit, entry OK | - **Check order:** in `GhidraMipsProfile.ts` the ISA check comes before the ABI check. So every `-march=mips32` file gets the ISA message, even with the o32 bit set. Only the mips32r2 file with the ABI bits cleared reaches the o32 message. - **Same as the issue:** the o32 bit in `e_flags` decides the language. The ABI flags record changes nothing. My rows match the issue's rows. - **What breaks:** the 64-bit language fails to decode two words, at 0x201a8 and 0x20230. Both words are 0x03e00008, which is `jr ra`. Ghidra logs "Unable to resolve constructor" for each. The entry function then ends in `halt_baddata()`. Pinned to 32-bit, the same bytes decompile to `uVar1 + (byte)rea_mips_marker[0]`. The issue saw no such failures. Its spot checks decoded the same in both languages. I don't know if the cause is Ghidra 12.1.4 or this fixture. - **Function count:** all 3 functions appear in every run, with the same names and addresses. The clang files carry debug symbols. So I also stripped two copies: one with `e_flags` 0x50001001, one with 0x50000001 and no record. Both give 2 functions with and without the pin. Ghidra does not find the unreferenced probe function. The language and entry results match the unstripped rows. - **Language record:** on the file REA opens, the analysis profile says `language_id: "auto-from-header"`. `GhidraLauncher.ts` passes `-processor` only for DOS targets. ### Not run - Big-endian files. - `npm run verify:ghidra:mips` on a11fbc0df. - Real firmware. I did not reproduce the 400 of 5,250 figure. I did not look for an open-source legacy MIPS32 ELF with `e_flags` 0x50000001. I used none. - Any change to REA, such as relaxing the check or pinning `-processor`. - Hopper and IDA. - Floating-point code. The fixture is integer only. - A file with no `.reginfo` section. The clang files keep it. <details> <summary>Refusal text</summary> The error has code `capability_unavailable` and category `unsupported_provider`. The Ghidra rejection code is `architecture_unsupported`. The two reasons are below. - ISA message: "This Ghidra MIPS lane requires EF_MIPS_ARCH_32R2; other ISA revisions need separate verification." - o32 message: "This Ghidra MIPS lane requires an explicit o32 ABI; unspecified, n32 and EABI targets remain unsupported." </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - RAM: 32 GB DDR4-3600 - Node: 24.18.0 - Main: a11fbc0df. Main has moved since, to 66c344e28. It has no changes in `src/ghidra` or the MIPS fixture. - Ghidra: 12.1.4 ([Apache-2.0](https://github.com/NationalSecurityAgency/ghidra/blob/master/LICENSE)) with JDK 21.0.12.1 - Compiler: clang 22.1.8 (Apache-2.0 WITH LLVM-exception OR NCSA) - Linker: LLD 22.1.8 (Apache-2.0 WITH LLVM-exception OR NCSA) - Other: GNU readelf 2.46.1 (GPL-3.0-or-later) </details> 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
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#547
No description provided.