[GH-ISSUE #2338] Ghidra MIPS: admit plain MIPS32 PIC shared libraries (ET_DYN) at their link base #774

Open
opened 2026-10-15 18:28:13 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @AurelioB on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2338

Problem

Plain MIPS32 PIC shared objects (ET_DYN, o32, MIPS32r2, no ASE flag), the bulk of any MIPS Linux firmware root filesystem, are refused by the Ghidra lane:

This Ghidra MIPS lane supports ET_EXEC and ET_REL only; shared images and PSP PRX need separate load verification.

ET_DYN is only admitted for the MIPS16 lane (#1851). Upstream main (d1aaeee40) still refuses e.g. libdsd2pcm.so (flags 0x70001007, hard-float, ABI flags record present).

Survey (one real MIPS32r2 o32 firmware rootfs)

  • 225 MIPS ELF files: 108 ET_EXEC, 30 .ko (ET_REL), 87 ET_DYN shared objects, all refused. ntpd and ntpdate are ET_DYN executables.
  • Dynamic relocation types in the 87: R_MIPS_NONE and R_MIPS_REL32 in all; R_MIPS_TLS_DTPMOD32 in 4, R_MIPS_TLS_DTPREL32 in 1, R_MIPS_TLS_TPREL32 in 12. No DT_TEXTREL.
  • 2 of the 87 (libwpa_client.so, libntfs-3g.so.86.0.0) have no ABI flags record.

With a prototype admission gate, Ghidra 12.1.4 imports and analyzes them. Procedure counts: libdsd2pcm 290, libmp3 337, libasound 3411, libz 198, libc 2957 (71 s via CLI). Calls through global GOT entries decompile with import names. TLS slots report UNSUPPORTED.

Ghidra's default placement mis-addresses page-zero GOT entries

Checking every dynamic relocation and GOT word against an independent readelf-based oracle found a real load defect at Ghidra's default placement:

  • Ghidra moves an image linked at 0 to image base 0x10000 (ElfLoaderOptionsFactory).
  • MIPS_ElfExtension.fixupGot then shifts local GOT entries with adjustTableEntryIfNonZero, so zero-valued local entries are not shifted. glibc (elf_machine_got_rel) adds the load base to every non-reserved local entry.
  • Zero local entries are the GOT page entries for page 0. GNU-ld-built libraries use them to call static functions in their first 64 KiB.

Example, libdl-2.22.so _DT_INIT: lw $25,-0x7fd8($gp); addiu $25,$25,0xcf0; jalr $25. GOT 0x13018 holds 0. At the default base, REA decompiles this as (*(code *)0xcf0)(), but the function is at 0x10cf0. libdsd2pcm's _init shows the same thing ((*(code *)0x45e0)()). In the corpus, 43 of 85 libraries failed the oracle on exactly this.

Importing at the image's own link base (-loader ElfLoader -loader-imagebase <lowest PT_LOAD vaddr>) applies no image shift. Every GOT entry is then correct as stored, and analysis addresses equal what readelf and objdump report.

Proposed lane

A separate elf32-dyn-o32-arch32r2-standard-v1 profile, beside the MIPS16 one:

  • Admits ET_DYN with no ASE bits, explicit o32 and MIPS32r2, and the executable lane's ABI flags requirements. It reuses the existing complete dynamic-relocation inspection (MipsElfSharedLoad).
  • Allowed relocation types: NONE, REL32 (symbol-bearing included; there is no MIPS16 mode-tag hazard in plain MIPS32), and the TLS loader slots DTPMOD32/DTPREL32/TPREL32. The TLS slots stay unapplied, with an explicit limitation.
  • Commits the ELF link base in the analysis profile and passes it to ElfLoader. The bridge handshake must report it. Images with an Android prelink trailer, which Ghidra prefers over PT_LOAD, are refused.
  • Real-Ghidra lane (verify:ghidra:mips32-shared): LLD fixtures in LE, BE and soft-float LE, laid out like GNU ld output. They include a call through a zero-valued local GOT page entry. Each fixture has these checks:
    • every REL32, local GOT entry and global GOT entry against an independent readelf oracle;
    • TLS slots UNSUPPORTED with unchanged bytes;
    • named calls to imports and to the static function in the decompilation;
    • a snapshot round-trip;
    • CLI evidence.

Non-goals: MIPS16 lane changes, n32/n64, microMIPS, images without an ABI flags record, resolving imports across images.

I have an implementation and will open a PR referencing this issue.

Originally created by @AurelioB on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2338 ## Problem Plain MIPS32 PIC shared objects (`ET_DYN`, o32, MIPS32r2, no ASE flag), the bulk of any MIPS Linux firmware root filesystem, are refused by the Ghidra lane: > This Ghidra MIPS lane supports ET_EXEC and ET_REL only; shared images and PSP PRX need separate load verification. `ET_DYN` is only admitted for the MIPS16 lane (#1851). Upstream `main` (`d1aaeee40`) still refuses e.g. `libdsd2pcm.so` (flags `0x70001007`, hard-float, ABI flags record present). ## Survey (one real MIPS32r2 o32 firmware rootfs) - 225 MIPS ELF files: 108 `ET_EXEC`, 30 `.ko` (`ET_REL`), **87 `ET_DYN` shared objects, all refused**. `ntpd` and `ntpdate` are `ET_DYN` executables. - Dynamic relocation types in the 87: `R_MIPS_NONE` and `R_MIPS_REL32` in all; `R_MIPS_TLS_DTPMOD32` in 4, `R_MIPS_TLS_DTPREL32` in 1, `R_MIPS_TLS_TPREL32` in 12. No `DT_TEXTREL`. - 2 of the 87 (`libwpa_client.so`, `libntfs-3g.so.86.0.0`) have no ABI flags record. With a prototype admission gate, Ghidra 12.1.4 imports and analyzes them. Procedure counts: `libdsd2pcm` 290, `libmp3` 337, `libasound` 3411, `libz` 198, `libc` 2957 (71 s via CLI). Calls through global GOT entries decompile with import names. TLS slots report `UNSUPPORTED`. ## Ghidra's default placement mis-addresses page-zero GOT entries Checking every dynamic relocation and GOT word against an independent readelf-based oracle found a real load defect at Ghidra's default placement: - Ghidra moves an image linked at 0 to image base `0x10000` (`ElfLoaderOptionsFactory`). - `MIPS_ElfExtension.fixupGot` then shifts local GOT entries with `adjustTableEntryIfNonZero`, so **zero-valued local entries are not shifted**. glibc (`elf_machine_got_rel`) adds the load base to every non-reserved local entry. - Zero local entries are the GOT page entries for page 0. GNU-ld-built libraries use them to call static functions in their first 64 KiB. Example, `libdl-2.22.so` `_DT_INIT`: `lw $25,-0x7fd8($gp); addiu $25,$25,0xcf0; jalr $25`. GOT `0x13018` holds 0. At the default base, REA decompiles this as `(*(code *)0xcf0)()`, but the function is at `0x10cf0`. `libdsd2pcm`'s `_init` shows the same thing (`(*(code *)0x45e0)()`). In the corpus, 43 of 85 libraries failed the oracle on exactly this. Importing at the image's own link base (`-loader ElfLoader -loader-imagebase <lowest PT_LOAD vaddr>`) applies no image shift. Every GOT entry is then correct as stored, and analysis addresses equal what readelf and objdump report. ## Proposed lane A separate `elf32-dyn-o32-arch32r2-standard-v1` profile, beside the MIPS16 one: - Admits `ET_DYN` with no ASE bits, explicit o32 and MIPS32r2, and the executable lane's ABI flags requirements. It reuses the existing complete dynamic-relocation inspection (`MipsElfSharedLoad`). - Allowed relocation types: `NONE`, `REL32` (symbol-bearing included; there is no MIPS16 mode-tag hazard in plain MIPS32), and the TLS loader slots `DTPMOD32`/`DTPREL32`/`TPREL32`. The TLS slots stay unapplied, with an explicit limitation. - Commits the ELF link base in the analysis profile and passes it to `ElfLoader`. The bridge handshake must report it. Images with an Android prelink trailer, which Ghidra prefers over `PT_LOAD`, are refused. - Real-Ghidra lane (`verify:ghidra:mips32-shared`): LLD fixtures in LE, BE and soft-float LE, laid out like GNU ld output. They include a call through a zero-valued local GOT page entry. Each fixture has these checks: - every REL32, local GOT entry and global GOT entry against an independent readelf oracle; - TLS slots `UNSUPPORTED` with unchanged bytes; - named calls to imports and to the static function in the decompilation; - a snapshot round-trip; - CLI evidence. Non-goals: MIPS16 lane changes, n32/n64, microMIPS, images without an ABI flags record, resolving imports across images. I have an implementation and will open a PR referencing this issue.
Author
Owner

@matbrgz commented on GitHub (Oct 12, 2026):

Triage & Implementation Review

Admitting standard MIPS32 PIC shared objects (ET_DYN, o32) at their link base is a sound addition for firmware analysis workloads where the majority of shared libraries in root filesystems are refused by the current ET_EXEC / ET_REL-only guard.

  • Load Base & Relocations: Loading at the link base (typically 0x0 or declared base) avoids synthetic rebasing distortions while preserving standard Ghidra symbol and function entry resolution for R_MIPS_REL32.
  • Implementation: This is implemented in PR #2362 (feat(ghidra): admit plain MIPS32 PIC shared images). Once PR #2362 passes CI and merges, this issue will be closed.
<!-- gh-comment-id:6134237528 --> @matbrgz commented on GitHub (Oct 12, 2026): ### Triage & Implementation Review Admitting standard MIPS32 PIC shared objects (`ET_DYN`, o32) at their link base is a sound addition for firmware analysis workloads where the majority of shared libraries in root filesystems are refused by the current `ET_EXEC` / `ET_REL`-only guard. - **Load Base & Relocations:** Loading at the link base (typically `0x0` or declared base) avoids synthetic rebasing distortions while preserving standard Ghidra symbol and function entry resolution for `R_MIPS_REL32`. - **Implementation:** This is implemented in PR #2362 (`feat(ghidra): admit plain MIPS32 PIC shared images`). Once PR #2362 passes CI and merges, this issue will be closed.
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#774
No description provided.