[GH-ISSUE #4177] [Bug] Ghidra pseudocode shows musl stdio streams as NULL: copy-relocated stderr in RELRO is folded to its on-file zero #1636

Open
opened 2026-10-16 21:08:16 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Rinkia on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4177

Before submitting

  • I searched the open and closed issues here for a related report (RELRO, copy relocation, stderr NULL): none found. A web search found no Ghidra report of this exact pattern either.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

decompile on OpenWrt's usign (MIPS32r2 BE, ET_EXEC, admitted by the Ghidra MIPS lane) returns pseudocode whose every fprintf(stderr, …) writes to a NULL stream:

__stream = (FILE *)FUN_00401b70(param_1,1);
if (__stream == (FILE *)0x0) {
  fwrite("Cannot open message file\n",1,0x19,(FILE *)0x0);
  …
fwrite("verification failed\n",1,0x14,(FILE *)0x0);

The source (usign main.c at f1f65026, the commit named in the package version 2020.05.23~f1f65026-r1) is fprintf(stderr, "Cannot open message file\n"). Apart from the stream argument, the pseudocode matches the source's control flow closely: the "Ed" checks (0x4564), the 512-byte read loop, and edsign_verify.

Cause (observed plus inferred)

  • The call site loads the stream from a fixed address: lui v0,0x42 ; lw a3,-0x80(v0), i.e. 0x41ff80.
  • readelf -D -r: 0041ff80 R_MIPS_COPY stderr (likewise stdin 0x41ff7c and stdout 0x41ff78). musl declares these as FILE *const, so the linker places the copies in .data.rel.ro.
  • readelf -l: GNU_RELRO 0x41ff70 … size 0x90, so 0x41ff80 is inside RELRO. The file bytes there are 0; the dynamic loader fills them in.
  • REA's segment inventory for this program shows Ghidra split the RW PT_LOAD exactly at the RELRO end: segment_5 0x41ff70–0x420000 and segment_5.split 0x420000–0x420070. That's consistent with the RELRO part being marked read-only. The decompiler then treats the read-only on-file value 0 as a constant. This step is my inference: REA's segments[] reports read/write/execute as null, so I couldn't confirm the block permissions through REA.

The pseudocode is therefore wrong at exactly the places that use the C library's standard streams. In OpenWrt that's every musl executable that isn't PIE, plus anything else whose copy-relocated const data lands in RELRO.

Reproduction

  • Image: OpenWrt 24.10.8 openwrt-24.10.8-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin, sha256 feeab4e3142feb865450ab044a42e198af9510d263e4b0f6d1cdcc701d296003 (release sha256sums with a good GPG signature from the OpenWrt Build System key).
  • Extraction: rea extract-firmware → usr/bin/usign, sha256 8e3fb8861223721a765c9ab7e95d98d00c72b5984b77e54629e5405e2ddea7e1; unsquashfs gives the same bytes.
rea trace     …/usr/bin/usign "Failed to decode signature" --provider ghidra   # -> FUN_00403c20
rea decompile …/usr/bin/usign 0x403c20 --provider ghidra                       # fwrite(..., (FILE *)0x0)
rea instructions …/usr/bin/usign 0x403c20 --provider ghidra                    # lw a3,-0x80(v0) @ 0x42xxxx
readelf -D -r usign | grep COPY ; readelf -l usign | grep RELRO

Expected behavior

Pseudocode must not present a loader-initialized value as a constant. Options:

  • before decompiling, mark ranges covered by R_*_COPY (and other dynamic relocations) inside read-only RELRO blocks as writable or volatile, or label them with the relocated symbol so Ghidra prints stderr rather than (FILE *)0x0;
  • at minimum, add a limitation to decompile/analyze_function Evidence when a function reads read-only memory that a dynamic relocation targets;
  • report block permissions in segments[], which are currently null, so the read-only marking is visible.

Impact and scope

  • Silent semantic error in decompiled output: a NULL stream argument reads like a real bug in the target.
  • Verified on one binary and three call sites. The pattern (musl FILE *const stdio, copy relocations into RELRO) applies to musl executables that aren't PIE generally. I haven't measured how many of the 15 executables REA admitted from this image (or x86/ARM musl builds) are affected.

REA version

rea-agents@6.5.0, Ghidra 12.1.4.

Runtime and platform

Linux x64 Docker on Windows 11 / Docker Desktop.

Target and artifact context

Public, signed OpenWrt release image (GPL); usign is ISC-licensed.

Interface

CLI.

Originally created by @Rinkia on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4177 ### Before submitting - [x] I searched the open and closed issues here for a related report (RELRO, copy relocation, stderr NULL): none found. A web search found no Ghidra report of this exact pattern either. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `decompile` on OpenWrt's `usign` (MIPS32r2 BE, `ET_EXEC`, admitted by the Ghidra MIPS lane) returns pseudocode whose every `fprintf(stderr, …)` writes to a NULL stream: ```c __stream = (FILE *)FUN_00401b70(param_1,1); if (__stream == (FILE *)0x0) { fwrite("Cannot open message file\n",1,0x19,(FILE *)0x0); … fwrite("verification failed\n",1,0x14,(FILE *)0x0); ``` The source (`usign` `main.c` at `f1f65026`, the commit named in the package version `2020.05.23~f1f65026-r1`) is `fprintf(stderr, "Cannot open message file\n")`. Apart from the stream argument, the pseudocode matches the source's control flow closely: the `"Ed"` checks (`0x4564`), the 512-byte read loop, and `edsign_verify`. ### Cause (observed plus inferred) - The call site loads the stream from a fixed address: `lui v0,0x42 ; lw a3,-0x80(v0)`, i.e. `0x41ff80`. - `readelf -D -r`: `0041ff80 R_MIPS_COPY stderr` (likewise `stdin` `0x41ff7c` and `stdout` `0x41ff78`). musl declares these as `FILE *const`, so the linker places the copies in `.data.rel.ro`. - `readelf -l`: `GNU_RELRO 0x41ff70 … size 0x90`, so `0x41ff80` is inside RELRO. The file bytes there are 0; the dynamic loader fills them in. - REA's segment inventory for this program shows Ghidra split the RW `PT_LOAD` exactly at the RELRO end: `segment_5 0x41ff70–0x420000` and `segment_5.split 0x420000–0x420070`. That's consistent with the RELRO part being marked read-only. The decompiler then treats the read-only on-file value 0 as a constant. This step is my inference: REA's `segments[]` reports `read`/`write`/`execute` as `null`, so I couldn't confirm the block permissions through REA. The pseudocode is therefore wrong at exactly the places that use the C library's standard streams. In OpenWrt that's every musl executable that isn't PIE, plus anything else whose copy-relocated `const` data lands in RELRO. ### Reproduction - **Image:** OpenWrt 24.10.8 `openwrt-24.10.8-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin`, sha256 `feeab4e3142feb865450ab044a42e198af9510d263e4b0f6d1cdcc701d296003` (release `sha256sums` with a good GPG signature from the OpenWrt Build System key). - **Extraction:** `rea extract-firmware` → `usr/bin/usign`, sha256 `8e3fb8861223721a765c9ab7e95d98d00c72b5984b77e54629e5405e2ddea7e1`; `unsquashfs` gives the same bytes. ```sh rea trace …/usr/bin/usign "Failed to decode signature" --provider ghidra # -> FUN_00403c20 rea decompile …/usr/bin/usign 0x403c20 --provider ghidra # fwrite(..., (FILE *)0x0) rea instructions …/usr/bin/usign 0x403c20 --provider ghidra # lw a3,-0x80(v0) @ 0x42xxxx readelf -D -r usign | grep COPY ; readelf -l usign | grep RELRO ``` ### Expected behavior Pseudocode must not present a loader-initialized value as a constant. Options: - before decompiling, mark ranges covered by `R_*_COPY` (and other dynamic relocations) inside read-only RELRO blocks as writable or volatile, or label them with the relocated symbol so Ghidra prints `stderr` rather than `(FILE *)0x0`; - at minimum, add a limitation to `decompile`/`analyze_function` Evidence when a function reads read-only memory that a dynamic relocation targets; - report block permissions in `segments[]`, which are currently `null`, so the read-only marking is visible. ### Impact and scope - Silent semantic error in decompiled output: a NULL stream argument reads like a real bug in the target. - Verified on one binary and three call sites. The pattern (musl `FILE *const` stdio, copy relocations into RELRO) applies to musl executables that aren't PIE generally. I haven't measured how many of the 15 executables REA admitted from this image (or x86/ARM musl builds) are affected. ### REA version `rea-agents@6.5.0`, Ghidra 12.1.4. ### Runtime and platform Linux x64 Docker on Windows 11 / Docker Desktop. ### Target and artifact context Public, signed OpenWrt release image (GPL); usign is ISC-licensed. ### Interface CLI.
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#1636
No description provided.