mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
* feat(domain): parse and validate GameCube/Wii DOL headers Add a pure parser for the 0x100-byte DOL header (7 text and 11 data slots, BSS, entry point). Used slots must lie after the header and inside the file and the 0x80000000..0x81800000 load window; text slots are 4-byte aligned; sections may not overlap in memory or in the file; the entry point must be inside a text section. BSS may overlap data sections, as .sdata/.sdata2 do in shipped DOLs. Refs #1741 * feat(binary): admit caller-selected GameCube/Wii DOL targets * feat(ghidra): import GameCube/Wii DOL targets through a section-map pre-script * test(ghidra): cover DOL launcher arguments, attestation and raw profile digests * test(ghidra): add the real verify:ghidra:dol lane and DOL docs * fix(ghidra): read only the DOL header and section ranges instead of the whole file * test(ghidra): derive DOL lane addresses from readelf and admit Gekko from the install catalog * fix(ghidra): attest DOL block permissions, space, overlay and load state * fix(ghidra): keep load-image permissions optional for older inspections * fix(contracts): accept DOL targets in binary_session output * fix(ghidra): adapt the DOL lane to the current upstream address schema Validate DOL load-image coordinates with the upstream observed-address schema, drop a duplicate import left by the rebase, and re-pin the raw profile digests to the unmodified upstream resolver. * fix(ghidra): preserve bridge inventory symbol provenance * fix(ghidra): align DOL verification with shared workflow contracts * feat(domain): parse and validate GameCube/Wii DOL headers Add a pure parser for the 0x100-byte DOL header (7 text and 11 data slots, BSS, entry point). Used slots must lie after the header and inside the file and the 0x80000000..0x81800000 load window; text slots are 4-byte aligned; sections may not overlap in memory or in the file; the entry point must be inside a text section. BSS may overlap data sections, as .sdata/.sdata2 do in shipped DOLs. Refs #1741 * feat(binary): admit caller-selected GameCube/Wii DOL targets * feat(ghidra): import GameCube/Wii DOL targets through a section-map pre-script * test(ghidra): cover DOL launcher arguments, attestation and raw profile digests * test(ghidra): add the real verify:ghidra:dol lane and DOL docs * fix(ghidra): read only the DOL header and section ranges instead of the whole file * test(ghidra): derive DOL lane addresses from readelf and admit Gekko from the install catalog * fix(ghidra): attest DOL block permissions, space, overlay and load state * fix(ghidra): keep load-image permissions optional for older inspections * fix(contracts): accept DOL targets in binary_session output * fix(ghidra): adapt the DOL lane to the current upstream address schema Validate DOL load-image coordinates with the upstream observed-address schema, drop a duplicate import left by the rebase, and re-pin the raw profile digests to the unmodified upstream resolver. * docs(ghidra): qualify the DOL memory window * refactor(ghidra): own DOL BSS ranges in the parsed header * docs(ghidra): record real DOL workflows and task costs * docs(ghidra): record final DOL integration verification * fix(mcp): share the native procedure search row contract * docs(ghidra): record public procedure search verification --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
289 lines
11 KiB
TypeScript
289 lines
11 KiB
TypeScript
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
|
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
|
import type { DolHeader } from "../domain/gamecubeDol.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { nativeAddressCoordinateInputSchema } from "../domain/native/nativeAddress.js";
|
|
import {
|
|
nativeLoadImageSchema,
|
|
type NativeLoadImage,
|
|
type NativeLoadImageCheck,
|
|
type NativeLoadImageObservation,
|
|
} from "../domain/native/nativeLoadImage.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import { ghidraDolLanguage } from "./GhidraDolProfile.js";
|
|
|
|
type DolTarget = Extract<BinaryTarget, { format: "gamecube-dol" }>;
|
|
|
|
/**
|
|
* Digests of the immutable DOL snapshot, streamed by the Ghidra client: whole
|
|
* file identity plus one digest per header section, in `header.sections` order.
|
|
*/
|
|
export interface DolSnapshotDigests {
|
|
readonly sha256: string;
|
|
readonly bytes: number;
|
|
readonly sectionSha256: readonly string[];
|
|
}
|
|
type Permissions = {
|
|
readonly readable: boolean;
|
|
readonly writable: boolean;
|
|
readonly executable: boolean;
|
|
};
|
|
/** Permissions the DOL import must set: text r-x, data rw-, BSS rw- (see ReaGhidraPrepareDol.java). */
|
|
const TEXT_PERMISSIONS: Permissions = {
|
|
readable: true,
|
|
writable: false,
|
|
executable: true,
|
|
};
|
|
const DATA_PERMISSIONS: Permissions = {
|
|
readable: true,
|
|
writable: true,
|
|
executable: false,
|
|
};
|
|
type FileFacts = {
|
|
readonly start: string;
|
|
readonly end: string;
|
|
readonly length: number;
|
|
readonly file_offset: number | null;
|
|
readonly sha256: string | null;
|
|
readonly source_file_index: number | null;
|
|
readonly initialized: boolean;
|
|
readonly loaded: boolean;
|
|
readonly overlay: boolean;
|
|
readonly address_space: string;
|
|
readonly readable?: boolean | undefined;
|
|
readonly writable?: boolean | undefined;
|
|
readonly executable?: boolean | undefined;
|
|
};
|
|
type BssFacts = Omit<FileFacts, "file_offset" | "sha256" | "source_file_index">;
|
|
const hex = (value: number): string => `0x${value.toString(16)}`;
|
|
const byStart = <
|
|
T extends { readonly start: string; readonly address_space: string },
|
|
>(
|
|
left: T,
|
|
right: T,
|
|
): number =>
|
|
left.address_space === right.address_space
|
|
? Number(
|
|
nativeAddressCoordinateInputSchema.parse(left.start).offset -
|
|
nativeAddressCoordinateInputSchema.parse(right.start).offset,
|
|
)
|
|
: left.address_space < right.address_space
|
|
? -1
|
|
: 1;
|
|
|
|
/**
|
|
* Verify the imported DOL against its header: every used section is one
|
|
* initialized, loaded, non-overlay mapping at its load address with the exact
|
|
* file bytes, BSS is only uninitialized gaps outside data sections, and the
|
|
* header entry is an external entry point.
|
|
*/
|
|
export const attestGhidraDolLoadImage = (
|
|
snapshot: DolSnapshotDigests,
|
|
target: DolTarget,
|
|
observations: NativeLoadImageObservation,
|
|
languageOverride: GhidraLanguageOverride | undefined,
|
|
): Result<NativeLoadImage, string> => {
|
|
if (snapshot.sha256 !== target.sha256)
|
|
return err("Ghidra DOL snapshot digest does not match the admitted target");
|
|
const header: DolHeader = target.dol;
|
|
if (snapshot.sectionSha256.length !== header.sections.length)
|
|
return err("Ghidra DOL snapshot section digests do not match the header");
|
|
const language = ghidraDolLanguage(languageOverride);
|
|
const checks: NativeLoadImageCheck[] = [];
|
|
const check = (
|
|
name: string,
|
|
expected: JsonValue,
|
|
observed: JsonValue,
|
|
fileOffset: number | null = null,
|
|
location: string | null = null,
|
|
): void => {
|
|
checks.push({
|
|
name,
|
|
matched: JSON.stringify(expected) === JSON.stringify(observed),
|
|
expected,
|
|
observed,
|
|
file_offset: fileOffset,
|
|
address: location,
|
|
});
|
|
};
|
|
|
|
// Field order is fixed: checks compare serialized values.
|
|
const fileFacts = (mapping: FileFacts) => ({
|
|
start: mapping.start,
|
|
end: mapping.end,
|
|
length: mapping.length,
|
|
file_offset: mapping.file_offset,
|
|
sha256: mapping.sha256,
|
|
source_file_index: mapping.source_file_index,
|
|
initialized: mapping.initialized,
|
|
loaded: mapping.loaded,
|
|
overlay: mapping.overlay,
|
|
address_space: mapping.address_space,
|
|
readable: mapping.readable ?? null,
|
|
writable: mapping.writable ?? null,
|
|
executable: mapping.executable ?? null,
|
|
});
|
|
const bssFacts = (mapping: BssFacts) => ({
|
|
start: mapping.start,
|
|
end: mapping.end,
|
|
length: mapping.length,
|
|
initialized: mapping.initialized,
|
|
loaded: mapping.loaded,
|
|
overlay: mapping.overlay,
|
|
address_space: mapping.address_space,
|
|
readable: mapping.readable ?? null,
|
|
writable: mapping.writable ?? null,
|
|
executable: mapping.executable ?? null,
|
|
});
|
|
|
|
const expectedFileMappings = header.sections
|
|
.map((section, index) => {
|
|
const permissions =
|
|
section.kind === "text" ? TEXT_PERMISSIONS : DATA_PERMISSIONS;
|
|
return {
|
|
start: hex(section.address),
|
|
end: hex(section.address + section.size - 1),
|
|
length: section.size,
|
|
file_offset: section.fileOffset,
|
|
sha256: snapshot.sectionSha256[index] ?? null,
|
|
source_file_index: 0,
|
|
initialized: true,
|
|
loaded: true,
|
|
overlay: false,
|
|
address_space: observations.default_address_space,
|
|
...permissions,
|
|
};
|
|
})
|
|
.sort(byStart);
|
|
const observedFileMappings = observations.mappings
|
|
.filter((mapping) => mapping.source_file_index !== null)
|
|
.map((mapping) => fileFacts(mapping))
|
|
.sort(byStart);
|
|
|
|
const bssRange = {
|
|
start: header.bss.address,
|
|
end: header.bss.address + header.bss.size,
|
|
};
|
|
const inBss = (address: string): boolean => {
|
|
const offset = nativeAddressCoordinateInputSchema.parse(address).offset;
|
|
return offset >= BigInt(bssRange.start) && offset < BigInt(bssRange.end);
|
|
};
|
|
const expectedBss = header.bssGaps.map((gap) => ({
|
|
start: hex(gap.address),
|
|
end: hex(gap.address + gap.size - 1),
|
|
length: gap.size,
|
|
initialized: false,
|
|
loaded: true,
|
|
overlay: false,
|
|
address_space: observations.default_address_space,
|
|
...DATA_PERMISSIONS,
|
|
}));
|
|
const observedBssBlocks = observations.mappings.filter(
|
|
(mapping) =>
|
|
mapping.source_file_index === null &&
|
|
!mapping.initialized &&
|
|
mapping.address_space === observations.default_address_space &&
|
|
inBss(mapping.start),
|
|
);
|
|
const observedBss = observedBssBlocks
|
|
.map((mapping) => bssFacts(mapping))
|
|
.sort(byStart);
|
|
// Permissions are attested only when measured; an older observation without them fails closed.
|
|
const unmeasured = observations.mappings
|
|
.filter(
|
|
(mapping) =>
|
|
(mapping.source_file_index !== null ||
|
|
observedBssBlocks.includes(mapping)) &&
|
|
(mapping.readable === undefined ||
|
|
mapping.writable === undefined ||
|
|
mapping.executable === undefined),
|
|
)
|
|
.map((mapping) => mapping.block);
|
|
|
|
const source = observations.source_files[0];
|
|
check("executable_format", "Raw Binary", observations.executable_format);
|
|
check("language_id", language.languageId, observations.language_id);
|
|
if (language.compilerSpecId !== undefined)
|
|
check(
|
|
"compiler_spec_id",
|
|
language.compilerSpecId,
|
|
observations.compiler_spec_id,
|
|
);
|
|
check("image_base", "0x0", observations.image_base);
|
|
check("source_file_count", 1, observations.source_files.length);
|
|
check("source_file_size", snapshot.bytes, source?.size ?? null);
|
|
check("original_file_sha256", target.sha256, source?.original_sha256 ?? null);
|
|
check("modified_file_sha256", target.sha256, source?.modified_sha256 ?? null);
|
|
check("file_backed_mappings", expectedFileMappings, observedFileMappings);
|
|
check("bss_mappings", expectedBss, observedBss);
|
|
// One check per observed block, so a mismatch names the block that broke.
|
|
for (const mapping of observations.mappings) {
|
|
if (mapping.source_file_index !== null) {
|
|
check(
|
|
`block ${mapping.block} at ${mapping.start}`,
|
|
expectedFileMappings.find(
|
|
(expected) => expected.start === mapping.start,
|
|
) ?? null,
|
|
fileFacts(mapping),
|
|
mapping.file_offset,
|
|
mapping.start,
|
|
);
|
|
} else if (observedBssBlocks.includes(mapping)) {
|
|
check(
|
|
`block ${mapping.block} at ${mapping.start}`,
|
|
expectedBss.find((expected) => expected.start === mapping.start) ??
|
|
null,
|
|
bssFacts(mapping),
|
|
null,
|
|
mapping.start,
|
|
);
|
|
}
|
|
}
|
|
check(
|
|
"initialized_unbacked_mappings",
|
|
0,
|
|
observations.mappings.filter(
|
|
(mapping) => mapping.initialized && mapping.source_file_index === null,
|
|
).length,
|
|
);
|
|
check(
|
|
"entry_point",
|
|
hex(header.entryPoint),
|
|
observations.entry_points.find(
|
|
(entry) => entry === hex(header.entryPoint),
|
|
) ?? null,
|
|
);
|
|
if (unmeasured.length > 0)
|
|
check("block_permissions_measured", [], unmeasured);
|
|
check("relocation_records", [], observations.relocations);
|
|
return ok(
|
|
nativeLoadImageSchema.parse({
|
|
status: checks.every((item) => item.matched) ? "verified" : "mismatch",
|
|
format: "gamecube-dol",
|
|
target_sha256: target.sha256,
|
|
architecture: "powerpc",
|
|
language_id: observations.language_id,
|
|
compiler_spec_id: observations.compiler_spec_id,
|
|
file_bytes: snapshot.bytes,
|
|
entry: { address: hex(header.entryPoint) },
|
|
sections: header.sections.map((section) => ({
|
|
kind: section.kind,
|
|
index: section.index,
|
|
file_offset: hex(section.fileOffset),
|
|
address: hex(section.address),
|
|
size: hex(section.size),
|
|
})),
|
|
bss: { address: hex(header.bss.address), size: hex(header.bss.size) },
|
|
observations,
|
|
checks,
|
|
limitations: [
|
|
"GameCube DOL is a header interpretation explicitly selected by the caller; the format is not identified from file bytes.",
|
|
"Admission covers cached MEM1 addresses 0x80000000..0x81800000 only; Wii MEM2 and uncached aliases are not supported by this lane.",
|
|
"Verification covers original/modified FileBytes identity, every used text and data section at its load address with exact file-byte digests, BSS as uninitialized gaps outside data sections, and the header entry. Each block is checked for address space, initialized/loaded/overlay state and read/write/execute permissions (text r-x, data and BSS rw-). Mapping ends are inclusive.",
|
|
"The 256-byte header and any bytes between sections are not mapped and are not verified. Language-defined uninitialized blocks outside BSS are preserved as observations and not attested.",
|
|
"A mismatching digest identifies its source range, not the first differing byte. No runtime, boot, or hardware state is modeled.",
|
|
],
|
|
}),
|
|
);
|
|
};
|