feat(ghidra): admit 32-bit big-endian PowerPC ELF (#1747)

* feat(ghidra): admit 32-bit big-endian PowerPC ELF

Recognize EM_PPC as a provider-neutral `powerpc` family that keeps its ELF
class, byte order, type and flags, and admit ELF32 big-endian ET_EXEC through
a bounded Ghidra lane. ELF64, little-endian, relocatable/shared objects and
EF_PPC_EMB are refused with a specific reason; Hopper and IDA do not gain
PowerPC support, and Windows Ghidra P0 is unchanged.

Ghidra's ELF loader selects PowerPC:BE:32:e500:default for a plain EM_PPC
header, which misreads 750-family code. The lane binds
PowerPC:BE:32:default/default in the profile (language_selection
"admitted-lane-v1"), imports with it and checks the reported language. A
language set through REA_GHIDRA_LANGUAGE_ID still replaces the lane default.

Add a source-owned fixture, fixed-encoding probe, GNU readelf cross-check and
the optional `verify:ghidra:ppc` real-provider lane, plus docs/ghidra-ppc.md.

Refs #1741

* style(ghidra): format shared PowerPC profile integration

* fix(ghidra): preserve caller-selected PowerPC languages

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Ariel Fikru Avicenna 2026-10-14 05:35:55 +07:00 committed by GitHub
parent 7e9fb6f1bb
commit c86b09a109
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1258 additions and 4 deletions

View File

@ -63,6 +63,7 @@ export default defineConfig({
{ text: "Ghidra DOS analysis", link: "/ghidra-dos" },
{ text: "Ghidra raw binary", link: "/raw-binary" },
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
{ text: "Ghidra PowerPC", link: "/ghidra-ppc" },
{ text: "Ghidra semantics", link: "/ghidra-provider" },
],
},

90
docs/ghidra-ppc.md Normal file
View File

@ -0,0 +1,90 @@
# PowerPC ELF analysis through Ghidra
This lane extends native ELF analysis to 32-bit big-endian PowerPC; it does
not execute PowerPC code. It is the first milestone of
[#1741](https://github.com/morluto/rea/issues/1741). GameCube/Wii DOL
executables, PPC64 and other PowerPC variants are separate follow-ups.
## Admission boundary
ELF `EM_PPC` (20) is retained as the provider-neutral `powerpc` family. The
target also retains class, byte order, `e_type` and raw `e_flags`. Recognition
does not mean every provider or PowerPC variant is supported. `EM_PPC64` (21)
is not recognized as this family.
The Ghidra lane admits ELF32 `ET_EXEC`, big endian. ELF64, little-endian,
relocatable and shared objects are refused with a specific unsupported
constraint. `EF_PPC_EMB` declares an embedded variant and is refused; the
remaining `e_flags` bits only declare relocation conventions and do not change
instruction decoding.
## Bound language
Ghidra's ELF loader selects `PowerPC:BE:32:e500:default` for a plain `EM_PPC`
header with `e_flags` 0. e500 is the embedded Book-E variant whose SPE unit
replaces the classic floating-point unit, so it misinterprets ordinary
750-family code. Nothing in the header distinguishes the two.
The lane therefore imports with `PowerPC:BE:32:default` and compiler spec
`default`, commits that choice in the analysis profile as
`language_selection: "provider-pinned-v1"`, and checks the language Ghidra
reports after import. A different language set through
`REA_GHIDRA_LANGUAGE_ID` (and optionally `REA_GHIDRA_COMPILER_SPEC_ID`)
replaces the lane default and is admitted against the installed language
catalog as usual; for example `PowerPC:BE:32:e500` for genuine e500 code.
PowerPC header facts are committed in the analysis profile so interpretation
changes invalidate profile-bound snapshots. The support-lane identity is
versioned independently of file identity. Evidence, lifecycle outputs and saved
snapshots preserve the `powerpc` family. Hopper and IDA adapters do not
implicitly gain PowerPC support. Windows Ghidra P0 remains PE x86/x86-64 only.
## Use
With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
```bash
rea analyze /absolute/path/to/ppc.elf --provider ghidra --json
rea function /absolute/path/to/ppc.elf function_name --provider ghidra --json
```
MCP uses the existing tools, including `open_binary` with an absolute path and
`provider_id: "ghidra"`. No PowerPC-specific tool catalog or emulator is
introduced.
## Verification
The source-owned freestanding fixture is `tests/conformance/c/ppc.c`. The
optional cross-target lane requires a caller-supplied Clang with the PowerPC
target, LLD, GNU `readelf`, Ghidra and its compatible JDK; ordinary host-native
checks do not acquire these tools. `REA_PPC_CLANG` selects Clang and
`REA_PPC_READELF` selects GNU readelf (for example
`/opt/homebrew/opt/binutils/bin/readelf` on macOS). These are test-time
prerequisites, not REA runtime providers.
```bash
GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:ppc
```
The lane compiles the fixture for a 750-family core (`-mcpu=750`) and never
executes it. GNU readelf independently supplies header facts and symbol
addresses, compared with the production REA target resolver before provider
startup. Its version, bounded command output and selected tool path are
retained in the verification report. The parser deliberately recognizes only
this fixture's GNU output, rejecting missing, ambiguous or unexpected fields
rather than substituting defaults.
Existing public MCP operations check Ghidra's **observed** language and
compiler specification and loaded-file hashes; an e500 import fails the lane.
The source-owned `ppc-probe.S` supplies fixed `addi` and conditional-branch
encodings: verification checks bytes, length, immediate and direct destination,
independent of decompiler variable names. Global and string bytes are checked at
independently reported symbol addresses.
CLI/MCP discovery and decompilation, direct calls, target/profile/Evidence
identity, snapshot round-trip and final process cleanup remain covered.
Pseudocode presence is a liveness check, not proof of semantic equivalence.
Instruction decoding and reader agreement do not establish runtime or complete
floating-point semantics. Parser/profile tests are narrower checks, not
substitutes for this real-provider run. Report the actual lanes run and leave
unexecuted verification pending.

View File

@ -186,6 +186,7 @@
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:seeds": "npm run build:cached && node scripts/verify/ghidra/seeds.mjs",
"verify:ghidra:mips": "npm run build:cached && node scripts/verify-real-ghidra-mips.mjs",
"verify:ghidra:ppc": "npm run build:cached && node scripts/verify-real-ghidra-ppc.mjs",
"verify:ghidra:riscv": "npm run build:cached && node scripts/verify-real-ghidra-riscv.mjs",
"verify:ghidra:6502": "npm run build:cached && node scripts/verify-real-ghidra-6502.mjs",
"verify:ghidra:mips-shared": "npm run build:cached && node scripts/verify-real-ghidra-mips-shared.mjs",

View File

@ -0,0 +1,282 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, 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 { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import {
inspectPowerPcReadelf,
assertPowerPcLoadedImage,
assertPowerPcProbe,
assertPowerPcGlobal,
} from "../tests/conformance/ghidra/ppc-checks.mjs";
import { parseEvidence } from "../dist/domain/evidence.js";
import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
import { functionDossierSchema } from "../dist/domain/hopperValues.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { buildPowerPcFixture } from "../tests/conformance/ghidra/ppc-fixture.mjs";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
// Optional cross-target lane. No tool acquisition and no target execution.
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-real-ghidra-ppc.mjs");
if (!["linux", "darwin"].includes(process.platform))
throw new Error(
"PowerPC verification requires a supported Linux/macOS Ghidra host",
);
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR");
const installation = inspectGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
});
if (installation.status !== "available")
throw new Error(
`Ghidra prerequisite unavailable: ${JSON.stringify(installation)}`,
);
const execute = promisify(execFile);
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
const run = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-ppc-proof-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
TMPDIR: runtime,
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
REA_PROCESS_RUN_ID: run.run_id,
GHIDRA_INSTALL_DIR: installation.installDir,
JAVA_HOME: installation.javaHome,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
};
for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
}
const reports = [];
let primaryFailure;
try {
{
const fixture = await buildPowerPcFixture(workspace);
const independent = await inspectPowerPcReadelf(fixture.path);
const resolved = await parseBinaryTarget(fixture.path);
if (!resolved.ok) throw resolved.error;
assert.deepEqual(
resolved.value.powerpc,
independent.powerpc,
"REA ELF interpretation disagrees with GNU readelf",
);
assert.equal(resolved.value.sha256, fixture.sha256);
const snapshotPath = join(workspace, "ppc.snapshot.json");
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "rea-real-ppc-proof", version: "1" });
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const call = async (name, args = {}) => {
const reply = await client.callTool(
{ name, arguments: args },
undefined,
{ timeout: 180000 },
);
return requireMcpOperationResult(reply, name);
};
try {
await client.connect(transport);
const target = await call("open_binary", {
path: fixture.path,
provider_id: "ghidra",
});
assert.equal(target.architecture, "powerpc");
assert.equal(target.sha256, fixture.sha256);
// ELF load-image verification is not the DOS comparison lane. Its retained
// observations still expose the actual Ghidra language and loaded bytes;
// the plain loader would have chosen e500 for this header.
const image = await call("inspect_native_load_image");
assertPowerPcLoadedImage(image.observations, fixture.sha256);
const procedures = await call("list_procedures");
const entry = procedures.find((p) => p.value === "rea_ppc_entry");
const leaf = procedures.find((p) => p.value === "rea_ppc_leaf");
assert.ok(entry, "Entry function missing from real Ghidra inventory");
assert.ok(leaf, "Leaf function missing from real Ghidra inventory");
assert.equal(
BigInt(entry.address),
BigInt(independent.symbols.rea_ppc_entry),
);
assert.equal(
BigInt(leaf.address),
BigInt(independent.symbols.rea_ppc_leaf),
);
const probe = independent.symbols.rea_ppc_probe;
assert.ok(
procedures.some(
(p) =>
p.value === "rea_ppc_probe" && BigInt(p.address) === BigInt(probe),
),
);
const move = await call("inspect_native_instruction", { address: probe });
const branch = await call("inspect_native_instruction", {
address: `0x${(BigInt(probe) + 8n).toString(16)}`,
});
assertPowerPcProbe(move, branch, probe);
const global = await call("read_bytes", {
address: independent.symbols.rea_ppc_global,
length: 4,
});
assert.equal(global.returned_bytes, 4);
assertPowerPcGlobal(global.bytes_hex);
const marker = await call("read_bytes", {
address: independent.symbols.rea_ppc_marker,
length: 29,
});
assert.equal(
marker.bytes_hex,
Buffer.from("rea-ppc-source-owned-fixture\0").toString("hex"),
);
const callees = await call("procedure_callees", {
procedure: entry.address,
});
assert.ok(
callees.includes(leaf.address),
"Expected direct call not recovered",
);
const assembly = await call("procedure_assembly", {
procedure: entry.address,
});
assert.ok(assembly.trim().length > 0);
const dossier = functionDossierSchema.parse(
await call("analyze_function", { procedure: entry.address }),
);
assert.ok(dossier.pseudocode.trim().length > 0);
const mapping = await call("address_to_file_offset", {
address: entry.address,
});
const memory = await call("read_bytes", {
address: entry.address,
length: 16,
});
const sourceBytes = await readFile(fixture.path);
assert.equal(memory.returned_bytes, 16);
assert.equal(
memory.bytes_hex,
sourceBytes
.subarray(mapping.file_offset, mapping.file_offset + 16)
.toString("hex"),
);
const saved = await call("close_binary", {
snapshot_path: snapshotPath,
});
assert.ok(saved.evidence_records > 0);
const snapshot = parseAnalysisSnapshot(
JSON.parse(await readFile(snapshotPath, "utf8")),
);
assert.equal(snapshot.target.architecture, "powerpc");
assert.equal(
snapshot.binding.analysis_profile.parameters.language_id,
"PowerPC:BE:32:default",
);
assert.ok(snapshot.evidence_bundle.records.length > 0);
for (const evidence of snapshot.evidence_bundle.records)
assert.equal(parseEvidence(evidence).subject?.architecture, "powerpc");
await call("open_binary", {
path: fixture.path,
provider_id: "ghidra",
snapshot_path: snapshotPath,
});
assert.deepEqual(await call("list_procedures"), procedures);
await call("close_binary");
// Same public operation through CLI, not a private injected provider.
const cli = await execute(
process.execPath,
[
entrypoint,
"function",
fixture.path,
"rea_ppc_entry",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(cli.stdout));
assert.equal(evidence.subject?.architecture, "powerpc");
assert.equal(evidence.subject?.digest.sha256, fixture.sha256);
const cliDossier = functionDossierSchema.parse(
evidence.normalized_result,
);
assert.equal(cliDossier.procedure.address, dossier.procedure.address);
assert.ok(cliDossier.pseudocode.trim().length > 0);
assert.equal(
createHash("sha256")
.update(await readFile(fixture.path))
.digest("hex"),
fixture.sha256,
"Analysis modified the source executable",
);
reports.push({
...fixture,
independent_reader: independent,
effective_language: image.observations.language_id,
effective_compiler_spec: image.observations.compiler_spec_id,
checks: [
"independent ELF header and symbols",
"loaded Ghidra identity and bound language",
"fixed instruction bytes and immediate",
"conditional branch destination",
"global word and marker bytes",
"CLI/MCP and snapshot identity",
],
procedures: procedures.length,
status: "passed",
});
} catch (cause) {
throw new Error(`PowerPC verification failed; MCP stderr: ${stderr}`, {
cause,
});
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
}
} catch (cause) {
primaryFailure = cause;
}
const completed = await completeVerifierRun(run);
if (
completed.process_lineage.status !== "verified" ||
completed.process_lineage.descendants.length !== 0
)
throw new Error(
`Cleanup unverified; retained diagnostic workspace: ${workspace}`,
{ cause: primaryFailure },
);
if (primaryFailure !== undefined)
throw new Error(
`PowerPC verification failed; retained workspace: ${workspace}`,
{
cause: primaryFailure,
},
);
await rm(workspace, { recursive: true, force: true });
console.log(JSON.stringify({ ...completed, fixtures: reports }, null, 2));

