mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor: consolidate ELF and CLR record interpretation
Share ELF note headers across note inspection, Flutter build IDs, Node SEA, and recorded-crash source binding. Decode REL/RELA records once for MIPS shared-load analysis and IL2CPP linking, removing superseded scalar helpers. Share the CLR header prefix between desktop detection and managed PE parsing while preserving their distinct read bounds and admission policies. Cover those differences and ignored R_NONE fields with production-boundary tests. Validation: 493 focused tests passed, two existing skips; static checks and test discovery passed. Public managed/desktop CLI and ELF notes CLI/MCP workflows exercised; no real external analysis providers were run.
This commit is contained in:
1 parent
eb51eb6915
commit
a7f50e3386
16 files changed
+382
-37
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { readElfRelocationEntry } from "../domain/elfRelocation.js";
|
||||
import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js";
|
||||
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
|
||||
import type { MipsElfMetadata } from "../domain/binaryTargetTypes.js";
|
||||
@@ -29,8 +30,6 @@ export const readMipsElfSharedLoad = async (
|
||||
checkCancelled: () => void,
|
||||
sections: readonly MipsElfRelocationSection[],
|
||||
): Promise<Result<SharedLoadFacts, string>> => {
|
||||
const u32 = (bytes: Buffer, offset: number): number =>
|
||||
little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
|
||||
const loads: {
|
||||
offset: number;
|
||||
address: number;
|
||||
@@ -219,15 +218,21 @@ export const readMipsElfSharedLoad = async (
|
||||
if (!chunk.ok) return chunk;
|
||||
for (let position = 0; position < chunk.value.length; position += 8) {
|
||||
checkCancelled();
|
||||
const info = u32(chunk.value, position + 4);
|
||||
const type = info & 0xff;
|
||||
const relocationAddress = u32(chunk.value, position);
|
||||
const parsedEntry = readElfRelocationEntry(
|
||||
chunk.value.subarray(position, position + 8),
|
||||
32,
|
||||
little,
|
||||
"rel",
|
||||
);
|
||||
if (!parsedEntry.ok) return err(parsedEntry.error.message);
|
||||
const { type, symbolIndex, offset: rawAddress } = parsedEntry.value;
|
||||
const relocationAddress = Number(rawAddress);
|
||||
if (type === 3 && mapped(relocationAddress, 4, false) === undefined)
|
||||
return err(
|
||||
"MIPS REL32 destination has no unique complete PT_LOAD memory range",
|
||||
);
|
||||
types.add(type);
|
||||
if (type === 3 && info >>> 8 !== 0) symbolRelativeCount += 1;
|
||||
if (type === 3 && symbolIndex !== 0) symbolRelativeCount += 1;
|
||||
}
|
||||
}
|
||||
return ok({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
peRvaToFileOffset,
|
||||
} from "../domain/binaryTarget.js";
|
||||
import { readManagedMetadataLayout } from "../dotnet/ManagedMetadataLayout.js";
|
||||
import { readManagedCliHeaderPrefix } from "../domain/managed/managedCliHeaderPrefix.js";
|
||||
import { readAssemblyReferenceName } from "../dotnet/ManagedMetadataInventoryRows.js";
|
||||
import { ManagedReaderFailure } from "../dotnet/ManagedReaderFailure.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
@@ -285,14 +286,14 @@ const inspectPeFile = async (
|
||||
"format",
|
||||
"CLI metadata directory cannot be mapped",
|
||||
);
|
||||
const cli = await read(cliOffset, 16);
|
||||
if (cli.readUInt32LE(0) < 72)
|
||||
const cli = readManagedCliHeaderPrefix(await read(cliOffset, 16));
|
||||
if (!cli.ok || cli.value.headerSize < 72)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
"CLI header declares a truncated runtime header",
|
||||
);
|
||||
const metadataBytes = cli.readUInt32LE(12);
|
||||
const metadataOffset = mapped(cli.readUInt32LE(8), metadataBytes);
|
||||
const metadataBytes = cli.value.metadata.size;
|
||||
const metadataOffset = mapped(cli.value.metadata.rva, metadataBytes);
|
||||
if (metadataOffset === undefined)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { readMachoLoadCommandTableHeader } from "../../domain/apple/machoLoadCom
|
||||
import {
|
||||
elfNoteAlignment,
|
||||
elfNoteLayout,
|
||||
readElfNoteHeader,
|
||||
} from "../../domain/native/elfNoteLayout.js";
|
||||
import {
|
||||
NODE_SEA_BLOB_TOO_LARGE,
|
||||
@@ -504,8 +505,10 @@ const scanElfNoteTable = async (
|
||||
const header = await readExact(carrier, note.offset + cursor, 12);
|
||||
if (header === undefined)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
const namesz = readU32(header, 0, little);
|
||||
const descsz = readU32(header, 4, little);
|
||||
const parsedHeader = readElfNoteHeader(header, little);
|
||||
if (!parsedHeader.ok)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
const { namesz, descsz } = parsedHeader.value;
|
||||
if (namesz > MAX_NOTE_NAME_BYTES)
|
||||
return failNoteTable(alreadyFound || found.length > 0);
|
||||
if (cursor > note.length - 12 || namesz > note.length - cursor - 12)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readElfRelocationEntry } from "./elfRelocation.js";
|
||||
|
||||
const formats = [
|
||||
{ bits: 32, littleEndian: false },
|
||||
{ bits: 32, littleEndian: true },
|
||||
{ bits: 64, littleEndian: false },
|
||||
{ bits: 64, littleEndian: true },
|
||||
] as const;
|
||||
|
||||
it.each(formats)(
|
||||
"preserves standard ELF$bits REL/RELA fields (littleEndian=$littleEndian)",
|
||||
({ bits, littleEndian }) => {
|
||||
const wide = bits === 64;
|
||||
const wordBytes = wide ? 8 : 4;
|
||||
const backing = Buffer.alloc(wordBytes * 3 + 19, 0xa5);
|
||||
const bytes = backing.subarray(7, 7 + wordBytes * 3);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const offset = wide ? 0xffffffffffffffffn : 0xffffffffn;
|
||||
const symbolIndex = wide ? 0xfedcba98 : 0xabcdef;
|
||||
const type = wide ? 0x89abcdef : 0x80;
|
||||
const info = (BigInt(symbolIndex) << (wide ? 32n : 8n)) | BigInt(type);
|
||||
const addend = wide ? -0x8000000000000000n : -0x80000000n;
|
||||
if (wide) {
|
||||
view.setBigUint64(0, offset, littleEndian);
|
||||
view.setBigUint64(8, info, littleEndian);
|
||||
view.setBigInt64(16, addend, littleEndian);
|
||||
} else {
|
||||
view.setUint32(0, Number(offset), littleEndian);
|
||||
view.setUint32(4, Number(info), littleEndian);
|
||||
view.setInt32(8, Number(addend), littleEndian);
|
||||
}
|
||||
const before = Buffer.from(backing);
|
||||
const rel = readElfRelocationEntry(
|
||||
backing.subarray(7),
|
||||
bits,
|
||||
littleEndian,
|
||||
"rel",
|
||||
);
|
||||
const rela = readElfRelocationEntry(
|
||||
backing.subarray(7),
|
||||
bits,
|
||||
littleEndian,
|
||||
"rela",
|
||||
);
|
||||
expect(backing.equals(before)).toBe(true);
|
||||
backing.fill(0);
|
||||
expect(rel).toEqual({
|
||||
ok: true,
|
||||
value: { offset, type, symbolIndex },
|
||||
});
|
||||
expect(rela).toEqual({
|
||||
ok: true,
|
||||
value: { offset, type, symbolIndex, addend },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(formats)(
|
||||
"refuses every incomplete ELF$bits REL/RELA entry (littleEndian=$littleEndian)",
|
||||
({ bits, littleEndian }) => {
|
||||
const wordBytes = bits === 64 ? 8 : 4;
|
||||
const bytes = Buffer.alloc(wordBytes * 3 + 7);
|
||||
for (let observed = 0; observed < wordBytes * 3; observed++) {
|
||||
const input = bytes.subarray(7, 7 + observed);
|
||||
const failure = {
|
||||
ok: false,
|
||||
error: {
|
||||
kind: "malformed",
|
||||
message: "ELF relocation entry is truncated",
|
||||
},
|
||||
};
|
||||
expect(readElfRelocationEntry(input, bits, littleEndian, "rela")).toEqual(
|
||||
failure,
|
||||
);
|
||||
if (observed < wordBytes * 2)
|
||||
expect(
|
||||
readElfRelocationEntry(input, bits, littleEndian, "rel"),
|
||||
).toEqual(failure);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
|
||||
interface ElfRelocationEntry {
|
||||
readonly offset: bigint;
|
||||
readonly type: number;
|
||||
readonly symbolIndex: number;
|
||||
}
|
||||
interface ElfRelaEntry extends ElfRelocationEntry {
|
||||
readonly addend: bigint;
|
||||
}
|
||||
interface ElfRelocationFailure {
|
||||
readonly kind: "malformed";
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export function readElfRelocationEntry(
|
||||
bytes: Uint8Array,
|
||||
bits: 32 | 64,
|
||||
littleEndian: boolean,
|
||||
encoding: "rel",
|
||||
): Result<ElfRelocationEntry, ElfRelocationFailure>;
|
||||
export function readElfRelocationEntry(
|
||||
bytes: Uint8Array,
|
||||
bits: 32 | 64,
|
||||
littleEndian: boolean,
|
||||
encoding: "rela",
|
||||
): Result<ElfRelaEntry, ElfRelocationFailure>;
|
||||
/** Decode standard REL/RELA fields; architecture-specific application remains with consumers. */
|
||||
export function readElfRelocationEntry(
|
||||
bytes: Uint8Array,
|
||||
bits: 32 | 64,
|
||||
littleEndian: boolean,
|
||||
encoding: "rel" | "rela",
|
||||
): Result<ElfRelocationEntry | ElfRelaEntry, ElfRelocationFailure> {
|
||||
const wordBytes = bits === 64 ? 8 : 4;
|
||||
if (bytes.byteLength < wordBytes * (encoding === "rela" ? 3 : 2))
|
||||
return err({
|
||||
kind: "malformed",
|
||||
message: "ELF relocation entry is truncated",
|
||||
});
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
const offset =
|
||||
bits === 64
|
||||
? view.getBigUint64(0, littleEndian)
|
||||
: BigInt(view.getUint32(0, littleEndian));
|
||||
const info =
|
||||
bits === 64
|
||||
? view.getBigUint64(wordBytes, littleEndian)
|
||||
: BigInt(view.getUint32(wordBytes, littleEndian));
|
||||
const entry = {
|
||||
offset,
|
||||
type: Number(info & (bits === 64 ? 0xffffffffn : 0xffn)),
|
||||
symbolIndex: Number(info >> (bits === 64 ? 32n : 8n)),
|
||||
};
|
||||
if (encoding === "rel") return ok(entry);
|
||||
const addend =
|
||||
bits === 64
|
||||
? view.getBigInt64(wordBytes * 2, littleEndian)
|
||||
: BigInt(view.getInt32(wordBytes * 2, littleEndian));
|
||||
return ok({ ...entry, addend });
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readManagedCliHeaderPrefix } from "./managedCliHeaderPrefix.js";
|
||||
|
||||
it("requires only the fixed prefix and leaves full-header admission to consumers", () => {
|
||||
const bytes = Buffer.alloc(16);
|
||||
bytes.writeUInt32LE(0xffffffff, 0);
|
||||
bytes.writeUInt16LE(0xffff, 4);
|
||||
bytes.writeUInt16LE(0x8000, 6);
|
||||
bytes.writeUInt32LE(0x80000000, 8);
|
||||
bytes.writeUInt32LE(0xffffffff, 12);
|
||||
expect(readManagedCliHeaderPrefix(bytes)).toEqual({
|
||||
ok: true,
|
||||
value: {
|
||||
headerSize: 0xffffffff,
|
||||
runtimeMajor: 0xffff,
|
||||
runtimeMinor: 0x8000,
|
||||
metadata: { rva: 0x80000000, size: 0xffffffff },
|
||||
},
|
||||
});
|
||||
for (let length = 0; length < 16; length++)
|
||||
expect(readManagedCliHeaderPrefix(bytes.subarray(0, length))).toMatchObject(
|
||||
{
|
||||
ok: false,
|
||||
error: { kind: "malformed" },
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { err, ok } from "../result.js";
|
||||
|
||||
/** IMAGE_COR20_HEADER's first 16 bytes; consumers own directory/range admission. */
|
||||
export const readManagedCliHeaderPrefix = (bytes: Buffer) => {
|
||||
if (bytes.length < 16)
|
||||
return err({
|
||||
kind: "malformed" as const,
|
||||
message: "CLI runtime header prefix is truncated",
|
||||
});
|
||||
return ok({
|
||||
headerSize: bytes.readUInt32LE(0),
|
||||
runtimeMajor: bytes.readUInt16LE(4),
|
||||
runtimeMinor: bytes.readUInt16LE(6),
|
||||
metadata: {
|
||||
rva: bytes.readUInt32LE(8),
|
||||
size: bytes.readUInt32LE(12),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { readElfNoteHeader } from "./elfNoteLayout.js";
|
||||
|
||||
it.each([false, true])(
|
||||
"preserves raw note sizes and type without admission (littleEndian=%s)",
|
||||
(littleEndian) => {
|
||||
const backing = Buffer.alloc(31, 0xa5);
|
||||
const bytes = backing.subarray(7, 19);
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
view.setUint32(0, 0xffffffff, littleEndian);
|
||||
view.setUint32(4, 0x80000001, littleEndian);
|
||||
view.setUint32(8, 0xfedcba98, littleEndian);
|
||||
const before = Buffer.from(backing);
|
||||
const parsed = readElfNoteHeader(backing.subarray(7), littleEndian);
|
||||
expect(backing.equals(before)).toBe(true);
|
||||
backing.fill(0);
|
||||
expect(parsed).toEqual({
|
||||
ok: true,
|
||||
value: { namesz: 0xffffffff, descsz: 0x80000001, type: 0xfedcba98 },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"refuses incomplete note headers (littleEndian=%s)",
|
||||
(littleEndian) => {
|
||||
const bytes = Buffer.alloc(19);
|
||||
for (let observed = 0; observed < 12; observed++)
|
||||
expect(
|
||||
readElfNoteHeader(bytes.subarray(7, 7 + observed), littleEndian),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: { kind: "malformed", message: "ELF note header is truncated" },
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
import { err, ok } from "../result.js";
|
||||
|
||||
/** ELF note padding follows binutils: only a declared alignment of 8 uses 8. */
|
||||
export const elfNoteAlignment = (declared: number | bigint): number =>
|
||||
declared === 8 || declared === 8n ? 8 : 4;
|
||||
@@ -14,3 +16,18 @@ export const elfNoteLayout = (
|
||||
next: Math.ceil((descriptor + descsz) / alignment) * alignment,
|
||||
};
|
||||
};
|
||||
|
||||
/** Fixed ELF note words; padding, owner names and descriptor admission belong to consumers. */
|
||||
export const readElfNoteHeader = (bytes: Uint8Array, littleEndian: boolean) => {
|
||||
if (bytes.byteLength < 12)
|
||||
return err({
|
||||
kind: "malformed" as const,
|
||||
message: "ELF note header is truncated",
|
||||
});
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
return ok({
|
||||
namesz: view.getUint32(0, littleEndian),
|
||||
descsz: view.getUint32(4, littleEndian),
|
||||
type: view.getUint32(8, littleEndian),
|
||||
});
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { readPeSectionHeader } from "../domain/peSectionHeader.js";
|
||||
import type { ManagedParseIssue } from "../domain/managed/managedArtifact.js";
|
||||
import { readPeCoffHeader } from "../domain/peCoffHeader.js";
|
||||
import { decodePeMachine } from "../domain/peMachine.js";
|
||||
import { readManagedCliHeaderPrefix } from "../domain/managed/managedCliHeaderPrefix.js";
|
||||
import {
|
||||
managedFailure,
|
||||
ManagedReaderFailure,
|
||||
@@ -251,7 +252,17 @@ const readCliHeader = (
|
||||
size: 72,
|
||||
scope: "cli.header",
|
||||
});
|
||||
const headerSize = bytes.readUInt32LE(offset);
|
||||
const prefix = readManagedCliHeaderPrefix(
|
||||
bytes.subarray(offset, offset + 16),
|
||||
);
|
||||
if (!prefix.ok)
|
||||
throw managedFailure(
|
||||
"invalid-cli-header",
|
||||
"cli.header",
|
||||
prefix.error.message,
|
||||
offset,
|
||||
);
|
||||
const { headerSize, runtimeMajor, runtimeMinor, metadata } = prefix.value;
|
||||
if (headerSize < 72 || headerSize > directory.size)
|
||||
throw managedFailure(
|
||||
"invalid-cli-header",
|
||||
@@ -259,7 +270,6 @@ const readCliHeader = (
|
||||
"CLI header size is outside its PE data directory",
|
||||
offset,
|
||||
);
|
||||
const metadata = readDirectory(bytes, offset + 8);
|
||||
if (metadata.rva === 0 || metadata.size === 0)
|
||||
throw managedFailure(
|
||||
"invalid-cli-header",
|
||||
@@ -283,8 +293,8 @@ const readCliHeader = (
|
||||
return {
|
||||
headerOffset: offset,
|
||||
headerSize,
|
||||
runtimeMajor: bytes.readUInt16LE(offset + 4),
|
||||
runtimeMinor: bytes.readUInt16LE(offset + 6),
|
||||
runtimeMajor,
|
||||
runtimeMinor,
|
||||
metadata,
|
||||
flags,
|
||||
entryPoint: bytes.readUInt32LE(offset + 20),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
||||
import {
|
||||
elfNoteAlignment,
|
||||
elfNoteLayout,
|
||||
readElfNoteHeader,
|
||||
} from "../domain/native/elfNoteLayout.js";
|
||||
import {
|
||||
readElfHeader,
|
||||
@@ -164,9 +165,9 @@ export const readElfBuildIds = async (
|
||||
return failure("ELF note header is truncated");
|
||||
break;
|
||||
}
|
||||
const namesz = notes.readUInt32LE(at);
|
||||
const descsz = notes.readUInt32LE(at + 4);
|
||||
const type = notes.readUInt32LE(at + 8);
|
||||
const parsedHeader = readElfNoteHeader(notes.subarray(at, at + 12), true);
|
||||
if (!parsedHeader.ok) return failure(parsedHeader.error.message);
|
||||
const { namesz, descsz, type } = parsedHeader.value;
|
||||
const layout = elfNoteLayout(namesz, descsz, range.alignment);
|
||||
const descriptor = at + layout.descriptor;
|
||||
const end = descriptor + descsz;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readElfRelocationEntry } from "../domain/elfRelocation.js";
|
||||
import { readElfDynamicEntry } from "../domain/elfDynamicEntry.js";
|
||||
import {
|
||||
BinaryMappingFailure,
|
||||
@@ -174,11 +175,21 @@ function* applyRelaTable(
|
||||
if (start === null) throw malformed("RELA table is not file-backed");
|
||||
for (let at = start; at < start + Number(size); at += 24) {
|
||||
if ((at - start) % (24 * 4096) === 0) yield;
|
||||
const info = source.image.readBigUInt64LE(at + 8),
|
||||
kind = Number(info & 0xffffffffn);
|
||||
const parsedEntry = readElfRelocationEntry(
|
||||
source.image.subarray(at, at + 24),
|
||||
64,
|
||||
true,
|
||||
"rela",
|
||||
);
|
||||
if (!parsedEntry.ok) throw malformed(parsedEntry.error.message);
|
||||
const {
|
||||
type: kind,
|
||||
offset: destination,
|
||||
symbolIndex,
|
||||
addend,
|
||||
} = parsedEntry.value;
|
||||
if (kind === 0) continue;
|
||||
const destination = source.image.readBigUInt64LE(at);
|
||||
if (kind !== source.relative || info >> 32n !== 0n) {
|
||||
if (kind !== source.relative || symbolIndex !== 0) {
|
||||
const width = source.unknownPointerWidths.get(kind);
|
||||
if (width === undefined)
|
||||
throw unsupported(`ELF relocation kind ${kind} is not decoded`);
|
||||
@@ -192,7 +203,7 @@ function* applyRelaTable(
|
||||
continue;
|
||||
}
|
||||
const target = fileOffset(source, destination, 8);
|
||||
const value = source.image.readBigInt64LE(at + 16) & 0xffffffffffffffffn;
|
||||
const value = BigInt.asUintN(64, addend);
|
||||
if (target !== null) output.bytes.writeBigUInt64LE(value, target);
|
||||
else if (
|
||||
isZeroFilledSpan(source.mappings, source.zeroFills, destination, 8)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
|
||||
import {
|
||||
elfNoteAlignment as noteAlignment,
|
||||
elfNoteLayout,
|
||||
readElfNoteHeader,
|
||||
} from "../../domain/native/elfNoteLayout.js";
|
||||
import type {
|
||||
ElfNotes,
|
||||
@@ -100,13 +101,6 @@ class ElfReader {
|
||||
readonly little: boolean,
|
||||
) {}
|
||||
|
||||
u32(offset: number): number {
|
||||
this.require(offset, 4);
|
||||
return this.little
|
||||
? this.bytes.readUInt32LE(offset)
|
||||
: this.bytes.readUInt32BE(offset);
|
||||
}
|
||||
|
||||
require(offset: number, length: number): void {
|
||||
if (
|
||||
!Number.isSafeInteger(offset) ||
|
||||
@@ -191,9 +185,12 @@ const parseNoteStream = (
|
||||
{ boundary: "capacity" },
|
||||
);
|
||||
const header = offset + at;
|
||||
const namesz = reader.u32(header);
|
||||
const descsz = reader.u32(header + 4);
|
||||
const type = reader.u32(header + 8);
|
||||
const parsedHeader = readElfNoteHeader(
|
||||
reader.slice(header, 12),
|
||||
reader.little,
|
||||
);
|
||||
if (!parsedHeader.ok) return fail(parsedHeader.error.message);
|
||||
const { namesz, descsz, type } = parsedHeader.value;
|
||||
// Align offsets from the start of this note. Independently rounding
|
||||
// namesz/descsz disagrees with binutils when alignment is 8 because the
|
||||
// 12-byte note header is not 8-aligned (GNU property namesz=4 then starts
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readElfNoteHeader } from "../../domain/native/elfNoteLayout.js";
|
||||
import { readElfSectionHeader } from "../../domain/elfSectionHeader.js";
|
||||
import { readElfProgramHeader } from "../../domain/elfProgramHeader.js";
|
||||
import { readElfRawTableDeclarations } from "../../domain/elfRawTableDeclarations.js";
|
||||
@@ -137,10 +138,13 @@ export function validateRecordedCrashStructure(
|
||||
}
|
||||
for (const note of report.notes) {
|
||||
const raw = bytesAt(BigInt(note.location.offset), 12);
|
||||
const parsedNote = readElfNoteHeader(raw, true);
|
||||
if (!parsedNote.ok) return fail();
|
||||
const { namesz, descsz, type } = parsedNote.value;
|
||||
if (
|
||||
BigInt(note.owner_location.bytes) !== BigInt(raw.readUInt32LE(0)) ||
|
||||
BigInt(note.descriptor_location.bytes) !== BigInt(raw.readUInt32LE(4)) ||
|
||||
raw.readUInt32LE(8) !== note.type_id
|
||||
BigInt(note.owner_location.bytes) !== BigInt(namesz) ||
|
||||
BigInt(note.descriptor_location.bytes) !== BigInt(descsz) ||
|
||||
type !== note.type_id
|
||||
)
|
||||
fail();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
buildNativePeFixture,
|
||||
} from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { readManagedMetadataLayout } from "../../../src/dotnet/ManagedMetadataLayout.js";
|
||||
import { readManagedPeLayout } from "../../../src/dotnet/ManagedPeReader.js";
|
||||
import { readAssemblyReference } from "../../../src/dotnet/ManagedMetadataInventoryRows.js";
|
||||
import {
|
||||
CPU,
|
||||
@@ -54,6 +55,59 @@ const put = async (root: string, path: string, bytes: Buffer | string) => {
|
||||
await writeFile(destination, bytes);
|
||||
};
|
||||
|
||||
it.each(["oversized-header", "prefix-only-section"] as const)(
|
||||
"keeps desktop CLR prefix admission distinct from full managed admission: %s",
|
||||
async (mode) => {
|
||||
const original = buildManagedPeFixture({
|
||||
references: ["UnityEngine.CoreModule"],
|
||||
});
|
||||
expect(readManagedPeLayout(original).cli).not.toBeNull();
|
||||
let image = Buffer.from(original);
|
||||
if (mode === "oversized-header") {
|
||||
// The directory declares 72 bytes; desktop needs only cb >= 72.
|
||||
image.writeUInt32LE(73, 0x200);
|
||||
} else {
|
||||
// Metadata remains in .text; a second section backs only the CLI prefix.
|
||||
image = Buffer.concat([original, original.subarray(0x200, 0x210)]);
|
||||
const coff = image.readUInt32LE(0x3c) + 4;
|
||||
const optional = coff + 20;
|
||||
const secondSection = optional + image.readUInt16LE(coff + 16) + 40;
|
||||
image.writeUInt16LE(2, coff + 2);
|
||||
image.writeUInt32LE(0x900000, optional + 96 + 14 * 8);
|
||||
image.write(".cli", secondSection, "ascii");
|
||||
image.writeUInt32LE(16, secondSection + 8);
|
||||
image.writeUInt32LE(0x900000, secondSection + 12);
|
||||
image.writeUInt32LE(16, secondSection + 16);
|
||||
image.writeUInt32LE(original.length, secondSection + 20);
|
||||
}
|
||||
expect(readManagedPeLayout(image)).toMatchObject({
|
||||
cli: null,
|
||||
cliIssue: {
|
||||
code:
|
||||
mode === "oversized-header"
|
||||
? "invalid-cli-header"
|
||||
: "invalid-directory",
|
||||
scope: "cli.header",
|
||||
},
|
||||
});
|
||||
const root = await createTestTempDirectory("rea-desktop-clr-prefix-");
|
||||
const path = "Game_Data/Managed/Assembly-CSharp.dll";
|
||||
await put(root, path, image);
|
||||
const { report } = await inspect(root);
|
||||
const file = report.files.find((row) => row.path === path);
|
||||
expect(file?.issues).toEqual([]);
|
||||
expect(file?.facts).toContainEqual({
|
||||
kind: "unity-assembly-reference",
|
||||
offset: expect.any(Number),
|
||||
value: {
|
||||
name: "UnityEngine.CoreModule",
|
||||
token: expect.any(String),
|
||||
managed_directory: "Game_Data/Managed",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "linux")(
|
||||
"keeps desktop sibling files with partial coverage for invalid UTF-8 names",
|
||||
async () => {
|
||||
|
||||
@@ -1840,3 +1840,20 @@ it.each([0n, FIXTURE_BINARY_BASE + 0x980n, FIXTURE_BINARY_BASE + 0x9b0n])(
|
||||
expect(bytes.readBigUInt64LE(0x900)).toBe(raw);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([183, 62])(
|
||||
"ignores R_NONE target and addend before relocation application (machine %s)",
|
||||
(machine) => {
|
||||
const image = createIl2cppBinaryFixture();
|
||||
const bytes = createElfFixture(image, {
|
||||
machine,
|
||||
unresolvedRelocations: [{ offset: 0x7fffffff, kind: 0 }],
|
||||
});
|
||||
const dynamic = Number(bytes.readBigUInt64LE(128));
|
||||
const relocationBytes = Number(bytes.readBigUInt64LE(dynamic + 24));
|
||||
const last = 0x1000 + relocationBytes - 24;
|
||||
bytes.writeBigUInt64LE(0xffffffffffffffffn, last);
|
||||
bytes.writeBigInt64LE(-0x8000000000000000n, last + 16);
|
||||
expect(link(bytes)).toEqual(link(createElfFixture(image, { machine })));
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user