Files
reverse-engineering-anything/tests/conformance/ghidra/loader-relocation.c
T
8e2853dd7b fix(ghidra): prevent loader relocations from constant folding (#10483)
* fix(ghidra): prevent loader relocations from constant folding

Ghidra imports loader relocation destinations with their file bytes. A
destination in a non-writable loaded block can be populated by the
runtime loader instead, for example a copy-relocated musl `stderr` in
RELRO, so the decompiler folded the on-disk zero and showed
`fprintf(NULL, ...)`.

Before initializing the decompiler, mark each loaded, initialized,
non-writable block containing a relocation destination volatile in the
ephemeral database, and add a limitation to procedures that read such a
destination. ELF shared objects and PIEs are included; relocatable
objects and core files are not. Permissions and source FileBytes are
unchanged.

Sessions without database-mutation authority (Windows P0 and existing
caller projects) record the limitation but do not mark blocks volatile,
and the analysis profile versions the two behaviors separately.

A real Linux Ghidra ELF probe runs in the Ghidra bridge CI lane.

Fixes #4177

Squashed from #4198 and rebased onto main. Rebase adaptations: the
read-only gate, the verifier renamed to verify-ghidra-loader-relocation
so it stays out of the manual-verifier glob, docs moved to
docs/testing-lanes.md and docs/providers/ghidra-provider.md.

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ghidra): await installation inspection in the relocation verifier

inspectGhidraInstallation became async on main after #4198's base, so the
verifier asserted on a pending Promise. Also wait for provider output to
close before reading it, like the sibling Ghidra verifiers, and keep the
workflow reference inside the docs site.

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ghidra): address relocation review findings

- Version the existing-project analysis profile with the read-only
  relocation semantics, since those sessions now add the limitation.
- Inspect the installation through the verifier helper that owns the
  file-reader resources.
- Build the fixture with -fPIC: with -fPIE, GCC 12 emits a copy
  relocation of rea_external_value into writable .bss, so the probe's
  GOT check could not hold.
- Run probe cleanup through withVerifierCleanup so a cleanup failure
  does not hide the probe failure, and keep the workspace when owned
  Ghidra cleanup is incomplete.
- Link the workflow with a full GitHub URL, per the docs guide.

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ghidra): reach the external data through the GOT with a weak extern

Building the whole fixture with -fPIC also turned the reader's direct
load of rea_loader_pointer into a GOT-relaxed lea plus two loads, and
the real-Ghidra lane then reported that the baseline no longer folds
the file constant. Keep -fPIE and declare only rea_external_value weak:
GCC and clang must reach a possibly absent symbol through a GOT entry,
which lands as R_X86_64_GLOB_DAT inside GNU_RELRO (checked with readelf
for GCC 12.2 and clang/lld).

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ghidra): protect only relocations Ghidra could not apply

Running the real-Ghidra lane locally (Ghidra 12.1.4, JDK 21, GCC 12)
showed that marking every non-writable block that holds a relocation
volatile also marks full-RELRO .got. External calls then decompile as
opaque pointer calls (`(*pcVar2)(param_1)` through
`PTR_rea_external_...` instead of `rea_external(param_1)`), which the
probe's GOT-stability check caught.

Ghidra applies GLOB_DAT and RELATIVE relocations, so those bytes already
hold the loader's value and folding them is correct. The #4177 case is
a copy relocation, which Ghidra imports as UNSUPPORTED with the file's
zero in place. Protect only destinations whose relocation status is not
APPLIED or APPLIED_OTHER.

The fixture now reads a provider `const` object that the linker copies
into .data.rel.ro, the #4177 shape. The probe requires that relocation
to be unapplied and requires applied GOT relocations to stay
non-volatile. Ghidra 12.1.4 already shows this x86-64 read by name
without protection, so the probe records whether the baseline folds and
compares before and after only when it does. The profile value becomes
volatile-unapplied-relocation-blocks-v1.

Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Arslan Basharat <arslanbasharat414@gmail.com>
Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 09:32:48 +01:00

59 lines
2.1 KiB
C

/* Source-owned Linux ELF fixture for the Ghidra loader-relocation boundary. */
typedef unsigned int rea_loader_word;
/* This file-backed value is the constant the unprotected decompiler may fold. */
__attribute__((used, visibility("default")))
const rea_loader_word rea_loader_value = 0x11111111u;
#if defined(__ELF__)
#define REA_LOADER_POINTER_SECTION ".data.rel.ro"
#elif defined(__APPLE__)
#define REA_LOADER_POINTER_SECTION "__DATA_CONST,__const"
#else
#error "The loader-relocation fixture requires an ELF or Mach-O compiler"
#endif
/* The loader fills this pointer before its RELRO block is sealed read-only. */
__attribute__((used, visibility("default"), section(REA_LOADER_POINTER_SECTION)))
const rea_loader_word *volatile const rea_loader_pointer = &rea_loader_value;
/* A const object defined by the provider. In a PIE, GCC and the linker copy
* it into RELRO with a copy relocation that Ghidra cannot apply, so the file
* holds zero where the loader writes the provider's value: the #4177 shape. */
extern const rea_loader_word rea_external_const;
__attribute__((noinline, used, visibility("default")))
rea_loader_word rea_loader_reader(void) {
return rea_external_const;
}
__attribute__((noinline, used, visibility("default")))
void rea_loader_writer(void) {
*(volatile rea_loader_word *)(void *)&rea_external_const = 0;
}
/* An applied relocation in the same RELRO segment; it is not protected. */
__attribute__((noinline, used, visibility("default")))
rea_loader_word rea_loader_pointer_reader(void) {
return *rea_loader_pointer;
}
extern rea_loader_word rea_external(int value);
/* Weak so -fPIE code reads it through a RELRO GOT entry. A strong reference
* lets GCC use a copy relocation into writable .bss instead. */
extern rea_loader_word rea_external_value __attribute__((weak));
__attribute__((noinline, used, visibility("default")))
rea_loader_word rea_loader_got_caller(rea_loader_word value) {
return rea_external((int)value) + rea_external_value;
}
__attribute__((used, visibility("default"))) void rea_loader_start(void) {
volatile rea_loader_word result = rea_loader_reader();
(void)result;
for (;;) {
}
}
#undef REA_LOADER_POINTER_SECTION