mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(ghidra): reconcile GNU MIPS ABI declarations
Parse bounded GNU file-scope FP/MSA attributes once, retain absent versus uninspected declarations, reject conflicts and unsupported argument ABIs, and include observed facts in profile identity. LE/BE filesystem and profile regressions pass; real Ghidra/MIPS behavior remains unverified. Fixes #3096 Co-authored-by: Aurelio Bernal <aurelio@bernalr.com>
This commit is contained in:
+17
-3
@@ -52,9 +52,23 @@ section names or the record being inside the initial 4 KiB header probe.
|
||||
Program and section declarations must agree; duplicate, conflicting, truncated
|
||||
or out-of-file records are rejected. Stripped section tables and section-only
|
||||
records are supported. Extended table numbering is explicitly unsupported.
|
||||
GNU `SHT_GNU_ATTRIBUTES` sections are refused by numeric section type until REA
|
||||
parses and reconciles their declarations; section names and payload sizes do not
|
||||
control detection or allocation. For carved ELFs whose section table extends
|
||||
GNU `SHT_GNU_ATTRIBUTES` sections are located by numeric section type; section
|
||||
names never control detection. REA reads at most 4096 payload bytes and parses
|
||||
format version `A`, the `gnu` vendor's file-scope `Tag_GNU_MIPS_ABI_FP` (4) and
|
||||
`Tag_GNU_MIPS_ABI_MSA` (8) only. Unknown vendors, scopes or tags, repeated
|
||||
declarations, malformed lengths, and out-of-file or oversized payloads remain
|
||||
refusals. A nonzero FP tag must match the separate ABI flags `fp_abi` record;
|
||||
value 0 (`ANY`) declares no requirement. The [GNU Object Attributes v1
|
||||
format](https://sourceware.org/binutils/docs/as/Object-Attributes-v1.html) and
|
||||
[GNU MIPS tag definitions](https://sourceware.org/binutils/docs/as/GNU-Object-Attributes.html)
|
||||
are the format references. The MSA tag describes the vector
|
||||
argument ABI, while the ABI flags ASE field describes instruction-set
|
||||
extensions; REA does not treat those as interchangeable. The 128-bit MSA
|
||||
argument ABI and unknown nonzero MSA values remain unsupported. Without a
|
||||
separate ABI flags record, FP attribute values are admitted only when absent,
|
||||
`ANY` (0), double precision (1), or FPXX (5), the values covered by the
|
||||
recordless profile. Parsed values are retained in the MIPS analysis profile.
|
||||
For carved ELFs whose section table extends
|
||||
beyond the captured file, REA examines every complete captured section header
|
||||
and records the unavailable headers as a limitation. The provider refuses an
|
||||
incomplete ABI declaration inspection even when a program-header ABI record is
|
||||
|
||||
@@ -551,6 +551,7 @@ const readExecutableMetadata = async (
|
||||
mips: {
|
||||
...parsed.value.mips,
|
||||
abiFlags: declarations.value.abiFlags,
|
||||
gnuAttributes: declarations.value.gnuAttributes,
|
||||
...(declarations.value.sharedLoad === undefined
|
||||
? {}
|
||||
: { sharedLoad: declarations.value.sharedLoad }),
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FileHandle } from "node:fs/promises";
|
||||
import type {
|
||||
MipsAbiFlags,
|
||||
MipsElfMetadata,
|
||||
MipsGnuAttributes,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
@@ -9,12 +10,17 @@ import {
|
||||
readMipsElfSharedLoad,
|
||||
type MipsElfRelocationSection,
|
||||
} from "./MipsElfSharedLoad.js";
|
||||
import {
|
||||
MIPS_GNU_ATTRIBUTES_MAX_BYTES,
|
||||
parseMipsGnuAttributes,
|
||||
} from "./MipsGnuAttributes.js";
|
||||
|
||||
const SHT_GNU_ATTRIBUTES = 0x6ffffff5;
|
||||
|
||||
/** Observed ABI and load declarations, retaining unavailable source coverage. */
|
||||
export interface MipsElfInspection {
|
||||
readonly abiFlags: MipsAbiFlags | null;
|
||||
readonly gnuAttributes: MipsGnuAttributes | null;
|
||||
readonly limitations: readonly string[];
|
||||
readonly sharedLoad?: MipsElfMetadata["sharedLoad"];
|
||||
}
|
||||
@@ -120,6 +126,7 @@ export const readMipsElfMetadata = async (
|
||||
let programHeaders: Buffer | undefined;
|
||||
const relocationSections: MipsElfRelocationSection[] = [];
|
||||
const limitations: string[] = [];
|
||||
let gnuAttributes: MipsGnuAttributes | null = null;
|
||||
for (const table of tables) {
|
||||
if (table.count === 0) {
|
||||
if (table.offset !== 0)
|
||||
@@ -153,10 +160,25 @@ export const readMipsElfMetadata = async (
|
||||
checkCancelled();
|
||||
const position = i * table.entrySize;
|
||||
const type = u32(contents.value, position + table.typeOffset);
|
||||
if (table.optional && type === SHT_GNU_ATTRIBUTES)
|
||||
return err(
|
||||
"MIPS ELF declares GNU ABI attributes that REA does not inspect; refusing provider selection until the declared ABI is parsed and reconciled",
|
||||
if (table.optional && type === SHT_GNU_ATTRIBUTES) {
|
||||
if (gnuAttributes !== null)
|
||||
return err("duplicate MIPS GNU attribute sections");
|
||||
const size = u32(contents.value, position + 20);
|
||||
if (size === 0 || size > MIPS_GNU_ATTRIBUTES_MAX_BYTES)
|
||||
return err(
|
||||
`MIPS GNU attribute section size ${size} is outside REA's inspected bound of ${MIPS_GNU_ATTRIBUTES_MAX_BYTES} bytes`,
|
||||
);
|
||||
const section = await read(
|
||||
u32(contents.value, position + 16),
|
||||
size,
|
||||
"GNU attribute section",
|
||||
);
|
||||
if (!section.ok) return section;
|
||||
const parsed = parseMipsGnuAttributes(section.value, little);
|
||||
if (!parsed.ok) return parsed;
|
||||
gnuAttributes = parsed.value;
|
||||
continue;
|
||||
}
|
||||
if (metadata.type === 3 && table.optional && (type === 4 || type === 9))
|
||||
relocationSections.push({
|
||||
type,
|
||||
@@ -208,7 +230,7 @@ export const readMipsElfMetadata = async (
|
||||
}
|
||||
: {};
|
||||
if (record === undefined)
|
||||
return ok({ abiFlags: null, limitations, ...shared });
|
||||
return ok({ abiFlags: null, gnuAttributes, limitations, ...shared });
|
||||
// Preserve declarations verbatim, including unknown versions/values. Provider
|
||||
// admission, not parsing, decides which interpretations have been verified.
|
||||
const abiFlags: MipsAbiFlags = {
|
||||
@@ -224,5 +246,14 @@ export const readMipsElfMetadata = async (
|
||||
flags1: u32(record, 16),
|
||||
flags2: u32(record, 20),
|
||||
};
|
||||
return ok({ abiFlags, limitations, ...shared });
|
||||
if (
|
||||
gnuAttributes !== null &&
|
||||
gnuAttributes.fpAbi !== null &&
|
||||
gnuAttributes.fpAbi !== 0 &&
|
||||
gnuAttributes.fpAbi !== abiFlags.fpAbi
|
||||
)
|
||||
return err(
|
||||
"conflicting MIPS ABI flags and GNU floating-point ABI declarations",
|
||||
);
|
||||
return ok({ abiFlags, gnuAttributes, limitations, ...shared });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseMipsGnuAttributes } from "./MipsGnuAttributes.js";
|
||||
|
||||
const u32 = (value: number, little: boolean): number[] => {
|
||||
const bytes = Buffer.alloc(4);
|
||||
if (little) bytes.writeUInt32LE(value);
|
||||
else bytes.writeUInt32BE(value);
|
||||
return [...bytes];
|
||||
};
|
||||
|
||||
const section = (
|
||||
attributes: readonly number[],
|
||||
{
|
||||
little = true,
|
||||
vendor = "gnu",
|
||||
scope = 1,
|
||||
}: {
|
||||
readonly little?: boolean;
|
||||
readonly vendor?: string;
|
||||
readonly scope?: number;
|
||||
} = {},
|
||||
): Buffer => {
|
||||
const scoped = [
|
||||
scope,
|
||||
...u32(1 + 4 + attributes.length, little),
|
||||
...attributes,
|
||||
];
|
||||
const name = [...Buffer.from(vendor, "latin1"), 0];
|
||||
return Buffer.from([
|
||||
0x41,
|
||||
...u32(4 + name.length + scoped.length, little),
|
||||
...name,
|
||||
...scoped,
|
||||
]);
|
||||
};
|
||||
|
||||
describe("MIPS GNU attribute parsing", () => {
|
||||
it.each([true, false])(
|
||||
"reads MIPS FP and MSA ABI attributes in ELF byte order (little=%s)",
|
||||
(little) => {
|
||||
expect(
|
||||
parseMipsGnuAttributes(section([4, 1, 8, 0], { little }), little),
|
||||
).toEqual({ ok: true, value: { fpAbi: 1, msa: 0 } });
|
||||
},
|
||||
);
|
||||
|
||||
it("retains a multi-byte value and a missing tag separately", () => {
|
||||
expect(parseMipsGnuAttributes(section([4, 0x85, 0x01]), true)).toEqual({
|
||||
ok: true,
|
||||
value: { fpAbi: 133, msa: null },
|
||||
});
|
||||
expect(parseMipsGnuAttributes(section([]), true)).toEqual({
|
||||
ok: true,
|
||||
value: { fpAbi: null, msa: null },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["unknown version", Buffer.from([0x42]), /format version/u],
|
||||
["truncated vendor length", Buffer.from([0x41, 1, 0]), /truncated/u],
|
||||
[
|
||||
"overrunning vendor subsection",
|
||||
Buffer.from([0x41, ...u32(64, true), 0x67, 0x6e, 0x75, 0]),
|
||||
/does not fit the section/u,
|
||||
],
|
||||
[
|
||||
"unterminated vendor",
|
||||
Buffer.from([0x41, ...u32(7, true), 0x67, 0x6e, 0x75]),
|
||||
/unterminated/u,
|
||||
],
|
||||
["non-GNU vendor", section([4, 1], { vendor: "arm" }), /non-GNU vendor/u],
|
||||
["section scope", section([4, 1], { scope: 2 }), /sections or symbols/u],
|
||||
["symbol scope", section([4, 1], { scope: 3 }), /sections or symbols/u],
|
||||
["unknown tag", section([6, 1]), /tag 6/u],
|
||||
[
|
||||
"duplicate FP tag",
|
||||
section([4, 1, 4, 1]),
|
||||
/duplicate MIPS GNU floating-point/u,
|
||||
],
|
||||
["duplicate MSA tag", section([8, 0, 8, 0]), /duplicate MIPS GNU MSA/u],
|
||||
[
|
||||
"truncated value",
|
||||
section([4, 0x81]),
|
||||
/malformed MIPS GNU attribute value/u,
|
||||
],
|
||||
[
|
||||
"value wider than 32 bits",
|
||||
section([4, 0xff, 0xff, 0xff, 0xff, 0x1f]),
|
||||
/malformed MIPS GNU attribute value/u,
|
||||
],
|
||||
] as const)("refuses %s", (_label, bytes, reason) => {
|
||||
const result = parseMipsGnuAttributes(bytes, true);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toMatch(reason);
|
||||
});
|
||||
|
||||
it("requires subsection lengths in the ELF byte order", () => {
|
||||
expect(
|
||||
parseMipsGnuAttributes(section([4, 1], { little: true }), false).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses a scope length beyond its vendor subsection", () => {
|
||||
const bytes = section([4, 1]);
|
||||
bytes.writeUInt32LE(64, 1 + 4 + 4 + 1);
|
||||
const result = parseMipsGnuAttributes(bytes, true);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toMatch(/scope length/u);
|
||||
});
|
||||
|
||||
it("refuses a repeated GNU vendor subsection", () => {
|
||||
const one = section([4, 1]);
|
||||
const result = parseMipsGnuAttributes(
|
||||
Buffer.concat([one, one.subarray(1)]),
|
||||
true,
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error).toMatch(/duplicate MIPS GNU attribute vendor/u);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { MipsGnuAttributes } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Bound the section read before its declared size can drive allocation. */
|
||||
export const MIPS_GNU_ATTRIBUTES_MAX_BYTES = 4096;
|
||||
|
||||
const FORMAT_VERSION = 0x41;
|
||||
const TAG_FILE = 1;
|
||||
const TAG_GNU_MIPS_ABI_FP = 4;
|
||||
const TAG_GNU_MIPS_ABI_MSA = 8;
|
||||
|
||||
/**
|
||||
* Read only GNU file-scope MIPS ABI attributes REA understands. Unknown
|
||||
* vendors, scopes and tags fail closed; their declarations are not skipped.
|
||||
*/
|
||||
export const parseMipsGnuAttributes = (
|
||||
bytes: Buffer,
|
||||
littleEndian: boolean,
|
||||
): Result<MipsGnuAttributes, string> => {
|
||||
if (bytes.length === 0 || bytes[0] !== FORMAT_VERSION)
|
||||
return err("unsupported MIPS GNU attribute section format version");
|
||||
|
||||
const readU32 = (offset: number): number =>
|
||||
littleEndian ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
||||
const readUleb = (
|
||||
offset: number,
|
||||
end: number,
|
||||
): { readonly value: number; readonly next: number } | undefined => {
|
||||
let value = 0;
|
||||
for (let index = 0; index < 5 && offset + index < end; index += 1) {
|
||||
const byte = bytes[offset + index]!;
|
||||
value += (byte & 0x7f) * 2 ** (7 * index);
|
||||
if ((byte & 0x80) === 0)
|
||||
return value <= 0xffffffff
|
||||
? { value, next: offset + index + 1 }
|
||||
: undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
let fpAbi: number | null = null;
|
||||
let msa: number | null = null;
|
||||
let vendorSeen = false;
|
||||
let position = 1;
|
||||
while (position < bytes.length) {
|
||||
if (bytes.length - position < 4)
|
||||
return err("truncated MIPS GNU attribute subsection length");
|
||||
const length = readU32(position);
|
||||
if (length < 4 || length > bytes.length - position)
|
||||
return err(
|
||||
"MIPS GNU attribute subsection length does not fit the section",
|
||||
);
|
||||
const end = position + length;
|
||||
const vendorEnd = bytes.indexOf(0, position + 4);
|
||||
if (vendorEnd < 0 || vendorEnd >= end)
|
||||
return err("unterminated MIPS GNU attribute vendor name");
|
||||
if (bytes.toString("latin1", position + 4, vendorEnd) !== "gnu")
|
||||
return err(
|
||||
"MIPS ELF declares non-GNU vendor attributes that REA does not inspect",
|
||||
);
|
||||
if (vendorSeen)
|
||||
return err("duplicate MIPS GNU attribute vendor subsections");
|
||||
vendorSeen = true;
|
||||
|
||||
let cursor = vendorEnd + 1;
|
||||
while (cursor < end) {
|
||||
const scopeStart = cursor;
|
||||
const scope = readUleb(cursor, end);
|
||||
if (scope === undefined)
|
||||
return err("malformed MIPS GNU attribute scope tag");
|
||||
if (scope.value !== TAG_FILE)
|
||||
return err(
|
||||
"MIPS GNU attributes scoped to sections or symbols are not inspected by REA",
|
||||
);
|
||||
if (end - scope.next < 4)
|
||||
return err("truncated MIPS GNU attribute scope length");
|
||||
const scopeLength = readU32(scope.next);
|
||||
if (
|
||||
scopeLength < scope.next + 4 - scopeStart ||
|
||||
scopeLength > end - scopeStart
|
||||
)
|
||||
return err(
|
||||
"MIPS GNU attribute scope length does not fit its subsection",
|
||||
);
|
||||
const scopeEnd = scopeStart + scopeLength;
|
||||
cursor = scope.next + 4;
|
||||
while (cursor < scopeEnd) {
|
||||
const tag = readUleb(cursor, scopeEnd);
|
||||
if (tag === undefined) return err("malformed MIPS GNU attribute tag");
|
||||
if (
|
||||
tag.value !== TAG_GNU_MIPS_ABI_FP &&
|
||||
tag.value !== TAG_GNU_MIPS_ABI_MSA
|
||||
)
|
||||
return err(
|
||||
`MIPS ELF declares GNU attribute tag ${tag.value}, which REA does not inspect`,
|
||||
);
|
||||
const value = readUleb(tag.next, scopeEnd);
|
||||
if (value === undefined)
|
||||
return err("malformed MIPS GNU attribute value");
|
||||
if (tag.value === TAG_GNU_MIPS_ABI_FP) {
|
||||
if (fpAbi !== null)
|
||||
return err("duplicate MIPS GNU floating-point ABI attributes");
|
||||
fpAbi = value.value;
|
||||
} else {
|
||||
if (msa !== null) return err("duplicate MIPS GNU MSA ABI attributes");
|
||||
msa = value.value;
|
||||
}
|
||||
cursor = value.next;
|
||||
}
|
||||
cursor = scopeEnd;
|
||||
}
|
||||
position = end;
|
||||
}
|
||||
return ok({ fpAbi, msa });
|
||||
};
|
||||
@@ -58,6 +58,8 @@ export type HeaderArchitecture = Exclude<
|
||||
export interface MipsElfMetadata extends ElfFamilyMetadata {
|
||||
/** Undefined until inspected; null means no ABI record was observed. */
|
||||
readonly abiFlags?: MipsAbiFlags | null;
|
||||
/** Undefined until inspected; null means no GNU attribute section was observed. */
|
||||
readonly gnuAttributes?: MipsGnuAttributes | null;
|
||||
/** Unavailable declaration sources; missing records there remain unknown. */
|
||||
readonly limitations?: readonly string[];
|
||||
/** Complete dynamic REL declarations, or the reason they could not be inspected. */
|
||||
@@ -86,6 +88,14 @@ export interface MipsAbiFlags {
|
||||
readonly flags2: number;
|
||||
}
|
||||
|
||||
/** File-scope GNU object attributes parsed from `SHT_GNU_ATTRIBUTES`. */
|
||||
export interface MipsGnuAttributes {
|
||||
/** Tag_GNU_MIPS_ABI_FP; null means this file did not declare the tag. */
|
||||
readonly fpAbi: number | null;
|
||||
/** Tag_GNU_MIPS_ABI_MSA vector-argument ABI; null means undeclared. */
|
||||
readonly msa: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ELF class and flags retain the width and ABI declarations used by this lane.
|
||||
* ISA extensions in `.riscv.attributes` are not inspected by this header parser.
|
||||
|
||||
@@ -37,6 +37,7 @@ const target = (change: Partial<MipsElfMetadata> = {}): BinaryTarget => ({
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x70001001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: {
|
||||
version: 0,
|
||||
isaLevel: 32,
|
||||
@@ -95,6 +96,7 @@ describe("bounded Ghidra MIPS admission", () => {
|
||||
flags1: 0,
|
||||
flags2: 0,
|
||||
},
|
||||
gnu_attributes: null,
|
||||
});
|
||||
expect(left.value.profile?.digest).not.toBe(right.value.profile?.digest);
|
||||
const windows = new GhidraProvider(
|
||||
@@ -470,7 +472,7 @@ describe("MIPS16 shared-image profile", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each(["sharedLoad", "abiFlags"] as const)(
|
||||
it.each(["sharedLoad", "abiFlags", "gnuAttributes"] as const)(
|
||||
"refuses an uninspected %s declaration",
|
||||
(declaration) => {
|
||||
const value = shared();
|
||||
@@ -484,9 +486,11 @@ describe("MIPS16 shared-image profile", () => {
|
||||
).toMatchObject({
|
||||
status: "unsupported",
|
||||
reason: expect.stringContaining(
|
||||
declaration === "abiFlags"
|
||||
? "inspected ABI flags"
|
||||
: "have not been inspected",
|
||||
{
|
||||
abiFlags: "inspected ABI flags",
|
||||
gnuAttributes: "GNU MIPS ABI attribute inspection is unavailable",
|
||||
sharedLoad: "have not been inspected",
|
||||
}[declaration],
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type {
|
||||
BinaryTarget,
|
||||
MipsElfMetadata,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
const MIPS_FP_ABI_SOFT = 3;
|
||||
@@ -65,6 +68,27 @@ const hasLegacyMipsHeaderShape = (target: BinaryTarget): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/** Return a refusal for GNU ABI attributes this existing MIPS lane cannot model. */
|
||||
export const ghidraMipsGnuAttributesUnsupportedReason = (
|
||||
metadata: MipsElfMetadata,
|
||||
): string | null => {
|
||||
const attributes = metadata.gnuAttributes;
|
||||
if (attributes === undefined)
|
||||
return "GNU MIPS ABI attribute inspection is unavailable; provider selection cannot treat it as absent.";
|
||||
if (attributes === null) return null;
|
||||
if (attributes.msa !== null && attributes.msa !== 0)
|
||||
return "The GNU MIPS MSA ABI attribute declares an unsupported or unknown vector argument ABI.";
|
||||
if (
|
||||
metadata.abiFlags === null &&
|
||||
attributes.fpAbi !== null &&
|
||||
attributes.fpAbi !== 0 &&
|
||||
attributes.fpAbi !== 1 &&
|
||||
attributes.fpAbi !== 5
|
||||
)
|
||||
return "Without a MIPS ABI flags record, the GNU floating-point ABI attribute must be absent, any, double-precision, or FPXX for the verified recordless lane.";
|
||||
return null;
|
||||
};
|
||||
|
||||
/** Keep family recognition separate from the deliberately bounded Ghidra lane. */
|
||||
export const ghidraMipsUnsupportedReason = (
|
||||
target: BinaryTarget,
|
||||
@@ -77,6 +101,9 @@ export const ghidraMipsUnsupportedReason = (
|
||||
const shared = isGhidraMips16SharedTarget(target);
|
||||
if (metadata.elfClass !== 32)
|
||||
return "This Ghidra MIPS lane supports ELF32 only; MIPS64 and n64 remain unsupported.";
|
||||
const gnuAttributesReason =
|
||||
ghidraMipsGnuAttributesUnsupportedReason(metadata);
|
||||
if (gnuAttributesReason !== null) return gnuAttributesReason;
|
||||
if (
|
||||
metadata.type !== MIPS_ET_EXEC &&
|
||||
metadata.type !== MIPS_ET_REL &&
|
||||
@@ -297,6 +324,17 @@ export const ghidraMipsProfileParameters = (
|
||||
flags1: abi.flags1,
|
||||
flags2: abi.flags2,
|
||||
},
|
||||
...(target.mips.gnuAttributes === undefined
|
||||
? {}
|
||||
: {
|
||||
gnu_attributes:
|
||||
target.mips.gnuAttributes === null
|
||||
? null
|
||||
: {
|
||||
fp_abi: target.mips.gnuAttributes.fpAbi,
|
||||
msa: target.mips.gnuAttributes.msa,
|
||||
},
|
||||
}),
|
||||
...(target.mips.sharedLoad === undefined
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { ghidraMipsUnsupportedReason } from "./GhidraMipsProfile.js";
|
||||
import {
|
||||
ghidraMipsGnuAttributesUnsupportedReason,
|
||||
ghidraMipsUnsupportedReason,
|
||||
} from "./GhidraMipsProfile.js";
|
||||
import { ghidraPowerPcUnsupportedReason } from "./GhidraPowerPcProfile.js";
|
||||
import { ghidraRiscvUnsupportedReason } from "./GhidraRiscvProfile.js";
|
||||
|
||||
@@ -27,6 +30,9 @@ export const ghidraProcessorUnsupportedReason = (
|
||||
return ghidraMipsUnsupportedReason(target);
|
||||
const metadata = target.mips;
|
||||
if (metadata === undefined) return "PSP analysis requires ELF metadata.";
|
||||
const gnuAttributesReason =
|
||||
ghidraMipsGnuAttributesUnsupportedReason(metadata);
|
||||
if (gnuAttributesReason !== null) return gnuAttributesReason;
|
||||
if (metadata.elfClass !== 32 || metadata.byteOrder !== "little")
|
||||
return "The PSP Allegrex profile requires little-endian ELF32.";
|
||||
if (metadata.type !== 2)
|
||||
|
||||
@@ -46,6 +46,7 @@ const target = {
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: pspAbiFlags,
|
||||
},
|
||||
} satisfies BinaryTarget;
|
||||
@@ -312,6 +313,16 @@ describe.skipIf(process.platform === "win32")(
|
||||
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses a separately declared unsupported MSA argument ABI", async () => {
|
||||
const { provider } = await installation();
|
||||
const altered = {
|
||||
...target,
|
||||
mips: { ...target.mips, gnuAttributes: { fpAbi: 2, msa: 1 } },
|
||||
} as BinaryTarget;
|
||||
expect(ghidraProcessorUnsupportedReason(altered)).toMatch(/MSA ABI/u);
|
||||
expect(provider.inspectTargetSupport(altered).status).toBe("unsupported");
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -388,6 +399,7 @@ describe.skipIf(process.platform === "win32")(
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
gnuAttributes: null,
|
||||
abiFlags: { ...pspAbiFlags, [field]: value },
|
||||
},
|
||||
};
|
||||
@@ -416,6 +428,7 @@ describe.skipIf(process.platform === "win32")(
|
||||
byteOrder: "little",
|
||||
type: 2,
|
||||
flags: 0x10a23001,
|
||||
gnuAttributes: null,
|
||||
...(abiFlags === undefined ? {} : { abiFlags }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { open, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { it } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
|
||||
import { readMipsElfMetadata } from "../../../src/application/MipsElfMetadata.js";
|
||||
@@ -53,6 +54,62 @@ const fixture = (little = true): Buffer => {
|
||||
bytes.set([0, 0, 32, 2, 1, 1, 0, 5], abiOffset);
|
||||
return bytes;
|
||||
};
|
||||
const attributeSection = (
|
||||
attributes: readonly number[],
|
||||
little = true,
|
||||
): Buffer => {
|
||||
const subsectionLength = 1 + 4 + attributes.length;
|
||||
const vendor = Buffer.from("gnu\0", "latin1");
|
||||
const sectionLength = 4 + vendor.length + subsectionLength;
|
||||
const bytes = Buffer.alloc(1 + sectionLength);
|
||||
bytes[0] = 0x41;
|
||||
if (little) {
|
||||
bytes.writeUInt32LE(sectionLength, 1);
|
||||
bytes.writeUInt32LE(subsectionLength, 1 + 4 + vendor.length + 1);
|
||||
} else {
|
||||
bytes.writeUInt32BE(sectionLength, 1);
|
||||
bytes.writeUInt32BE(subsectionLength, 1 + 4 + vendor.length + 1);
|
||||
}
|
||||
vendor.copy(bytes, 5);
|
||||
bytes[9] = 1; // Tag_File scope.
|
||||
bytes.set(attributes, 14);
|
||||
return bytes;
|
||||
};
|
||||
const withGnuAttributes = (
|
||||
source: Buffer,
|
||||
payloads: readonly Buffer[],
|
||||
little = true,
|
||||
): Buffer => {
|
||||
const payloadStart = abiOffset + 24;
|
||||
const payloadBytes = payloads.reduce(
|
||||
(total, payload) => total + payload.length,
|
||||
0,
|
||||
);
|
||||
const bytes = Buffer.alloc(
|
||||
Math.max(source.length, payloadStart + payloadBytes),
|
||||
);
|
||||
source.copy(bytes);
|
||||
let payloadOffset = payloadStart;
|
||||
const write16 = (value: number, offset: number) =>
|
||||
little
|
||||
? bytes.writeUInt16LE(value, offset)
|
||||
: bytes.writeUInt16BE(value, offset);
|
||||
const write32 = (value: number, offset: number) =>
|
||||
little
|
||||
? bytes.writeUInt32LE(value, offset)
|
||||
: bytes.writeUInt32BE(value, offset);
|
||||
write16(2 + payloads.length, 48);
|
||||
write16(0, 50); // Section names are not needed for numeric-type detection.
|
||||
payloads.forEach((payload, index) => {
|
||||
const header = 128 + (2 + index) * 40;
|
||||
write32(0x6ffffff5, header + 4);
|
||||
write32(payloadOffset, header + 16);
|
||||
write32(payload.length, header + 20);
|
||||
payload.copy(bytes, payloadOffset);
|
||||
payloadOffset += payload.length;
|
||||
});
|
||||
return bytes;
|
||||
};
|
||||
const writeFixture = async (bytes: Buffer): Promise<string> => {
|
||||
const directory = await createTestTempDirectory("rea-mips-abi-test-");
|
||||
const path = join(directory, "fixture.elf");
|
||||
@@ -77,7 +134,7 @@ const resolveFixture = async (bytes: Buffer) => {
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
return result.value;
|
||||
};
|
||||
const inspect = async (
|
||||
const inspectDeclarations = async (
|
||||
bytes: Buffer,
|
||||
little = true,
|
||||
checkCancelled: () => void = () => {},
|
||||
@@ -90,11 +147,19 @@ const inspect = async (
|
||||
metadata(little),
|
||||
checkCancelled,
|
||||
);
|
||||
return result.ok ? ok(result.value.abiFlags) : result;
|
||||
return result;
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
};
|
||||
const inspect = async (
|
||||
bytes: Buffer,
|
||||
little = true,
|
||||
checkCancelled: () => void = () => {},
|
||||
) => {
|
||||
const result = await inspectDeclarations(bytes, little, checkCancelled);
|
||||
return result.ok ? ok(result.value.abiFlags) : result;
|
||||
};
|
||||
const target = (mips: MipsElfMetadata) => ({
|
||||
path: "/fixture.elf",
|
||||
sha256: "a".repeat(64),
|
||||
@@ -102,7 +167,7 @@ const target = (mips: MipsElfMetadata) => ({
|
||||
format: "elf" as const,
|
||||
architecture: "mips" as const,
|
||||
availableArchitectures: ["mips"] as const,
|
||||
mips,
|
||||
mips: { gnuAttributes: null, ...mips },
|
||||
});
|
||||
|
||||
for (const little of [true, false]) {
|
||||
@@ -374,25 +439,117 @@ it("projects a malformed out-of-probe ABI record as a target error", async () =>
|
||||
});
|
||||
|
||||
for (const little of [true, false]) {
|
||||
it(`refuses GNU ABI attributes by section type without reading attacker-controlled names or sizes (${little ? "LE" : "BE"})`, async () => {
|
||||
const bytes = fixture(little);
|
||||
const write16 = little
|
||||
? (value: number, offset: number) => bytes.writeUInt16LE(value, offset)
|
||||
: (value: number, offset: number) => bytes.writeUInt16BE(value, offset);
|
||||
const write32 = little
|
||||
? (value: number, offset: number) => bytes.writeUInt32LE(value, offset)
|
||||
: (value: number, offset: number) => bytes.writeUInt32BE(value, offset);
|
||||
write32(0xffffffff, 168); // Unknown name; the section type is authoritative.
|
||||
write32(0x6ffffff5, 172); // SHT_GNU_ATTRIBUTES.
|
||||
write32(0xfffffff0, 184); // Must not be read as a payload offset.
|
||||
write32(0xffffffff, 188); // Must not control allocation.
|
||||
write16(0, 50); // No section-name string table is required for detection.
|
||||
const result = await inspect(bytes, little);
|
||||
assert.ok(!result.ok);
|
||||
assert.match(result.error, /GNU ABI attributes that REA does not inspect/u);
|
||||
it(`reads bounded GNU FP/MSA attributes through the full target resolver (${little ? "LE" : "BE"})`, async () => {
|
||||
const bytes = withGnuAttributes(
|
||||
fixture(little),
|
||||
[attributeSection([4, 5, 8, 0], little)],
|
||||
little,
|
||||
);
|
||||
const result = await inspectDeclarations(bytes, little);
|
||||
assert.ok(result.ok);
|
||||
assert.deepEqual(result.value.gnuAttributes, { fpAbi: 5, msa: 0 });
|
||||
const resolved = await resolveFixture(bytes);
|
||||
assert.deepEqual(resolved.mips.gnuAttributes, { fpAbi: 5, msa: 0 });
|
||||
assert.equal(ghidraMipsUnsupportedReason(resolved), null);
|
||||
const profileParameters = jsonObjectSchema.parse(
|
||||
ghidraMipsProfileParameters(resolved),
|
||||
);
|
||||
expect(profileParameters.mips_elf).toMatchObject({
|
||||
gnu_attributes: { fp_abi: 5, msa: 0 },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("reconciles FP tags with ABI flags while treating FP ANY as no requirement", async () => {
|
||||
const any = withGnuAttributes(fixture(), [attributeSection([4, 0])]);
|
||||
const accepted = await inspectDeclarations(any);
|
||||
assert.ok(accepted.ok);
|
||||
assert.equal(accepted.value.gnuAttributes?.fpAbi, 0);
|
||||
|
||||
const conflict = withGnuAttributes(fixture(), [attributeSection([4, 1])]);
|
||||
const rejected = await inspectDeclarations(conflict);
|
||||
assert.ok(!rejected.ok);
|
||||
assert.match(
|
||||
rejected.error,
|
||||
/conflicting MIPS ABI flags and GNU floating-point ABI/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("retains unsupported MSA declarations but refuses provider selection", async () => {
|
||||
const bytes = withGnuAttributes(fixture(), [attributeSection([8, 1])]);
|
||||
const resolved = await resolveFixture(bytes);
|
||||
assert.deepEqual(resolved.mips.gnuAttributes, { fpAbi: null, msa: 1 });
|
||||
assert.match(
|
||||
ghidraMipsUnsupportedReason(resolved) ?? "",
|
||||
/MSA ABI attribute declares an unsupported/u,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, null],
|
||||
[1, null],
|
||||
[5, null],
|
||||
[2, /GNU floating-point ABI attribute/u],
|
||||
[3, /GNU floating-point ABI attribute/u],
|
||||
] as const)(
|
||||
"applies recordless admission to GNU FP value %i",
|
||||
async (fpAbi, reason) => {
|
||||
const bytes = fixture();
|
||||
bytes.writeUInt32LE(0x50000001, 36); // Recordless MIPS32 legacy header.
|
||||
bytes.writeUInt32LE(0, 52); // No PT_MIPS_ABIFLAGS.
|
||||
bytes.writeUInt32LE(0, 172); // No SHT_MIPS_ABIFLAGS.
|
||||
const withAttributes = withGnuAttributes(bytes, [
|
||||
attributeSection([4, fpAbi]),
|
||||
]);
|
||||
const resolved = await resolveFixture(withAttributes);
|
||||
const unsupported = ghidraMipsUnsupportedReason(resolved);
|
||||
if (reason === null) assert.equal(unsupported, null);
|
||||
else assert.match(unsupported ?? "", reason);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["an unknown attribute tag", [6, 1], /tag 6/u],
|
||||
[
|
||||
"a duplicate attribute section",
|
||||
[4, 5],
|
||||
/duplicate MIPS GNU attribute sections/u,
|
||||
],
|
||||
] as const)(
|
||||
"refuses %s while inspecting section contents",
|
||||
async (label, attr, reason) => {
|
||||
const payload = attributeSection(attr);
|
||||
const bytes = withGnuAttributes(
|
||||
fixture(),
|
||||
label === "a duplicate attribute section"
|
||||
? [payload, payload]
|
||||
: [payload],
|
||||
);
|
||||
const result = await inspectDeclarations(bytes);
|
||||
assert.ok(!result.ok);
|
||||
assert.match(result.error, reason);
|
||||
},
|
||||
);
|
||||
|
||||
it("checks the GNU attribute size bound before attempting its file read", async () => {
|
||||
const bytes = withGnuAttributes(fixture(), [attributeSection([4, 5])]);
|
||||
bytes.writeUInt32LE(0xfffffff0, 224);
|
||||
bytes.writeUInt32LE(4097, 228);
|
||||
const result = await inspectDeclarations(bytes);
|
||||
assert.ok(!result.ok);
|
||||
assert.match(result.error, /outside REA's inspected bound of 4096 bytes/u);
|
||||
});
|
||||
|
||||
it("uses section type and bounds the GNU attribute payload extent", async () => {
|
||||
const bytes = withGnuAttributes(fixture(), [attributeSection([4, 5])]);
|
||||
bytes.writeUInt32LE(0xffffffff, 208); // Unknown name; type remains authoritative.
|
||||
bytes.writeUInt16LE(0, 50); // No section-name string table is required.
|
||||
bytes.writeUInt32LE(bytes.length + 1, 224);
|
||||
const result = await inspectDeclarations(bytes);
|
||||
assert.ok(!result.ok);
|
||||
assert.match(result.error, /GNU attribute section lies outside the file/u);
|
||||
});
|
||||
|
||||
it("preserves cancellation instead of turning it into malformed target data", async () => {
|
||||
const directory = await createTestTempDirectory("rea-mips-resolution-");
|
||||
const path = join(directory, "fixture.elf");
|
||||
|
||||
@@ -90,6 +90,7 @@ for (const little of [true, false]) {
|
||||
type: 3,
|
||||
flags: 0x74001005,
|
||||
abi_flags: null,
|
||||
gnu_attributes: null,
|
||||
shared_load: {
|
||||
status: "complete",
|
||||
relocation_count: 2,
|
||||
|
||||
Reference in New Issue
Block a user