diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index 132d09b8ff..1e6ce267a1 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -334,13 +334,17 @@ count and SHA-256. `"unknown"` when the version table cannot be read. A hidden version is not the default version that a by-name `dlsym` lookup selects. -Unreadable names, out-of-range name ordinals, unreadable forwarders and -unresolved version indexes are listed in `issues`, the affected entries are -not reported, and coverage is `partial`. An ELF without section headers is -`unsupported`: enumerating dynamic symbols from the dynamic segment is not +Unreadable names, out-of-range name ordinals and unreadable forwarders are +listed in `issues`, their entries are not reported, and coverage is `partial`. +Unresolved symbol versions keep their rows: the version name is null for an +index no definition names, and `version` is `"unknown"` when the version table +cannot be read; coverage is likewise `partial`. A dynamic symbol table without +its null entry is malformed. An ELF without section headers is `unsupported`: +enumerating dynamic symbols from the dynamic segment is not implemented. Mach-O exports are reported by `inspect_macho`. `max_file_bytes` defaults to 256 MiB (maximum 512 MiB; CLI `--max-file-bytes`). -An export or dynamic symbol table above 262144 entries fails without partial -success. Export rows do not establish that a symbol is reachable, safe to call +Export directories in PE headers are read like those in sections. An export or +dynamic symbol table above 262144 entries, more than 262144 rows, or more than +16 MiB of retained names fails without partial success. Export rows do not establish that a symbol is reachable, safe to call or loaded at runtime. diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts index 4cc9920559..32b934a92b 100644 --- a/src/native/exports/NativeExportParser.test.ts +++ b/src/native/exports/NativeExportParser.test.ts @@ -282,6 +282,82 @@ describe("ELF dynamic symbol exports", () => { }); }); +describe("export inventory review regressions", () => { + it("reads an export directory stored in the PE headers", () => { + const result = parse( + peExportImage({ + inHeaders: true, + functions: [{ rva: 0x2000 }, { forwarder: "OTHER.Target" }], + names: [ + { name: "HeaderExport", index: 0 }, + { name: "HeaderForward", index: 1 }, + ], + }), + ); + + expect(result).toMatchObject({ + export_directory: { rva: 0x200, dll_name: "fixture.dll" }, + coverage: { status: "complete", exports: 2 }, + }); + expect(result.exports.map(({ name }) => name)).toEqual([ + "HeaderExport", + "HeaderForward", + ]); + }); + + it("bounds retained name text before storing rows", () => { + // 300 rows naming one 60000-byte string retain about 18 MB of text. + const long = "n".repeat(60000); + const bytes = elfExportImage({ + symbols: Array.from({ length: 300 }, () => ({ name: long })), + }); + + expect(bytes.length).toBeLessThan(100_000); + expect(() => parseNativeExports(bytes, artifact)).toThrow( + BinaryResourceFailure, + ); + }); + + it("keeps exports when the version table lies outside the file", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "a" }, { name: "b", version: 2 }], + versionDefinitions: ["lib.so", "V1"], + versionsOutsideFile: true, + }), + ); + + expect( + result.format === "elf" && result.exports.map(({ version }) => version), + ).toEqual(["unknown", "unknown"]); + expect(result.coverage.status).toBe("partial"); + }); + + it("reports an empty version name as unresolved", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "a", version: 2 }], + versionDefinitions: ["lib.so", ""], + }), + ); + + expect( + result.format === "elf" && result.exports.map(({ version }) => version), + ).toEqual([{ index: 2, name: null, hidden: false }]); + expect(result.issues).toMatchObject([{ code: "version_unresolved" }]); + expect(result.coverage.status).toBe("partial"); + }); + + it("rejects a dynamic symbol table without its null entry", () => { + expect(() => + parseNativeExports( + elfExportImage({ symbols: [], emptyDynamicSymbols: true }), + artifact, + ), + ).toThrow(BinaryFormatFailure); + }); +}); + it("refuses formats other than PE and ELF", () => { const macho = Buffer.alloc(64); macho.writeUInt32LE(0xfeedfacf, 0); diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts index f281535a14..a6bbede256 100644 --- a/src/native/exports/NativeExportParser.ts +++ b/src/native/exports/NativeExportParser.ts @@ -4,7 +4,7 @@ import { BinaryResourceFailure, mappedFileOffset, } from "../metadata/BinaryContainer.js"; -import { readElfImage } from "../metadata/ElfImage.js"; +import { architectureForElfMachine } from "../metadata/ElfImage.js"; import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js"; import { readNativeDirectoryFromBuffer, @@ -28,6 +28,25 @@ const SHT_GNU_VERSYM = 0x6fffffff; const PE_EXPORT_DIRECTORY = 0; const PE_EXPORT_DIRECTORY_BYTES = 40; const MAXIMUM_NAME_BYTES = 4096; +/** Text retained across all rows; names can repeat one long string table entry. */ +const MAXIMUM_RETAINED_TEXT_BYTES = 16 * 1024 * 1024; + +/** Charge every retained row and its text before it is stored. */ +const createRetention = () => { + let rows = 0; + let text = 0; + return (...strings: readonly (string | null)[]): void => { + rows++; + rowBudget(rows, "Export inventory"); + for (const value of strings) text += value?.length ?? 0; + if (text > MAXIMUM_RETAINED_TEXT_BYTES) + throw new BinaryResourceFailure( + "structure", + MAXIMUM_RETAINED_TEXT_BYTES, + `Export names exceed the ${String(MAXIMUM_RETAINED_TEXT_BYTES)}-byte retained-text allowance`, + ); + }; +}; const hex = (value: bigint | number) => `0x${value.toString(16)}`; @@ -88,13 +107,12 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => { ], }; const reader = new BinaryReader(bytes, true); + const mappings = + image.pe_header_mapping === undefined + ? image.mappings + : [image.pe_header_mapping, ...image.mappings]; const map = (rva: number, size: number): number => - mappedFileOffset( - image.mappings, - base + BigInt(rva), - size, - image.zero_fills, - ); + mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills); const cString = (rva: number): string => { const start = map(rva, 1); const end = bytes @@ -167,6 +185,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => { } const forwarderEnd = directory.virtual_address + directory.size; + const retain = createRetention(); const exports: PeExport[] = []; for (let index = 0; index < functionCount; index++) { const rva = reader.u32(functions + index * 4); @@ -188,11 +207,15 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => { } else target = { address: hex(base + BigInt(rva)), forwarder: null }; const exportNames = namesByFunction.get(index); if (exportNames !== undefined) - for (const name of exportNames) + for (const name of exportNames) { + retain(name, target.forwarder); exports.push({ name, ordinal, ...target }); + } // A slot whose only name was unreadable is reported through its issue. - else if (!namedFunctions.has(index)) + else if (!namedFunctions.has(index)) { + retain(target.forwarder); exports.push({ name: null, ordinal, ...target }); + } } return { ...result, @@ -293,15 +316,14 @@ const readVersionDefinitions = ( /** Read defined exported entries of `.dynsym` with their symbol versions. */ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { - const image = readElfImage(bytes); const directory = readNativeDirectoryFromBuffer(bytes, "elf"); const reader = new BinaryReader(bytes, directory.little); const result = { artifact, format: "elf" as const, - architecture: image.architecture, - bits: image.bits, - byte_order: image.byte_order, + architecture: architectureForElfMachine(directory.machine), + bits: directory.bits, + byte_order: directory.little ? ("little" as const) : ("big" as const), }; const { sections } = directory; if (sections.length === 0) @@ -335,11 +357,13 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { if ( dynsym.entry_size !== BigInt(width) || dynsym.size % BigInt(width) !== 0n || + // The table must hold at least its mandatory null symbol. + dynsym.size < BigInt(width) || strings?.type !== SHT_STRTAB ) throw new BinaryFormatFailure( "malformed", - "ELF dynamic symbol table entry size, length or string-table link is invalid", + "ELF dynamic symbol table entry size, length, null entry or string-table link is invalid", ); const count = Number(dynsym.size / BigInt(width)); rowBudget(count, "ELF dynamic symbol table"); @@ -364,17 +388,25 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { ); let versions: number | null = null; if (versym !== undefined) { - if (versym.size === BigInt(count * 2)) + try { + if (versym.size !== BigInt(count * 2)) + throw new BinaryFormatFailure( + "malformed", + `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`, + ); versions = reader.table(versym.offset, count, 2, "ELF symbol versions"); - else + } catch (cause) { + // Keep every export; their versions become unknown. issues.push({ code: "version_unresolved", location: hex(versym.offset), - message: `Symbol version table has ${String(versym.size)} bytes for ${String(count)} symbols`, + message: failureMessage(cause), }); + } } const definitions = readVersionDefinitions(reader, sections, issues); const unresolvedVersions = new Set(); + const retain = createRetention(); const exports: ElfExport[] = []; for (let index = 1; index < count; index++) { const at = table + index * width; @@ -399,18 +431,19 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { const versionIndex = raw & 0x7fff; if (versionIndex === 0) continue; if (versionIndex >= 2) { - const name = definitions.get(versionIndex) ?? null; + const defined = definitions.get(versionIndex); + const name = defined === undefined || defined === "" ? null : defined; if (name === null && !unresolvedVersions.has(versionIndex)) { unresolvedVersions.add(versionIndex); issues.push({ code: "version_unresolved", location: hex(symbol.value), - message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, is named by no version definition`, + message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, has no named version definition`, }); } version = { index: versionIndex, - name: name === "" ? null : name, + name, hidden: (raw & 0x8000) !== 0, }; } @@ -434,6 +467,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { }); continue; } + retain(name, typeof version === "object" ? (version?.name ?? null) : null); exports.push({ name, value: hex(symbol.value), diff --git a/src/native/metadata/BinaryContainer.ts b/src/native/metadata/BinaryContainer.ts index e00e44ddaa..fc063529c9 100644 --- a/src/native/metadata/BinaryContainer.ts +++ b/src/native/metadata/BinaryContainer.ts @@ -95,6 +95,11 @@ export interface BinaryContainer { readonly byte_order: "little" | "big"; /** Declared PE preferred base, when available. */ readonly image_base?: bigint; + /** + * File-backed PE headers (`SizeOfHeaders`, clipped to the file), which the + * loader maps at the image base. Kept apart from section `mappings`. + */ + readonly pe_header_mapping?: FileMapping; /** PE optional-header data directories, indexed as declared. */ readonly pe_data_directories?: readonly { readonly virtual_address: number; diff --git a/src/native/metadata/ElfImage.ts b/src/native/metadata/ElfImage.ts index 86a5251c80..1fba9c8277 100644 --- a/src/native/metadata/ElfImage.ts +++ b/src/native/metadata/ElfImage.ts @@ -104,7 +104,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => { }; /** Map ELF machine identifiers to stable architecture names. */ -const architectureForElfMachine = (machine: number): string => { +export const architectureForElfMachine = (machine: number): string => { const names = new Map([ [3, "386"], [8, "mips"], diff --git a/src/native/metadata/PeImage.ts b/src/native/metadata/PeImage.ts index 60ab3eab72..bf36e6954c 100644 --- a/src/native/metadata/PeImage.ts +++ b/src/native/metadata/PeImage.ts @@ -87,6 +87,11 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { return { virtual_address: reader.u32(entry), size: reader.u32(entry + 4) }; }); const imageBase = reader.word(optional + (bits === 64 ? 24 : 28), bits); + const headerMapping: FileMapping = { + address: imageBase, + offset: 0, + size: Math.min(reader.u32(optional + 60), bytes.length), + }; const count = reader.u16(pe + 6); const start = reader.table( BigInt(optional + optionalSize), @@ -157,6 +162,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { format: "pe", image_base: imageBase, pe_data_directories: dataDirectories, + pe_header_mapping: headerMapping, architecture: names.get(machine) ?? `pe-machine-${machine}`, bits, byte_order: "little", diff --git a/tests/fixtures/binaryDiagnostics/nativeExports.ts b/tests/fixtures/binaryDiagnostics/nativeExports.ts index 08e3118313..7bada8dc7f 100644 --- a/tests/fixtures/binaryDiagnostics/nativeExports.ts +++ b/tests/fixtures/binaryDiagnostics/nativeExports.ts @@ -7,6 +7,8 @@ export type PeFixtureFunction = const SECTION_RVA = 0x1000; const SECTION_OFFSET = 0x400; const SECTION_BYTES = 0x400; +/** SizeOfHeaders; header RVAs map to the same file offsets. */ +const HEADER_BYTES = 0x400; /** * Minimal PE32/PE32+ image whose only section holds an export directory at @@ -24,6 +26,8 @@ export const peExportImage = (options: { }; readonly exportDirectory?: boolean; readonly declaredFunctions?: number; + /** Store the export directory and its tables in the PE headers. */ + readonly inHeaders?: boolean; }): Buffer => { const bits = options.bits ?? 64; const bytes = Buffer.alloc(SECTION_OFFSET + SECTION_BYTES); @@ -41,6 +45,7 @@ export const peExportImage = (options: { if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24); else bytes.writeUInt32LE(0x10000000, optional + 28); bytes.writeUInt32LE(16, optional + directoriesAt - 4); + bytes.writeUInt32LE(HEADER_BYTES, optional + 60); const section = optional + optionalSize; bytes.write(".edata", section, "ascii"); bytes.writeUInt32LE(SECTION_BYTES, section + 8); @@ -50,16 +55,18 @@ export const peExportImage = (options: { bytes.writeUInt32LE(0x40000040, section + 36); if (options.exportDirectory === false) return bytes; - const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET; + const at = (rva: number) => + rva < HEADER_BYTES ? rva : rva - SECTION_RVA + SECTION_OFFSET; + const base = options.inHeaders === true ? 0x200 : SECTION_RVA; const functions = options.functions ?? []; const names = options.names ?? []; // Directory, address table, name table, ordinals, then strings; forwarders // stay inside the 0x200-byte declared directory range. - const eat = SECTION_RVA + 0x40; - const nameTable = SECTION_RVA + 0x80; - const ordinalTable = SECTION_RVA + 0xc0; - let forwarderText = SECTION_RVA + 0x100; - let text = SECTION_RVA + 0x200; + const eat = base + 0x40; + const nameTable = base + 0x80; + const ordinalTable = base + 0xc0; + let forwarderText = base + 0x100; + let text = base + (options.inHeaders === true ? 0x180 : 0x200); const string = (value: string, inside: boolean): number => { const rva = inside ? forwarderText : text; bytes.write(`${value}\0`, at(rva), "latin1"); @@ -67,9 +74,9 @@ export const peExportImage = (options: { else text += value.length + 1; return rva; }; - bytes.writeUInt32LE(SECTION_RVA, optional + directoriesAt); + bytes.writeUInt32LE(base, optional + directoriesAt); bytes.writeUInt32LE(0x200, optional + directoriesAt + 4); - const directory = at(SECTION_RVA); + const directory = at(base); bytes.writeUInt32LE(0x5f5e0ff, directory + 4); bytes.writeUInt32LE(string("fixture.dll", false), directory + 12); bytes.writeUInt32LE(options.ordinalBase ?? 1, directory + 16); @@ -117,7 +124,7 @@ export interface ElfFixtureSymbol { /** * Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and - * `.gnu.version_d`. Version definitions are indexed from 1 in order. + * `.gnu.version_d`, laid out in order. Version definitions are indexed from 1. */ export const elfExportImage = (options: { readonly bits?: 32 | 64; @@ -127,13 +134,53 @@ export const elfExportImage = (options: { readonly versionDefinitions?: readonly string[]; /** Declare a `.gnu.version` table that is one entry short. */ readonly truncatedVersions?: boolean; + /** Point `.gnu.version` past the end of the file. */ + readonly versionsOutsideFile?: boolean; + /** Declare a zero-byte `.dynsym`, without its null entry. */ + readonly emptyDynamicSymbols?: boolean; readonly sectionHeaders?: boolean; readonly dynamicSymbols?: boolean; }): Buffer => { const bits = options.bits ?? 64; const little = options.little ?? true; const wide = bits === 64; - const bytes = Buffer.alloc(0x1000); + const symbolWidth = wide ? 24 : 16; + const sectionWidth = wide ? 64 : 40; + const symbols = [null, ...options.symbols]; + const definitions = options.versionDefinitions ?? []; + + const dynstr: string[] = ["\0"]; + let dynstrBytes = 1; + const strings = new Map(); + const intern = (value: string): number => { + const known = strings.get(value); + if (known !== undefined) return known; + const offset = dynstrBytes; + strings.set(value, offset); + dynstr.push(`${value}\0`); + dynstrBytes += value.length + 1; + return offset; + }; + const definitionNames = definitions.map(intern); + const symbolNames = symbols.map((symbol) => + symbol === null ? 0 : (symbol.nameOffset ?? intern(symbol.name)), + ); + const shstrtab = + "\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0"; + let cursor = 0x100; + const place = (size: number): number => { + const offset = cursor; + cursor = Math.ceil((cursor + size) / 16) * 16; + return offset; + }; + const dynsymAt = place(symbols.length * symbolWidth); + const dynstrAt = place(dynstrBytes); + const versymAt = place(symbols.length * 2); + const verdefAt = place(definitions.length * 28); + const shstrtabAt = place(shstrtab.length); + const sectionAt = place(6 * sectionWidth); + const bytes = Buffer.alloc(cursor); + const u16 = (offset: number, value: number) => little ? bytes.writeUInt16LE(value, offset) @@ -151,53 +198,36 @@ export const elfExportImage = (options: { u16(16, 3); u16(18, options.machine ?? (wide ? 62 : 40)); u32(20, 1); - const headerSize = wide ? 64 : 52; - u16(wide ? 52 : 40, headerSize); + u16(wide ? 52 : 40, wide ? 64 : 52); u16(wide ? 54 : 42, wide ? 56 : 32); - u16(wide ? 58 : 46, wide ? 64 : 40); + u16(wide ? 58 : 46, sectionWidth); if (options.sectionHeaders === false) return bytes; - const dynstr = ["\0"]; - let dynstrBytes = 1; - const intern = (value: string): number => { - const offset = dynstrBytes; - dynstr.push(`${value}\0`); - dynstrBytes += value.length + 1; - return offset; - }; - const definitions = options.versionDefinitions ?? []; - const definitionNames = definitions.map(intern); - const symbolWidth = wide ? 24 : 16; - const symbols = [null, ...options.symbols]; - const dynsymAt = 0x200; symbols.forEach((symbol, index) => { if (symbol === null) return; const at = dynsymAt + index * symbolWidth; const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2); const other = symbol.visibility ?? 0; - const name = symbol.nameOffset ?? intern(symbol.name); - u32(at, name); + const value = symbol.value ?? BigInt(0x1000 + index * 16); + u32(at, symbolNames[index]!); if (wide) { bytes[at + 4] = info; bytes[at + 5] = other; u16(at + 6, symbol.section ?? 1); - word(at + 8, symbol.value ?? BigInt(0x1000 + index * 16)); + word(at + 8, value); word(at + 16, symbol.size ?? 16n); } else { - word(at + 4, symbol.value ?? BigInt(0x1000 + index * 16)); + word(at + 4, value); word(at + 8, symbol.size ?? 16n); bytes[at + 12] = info; bytes[at + 13] = other; u16(at + 14, symbol.section ?? 1); } }); - const dynstrAt = 0x600; Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt); - const versymAt = 0x900; symbols.forEach((symbol, index) => u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)), ); - const verdefAt = 0xa00; definitions.forEach((_, index) => { const at = verdefAt + index * 28; u16(at, 1); @@ -208,49 +238,21 @@ export const elfExportImage = (options: { u32(at + 16, index === definitions.length - 1 ? 0 : 28); u32(at + 20, definitionNames[index]!); }); - const shstrtab = - "\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.shstrtab\0"; - const shstrtabAt = 0xc00; bytes.write(shstrtab, shstrtabAt, "latin1"); - const sections: (readonly [ - number, - number, - number, - number, - number, - number, - number, - ])[] = [ - // name, type, offset, size, link, info, entsize - [0, 0, 0, 0, 0, 0, 0], - ...(options.dynamicSymbols === false - ? [] - : ([ - [1, 11, dynsymAt, symbols.length * symbolWidth, 2, 1, symbolWidth], - [9, 3, dynstrAt, dynstrBytes, 0, 0, 0], - [ - 17, - 0x6fffffff, - versymAt, - (symbols.length - (options.truncatedVersions === true ? 1 : 0)) * 2, - 1, - 0, - 2, - ], - [ - 30, - 0x6ffffffd, - verdefAt, - definitions.length * 28, - 2, - definitions.length, - 0, - ], - ] as const)), - [45, 3, shstrtabAt, shstrtab.length, 0, 0, 0], - ]; - const sectionAt = 0xd00; - const sectionWidth = wide ? 64 : 40; + const sections = elfSectionRows(options, { + dynsymAt, + dynsym: symbols.length * symbolWidth, + symbolWidth, + dynstrAt, + dynstrBytes, + versymAt, + versym: symbols.length * 2, + verdefAt, + definitions: definitions.length, + shstrtabAt, + shstrtab: shstrtab.length, + end: cursor, + }); sections.forEach(([name, type, offset, size, link, info, entsize], index) => { const at = sectionAt + index * sectionWidth; u32(at, name); @@ -266,3 +268,69 @@ export const elfExportImage = (options: { u16(wide ? 62 : 50, sections.length - 1); return bytes; }; + +type ElfSectionRow = readonly [ + name: number, + type: number, + offset: number, + size: number, + link: number, + info: number, + entsize: number, +]; + +/** Section header rows for the fixture layout, with the requested defects. */ +const elfSectionRows = ( + options: Parameters[0], + layout: { + readonly dynsymAt: number; + readonly dynsym: number; + readonly symbolWidth: number; + readonly dynstrAt: number; + readonly dynstrBytes: number; + readonly versymAt: number; + readonly versym: number; + readonly verdefAt: number; + readonly definitions: number; + readonly shstrtabAt: number; + readonly shstrtab: number; + readonly end: number; + }, +): readonly ElfSectionRow[] => [ + [0, 0, 0, 0, 0, 0, 0], + ...(options.dynamicSymbols === false + ? [] + : ([ + [ + 1, + 11, + layout.dynsymAt, + options.emptyDynamicSymbols === true ? 0 : layout.dynsym, + 2, + 1, + layout.symbolWidth, + ], + [9, 3, layout.dynstrAt, layout.dynstrBytes, 0, 0, 0], + [ + 17, + 0x6fffffff, + options.versionsOutsideFile === true + ? layout.end + 64 + : layout.versymAt, + layout.versym - (options.truncatedVersions === true ? 2 : 0), + 1, + 0, + 2, + ], + [ + 30, + 0x6ffffffd, + layout.verdefAt, + layout.definitions * 28, + 2, + layout.definitions, + 0, + ], + ] satisfies ElfSectionRow[])), + [45, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0], +];