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>
This commit is contained in:
authored and GitHub committed 2026-11-14 09:32:48 +01:00
1 parent add0aa1293
commit 8e2853dd7b
14 files changed
+998 -1

No files matched your search

+9 -1
View File
@@ -14,7 +14,7 @@ jobs:
ghidra-bridge-sources:
name: Compile packaged Ghidra bridge sources
runs-on: ubuntu-24.04
timeout-minutes: 15
timeout-minutes: 20
env:
GHIDRA_RELEASE_URL: https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.4_build/ghidra_12.1.4_PUBLIC_20260921.zip
GHIDRA_RELEASE_SHA256: ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
@@ -46,6 +46,14 @@ jobs:
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
test -n "${!java_home_variable}"
JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk
- name: Install locked dependencies for loader relocation regression
run: npm ci
- name: Verify loader relocation handling with the native decompiler
shell: bash
run: |
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
export JAVA_HOME="${!java_home_variable}"
npm run verify:ghidra:loader-relocation
- name: Inspect real ARM, Thumb and MIPS instruction context through the bridge
shell: bash
run: |
+124
View File
@@ -38,6 +38,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
@@ -61,6 +62,7 @@ import ghidra.app.decompiler.ClangCaseToken;
import ghidra.app.decompiler.ClangNode;
import ghidra.framework.Application;
import ghidra.app.util.bin.format.pdb.PdbParserConstants;
import ghidra.app.util.opinion.ElfLoader;
import ghidra.framework.options.Options;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressIterator;
@@ -126,6 +128,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
private static final int DEFAULT_HIGH_PCODE_DEF_USE_EDGES = 12000;
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final Map<FileBytes, Boolean> targetSourceIdentity = new IdentityHashMap<>();
private final NavigableMap<Address, LoaderInitializedRelocation> loaderInitializedRelocations =
new TreeMap<>();
private String admittedTargetSha256;
private static final Set<String> DESCRIPTOR_KEYS = Set.of(
"transport",
@@ -257,6 +261,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (!descriptor.transport.equals("unix-socket") && descriptor.analysisExtensions.size() > 0)
throw new IOException("Windows Ghidra P0 does not admit metadata recovery or database mutation.");
analysisExtensions.analyze(descriptor.analysisExtensions, currentProgram, monitor);
// Read-only sessions keep the discovered limitation but cannot mark blocks volatile.
protectLoaderInitializedRelocations(
!descriptor.existingProject && descriptor.transport.equals("unix-socket"));
initializeDecompiler();
checkpointModification = currentProgram.getModificationNumber();
serve(descriptor);
@@ -1046,6 +1053,89 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
/**
* Ghidra applies most loader relocations, but one it cannot apply (for
* example an ELF copy relocation in RELRO, whose value comes from another
* object) keeps its on-disk bytes while the runtime loader supplies a
* different value. Mark a non-writable loaded block holding such a
* destination volatile before opening the decompiler so those bytes cannot
* be constant-propagated as if they were runtime values.
*
* This changes only the ephemeral analysis database: source FileBytes and
* the original target remain untouched. MemoryBlock exposes volatility for
* a whole block, so the mitigation is intentionally conservative within a
* block while preserving its read/write/execute permissions.
*
* Without database-mutation authority (Windows P0 or a caller's existing
* project) the relocations are still recorded so affected procedures carry
* the limitation, but no block is marked volatile.
*/
private void protectLoaderInitializedRelocations(boolean markVolatile) throws Exception {
String elfFileType = currentProgram.getOptions(
ghidra.program.model.listing.Program.PROGRAM_INFO
).getString(ElfLoader.ELF_FILE_TYPE_PROPERTY, "");
// Ghidra also marks ELF ET_DYN and ET_CORE programs relocatable.
if (currentProgram.getRelocationTable().isRelocatable() &&
!elfFileType.equals("shared object")) return;
Map<Address, LoaderInitializedRelocation> discovered = new TreeMap<>();
Set<MemoryBlock> blocks = java.util.Collections.newSetFromMap(new IdentityHashMap<>());
Iterator<Relocation> iterator = currentProgram.getRelocationTable().getRelocations();
while (iterator.hasNext()) {
monitor.checkCancelled();
Relocation relocation = iterator.next();
// An applied relocation already holds the value the loader writes
// (for example GLOB_DAT or RELATIVE), so folding it is correct and
// keeping its block foldable preserves GOT-call pseudocode.
Relocation.Status status = relocation.getStatus();
if (status == Relocation.Status.APPLIED || status == Relocation.Status.APPLIED_OTHER)
continue;
Address location = relocation.getAddress();
MemoryBlock block = currentProgram.getMemory().getBlock(location);
if (block == null || !block.isLoaded() || !block.isInitialized() || block.isWrite())
continue;
long lengthInBytes = relocation.getLength();
int addressableUnitSize = location.getAddressSpace().getAddressableUnitSize();
long lengthInAddressableUnits = Math.max(
1L,
(lengthInBytes + addressableUnitSize - 1L) / addressableUnitSize
);
discovered.put(
location,
new LoaderInitializedRelocation(
location,
lengthInAddressableUnits,
block.getName(),
relocation.getSymbolName()
)
);
blocks.add(block);
}
if (discovered.isEmpty()) return;
loaderInitializedRelocations.putAll(discovered);
if (!markVolatile) return;
int transaction = currentProgram.startTransaction("REA loader relocation semantics");
boolean commit = false;
try {
for (MemoryBlock block : blocks) {
monitor.checkCancelled();
if (!block.isVolatile()) block.setVolatile(true);
}
commit = true;
}
finally {
currentProgram.endTransaction(transaction, commit);
}
}
private LoaderInitializedRelocation loaderInitializedRelocationAt(Address address) {
Map.Entry<Address, LoaderInitializedRelocation> entry =
loaderInitializedRelocations.floorEntry(address);
if (entry == null) return null;
LoaderInitializedRelocation relocation = entry.getValue();
return relocation.contains(address) ? relocation : null;
}
private DecompileOptions decompileOptions(Integer payloadMiB) {
DecompileOptions options = new DecompileOptions();
options.setMaxInstructions(Integer.MAX_VALUE);
@@ -3990,6 +4080,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
private final Function function;
private final List<String> limitations = new ArrayList<>();
private boolean inferredNoReturnCall;
private final Set<Address> observedLoaderRelocations = new HashSet<>();
private Instruction beforePrevious;
private Instruction previous;
@@ -4004,6 +4095,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
limitations.add(terminal.limitation());
if (terminal.inferredNoReturn()) inferredNoReturnCall = true;
}
for (Reference reference : instruction.getReferencesFrom()) {
if (!reference.getReferenceType().isRead()) continue;
LoaderInitializedRelocation relocation = loaderInitializedRelocationAt(
reference.getToAddress()
);
if (relocation == null || !observedLoaderRelocations.add(relocation.address()))
continue;
limitations.add(loaderInitializedRelocationLimitation(relocation));
}
if (beforePrevious != null && previous != null) {
String seh = borlandSehHandlerLimitation(
function, beforePrevious, previous, instruction);
@@ -4050,6 +4150,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
return bookmark != null && "Non-Returning Function Found".equals(bookmark.getComment());
}
private static String loaderInitializedRelocationLimitation(
LoaderInitializedRelocation relocation) {
String symbol = relocation.symbolName();
String symbolDetail = symbol == null || symbol.isBlank()
? "an unnamed relocation"
: "the relocation for " + symbol;
return "This procedure reads bytes affected by the loader relocation at " +
canonicalAddress(relocation.address()) + " in non-writable block " +
relocation.blockName() +
", which contains " + symbolDetail +
"; the file-backed value may differ from the loader-initialized runtime value, " +
"so constants derived from this location are not authoritative.";
}
private String borlandSehHandlerLimitation(
Function function, Instruction handlerPush, Instruction framePush,
Instruction frameLink) {
@@ -5484,6 +5598,16 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
private record AddressNameConflict(Namespace namespace, String leaf, List<Address> requesters, List<Address> holders) {}
private record AddressNameFailure(String reason, AddressNameConflict conflict) {}
private record LoaderInitializedRelocation(
Address address, long lengthInAddressableUnits, String blockName, String symbolName
) {
private boolean contains(Address candidate) {
if (!candidate.hasSameAddressSpace(address) || candidate.compareTo(address) < 0)
return false;
long displacement = candidate.subtract(address);
return displacement >= 0 && displacement < lengthInAddressableUnits;
}
}
private record FunctionEntry(Function function, InventoryItem item) {}
private record ProcedureNameMatch(boolean primary, List<String> aliases) {
boolean matched() {
+15
View File
@@ -362,6 +362,21 @@ flag and affected callers are repaired in the same pass. Unknown imports and
genuine no-return imports keep their flags. Analysis profiles version this repair
so snapshots produced before it are not reused. Windows P0 does not run it.
Ghidra applies most loader relocations, but it cannot apply some, such as an
ELF copy relocation into RELRO (for example musl's `stderr`). Those destinations
keep their file bytes while the runtime loader writes another object's value.
Before decompiling, a fresh non-Windows import marks each loaded, initialized,
non-writable block that holds such an unapplied relocation destination volatile
in the ephemeral database, so the decompiler cannot fold the on-disk value into
a constant. Applied relocations such as GOT entries stay foldable, so external
calls keep their names. Permissions and source `FileBytes` are unchanged; the
whole block is affected because Ghidra sets volatility per block. A procedure
that reads an unapplied destination adds a limitation naming the relocation.
Windows P0 and existing projects (`REA_GHIDRA_PROJECT_PATH`) have no
database-mutation authority: they add the same limitation, but their pseudocode
may still show the file-backed value. ELF relocatable objects and core files are
excluded. Analysis profiles version this behavior.
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64/AArch64 ELF on Linux or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
+20
View File
@@ -549,6 +549,26 @@ does not call CLI `search-bytes`, and does not page an overlay search across a
snapshot reopen. AArch64 remains covered by deterministic tests rather than this
real-provider lane. See [raw binary analysis](providers/raw-binary.md).
`npm run verify:ghidra:loader-relocation` runs
`tests/conformance/ghidra/ReaLoaderRelocationProbe.java` inside a real Linux
Ghidra ELF import. The source-owned PIE fixture reads a `const` object from a
provider library, which the linker copies into `.data.rel.ro` with a copy
relocation that Ghidra imports as unapplied, the shape of #4177. The probe
requires that relocation and the reader's read reference to it, a non-writable
file-backed block, and the bridge's volatile marking and limitation. Source
`FileBytes` and block permissions must be unchanged. Applied GOT relocations
must stay non-volatile, and a full-RELRO `-fno-plt` GOT caller must decompile
identically before and after protection. Ghidra 12.1.4 already shows this
x86-64 read by name without protection, so the report records whether the
baseline folded the file-backed zero and compares before and after only when it
did. The decompiler effect on the original MIPS case was observed on the
reporter's binary, not in this lane. The lane never executes the fixture and
needs Ghidra/JDK with the native decompiler and a Linux C compiler. The
change-selected [Ghidra bridge CI lane](https://github.com/morluto/rea/blob/main/.github/workflows/ghidra-bridge-sources.yml)
runs it with its checksum-verified Ghidra distribution. Read-only sessions
(Windows P0 and caller-owned existing projects) record the same limitation but
do not mark blocks volatile; no real-provider lane covers them.
`npm run verify:ghidra:address-context` runs on Linux with a caller-installed
Ghidra, full JDK, and an x86-64 ELF GCC toolchain (`CC` selects another compatible
compiler). It builds
+1
View File
@@ -231,6 +231,7 @@
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:kuna": "npm run build:cached && node scripts/verify-real-kuna.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:loader-relocation": "npm run build:cached && node scripts/verify-ghidra-loader-relocation.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:raw": "npm run build:cached && node scripts/verify-real-ghidra-raw.mjs",
"verify:ghidra:address-context": "npm run build:cached && node scripts/verify-real-ghidra-address-context.mjs",
+3
View File
@@ -640,6 +640,9 @@ const PATH_SCOPES = {
"tests/conformance/ghidra/ReaInstructionContextProbe.java",
"tests/conformance/ghidra/ReaComputedCallCoverageProbe.java",
"tests/conformance/ghidra/ReaDecodeProbe.java",
"scripts/verify-ghidra-loader-relocation.mjs",
"tests/conformance/ghidra/ReaLoaderRelocationProbe.java",
"tests/conformance/ghidra/loader-relocation*.c",
".github/workflows/ghidra-bridge-sources.yml",
],
website: [
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../dist/process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const exec = promisify(execFile);
const expectedChecks = [
"bridge_source_bound",
"relocation_protection_precedes_decompiler",
"fixture_is_elf_shared_object",
"dynamic_elf_relocation_table_is_relocatable",
"importer_relocation_present",
"relocation_not_applied_by_importer",
"reader_references_relocation",
"writer_references_relocation",
"got_function_relocation_present",
"got_data_relocation_present",
"got_caller_references_loader_relocation",
"destination_is_loaded_initialized",
"destination_is_file_backed",
"destination_is_non_writable",
"destination_not_volatile_before_protection",
"destination_block_is_volatile",
"destination_permissions_preserved",
"source_filebytes_unchanged",
"evidence_limitation_represents_loader_uncertainty",
"non_read_relocation_reference_not_reported_as_read",
"relocation_lookup_covers_last_affected_byte",
"applied_got_relocations_not_volatile",
"native_decompiler_available",
"baseline_got_pseudocode_resolves_external_symbols",
"protected_got_pseudocode_keeps_external_symbols",
"full_relro_got_caller_pseudocode_is_stable",
];
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-ghidra-loader-relocation.mjs");
if (process.platform !== "linux")
throw new Error(
"Loader-relocation verification requires a Linux Ghidra host with an ELF linker.",
);
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error(
"Set GHIDRA_INSTALL_DIR to a bring-your-own Ghidra installation.",
);
const installation = await inspectVerifierGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
});
assert.ok(
installation.analyzeHeadlessPath !== null &&
(installation.status === "available" ||
installation.rejection?.code === "executable_missing"),
JSON.stringify(installation),
);
const run = createVerifierRun();
const workspace = await mkdtemp(
join(tmpdir(), "rea-ghidra-loader-relocation-"),
);
const project = join(workspace, "project");
const target = join(workspace, "rea-loader-relocation.elf");
const provider = join(workspace, "librea-loader-provider.so");
const fixture = fileURLToPath(
new URL("../tests/conformance/ghidra/loader-relocation.c", import.meta.url),
);
const providerFixture = fileURLToPath(
new URL(
"../tests/conformance/ghidra/loader-relocation-provider.c",
import.meta.url,
),
);
const bridgeDirectory = fileURLToPath(
new URL("../bridge/ghidra", import.meta.url),
);
const fixtureDirectory = fileURLToPath(
new URL("../tests/conformance/ghidra", import.meta.url),
);
const bridge = join(bridgeDirectory, "ReaGhidraBridge.java");
const compiler = process.env.REA_CC ?? "cc";
let supervisor;
let report;
// Owned Ghidra processes may still use the workspace until cleanup proves
// they are gone, so the workspace is released only after that proof.
let workspaceReleasable = true;
await withVerifierCleanup(async () => {
await mkdir(project);
await buildProvider(compiler, providerFixture, provider);
await buildFixture(compiler, fixture, provider, target);
const targetDigest = digest(await readFile(target));
const launch = await spawnOwnedProviderProcess({
command: installation.analyzeHeadlessPath,
arguments: [
project,
"LoaderRelocation",
"-import",
target,
"-readOnly",
"-deleteProject",
"-scriptPath",
`${bridgeDirectory};${fixtureDirectory}`,
"-postScript",
"ReaLoaderRelocationProbe.java",
bridge,
],
runId: run.run_id,
expectedCommand: null,
});
supervisor = new ProviderProcessSupervisor({
...launch,
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
});
assert.ok(
await supervisor.waitForExit(180_000),
"Ghidra loader-relocation probe exceeded its 180-second deadline",
);
assert.ok(
await supervisor.waitForOutputClose(1000),
"Ghidra loader-relocation probe output did not close",
);
const snapshot = supervisor.snapshot();
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
assert.equal(snapshot.exitCode, 0, output);
const matches = [
...output.matchAll(/REA_LOADER_RELOCATION_PROBE_JSON (\{[^\r\n]*\})/gu),
];
assert.equal(matches.length, 1, output);
report = JSON.parse(matches[0][1]);
assert.equal(report.status, "passed");
assert.equal(report.ghidra_version, installation.providerVersion);
assert.deepEqual(
expectedChecks.every((check) => report.checks.includes(check)),
true,
);
assert.equal(report.source_filebytes_unchanged, true);
assert.equal(report.permissions, "read=true,write=false,execute=false");
assert.ok(
report.limitations.some(
(value) =>
value.includes("non-writable block") &&
value.includes("file-backed value may differ") &&
value.includes("not authoritative"),
),
);
assert.equal(report.decompiler_verified, true);
assert.ok(report.checks.includes("pseudocode_does_not_use_file_constant"));
// Ghidra 12.1.4 shows this x86-64 copy-relocated read by name even
// unprotected, so the before/after difference is checked only when the
// baseline folds the file-backed zero.
assert.equal(typeof report.baseline_folds_file_constant, "boolean");
if (report.baseline_folds_file_constant)
assert.ok(
report.checks.includes("protected_pseudocode_is_not_baseline_constant"),
);
assert.ok(
report.checks.includes("full_relro_got_caller_pseudocode_is_stable"),
);
assert.equal(digest(await readFile(target)), targetDigest);
report = {
...report,
target_sha256: targetDigest,
decompiler_scope: report.baseline_folds_file_constant
? "native Ghidra baseline folded the file constant; protected pseudocode does not"
: "native Ghidra baseline did not fold the file constant on this toolchain; protected pseudocode does not use it and GOT callers are unchanged",
};
}, [
async () => {
if (supervisor === undefined) return;
const stopped = await supervisor.stop();
supervisor.dispose();
workspaceReleasable = stopped.status !== "incomplete";
assert.ok(
workspaceReleasable,
`Owned Ghidra cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`,
);
},
async () => {
if (workspaceReleasable)
await rm(workspace, { recursive: true, force: true });
},
]);
assert.ok(report);
console.log(
JSON.stringify({
...report,
cleanup: "complete",
verifier_run: await completeVerifierRun(run),
}),
);
async function buildProvider(compiler, source, output) {
const arguments_ = [
"-O0",
"-fPIC",
"-shared",
"-nostdlib",
source,
"-o",
output,
];
await exec(compiler, arguments_, { timeout: 60_000 });
}
async function buildFixture(compiler, source, provider, output) {
const arguments_ = [
"-O0",
"-fPIE",
"-fno-plt",
"-pie",
"-nostdlib",
"-Wl,-z,relro,-z,now",
"-Wl,-e,rea_loader_start",
"-Wl,-rpath,$ORIGIN",
source,
provider,
"-o",
output,
];
await exec(compiler, arguments_, { timeout: 60_000 });
}
function digest(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
+9
View File
@@ -76,6 +76,9 @@ const sharedAnalysisSemantics = {
byte_search_coverage: "separate-loaded-overlays-v1",
function_boundary_observations: "ghidra-function-boundary-limitations-v5",
} as const;
const loaderRelocationSemantics = "volatile-unapplied-relocation-blocks-v1";
// Read-only sessions record relocation limitations without marking blocks volatile.
const readOnlyLoaderRelocationSemantics = "read-only-relocation-limitations-v1";
/**
* The processor language REA pins for this target, or undefined when Ghidra may
@@ -324,6 +327,11 @@ export const resolveGhidraAnalysisProfile = async (
}
: {}),
...sharedAnalysisSemantics,
// Windows P0 has no database-mutation authority: limitations only.
loader_relocation_semantics:
installation.platform === "win32"
? readOnlyLoaderRelocationSemantics
: loaderRelocationSemantics,
no_return_repair:
installation.platform === "win32"
? "disabled-windows-p0"
@@ -491,6 +499,7 @@ export const existingGhidraAnalysisProfile = (
...sharedAnalysisSemantics,
selected_project_relationship_coverage: "prior-analysis-unknown-v1",
annotation_policy: "read-only-existing-project-v1",
loader_relocation_semantics: readOnlyLoaderRelocationSemantics,
existing_project: project,
original_analysis: { completeness: "unknown", settings: "unknown" },
no_return_repair: "disabled-existing-project",
@@ -108,6 +108,7 @@ import {
functionCoordinateContextsEqual,
} from "../domain/native/functionCoordinateContext.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { existingGhidraAnalysisProfile } from "./GhidraAnalysisProfile.js";
import {
createAnalysisExecution,
type AnalysisOperation,
@@ -855,6 +856,33 @@ describe("Ghidra platform support", () => {
}
});
it("versions loader relocation semantics by database-mutation authority", async () => {
const semantics = async (platform: NodeJS.Platform) => {
const resolved = await provider(
installationHost(platform),
).resolveAnalysisProfile(peTarget("x86_64"));
if (!resolved.ok) throw resolved.error;
return resolved.value.profile?.parameters.loader_relocation_semantics;
};
// Windows P0 cannot mark blocks volatile, so its snapshots must not replay
// as volatile-protected pseudocode.
expect(await semantics("win32")).toBe(
"read-only-relocation-limitations-v1",
);
expect(await semantics("linux")).toBe(
"volatile-unapplied-relocation-blocks-v1",
);
// A caller's existing project is read-only too, so its saved results must
// not match a profile that predates the relocation limitation.
expect(
existingGhidraAnalysisProfile(
peTarget("x86_64"),
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{ project_name: "fixture" },
).parameters.loader_relocation_semantics,
).toBe("read-only-relocation-limitations-v1");
});
it("commits the PE role and managed classification to profile identity", async () => {
const ghidra = provider(installationHost("win32"));
const application = peTarget("x86_64");
@@ -1455,6 +1483,7 @@ describe("Ghidra client projection", () => {
call_relationship_coverage: "observed-call-inventory-and-discovery-v7",
function_dossier_coverage: "observed-facets-and-call-relationships-v1",
native_value_flow_projection: "accepted-runtime-budgets-v1",
loader_relocation_semantics: "volatile-unapplied-relocation-blocks-v1",
});
const result = await ghidra
@@ -768,6 +768,18 @@ it.each([
"scripts/verify-ghidra-bridge-sources.mjs",
{ code: true, ghidra_bridge: true, runtime: false, package: false },
],
[
"scripts/verify-ghidra-loader-relocation.mjs",
{ code: true, ghidra_bridge: true, runtime: false, package: false },
],
[
"tests/conformance/ghidra/ReaLoaderRelocationProbe.java",
{ code: true, ghidra_bridge: true, runtime: false, package: false },
],
[
"tests/conformance/ghidra/loader-relocation-provider.c",
{ code: true, ghidra_bridge: true, runtime: false, package: false },
],
[
"src/ghidra/GhidraBridgeSources.ts",
{ code: true, ghidra_bridge: true, runtime: true, package: true },
+13
View File
@@ -91,6 +91,19 @@ no-overwrite CLI delivery. It checks the target and project files for changes.
from the worst-case line size before allocating the source. Larger or
invalid counts are rejected before installation inspection or compilation.
- `loader-relocation.c`, `loader-relocation-provider.c` and
`ReaLoaderRelocationProbe.java` exercise the production `ReaGhidraBridge`
against a real imported Linux full-RELRO PIE ELF and a small shared library.
The fixture's `.data.rel.ro` pointer is populated by an importer-created
relocation. The probe proves that the `ET_DYN` program is marked relocatable
by Ghidra but is still protected, checks the relocation byte range and
read/write reference handling, and compares baseline/protected pseudocode.
It also checks that a `-fno-plt` GOT caller retains identical pseudocode when
its relocation blocks become volatile. Source `FileBytes` and block
permissions must remain unchanged. `npm run
verify:ghidra:loader-relocation` requires Linux, BYO Ghidra/JDK with its
native decompiler, and a C compiler; the fixture is never executed.
`scripts/verify-real-ghidra.mjs` uses `cc`, `clang`, and `lld-link` by default;
`REA_CC`, `REA_CLANG`, and `REA_LLD_LINK` can select alternate commands. The
verifier requires the verified bring-your-own Ghidra 12.1.4 installation through
@@ -0,0 +1,447 @@
//@category REA Verification
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import generic.jar.ResourceFile;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.util.opinion.ElfLoader;
import ghidra.app.script.GhidraScript;
import ghidra.app.script.GhidraScriptProvider;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.program.database.mem.FileBytes;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
import ghidra.program.model.reloc.Relocation;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.SymbolIterator;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Iterator;
import java.util.List;
/**
* Exercises the production loader-relocation boundary on a real imported
* Linux ELF Ghidra Program. The relocation and instruction reference must
* come from the importer; the source file and its FileBytes are never changed.
*/
public final class ReaLoaderRelocationProbe extends GhidraScript {
private record FileSnapshot(FileBytes source, byte[] original, byte[] modified) {}
private final JsonArray checks = new JsonArray();
private void require(String name, boolean condition) {
if (!condition) throw new IllegalStateException("Loader relocation assertion failed: " + name);
checks.add(name);
}
private Function function(String name) {
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
Function found = null;
while (functions.hasNext()) {
Function candidate = functions.next();
if (!candidate.getName().equals(name) && !candidate.getName().equals("_" + name)) continue;
require("unique_function_" + name, found == null);
found = candidate;
}
require("function_present_" + name, found != null);
return found;
}
private static byte[] bytes(FileBytes fileBytes, boolean original) throws Exception {
byte[] result = new byte[Math.toIntExact(fileBytes.getSize())];
int read = original
? fileBytes.getOriginalBytes(0, result)
: fileBytes.getModifiedBytes(0, result, 0, result.length);
if (read != result.length) throw new IllegalStateException("Incomplete FileBytes capture");
return result;
}
private static String digest(byte[] value) throws Exception {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value));
}
private static Method privateMethod(Class<?> owner, String name, Class<?>... types)
throws Exception {
Method method = owner.getDeclaredMethod(name, types);
method.setAccessible(true);
return method;
}
private static Object invoke(Method method, Object target, Object... arguments) throws Exception {
try {
return method.invoke(target, arguments);
}
catch (InvocationTargetException failure) {
throw new IllegalStateException("Production bridge method failed: " + method.getName(), failure.getCause());
}
}
private static void setField(Object target, String name, Object value) throws Exception {
for (Class<?> owner = target.getClass(); owner != null; owner = owner.getSuperclass()) {
try {
Field field = owner.getDeclaredField(name);
field.setAccessible(true);
field.set(target, value);
return;
}
catch (NoSuchFieldException ignored) {
// Continue through HeadlessScript's inherited state.
}
}
throw new IllegalStateException("Missing bridge field: " + name);
}
private static JsonObject procedureQuery(String document, String procedure) {
JsonObject query = new JsonObject();
query.addProperty("document", document);
query.addProperty("procedure", procedure);
return query;
}
private Address symbolAddress(String name) {
SymbolIterator symbols = currentProgram.getSymbolTable().getSymbols(name);
Address found = null;
while (symbols.hasNext()) {
Address address = symbols.next().getAddress();
if (found != null && !found.equals(address))
throw new IllegalStateException("Multiple addresses for fixture symbol " + name);
found = address;
}
if (found == null) throw new IllegalStateException("Missing fixture symbol " + name);
return found;
}
private Relocation relocationAt(Address address) {
Iterator<Relocation> relocations = currentProgram.getRelocationTable().getRelocations();
Relocation found = null;
while (relocations.hasNext()) {
Relocation candidate = relocations.next();
if (!candidate.getAddress().equals(address)) continue;
if (found != null) throw new IllegalStateException("Multiple relocations at " + canonical(address));
found = candidate;
}
return found;
}
private Relocation relocationForSymbol(String name) {
Iterator<Relocation> relocations = currentProgram.getRelocationTable().getRelocations();
while (relocations.hasNext()) {
Relocation candidate = relocations.next();
if (name.equals(candidate.getSymbolName())) return candidate;
}
return null;
}
private static boolean targetBacked(MemoryBlock block, FileBytes target) {
for (MemoryBlockSourceInfo source : block.getSourceInfos())
if (source.getFileBytes().orElse(null) == target) return true;
return false;
}
private Reference readerReference(Function reader, Address target) {
for (Instruction instruction : currentProgram.getListing().getInstructions(reader.getBody(), true)) {
for (Reference reference : instruction.getReferencesFrom()) {
if (reference.getToAddress().equals(target) && reference.getReferenceType().isRead())
return reference;
}
}
return null;
}
private Reference writerReference(Function writer, Address target) {
for (Instruction instruction : currentProgram.getListing().getInstructions(writer.getBody(), true)) {
for (Reference reference : instruction.getReferencesFrom()) {
if (reference.getToAddress().equals(target) && reference.getReferenceType().isWrite())
return reference;
}
}
return null;
}
private Reference readReference(Function function, Address target) {
for (Instruction instruction : currentProgram.getListing().getInstructions(function.getBody(), true)) {
for (Reference reference : instruction.getReferencesFrom()) {
if (reference.getToAddress().equals(target) && reference.getReferenceType().isRead())
return reference;
}
}
return null;
}
private static List<FileSnapshot> snapshotFileBytes(List<FileBytes> sources) throws Exception {
List<FileSnapshot> snapshots = new java.util.ArrayList<>();
for (FileBytes source : sources)
snapshots.add(new FileSnapshot(source, bytes(source, true), bytes(source, false)));
return snapshots;
}
private static boolean fileBytesUnchanged(List<FileSnapshot> snapshots) throws Exception {
for (FileSnapshot snapshot : snapshots) {
if (!java.util.Arrays.equals(snapshot.original(), bytes(snapshot.source(), true)) ||
!java.util.Arrays.equals(snapshot.modified(), bytes(snapshot.source(), false))) return false;
}
return true;
}
/** The copy destination's file bytes are zero; the loader writes 0x11111111. */
private static boolean containsFileConstant(String pseudocode) {
return java.util.regex.Pattern.compile("\\breturn (0|0x0);").matcher(pseudocode).find();
}
private static void disposeDecompiler(Object bridge) throws Exception {
Object value = getField(bridge, "decompiler");
if (value instanceof DecompInterface interface_) {
interface_.dispose();
setField(bridge, "decompiler", null);
}
}
private static String permissions(MemoryBlock block) {
return "read=" + block.isRead() + ",write=" + block.isWrite() + ",execute=" + block.isExecute();
}
@Override
public void run() throws Exception {
String[] arguments = getScriptArgs();
if (arguments.length != 1) throw new IllegalArgumentException("Expected exact bridge source path");
File expectedBridge = new File(arguments[0]).getCanonicalFile();
ResourceFile resource = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
if (resource == null) throw new IllegalStateException("ReaGhidraBridge.java is absent from scriptPath");
File actualBridge = new File(resource.getAbsolutePath()).getCanonicalFile();
require("bridge_source_bound", expectedBridge.equals(actualBridge));
String source = Files.readString(actualBridge.toPath());
int protectionCall = source.indexOf("protectLoaderInitializedRelocations(");
int decompilerCall = source.indexOf("initializeDecompiler();");
require("relocation_protection_precedes_decompiler", protectionCall >= 0 && protectionCall < decompilerCall);
GhidraScript bridge = GhidraScriptUtil.getProvider(resource).getScriptInstance(resource, errorWriter);
bridge.set(getState(), getControls());
setField(bridge, "sessionDefaultAddressSpace", currentProgram.getAddressFactory().getDefaultAddressSpace());
setField(bridge, "admittedTargetSha256", currentProgram.getExecutableSHA256().toLowerCase());
Memory memory = currentProgram.getMemory();
List<FileBytes> sources = memory.getAllFileBytes();
if (sources.isEmpty()) throw new IllegalStateException("Imported Program has no FileBytes");
FileBytes targetSource = sources.get(0);
List<FileSnapshot> sourceBefore = snapshotFileBytes(sources);
Function reader = function("rea_loader_reader");
Function writer = function("rea_loader_writer");
Function gotCaller = function("rea_loader_got_caller");
// The copy-relocation destination Ghidra cannot apply (#4177's shape).
Address pointer = symbolAddress("rea_external_const");
Relocation relocation = relocationAt(pointer);
String elfFileType = currentProgram.getOptions(Program.PROGRAM_INFO)
.getString(ElfLoader.ELF_FILE_TYPE_PROPERTY, "");
require("fixture_is_elf_shared_object", elfFileType.equals("shared object"));
require("dynamic_elf_relocation_table_is_relocatable",
currentProgram.getRelocationTable().isRelocatable());
require("importer_relocation_present",
relocation != null && currentProgram.getRelocationTable().hasRelocation(pointer));
if (relocation == null) throw new IllegalStateException("Importer did not create copy relocation");
require("relocation_not_applied_by_importer",
relocation.getStatus() != Relocation.Status.APPLIED &&
relocation.getStatus() != Relocation.Status.APPLIED_OTHER);
Reference reference = readerReference(reader, pointer);
require("reader_references_relocation", reference != null);
require("writer_references_relocation", writerReference(writer, pointer) != null);
Relocation functionGotRelocation = relocationForSymbol("rea_external");
Relocation dataGotRelocation = relocationForSymbol("rea_external_value");
require("got_function_relocation_present", functionGotRelocation != null);
require("got_data_relocation_present", dataGotRelocation != null);
require("got_caller_references_loader_relocation",
readReference(gotCaller, functionGotRelocation.getAddress()) != null ||
readReference(gotCaller, dataGotRelocation.getAddress()) != null);
MemoryBlock block = memory.getBlock(pointer);
if (block == null) throw new IllegalStateException("Pointer relocation has no loaded memory block");
boolean readable = block.isRead();
boolean writable = block.isWrite();
boolean executable = block.isExecute();
require("destination_is_loaded_initialized", block.isLoaded() && block.isInitialized());
require("destination_is_file_backed", targetBacked(block, targetSource));
require("destination_is_non_writable", !writable);
require("destination_not_volatile_before_protection", !block.isVolatile());
boolean decompilerAvailable = false;
String baselinePseudocode = null;
// Observed, not required: Ghidra 12.1.4 shows this x86-64 read by name.
boolean baselineFolds = false;
String protectedPseudocode = null;
String baselineGotPseudocode = null;
String protectedGotPseudocode = null;
List<String> limitations = null;
Method initialize = privateMethod(bridge.getClass(), "initializeDecompiler");
Method pseudocodeMethod = privateMethod(bridge.getClass(), "procedurePseudocode", JsonObject.class);
try {
try {
invoke(initialize, bridge);
decompilerAvailable = true;
}
catch (IllegalStateException unavailable) {
throw new IllegalStateException(
"The Linux loader-relocation probe requires Ghidra's native decompiler",
unavailable.getCause()
);
}
require("native_decompiler_available", decompilerAvailable);
JsonObject baseline = (JsonObject)invoke(
pseudocodeMethod,
bridge,
procedureQuery(currentProgram.getName(), reader.getName())
);
baselinePseudocode = baseline.get("value").getAsString();
baselineFolds = containsFileConstant(baselinePseudocode);
JsonObject baselineGot = (JsonObject)invoke(
pseudocodeMethod,
bridge,
procedureQuery(currentProgram.getName(), gotCaller.getName())
);
baselineGotPseudocode = baselineGot.get("value").getAsString();
require("baseline_got_pseudocode_resolves_external_symbols",
baselineGotPseudocode.contains("rea_external") &&
baselineGotPseudocode.contains("rea_external_value"));
disposeDecompiler(bridge);
Method protect = privateMethod(
bridge.getClass(), "protectLoaderInitializedRelocations", boolean.class);
invoke(protect, bridge, true);
require("destination_block_is_volatile", block.isVolatile());
require("destination_permissions_preserved",
block.isRead() == readable && block.isWrite() == writable && block.isExecute() == executable);
require("source_filebytes_unchanged", fileBytesUnchanged(sourceBefore));
Method limitationsMethod = privateMethod(
bridge.getClass(), "functionBoundaryLimitations", Function.class, Iterable.class
);
@SuppressWarnings("unchecked")
List<String> observedLimitations = (List<String>)invoke(
limitationsMethod,
bridge,
reader,
currentProgram.getListing().getInstructions(reader.getBody(), true)
);
limitations = observedLimitations;
require("evidence_limitation_represents_loader_uncertainty",
limitations.stream().anyMatch(value ->
value.contains("non-writable block " + block.getName()) &&
value.contains("file-backed value may differ") &&
value.contains("not authoritative")));
@SuppressWarnings("unchecked")
List<String> writerLimitations = (List<String>)invoke(
limitationsMethod,
bridge,
writer,
currentProgram.getListing().getInstructions(writer.getBody(), true)
);
require("non_read_relocation_reference_not_reported_as_read",
writerLimitations.stream().noneMatch(value ->
value.contains("file-backed value may differ")));
Method relocationLookup = privateMethod(
bridge.getClass(), "loaderInitializedRelocationAt", Address.class
);
int addressableUnitSize = pointer.getAddressSpace().getAddressableUnitSize();
long relocationLengthInAddressableUnits = Math.max(
1L,
(relocation.getLength() + (long)addressableUnitSize - 1L) / addressableUnitSize
);
Address lastAffectedAddress = pointer.add(relocationLengthInAddressableUnits - 1L);
require("relocation_lookup_covers_last_affected_byte",
invoke(relocationLookup, bridge, lastAffectedAddress) != null);
MemoryBlock functionGotBlock = memory.getBlock(functionGotRelocation.getAddress());
MemoryBlock dataGotBlock = memory.getBlock(dataGotRelocation.getAddress());
// Applied GOT relocations already hold the loader's value; protecting
// them would turn external calls into opaque pointer calls.
require("applied_got_relocations_not_volatile",
functionGotBlock != null && !functionGotBlock.isVolatile() &&
dataGotBlock != null && !dataGotBlock.isVolatile());
invoke(initialize, bridge);
JsonObject protectedResult = (JsonObject)invoke(
pseudocodeMethod,
bridge,
procedureQuery(currentProgram.getName(), reader.getName())
);
protectedPseudocode = protectedResult.get("value").getAsString();
require("pseudocode_does_not_use_file_constant", !containsFileConstant(protectedPseudocode));
if (baselineFolds)
require("protected_pseudocode_is_not_baseline_constant",
!protectedPseudocode.equals(baselinePseudocode));
JsonObject protectedGot = (JsonObject)invoke(
pseudocodeMethod,
bridge,
procedureQuery(currentProgram.getName(), gotCaller.getName())
);
protectedGotPseudocode = protectedGot.get("value").getAsString();
require("protected_got_pseudocode_keeps_external_symbols",
protectedGotPseudocode.contains("rea_external") &&
protectedGotPseudocode.contains("rea_external_value"));
require("full_relro_got_caller_pseudocode_is_stable",
protectedGotPseudocode.equals(baselineGotPseudocode));
}
finally {
disposeDecompiler(bridge);
}
JsonObject report = new JsonObject();
report.addProperty("status", "passed");
report.addProperty("ghidra_version", ghidra.framework.Application.getApplicationVersion());
report.addProperty("bridge_source_sha256", digest(Files.readAllBytes(actualBridge.toPath())));
report.addProperty("target_source_sha256", digest(sourceBefore.get(0).original()));
report.addProperty("relocation_address", canonical(pointer));
report.addProperty("relocation_type", relocation.getType());
report.addProperty("relocation_status", relocation.getStatus().toString());
report.addProperty("block", block.getName());
report.addProperty("reader_reference", canonical(reference.getFromAddress()));
report.addProperty("source_filebytes_unchanged", fileBytesUnchanged(sourceBefore));
report.addProperty("permissions", permissions(block));
report.addProperty("decompiler_verified", decompilerAvailable);
report.addProperty("baseline_folds_file_constant", baselineFolds);
report.addProperty("baseline_pseudocode", baselinePseudocode);
report.addProperty("protected_pseudocode", protectedPseudocode);
report.addProperty("baseline_got_caller_pseudocode", baselineGotPseudocode);
report.addProperty("protected_got_caller_pseudocode", protectedGotPseudocode);
if (limitations == null) throw new IllegalStateException("Loader limitation probe did not run");
report.add("limitations", GSON_ARRAY(limitations));
report.add("checks", checks);
report.addProperty("scope", "real Linux ELF importer relocation and production bridge private methods; no target execution and no source FileBytes mutation");
println("REA_LOADER_RELOCATION_PROBE_JSON " + report);
}
private static JsonArray GSON_ARRAY(List<String> values) {
JsonArray result = new JsonArray();
for (String value : values) result.add(value);
return result;
}
private static Object getField(Object target, String name) throws Exception {
for (Class<?> owner = target.getClass(); owner != null; owner = owner.getSuperclass()) {
try {
Field field = owner.getDeclaredField(name);
field.setAccessible(true);
return field.get(target);
}
catch (NoSuchFieldException ignored) { }
}
throw new IllegalStateException("Missing bridge field: " + name);
}
private static String canonical(Address address) {
return address.getAddressSpace().getName() + ":0x" + Long.toUnsignedString(address.getOffset(), 16);
}
}
@@ -0,0 +1,12 @@
typedef unsigned int rea_loader_word;
__attribute__((visibility("default")))
rea_loader_word rea_external(int value) {
return (rea_loader_word)value + 3u;
}
__attribute__((visibility("default")))
rea_loader_word rea_external_value = 0x22222222u;
__attribute__((visibility("default")))
const rea_loader_word rea_external_const = 0x11111111u;
@@ -0,0 +1,58 @@
/* 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