View File

@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { parseExecutableHeader } from "./binaryTarget.js";
const header = (little: boolean, bits: 32 | 64 = 32, machine = 20): Buffer => {
const bytes = Buffer.alloc(bits === 32 ? 52 : 64);
bytes.set([0x7f, 0x45, 0x4c, 0x46, bits === 32 ? 1 : 2, little ? 1 : 2, 1]);
if (little) {
bytes.writeUInt16LE(2, 16);
bytes.writeUInt16LE(machine, 18);
bytes.writeUInt32LE(1, 20);
bytes.writeUInt16LE(bytes.length, bits === 32 ? 40 : 52);
} else {
bytes.writeUInt16BE(2, 16);
bytes.writeUInt16BE(machine, 18);
bytes.writeUInt32BE(1, 20);
bytes.writeUInt16BE(bytes.length, bits === 32 ? 40 : 52);
}
return bytes;
};
describe("PowerPC ELF header boundaries", () => {
it.each([false, true])(
"retains source endian and flags (little=%s)",
(little) => {
const parsed = parseExecutableHeader(header(little), "x64");
if (!parsed.ok) throw new Error(parsed.error);
expect(parsed.value).toEqual({
format: "elf",
architecture: "powerpc",
availableArchitectures: ["powerpc"],
powerpc: {
elfClass: 32,
byteOrder: little ? "little" : "big",
type: 2,
flags: 0,
},
});
},
);
it("reads ELF64 flags at their own offset without claiming support", () => {
const bytes = header(false, 64);
bytes.writeUInt32BE(0x80000000, 48);
const parsed = parseExecutableHeader(bytes, "x64");
if (!parsed.ok) throw new Error(parsed.error);
expect(parsed.value).toMatchObject({
architecture: "powerpc",
powerpc: { elfClass: 64, byteOrder: "big", flags: 0x80000000 },
});
});
it("does not recognize EM_PPC64 as the 32-bit family", () => {
expect(parseExecutableHeader(header(false, 64, 21), "x64")).toEqual({
ok: false,
error: "unsupported ELF architecture",
});
});
});

