/** * Just enough of the ELF format to describe a native file the packager is about to copy (ADR 0049): * which machine it was built for, which shared libraries and C library symbol versions it needs, and * which Node.js addon interface it exports. Read from the file's own bytes, never by running it or * shelling out to a system tool. * * Only 64-bit little-endian ELF is understood, because that is every target this packager produces. * Anything else that starts with the ELF magic is reported as unsupported rather than guessed at. */ export class ElfFormatError extends Error { constructor() { super("The file is not a 64-bit little-endian ELF object this packager can read."); this.name = "ElfFormatError"; } } /** `EM_X86_64` in the ELF machine field. */ export const ELF_MACHINE_X86_64 = 62; export interface ElfDescription { readonly machine: number; /** `DT_NEEDED` entries: the shared libraries the file links against. */ readonly needed: readonly string[]; /** Symbol version names the file requires, for example `GLIBC_2.28`. */ readonly requiredVersions: readonly string[]; /** Names of the dynamic symbols the file itself defines. */ readonly exported: ReadonlySet; /** The value a Node-API addon's `node_api_module_get_api_version_v1` returns, when it exports * that function and its body is the plain constant return every compiler emits for it. * `undefined` when the function is absent; `"unrecognized"` when its body is something else. */ readonly nodeApiModuleVersion: number | "unrecognized" | undefined; } const SHT_NOBITS = 8; const SHT_DYNSYM = 11; const SHT_DYNAMIC = 6; const SHT_GNU_VERNEED = 0x6ffffffe; const DT_NULL = 0; const DT_NEEDED = 1; export function isElf(data: Uint8Array): boolean { return ( data.length >= 4 && data[0] === 0x7f && data[1] === 0x45 && data[2] === 0x4c && data[3] === 0x46 ); } interface Section { readonly type: number; readonly address: number; readonly offset: number; readonly size: number; readonly link: number; readonly entrySize: number; } export function describeElf(input: Uint8Array): ElfDescription { const data = Buffer.from(input.buffer, input.byteOffset, input.byteLength); if (!isElf(data) || data.length < 64 || data[4] !== 2 || data[5] !== 1) { throw new ElfFormatError(); } const u16 = (offset: number): number => { if (offset < 0 || offset + 2 > data.length) throw new ElfFormatError(); return data.readUInt16LE(offset); }; const u32 = (offset: number): number => { if (offset < 0 || offset + 4 > data.length) throw new ElfFormatError(); return data.readUInt32LE(offset); }; const u64 = (offset: number): number => { if (offset < 0 || offset + 8 > data.length) throw new ElfFormatError(); const value = data.readBigUInt64LE(offset); if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new ElfFormatError(); return Number(value); }; const string = (tableOffset: number, tableSize: number, index: number): string => { if (index >= tableSize) throw new ElfFormatError(); const start = tableOffset + index; const end = data.indexOf(0, start); if (end === -1 || end > tableOffset + tableSize) throw new ElfFormatError(); return data.toString("latin1", start, end); }; const machine = u16(18); const sectionOffset = u64(40); const sectionEntrySize = u16(58); const sectionCount = u16(60); if (sectionEntrySize < 64 || sectionCount === 0) { throw new ElfFormatError(); } const sections: Section[] = []; for (let index = 0; index < sectionCount; index += 1) { const base = sectionOffset + index * sectionEntrySize; sections.push({ address: u64(base + 16), entrySize: u64(base + 56), link: u32(base + 40), offset: u64(base + 24), size: u64(base + 32), type: u32(base + 4), }); } const linkedStrings = (section: Section): { offset: number; size: number } => { const table = sections[section.link]; if (table === undefined || table.offset + table.size > data.length) throw new ElfFormatError(); return { offset: table.offset, size: table.size }; }; const needed: string[] = []; const requiredVersions: string[] = []; const exported = new Set(); let apiVersionAddress: number | undefined; for (const section of sections) { if (section.type === SHT_DYNAMIC) { const strings = linkedStrings(section); for (let entry = section.offset; entry + 16 <= section.offset + section.size; entry += 16) { const tag = u64(entry); if (tag === DT_NULL) break; if (tag === DT_NEEDED) needed.push(string(strings.offset, strings.size, u64(entry + 8))); } } else if (section.type === SHT_DYNSYM) { const strings = linkedStrings(section); const size = section.entrySize === 0 ? 24 : section.entrySize; for (let entry = section.offset; entry + 24 <= section.offset + section.size; entry += size) { const nameIndex = u32(entry); const sectionIndex = u16(entry + 6); if (nameIndex === 0 || sectionIndex === 0) continue; const name = string(strings.offset, strings.size, nameIndex); exported.add(name); if (name === "node_api_module_get_api_version_v1") { apiVersionAddress = u64(entry + 8); } } } else if (section.type === SHT_GNU_VERNEED) { const strings = linkedStrings(section); let need = section.offset; for (let guard = 0; guard < 4096; guard += 1) { const count = u16(need + 2); let aux = need + u32(need + 8); for (let index = 0; index < count; index += 1) { requiredVersions.push(string(strings.offset, strings.size, u32(aux + 8))); const next = u32(aux + 12); if (next === 0) break; aux += next; } const next = u32(need + 12); if (next === 0) break; need += next; } } } let nodeApiModuleVersion: number | "unrecognized" | undefined; if (apiVersionAddress !== undefined) { const text = sections.find( (section) => section.type !== SHT_NOBITS && section.address <= apiVersionAddress && apiVersionAddress < section.address + section.size, ); nodeApiModuleVersion = "unrecognized"; if (text !== undefined) { let offset = text.offset + (apiVersionAddress - text.address); // An optional `endbr64` (Intel CET), then `mov eax, imm32; ret`: what every compiler emits for // a function that returns the NAPI_VERSION constant. Anything else is not guessed at. if ( data[offset] === 0xf3 && data[offset + 1] === 0x0f && data[offset + 2] === 0x1e && data[offset + 3] === 0xfa ) { offset += 4; } if (data[offset] === 0xb8 && data[offset + 5] === 0xc3) { nodeApiModuleVersion = u32(offset + 1); } } } return Object.freeze({ exported, machine, needed: Object.freeze(needed), nodeApiModuleVersion, requiredVersions: Object.freeze(requiredVersions), }); }