Files
reverse-engineering-anything/src/flutter/ElfBuildId.ts
T
morluto 9341b116ef refactor: consolidate binary artifact interpretation and fixture ownership
Share exact ELF program-header facts across nine consumers while preserving their admission and precision policies. Centralize conformance path and digest binding, retain verifier-specific checks, and route fixture-owner changes to CI qualification.
2026-10-26 16:21:11 +08:00

215 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readElfProgramHeader } from "../domain/elfProgramHeader.js";
import { MAX_FLUTTER_BUILD_ID_BYTES } from "../domain/flutter/flutterBuildAnalysis.js";
import type { FileHandle } from "node:fs/promises";
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
import {
elfNoteAlignment,
elfNoteLayout,
} from "../domain/native/elfNoteLayout.js";
import {
readElfHeader,
ELF_HEADER_MAX_BYTES,
type ElfParseFailure,
} from "./DartElf.js";
const MAX_METADATA_BYTES = 8 * 1024 * 1024;
const MAX_BUILD_IDS = 64;
// Range bookkeeping and separate positional reads need a budget independent
// of payload bytes: tiny/overlapping declarations can otherwise fan out I/O.
const MAX_NOTE_RANGES = 1024;
export interface ElfBuildIds {
readonly candidates: readonly string[];
readonly failures: readonly ElfParseFailure[];
}
/** Read only ELF-declared PT_NOTE/SHT_NOTE ranges from one owned engine image. */
export const readElfBuildIds = async (
file: FileHandle,
totalBytes: number,
signal: AbortSignal | undefined,
): Promise<ElfBuildIds> => {
const candidates = new Set<string>();
const failures: ElfParseFailure[] = [];
const failure = (reason: string): ElfBuildIds => ({
candidates: [...candidates],
failures: [...failures, { reason }],
});
let examined = 0;
const read = async (offset: number, size: number): Promise<Buffer | null> => {
signal?.throwIfAborted();
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
offset < 0 ||
size < 0 ||
size > totalBytes - offset ||
size > MAX_METADATA_BYTES - examined
)
return null;
examined += size;
const bytes = Buffer.alloc(size);
let copied = 0;
while (copied < size) {
const result = await file.read(
bytes,
copied,
size - copied,
offset + copied,
);
signal?.throwIfAborted();
if (result.bytesRead === 0) return null;
copied += result.bytesRead;
}
return bytes;
};
const prefix = await read(0, Math.min(totalBytes, ELF_HEADER_MAX_BYTES));
if (prefix === null) return failure("ELF header could not be read");
const parsed = readElfHeader(prefix, totalBytes);
if (parsed.failure !== undefined) return failure(parsed.failure.reason);
const h = parsed.header;
const ranges = new Map<
string,
{ offset: number; size: number; alignment: number }
>();
let compressedNotes = false;
tables: for (const table of [
{
offset: h.e_phoff,
width: h.e_phentsize,
count: h.e_phnum,
program: true,
},
{
offset: h.e_shoff,
width: h.e_shentsize,
count: h.e_shnum,
program: false,
},
]) {
const bytes = await read(table.offset, table.width * table.count);
if (bytes === null)
return failure(
"ELF note tables exceed the 8 MiB metadata budget or are truncated",
);
for (let index = 0; index < table.count; index += 1) {
if (index % 4096 === 0) {
await yieldToEventLoop();
signal?.throwIfAborted();
}
const base = index * table.width;
const parsedProgram = table.program
? readElfProgramHeader(
bytes.subarray(base, base + table.width),
h.is64 ? 64 : 32,
true,
)
: undefined;
if (parsedProgram !== undefined && !parsedProgram.ok)
return failure(parsedProgram.error.message);
const program = parsedProgram?.value;
if (
(program?.type ?? bytes.readUInt32LE(base + 4)) !==
(table.program ? 4 : 7)
)
continue;
const offset =
program !== undefined
? Number(program.offset)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x18))
: bytes.readUInt32LE(base + 0x10);
const size =
program !== undefined
? Number(program.fileSize)
: h.is64
? Number(bytes.readBigUInt64LE(base + 0x20))
: bytes.readUInt32LE(base + 0x14);
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
size > totalBytes - offset
)
return failure("ELF note range is not file-backed");
if (!table.program) {
const flags = h.is64
? bytes.readBigUInt64LE(base + 8)
: BigInt(bytes.readUInt32LE(base + 8));
if ((flags & 0x800n) !== 0n) {
if (!compressedNotes) {
failures.push({
reason: "Compressed ELF note sections are unsupported",
});
compressedNotes = true;
}
continue;
}
}
if (size === 0) continue;
const alignment = elfNoteAlignment(
program?.alignment ??
(h.is64
? bytes.readBigUInt64LE(base + 48)
: bytes.readUInt32LE(base + 32)),
);
const key = `${offset}:${size}:${alignment}`;
if (ranges.has(key)) continue;
if (ranges.size === MAX_NOTE_RANGES) {
failures.push({
reason:
"ELF notes exceed the 1024-range note-range inventory/read budget",
});
break tables;
}
ranges.set(key, { offset, size, alignment });
}
}
for (const range of ranges.values()) {
const notes = await read(range.offset, range.size);
if (notes === null)
return failure(
"ELF notes exceed the 8 MiB metadata budget or are truncated",
);
let yieldedAt = 0;
for (let at = 0; at < notes.length;) {
if (at - yieldedAt >= 64 * 1024) {
await yieldToEventLoop();
signal?.throwIfAborted();
yieldedAt = at;
}
if (notes.length - at < 12) {
if (notes.subarray(at).some((byte) => byte !== 0))
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 layout = elfNoteLayout(namesz, descsz, range.alignment);
const descriptor = at + layout.descriptor;
const end = descriptor + descsz;
if (end > notes.length)
return failure("ELF note descriptor exceeds its declared note range");
if (
namesz === 4 &&
type === 3 &&
notes.subarray(at + 12, at + 16).equals(Buffer.from("GNU\0"))
) {
if (descsz < 1 || descsz > MAX_FLUTTER_BUILD_ID_BYTES)
return failure(
`GNU build-id descriptor is outside the 1–${MAX_FLUTTER_BUILD_ID_BYTES} byte projection`,
);
const candidate = notes.subarray(descriptor, end).toString("hex");
if (!candidates.has(candidate) && candidates.size === MAX_BUILD_IDS)
return failure(
"ELF build-id candidates exceed the 64-value evidence budget",
);
candidates.add(candidate);
}
at += layout.next;
}
}
signal?.throwIfAborted();
return { candidates: [...candidates], failures };
};