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:
morluto
2026-10-14 14:46:35 +08:00
parent 1341e8c831
commit 9387282274
12 changed files with 545 additions and 34 deletions
+17 -3
View File
@@ -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
+1
View File
@@ -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 }),
+36 -5
View File
@@ -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 });
};
+121
View File
@@ -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);
});
});
+115
View File
@@ -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 });
};
+10
View File
@@ -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.
+8 -4
View File
@@ -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],
),
});
},
+39 -1
View File
@@ -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
? {}
: {
+7 -1
View File
@@ -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 }),
},
};
+177 -20
View File
@@ -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,