View File

@ -5,6 +5,7 @@ import type {
MipsElfMetadata,
PeCodeViewRecord,
RiscvElfMetadata,
PowerPcElfMetadata,
UniversalMachOSlice,
} from "./binaryTargetTypes.js";
import { err, ok, type Result } from "./result.js";
@ -30,6 +31,12 @@ export type ExecutableMetadata =
readonly availableArchitectures: readonly "riscv"[];
readonly riscv: RiscvElfMetadata;
}
| {
readonly format: "elf";
readonly architecture: "powerpc";
readonly availableArchitectures: readonly "powerpc"[];
readonly powerpc: PowerPcElfMetadata;
}
| {
readonly format: "mach-o" | "elf" | "dos-mz" | "dos-com";
readonly architecture: HeaderArchitecture;
@ -201,7 +208,11 @@ const parseElf = (
const machine = little ? bytes.readUInt16LE(18) : bytes.readUInt16BE(18);
const architecture = elfArchitecture(machine);
if (architecture === undefined) return err("unsupported ELF architecture");
if (architecture === "mips" || architecture === "riscv") {
if (
architecture === "mips" ||
architecture === "riscv" ||
architecture === "powerpc"
) {
const flagsOffset = elfClass === 32 ? 36 : 48;
const metadata: ElfFamilyMetadata = {
elfClass,
@ -211,6 +222,13 @@ const parseElf = (
? bytes.readUInt32LE(flagsOffset)
: bytes.readUInt32BE(flagsOffset),
};
if (architecture === "powerpc")
return ok({
format: "elf",
architecture,
availableArchitectures: [architecture],
powerpc: metadata,
});
return architecture === "mips"
? ok({
format: "elf",
@ -532,6 +550,8 @@ const elfArchitecture = (
return "x86_64";
case 8: // EM_MIPS: ISA, ABI and byte order remain separate ELF facts.
return "mips";
case 20: // EM_PPC; byte order and processor variant remain ELF declarations.
return "powerpc";
case 40:
return "arm";
case 183:

View File

@ -9,6 +9,7 @@ export const BINARY_ARCHITECTURES = [
"mips",
"6502",
"riscv",
"powerpc",
] as const;
export type BinaryArchitecture = (typeof BINARY_ARCHITECTURES)[number];
@ -36,6 +37,7 @@ export interface PeCodeViewRecord {
export const ELF_METADATA_ARCHITECTURES = [
"mips",
"riscv",
"powerpc",
] as const satisfies readonly BinaryArchitecture[];
export type ElfMetadataArchitecture =
(typeof ELF_METADATA_ARCHITECTURES)[number];
@ -90,6 +92,9 @@ export interface MipsAbiFlags {
*/
export type RiscvElfMetadata = ElfFamilyMetadata;
/** Raw PowerPC declarations; family identity does not select a processor variant. */
export type PowerPcElfMetadata = ElfFamilyMetadata;
/** Byte range of the selected slice inside its universal Mach-O container. */
export interface UniversalMachOSlice {
readonly offset: number;
@ -140,6 +145,15 @@ export type BinaryTarget =
readonly executableRole?: never;
readonly managed?: never;
})
| (BinaryTargetIdentity & {
readonly kind: "executable";
readonly format: "elf";
readonly architecture: "powerpc";
readonly availableArchitectures: readonly "powerpc"[];
readonly powerpc: PowerPcElfMetadata;
readonly executableRole?: never;
readonly managed?: never;
})
| (ExecutableTarget & {
readonly format: "mach-o" | "elf" | "dos-mz" | "dos-com";
readonly universalSlice?: never;

View File

@ -21,6 +21,10 @@ import {
ghidraRiscvLanguageOverride,
ghidraRiscvProfileParameters,
} from "./GhidraRiscvProfile.js";
import {
ghidraPowerPcLanguage,
ghidraPowerPcProfileParameters,
} from "./GhidraPowerPcProfile.js";
import { ghidraRawBinaryLanguage } from "./GhidraRawBinaryLanguage.js";
import {
@ -44,7 +48,9 @@ import {
export const ghidraPinnedLanguage = (
target: BinaryTarget,
): GhidraLanguageOverride | undefined =>
ghidraMipsLanguageOverride(target) ?? ghidraRiscvLanguageOverride(target);
ghidraMipsLanguageOverride(target) ??
ghidraRiscvLanguageOverride(target) ??
ghidraPowerPcLanguage(target);
/** Resolve version-bound, deterministic semantics before Ghidra imports a target. */
export const resolveGhidraAnalysisProfile = async (
@ -155,6 +161,7 @@ export const resolveGhidraAnalysisProfile = async (
const pinnedLanguage = ghidraPinnedLanguage(target);
if (
pinnedLanguage !== undefined &&
target.architecture !== "powerpc" &&
languageOverride !== undefined &&
(languageOverride.languageId !== pinnedLanguage.languageId ||
(languageOverride.compilerSpecId !== undefined &&
@ -277,6 +284,7 @@ export const resolveGhidraAnalysisProfile = async (
...analyzerParameters,
...ghidraMipsProfileParameters(target, languageOverride),
...ghidraRiscvProfileParameters(target),
...ghidraPowerPcProfileParameters(target),
...(pspExtension?.ok === true
? {
mips_support_lane: "psp-elf32-exec-eabi32-allegrex-v2",

View File

@ -0,0 +1,166 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { parseConfig } from "../config/parseConfig.js";
import type {
BinaryTarget,
PowerPcElfMetadata,
} from "../domain/binaryTargetTypes.js";
import { silentLogger } from "../logger.js";
import { HopperProvider } from "../hopper/HopperProvider.js";
import { hopperLoaderArgsForTarget } from "../hopper/HopperAnalysisProfile.js";
import { IdaProvider } from "../ida/IdaProvider.js";
import {
ghidraPinnedLanguage,
ghidraProfileLanguageOverride,
} from "./GhidraAnalysisProfile.js";
import { GhidraProvider } from "./GhidraProvider.js";
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
// Abbreviated from Ghidra 12.1.4 Processors/PowerPC/data/languages/ppc.ldefs.
const PPC_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
<language_definitions>
<language processor="PowerPC" endian="big" size="32" variant="default"
id="PowerPC:BE:32:default">
<compiler name="default" spec="ppc_32.cspec" id="default"/>
</language>
<language processor="PowerPC" endian="big" size="32" variant="e500"
id="PowerPC:BE:32:e500">
<compiler name="default" spec="ppc_32_e500_be.cspec" id="default"/>
</language>
</language_definitions>
`;
// Fake installation inspection tests admission/profile contracts, not Ghidra.
const host: GhidraInstallationHost = {
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.4\n",
executable: () => true,
probeJava: () => ({
version: "21.0.11",
major: 21,
home: "/jdk-21",
bits: 64,
runtime: "jdk",
}),
};
const configFor = (environment: NodeJS.ProcessEnv) => {
const parsed = parseConfig({ GHIDRA_INSTALL_DIR: "/ghidra", ...environment });
if (!parsed.ok) throw parsed.error;
return parsed.value;
};
const config = configFor({});
const target = (change: Partial<PowerPcElfMetadata> = {}): BinaryTarget => ({
path: "/fixture.elf",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "powerpc",
availableArchitectures: ["powerpc"],
powerpc: { elfClass: 32, byteOrder: "big", type: 2, flags: 0, ...change },
});
describe("bounded Ghidra PowerPC admission", () => {
it("binds the generic language instead of the loader's e500 choice", async () => {
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
expect(ghidra.inspectTargetSupport(target()).status).toBe("supported");
const resolved = await ghidra.resolveAnalysisProfile(target());
if (!resolved.ok) throw resolved.error;
const parameters = resolved.value.profile?.parameters ?? {};
expect(parameters).toMatchObject({
architecture: "powerpc",
language_id: "PowerPC:BE:32:default",
compiler_spec_id: "default",
language_selection: "provider-pinned-v1",
powerpc_elf: { elf_class: 32, byte_order: "big", type: 2, flags: 0 },
powerpc_support_lane: "elf32-be-exec-generic-v1",
});
expect(ghidraPinnedLanguage(target())).toEqual({
languageId: "PowerPC:BE:32:default",
compilerSpecId: "default",
});
expect(ghidraProfileLanguageOverride(parameters)).toEqual(
ghidraPinnedLanguage(target()),
);
});
it("lets a configured language replace the lane default", async () => {
const installDir = await createTestTempDirectory("rea-ghidra-ppc-");
const languages = join(
installDir,
"Ghidra/Processors/PowerPC/data/languages",
);
await mkdir(languages, { recursive: true });
await writeFile(join(languages, "ppc.ldefs"), PPC_LDEFS);
const ghidra = new GhidraProvider(
configFor({
GHIDRA_INSTALL_DIR: installDir,
REA_GHIDRA_LANGUAGE_ID: "PowerPC:BE:32:e500",
}),
silentLogger,
{},
host,
);
const resolved = await ghidra.resolveAnalysisProfile(target());
if (!resolved.ok) throw resolved.error;
const parameters = resolved.value.profile?.parameters ?? {};
expect(parameters).toMatchObject({
language_id: "PowerPC:BE:32:e500",
language_selection: "configured-v1",
});
expect(ghidraProfileLanguageOverride(parameters)).toEqual({
languageId: "PowerPC:BE:32:e500",
});
});
it("commits header flags so a changed interpretation changes the profile", async () => {
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
const left = await ghidra.resolveAnalysisProfile(target());
const right = await ghidra.resolveAnalysisProfile(
target({ flags: 0x00010000 }),
);
if (!left.ok) throw left.error;
if (!right.ok) throw right.error;
expect(left.value.profile?.digest).not.toBe(right.value.profile?.digest);
});
it("does not widen Windows P0", () => {
const windows = new GhidraProvider(
config,
silentLogger,
{},
{ ...host, platform: "win32" },
);
expect(windows.inspectTargetSupport(target()).status).toBe("unsupported");
});
it.each([
[{ elfClass: 64 }, "ELF32"],
[{ byteOrder: "little" }, "big-endian"],
[{ type: 1 }, "ET_EXEC"],
[{ type: 3 }, "ET_EXEC"],
[{ flags: 0x80000000 }, "EF_PPC_EMB"],
] satisfies [Partial<PowerPcElfMetadata>, string][])(
"refuses an unverified PowerPC interpretation %j",
async (change, reason) => {
const ghidra = new GhidraProvider(config, silentLogger, {}, host);
const value = target(change);
expect(ghidra.inspectTargetSupport(value)).toMatchObject({
status: "unsupported",
reason: expect.stringContaining(reason),
});
const resolved = await ghidra.resolveAnalysisProfile(value);
expect(resolved.ok).toBe(false);
},
);
it("keeps PowerPC out of unverified Hopper and IDA adapter lanes", () => {
const hopper = new HopperProvider(config, silentLogger, {}, "linux");
expect(hopper.inspectTargetSupport(target()).status).toBe("unsupported");
expect(hopperLoaderArgsForTarget(target()).ok).toBe(false);
const ida = new IdaProvider(config, {});
expect(ida.inspectTargetSupport(target()).status).toBe("unsupported");
});
});

View File

@ -0,0 +1,67 @@
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
/**
* Generic 32-bit big-endian PowerPC. Ghidra's ELF loader selects the e500
* (Book-E/SPE) variant for a plain EM_PPC header, so the lane binds this
* language instead of accepting the loader's choice.
*/
export const GHIDRA_POWERPC_LANGUAGE: GhidraLanguageOverride = {
languageId: "PowerPC:BE:32:default",
compilerSpecId: "default",
};
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
export const ghidraPowerPcUnsupportedReason = (
target: BinaryTarget,
): string | null => {
if (target.kind !== "executable" || target.architecture !== "powerpc")
return null;
const metadata = target.powerpc;
if (target.format !== "elf" || metadata === undefined)
return "PowerPC analysis requires ELF header metadata; a family name alone does not select a processor variant.";
if (metadata.elfClass !== 32)
return "This Ghidra PowerPC lane supports ELF32 only; PPC64 remains unsupported.";
if (metadata.byteOrder !== "big")
return "This Ghidra PowerPC lane supports big-endian ELF32 only; little-endian PowerPC needs separate verification.";
if (metadata.type !== 2)
return "This Ghidra PowerPC lane supports ET_EXEC only; relocatable objects and shared images need separate load verification.";
// EF_PPC_EMB (0x80000000) marks embedded variants; other bits only declare
// relocation conventions that do not change instruction decoding.
if ((metadata.flags & 0x80000000) !== 0)
return "EF_PPC_EMB declares an embedded PowerPC variant that needs a separately supported processor profile.";
return null;
};
/**
* The language the lane imports with, unless REA_GHIDRA_LANGUAGE_ID already
* selected one through the committed profile.
*/
export const ghidraPowerPcLanguage = (
target: BinaryTarget,
): GhidraLanguageOverride | undefined =>
target.kind === "executable" && target.architecture === "powerpc"
? GHIDRA_POWERPC_LANGUAGE
: undefined;
/** Commit source ELF interpretation without confusing target ISA with host CPU. */
export const ghidraPowerPcProfileParameters = (
target: BinaryTarget,
): Readonly<Record<string, JsonValue>> => {
if (
target.kind !== "executable" ||
target.architecture !== "powerpc" ||
target.powerpc === undefined
)
return {};
return {
powerpc_elf: {
elf_class: target.powerpc.elfClass,
byte_order: target.powerpc.byteOrder,
type: target.powerpc.type,
flags: target.powerpc.flags,
},
powerpc_support_lane: "elf32-be-exec-generic-v1",
};
};

View File

@ -68,6 +68,7 @@ const SUPPORTED_ARCHITECTURES = new Set([
"mips",
"6502",
"riscv",
"powerpc",
]);
/** Ghidra candidate backed by an isolated ephemeral headless import. */

View File

@ -1,5 +1,6 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { ghidraMipsUnsupportedReason } from "./GhidraMipsProfile.js";
import { ghidraPowerPcUnsupportedReason } from "./GhidraPowerPcProfile.js";
import { ghidraRiscvUnsupportedReason } from "./GhidraRiscvProfile.js";
/** Allegrex's ELF machine declaration, not a filename or inferred instruction. */
@ -14,6 +15,8 @@ export const isGhidraPspTarget = (target: BinaryTarget): boolean =>
export const ghidraProcessorUnsupportedReason = (
target: BinaryTarget,
): string | null => {
if (target.kind === "executable" && target.architecture === "powerpc")
return ghidraPowerPcUnsupportedReason(target);
if (target.kind === "executable" && target.architecture === "riscv")
return ghidraRiscvUnsupportedReason(target);
if (

View File

@ -110,6 +110,7 @@ const HOPPER_DECLINED_ARCHITECTURE_LABELS: Record<
> = {
mips: "MIPS",
riscv: "RISC-V",
powerpc: "PowerPC",
};
/** Derive Hopper's complete non-interactive CLI loader selection. */

View File

@ -126,7 +126,11 @@ export class HopperProvider implements AnalysisProviderCandidate {
status: "unsupported",
code: "architecture_unsupported",
reason: `REA's Hopper adapter does not admit ${
target.architecture === "mips" ? "MIPS" : "RISC-V"
target.architecture === "mips"
? "MIPS"
: target.architecture === "riscv"
? "RISC-V"
: "PowerPC"
} targets.`,
diagnostics,
};

View File

@ -113,7 +113,11 @@ export class IdaProvider implements AnalysisProviderCandidate {
status: "unsupported",
code: "architecture_unsupported",
reason: `REA's IDA adapter has no verified ${
target.architecture === "mips" ? "MIPS" : "RISC-V"
target.architecture === "mips"
? "MIPS"
: target.architecture === "riscv"
? "RISC-V"
: "PowerPC"
} lane. Select a supported Ghidra profile instead.`,
diagnostics,
};

View File

@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import {
createAnalysisSnapshotEntry,
parseAnalysisSnapshot,
serializeAnalysisSnapshot,
snapshotBinding,
snapshotTarget,
snapshotMatchesProfile,
} from "../../../src/domain/analysisSnapshot.js";
import { sessionOutputSchemas } from "../../../src/contracts/toolOutputSchemaGroups.js";
const target: BinaryTarget = {
path: "/source-owned.elf",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "powerpc",
availableArchitectures: ["powerpc"],
powerpc: { elfClass: 32, byteOrder: "big", type: 2, flags: 0 },
};
const provider = { id: "ghidra", name: "Ghidra", version: "12.1.4" };
const profile = createAnalysisProfile(provider, {
powerpc_elf: { elf_class: 32, byte_order: "big", type: 2, flags: 0 },
});
describe("PowerPC identity through lifecycle, Evidence and snapshot consumers", () => {
it("round-trips query bindings without relaxing integrity", () => {
const result = [{ address: "0x100101b0", value: "rea_ppc_entry" }];
const parameters = {};
const evidence = createEvidence(target, provider, {
operation: "list_procedures",
parameters,
result,
analysisProfile: profile,
});
const binding = snapshotBinding(profile);
const entry = createAnalysisSnapshotEntry({
target: snapshotTarget(target),
binding,
operation: "list_procedures",
parameters,
execution: {
result,
rawResult: null,
provider,
analysisProfile: profile,
limitations: [],
locations: [],
subject: target,
},
});
const snapshot = {
target: snapshotTarget(target),
binding,
entries: [entry],
workflow_entries: [],
evidence_bundle: createEvidenceBundle([evidence]),
};
const restored = parseAnalysisSnapshot(
JSON.parse(serializeAnalysisSnapshot(snapshot)),
);
expect(restored.target.architecture).toBe("powerpc");
expect(restored.evidence_bundle.records[0]?.subject?.architecture).toBe(
"powerpc",
);
expect(restored.entries[0]?.execution.subject?.architecture).toBe(
"powerpc",
);
expect(
sessionOutputSchemas.open_binary.parse({ result: target }).result
.architecture,
).toBe("powerpc");
expect(() =>
parseEvidence({
...evidence,
subject: { ...evidence.subject, architecture: "x86" },
}),
).toThrow("semantic identifier");
const changed = createAnalysisProfile(provider, {
powerpc_elf: {
elf_class: 32,
byte_order: "big",
type: 2,
flags: 0x00010000,
},
});
expect(snapshotMatchesProfile(binding, changed)).toBe(false);
});
});

14
tests/conformance/c/ppc.c Normal file
View File

@ -0,0 +1,14 @@
/* Freestanding, source-owned static-analysis fixture. Never executed by REA. */
volatile unsigned int rea_ppc_global = 7;
const char rea_ppc_marker[] = "rea-ppc-source-owned-fixture";
__attribute__((noinline)) unsigned int rea_ppc_leaf(unsigned int value)
{
return value * 3u + rea_ppc_global;
}
__attribute__((noinline)) unsigned int rea_ppc_entry(unsigned int selector)
{
unsigned int adjusted = selector > 10u ? selector : selector + 1u;
return rea_ppc_leaf(adjusted) + (unsigned char)rea_ppc_marker[0];
}

View File

@ -0,0 +1,29 @@
import type { PowerPcElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
export interface PowerPcReadelfFacts {
powerpc: PowerPcElfMetadata;
symbols: Record<
| "rea_ppc_entry"
| "rea_ppc_leaf"
| "rea_ppc_probe"
| "rea_ppc_global"
| "rea_ppc_marker",
string
>;
}
export function parsePowerPcReadelf(text: string): PowerPcReadelfFacts;
export function inspectPowerPcReadelf(
path: string,
): Promise<
PowerPcReadelfFacts & { command: string; version: string; raw: string }
>;
export function assertPowerPcLoadedImage(
observations: unknown,
sha256: string,
): void;
export function assertPowerPcProbe(
move: unknown,
branch: unknown,
address: string,
): void;
export function assertPowerPcGlobal(bytes: string): void;

View File

@ -0,0 +1,132 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execute = promisify(execFile);
// The lane binds this language; Ghidra's ELF loader alone selects e500.
const LANGUAGE = "PowerPC:BE:32:default";
const wordHex = (value) => {
const bytes = Buffer.alloc(4);
bytes.writeUInt32BE(value);
return bytes.toString("hex");
};
/** Parse only the documented GNU readelf fields needed by our tiny fixture. */
export function parsePowerPcReadelf(text) {
const field = (expression, label) => {
const matches = [...text.matchAll(expression)];
assert.equal(matches.length, 1, `Missing or ambiguous readelf ${label}`);
return matches[0][1].trim();
};
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
assert.equal(
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
"big",
);
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
assert.equal(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), "PowerPC");
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+)\s*$/gmu, "flags"));
assert.equal(flags, 0, "Fixture ELF flags differ from build intent");
const symbols = {};
for (const name of [
"rea_ppc_entry",
"rea_ppc_leaf",
"rea_ppc_probe",
"rea_ppc_global",
"rea_ppc_marker",
]) {
const value = field(
new RegExp(
`^\\s*\\d+:\\s+([\\da-f]+)\\s+\\d+\\s+(?:FUNC|OBJECT)\\s+GLOBAL\\s+\\S+\\s+\\d+\\s+${name}\\s*$`,
"gmu",
),
`symbol ${name}`,
);
symbols[name] = `0x${BigInt(`0x${value}`).toString(16)}`;
}
return {
powerpc: { elfClass: 32, byteOrder: "big", type: 2, flags },
symbols,
};
}
/** Independent reader is a bounded test prerequisite, never an REA provider. */
export async function inspectPowerPcReadelf(path) {
const command = process.env.REA_PPC_READELF ?? "readelf";
const options = {
env: { ...process.env, LC_ALL: "C" },
timeout: 10000,
maxBuffer: 1024 * 1024,
};
const version = await execute(command, ["--version"], options);
assert.match(
version.stdout,
/^GNU readelf\b/u,
"This lane requires GNU readelf",
);
const result = await execute(command, ["-h", "-s", "-W", path], options);
assert.equal(
result.stderr.trim(),
"",
`readelf diagnostics: ${result.stderr}`,
);
return {
...parsePowerPcReadelf(result.stdout),
command,
version: version.stdout.split("\n")[0],
raw: result.stdout,
};
}
/** Check effective Ghidra observations, not the requested analysis profile. */
export function assertPowerPcLoadedImage(observations, sha256) {
assert.equal(observations.language_id, LANGUAGE);
assert.equal(observations.compiler_spec_id, "default");
assert.ok(
observations.source_files.some(
(file) =>
file.original_sha256 === sha256 && file.modified_sha256 === sha256,
),
"Ghidra loaded source identity changed",
);
}
/** Verify the exact constant/conditional destination in ppc-probe.S. */
export function assertPowerPcProbe(move, branch, address) {
const start = BigInt(address);
for (const [instruction, offset, word] of [
[move, 0n, 0x38641234],
[branch, 8n, 0x41820008],
]) {
assert.equal(instruction.status, "decoded");
assert.equal(BigInt(instruction.address), start + offset);
assert.equal(instruction.architecture, LANGUAGE);
assert.equal(instruction.length, 4);
assert.equal(instruction.bytes, wordHex(word));
}
assert.match(move.mnemonic, /^ADDI$/iu);
assert.ok(
move.operands.some((operand) =>
operand.components.some(
(part) =>
part.kind === "immediate" &&
part.value !== null &&
BigInt(part.value) === 0x1234n,
),
),
"ADDI immediate was not recovered",
);
assert.match(branch.mnemonic, /^BEQ$/iu);
assert.equal(branch.flow.kind, "jump");
assert.equal(branch.flow.conditional, true);
assert.equal(branch.flow.computed, false);
assert.deepEqual(
branch.flow.direct_destinations.map((value) => BigInt(value)),
[start + 16n],
);
}
/** Four bytes must encode the known source-owned global in big-endian order. */
export function assertPowerPcGlobal(bytes) {
assert.equal(bytes, wordHex(7));
}

View File

@ -0,0 +1,187 @@
import { describe, expect, it } from "vitest";
import {
assertPowerPcGlobal,
assertPowerPcLoadedImage,
assertPowerPcProbe,
parsePowerPcReadelf,
} from "./ppc-checks.mjs";
// GNU readelf 2.47 output for the source-owned Clang 23 / LLD fixture.
// This tests the reader boundary, not Ghidra; the real lane rebuilds it.
const readelf = `ELF Header:
Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, big endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: PowerPC
Version: 0x1
Entry point address: 0x100101b0
Start of program headers: 52 (bytes into file)
Start of section headers: 2188 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 18
Section header string table index: 16
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS ppc.c
2: 10010180 48 FUNC GLOBAL DEFAULT 4 rea_ppc_leaf
3: 1002023c 4 OBJECT GLOBAL DEFAULT 5 rea_ppc_global
4: 100101b0 116 FUNC GLOBAL DEFAULT 4 rea_ppc_entry
5: 100000f4 29 OBJECT GLOBAL DEFAULT 1 rea_ppc_marker
6: 10010224 24 FUNC GLOBAL DEFAULT 4 rea_ppc_probe
`;
const sha256 = "a".repeat(64);
const image = {
language_id: "PowerPC:BE:32:default",
compiler_spec_id: "default",
source_files: [{ original_sha256: sha256, modified_sha256: sha256 }],
};
const move = {
address: "0x10010224",
status: "decoded",
architecture: "PowerPC:BE:32:default",
length: 4,
bytes: "38641234",
mnemonic: "addi",
operands: [
{ index: 2, components: [{ kind: "immediate", value: "0x1234" }] },
],
};
const branch = {
address: "0x1001022c",
status: "decoded",
architecture: "PowerPC:BE:32:default",
length: 4,
bytes: "41820008",
mnemonic: "beq",
flow: {
kind: "jump",
conditional: true,
computed: false,
direct_destinations: ["0x10010234"],
},
};
describe("PowerPC real-verifier independent expectations", () => {
it("reads GNU header and defined symbols without a Ghidra-derived oracle", () => {
const parsed = parsePowerPcReadelf(readelf);
expect(parsed.powerpc).toEqual({
elfClass: 32,
byteOrder: "big",
type: 2,
flags: 0,
});
expect(parsed.symbols.rea_ppc_probe).toBe("0x10010224");
});
it.each([
["wrong class", "ELF32", "ELF64"],
["wrong byte order", "big endian", "little endian"],
[
"wrong machine",
"Machine: PowerPC",
"Machine: PowerPC64",
],
["wrong object type", "EXEC (Executable file)", "DYN (Shared object file)"],
[
"wrong flags",
"Flags: 0x0",
"Flags: 0x80000000",
],
["missing symbol", "rea_ppc_probe", "unrelated_probe"],
])(
"rejects %s instead of returning a guessed fixture interpretation",
(_name, from, to) => {
expect(() => parsePowerPcReadelf(readelf.replace(from, to))).toThrow();
},
);
it("rejects ambiguous producer fields", () => {
expect(() => parsePowerPcReadelf(`${readelf}\nClass: ELF32\n`)).toThrow(
/ambiguous/u,
);
});
it("checks the effective loaded image, including Ghidra's e500 default", () => {
assertPowerPcLoadedImage(image, sha256);
expect(() =>
assertPowerPcLoadedImage(
{ ...image, language_id: "PowerPC:BE:32:e500" },
sha256,
),
).toThrow();
expect(() =>
assertPowerPcLoadedImage(
{ ...image, compiler_spec_id: "macosx" },
sha256,
),
).toThrow();
expect(() =>
assertPowerPcLoadedImage(
{
...image,
source_files: [
{ original_sha256: sha256, modified_sha256: "b".repeat(64) },
],
},
sha256,
),
).toThrow();
});
it("checks exact probe bytes, immediate and conditional destination", () => {
assertPowerPcProbe(move, branch, "0x10010224");
});
it("rejects plausible nonempty but incorrect instruction observations", () => {
expect(() =>
assertPowerPcProbe({ ...move, bytes: "38641235" }, branch, "0x10010224"),
).toThrow();
expect(() =>
assertPowerPcProbe({ ...move, length: 2 }, branch, "0x10010224"),
).toThrow();
expect(() =>
assertPowerPcProbe(
{
...move,
operands: [
{ index: 2, components: [{ kind: "immediate", value: "0x1235" }] },
],
},
branch,
"0x10010224",
),
).toThrow();
expect(() =>
assertPowerPcProbe(
move,
{
...branch,
flow: { ...branch.flow, direct_destinations: ["0x10010230"] },
},
"0x10010224",
),
).toThrow();
expect(() =>
assertPowerPcProbe(
move,
{ ...branch, flow: { ...branch.flow, conditional: false } },
"0x10010224",
),
).toThrow();
});
it("checks the big-endian global word", () => {
assertPowerPcGlobal("00000007");
expect(() => assertPowerPcGlobal("07000000")).toThrow();
});
});

View File

@ -0,0 +1,62 @@
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
const execute = promisify(execFile);
/** Build a source-owned freestanding ELF; never execute its target code. */
export async function buildPowerPcFixture(directory) {
const compiler = process.env.REA_PPC_CLANG ?? "clang";
const target = "powerpc-unknown-linux-gnu";
const source = fileURLToPath(new URL("../c/ppc.c", import.meta.url));
const probe = fileURLToPath(new URL("./ppc-probe.S", import.meta.url));
const path = join(directory, `${target}.elf`);
const args = [
`--target=${target}`,
// A 750-family core with the classic FPU, unlike the e500 SPE variant.
"-mcpu=750",
"-fno-pic",
"-fuse-ld=lld",
"-nostdlib",
"-static",
"-O0",
"-g",
"-Wl,-e,rea_ppc_entry",
"-Wl,--build-id=none",
source,
probe,
"-o",
path,
];
const version = await execute(compiler, ["--version"], { timeout: 10000 });
await execute(compiler, args, { timeout: 60000 });
const bytes = await readFile(path);
if (
bytes.length < 52 ||
bytes.toString("hex", 0, 4) !== "7f454c46" ||
bytes[4] !== 1 ||
bytes[5] !== 2 ||
bytes.readUInt16BE(16) !== 2 ||
bytes.readUInt16BE(18) !== 20 ||
bytes.readUInt32BE(36) !== 0
)
throw new Error(
"Compiler did not produce the declared ELF32 big-endian PowerPC fixture",
);
return {
path,
flags: bytes.readUInt32BE(36),
sha256: createHash("sha256").update(bytes).digest("hex"),
source_sha256: createHash("sha256")
.update(await readFile(source))
.digest("hex"),
probe_sha256: createHash("sha256")
.update(await readFile(probe))
.digest("hex"),
compiler: version.stdout.trim(),
arguments: args,
};
}

View File

@ -0,0 +1,17 @@
/* Source-owned 32-bit PowerPC instructions with fixed, reviewable encodings.
* No target execution. The conditional branch at +8 targets +16.
*/
.text
.p2align 2
.globl rea_ppc_probe
.type rea_ppc_probe, @function
rea_ppc_probe:
addi 3, 4, 0x1234
cmpwi 0, 4, 0
beq 0, .Lzero
blr
.Lzero:
li 3, 7
blr
.size rea_ppc_probe, .-rea_ppc_probe
.section .note.GNU-stack,"",@progbits