Files
reverse-engineering-anything/src/flutter/ElfBuildId.ts
T
RRFZ 572d0c2340 fix(flutter): honor ELF note alignment when reading build IDs (#6423)
* fix(flutter): honor ELF note alignment when reading build IDs

* test(flutter): key shared note ranges by effective alignment

Cover a program and section declaration that share one note range: differing effective alignments (8 and 4) must both be read, while declarations that both normalize to four bytes (1 and 4) still share one read. 24-byte build IDs pad identically at both alignments, so only the range key decides the read count.
2026-10-23 17:53:43 +08:00

187 lines
5.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 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 }
>();
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;
if (
bytes.readUInt32LE(base + (table.program ? 0 : 4)) !==
(table.program ? 4 : 7)
)
continue;
const offsetAt =
base + (table.program ? (h.is64 ? 8 : 4) : h.is64 ? 0x18 : 0x10);
const sizeAt =
base + (table.program ? (h.is64 ? 0x20 : 0x10) : h.is64 ? 0x20 : 0x14);
const offset = h.is64
? Number(bytes.readBigUInt64LE(offsetAt))
: bytes.readUInt32LE(offsetAt);
const size = h.is64
? Number(bytes.readBigUInt64LE(sizeAt))
: bytes.readUInt32LE(sizeAt);
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
size > totalBytes - offset
)
return failure("ELF note range is not file-backed");
if (size === 0) continue;
const alignmentAt =
base + (table.program ? (h.is64 ? 48 : 28) : h.is64 ? 48 : 32);
const alignment = elfNoteAlignment(
h.is64
? bytes.readBigUInt64LE(alignmentAt)
: bytes.readUInt32LE(alignmentAt),
);
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 < 4 || descsz > 64)
return failure(
"GNU build-id descriptor exceeds the 4–64 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 };
};