Files
rea/src/flutter/ElfBuildId.ts
T
morluto 75a757d8ef fix(elf): distinguish compressed note storage from decoded notes
Refuse compressed note sections with typed unsupported-target semantics before framing or decoding, preserving stored-range checks and physical descriptor-offset provenance. Flutter retains independent build IDs with partial coverage when compressed notes are skipped. Verify the existing shared alignment fix with prefixed notes across classes and table kinds.

Fixes #6869
Closes #6563
2026-10-24 02:34:35 +08:00

203 lines
6.5 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 { 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;
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 (!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 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 < 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 };
};