diff --git a/docs/binary-diagnostics.md b/docs/binary-diagnostics.md index 1e6ce267a1..53e10642ab 100644 --- a/docs/binary-diagnostics.md +++ b/docs/binary-diagnostics.md @@ -329,17 +329,19 @@ count and SHA-256. - ELF rows list defined `.dynsym` symbols with global, weak or GNU-unique binding and default or protected visibility. Each row keeps `value` (`st_value` as stored; bit 0 marks Thumb code on 32-bit ARM), `size`, `type` - and `binding`. `version` is null for an unversioned symbol, the - `.gnu.version_d` index, name and `hidden` flag for a versioned one, or - `"unknown"` when the version table cannot be read. A hidden version is not + and `binding`. `version` is null for an unversioned symbol. A versioned + symbol reports its GNU index, a name resolved from `.gnu.version_d` or + `.gnu.version_r`, and the `hidden` flag from `.gnu.version`. `"unknown"` + means the symbol-version table could not be read. A hidden version is not the default version that a by-name `dlsym` lookup selects. 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`: +Unresolved symbol versions keep their rows: the version name is null when no +unambiguous definition or requirement names the index, 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`. diff --git a/src/native/exports/NativeExportParser.test.ts b/src/native/exports/NativeExportParser.test.ts index 32b934a92b..c8d62d3832 100644 --- a/src/native/exports/NativeExportParser.test.ts +++ b/src/native/exports/NativeExportParser.test.ts @@ -282,6 +282,134 @@ describe("ELF dynamic symbol exports", () => { }); }); +describe("ELF version requirements", () => { + it("resolves defined copy-relocated symbols through version requirements", () => { + const result = parse( + elfExportImage({ + symbols: [ + { name: "stdout", type: 1, value: 0x28268n, version: 3 }, + { name: "stderr", type: 1, value: 0x282a0n, version: 0x8003 }, + ], + versionRequirements: [{ index: 3, name: "GLIBC_2.2.5" }], + }), + ); + + expect(result).toMatchObject({ + coverage: { status: "complete", exports: 2 }, + issues: [], + exports: [ + { + name: "stdout", + value: "0x28268", + version: { index: 3, name: "GLIBC_2.2.5", hidden: false }, + }, + { + name: "stderr", + value: "0x282a0", + version: { index: 3, name: "GLIBC_2.2.5", hidden: true }, + }, + ], + }); + }); + + it("keeps version definitions and requirements in the same symbol lookup", () => { + const result = parse( + elfExportImage({ + symbols: [ + { name: "defined_version", version: 2 }, + { name: "required_version", version: 4 }, + ], + versionDefinitions: ["libfixture.so", "FIXTURE_1"], + versionRequirements: [ + { index: 3, name: "GLIBC_2.2.5" }, + { index: 4, name: "GLIBC_2.3" }, + ], + }), + ); + + expect(result).toMatchObject({ + coverage: { status: "complete", exports: 2 }, + issues: [], + exports: [ + { name: "defined_version", version: { index: 2, name: "FIXTURE_1" } }, + { + name: "required_version", + version: { index: 4, name: "GLIBC_2.3" }, + }, + ], + }); + }); + + it("keeps conflicting names for one version index unknown", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "ambiguous", version: 2 }], + versionDefinitions: ["libfixture.so", "DEFINITION"], + versionRequirements: [{ index: 2, name: "REQUIREMENT" }], + }), + ); + + expect(result.exports).toMatchObject([ + { name: "ambiguous", version: { index: 2, name: null, hidden: false } }, + ]); + expect(result.coverage.status).toBe("partial"); + expect(result.issues).toMatchObject([{ code: "version_unresolved" }]); + }); + + it("retains exports and reports a truncated version-requirement chain", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "stdout", type: 1, version: 3 }], + versionRequirements: [{ index: 3, name: "GLIBC_2.2.5" }], + truncatedVersionRequirements: true, + }), + ); + + expect(result.exports).toMatchObject([ + { name: "stdout", version: { index: 3, name: null, hidden: false } }, + ]); + expect(result.coverage.status).toBe("partial"); + expect(result.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "version_unresolved", + message: expect.stringContaining( + "auxiliary chain leaves its section", + ), + }), + ]), + ); + }); + + it("marks a definition chain that ends before its declared count", () => { + const result = parse( + elfExportImage({ + symbols: [{ name: "defined_version", version: 2 }], + versionDefinitions: ["libfixture.so", "FIXTURE_1"], + truncatedVersionDefinitions: true, + }), + ); + + expect(result.exports).toMatchObject([ + { + name: "defined_version", + version: { index: 2, name: null, hidden: false }, + }, + ]); + expect(result.coverage.status).toBe("partial"); + expect(result.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "version_unresolved", + message: expect.stringContaining( + "chain ends before its declared count", + ), + }), + ]), + ); + }); +}); + describe("export inventory review regressions", () => { it("reads an export directory stored in the PE headers", () => { const result = parse( diff --git a/src/native/exports/NativeExportParser.ts b/src/native/exports/NativeExportParser.ts index a6bbede256..e127d45fc9 100644 --- a/src/native/exports/NativeExportParser.ts +++ b/src/native/exports/NativeExportParser.ts @@ -24,6 +24,7 @@ type Artifact = NativeExports["artifact"]; const SHT_STRTAB = 3; const SHT_DYNSYM = 11; const SHT_GNU_VERDEF = 0x6ffffffd; +const SHT_GNU_VERNEED = 0x6ffffffe; const SHT_GNU_VERSYM = 0x6fffffff; const PE_EXPORT_DIRECTORY = 0; const PE_EXPORT_DIRECTORY_BYTES = 40; @@ -251,67 +252,185 @@ const ELF_BINDINGS: Readonly> = { 10: "unique", }; -/** Version definitions by index, read from `.gnu.version_d`. */ -const readVersionDefinitions = ( +const versionSectionData = ( + reader: BinaryReader, + sections: readonly NativeSection[], + section: NativeSection, + label: string, +): { + table: { readonly offset: number; readonly size: number }; + names: Buffer; +} => { + const strings = sections[section.link]; + if (strings?.type !== SHT_STRTAB) + throw new BinaryFormatFailure( + "malformed", + `${label} do not link to a string table`, + ); + const stringRange = reader.fileRange( + strings.offset, + strings.size, + `${label} strings`, + ); + const table = reader.fileRange(section.offset, section.size, label); + return { + table, + names: reader.bytes.subarray( + stringRange.offset, + stringRange.offset + stringRange.size, + ), + }; +}; + +/** Version names by index, read from `.gnu.version_d` and `.gnu.version_r`. */ +const readVersionNames = ( reader: BinaryReader, sections: readonly NativeSection[], issues: NativeExportIssue[], -): Map => { - const definitions = new Map(); +): Map => { + const namesByIndex = new Map(); + const addName = (index: number, name: string): void => { + const previous = namesByIndex.get(index); + if (previous === undefined) namesByIndex.set(index, name || null); + else if (previous !== name) namesByIndex.set(index, null); + }; const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF); - if (verdef === undefined) return definitions; - try { - const strings = sections[verdef.link]; - if (strings?.type !== SHT_STRTAB) - throw new BinaryFormatFailure( - "malformed", - "Version definitions do not link to a string table", + if (verdef !== undefined) { + try { + const { table, names } = versionSectionData( + reader, + sections, + verdef, + "Version definitions", ); - const names = reader.fileRange( - strings.offset, - strings.size, - "Version strings", - ); - const nameBytes = reader.bytes.subarray( - names.offset, - names.offset + names.size, - ); - const table = reader.fileRange( - verdef.offset, - verdef.size, - "Version definitions", - ); - let offset = 0; - for (let index = 0; index < verdef.info; index++) { - if (offset + 20 > table.size) - throw new BinaryFormatFailure( - "malformed", - "Version definition chain leaves its section", + let offset = 0; + for (let index = 0; index < verdef.info; index++) { + if (offset + 20 > table.size) + throw new BinaryFormatFailure( + "malformed", + "Version definition chain leaves its section", + ); + const at = table.offset + offset; + const versionIndex = reader.u16(at + 4) & 0x7fff; + const aux = reader.u32(at + 12); + const next = reader.u32(at + 16); + const auxiliaryCount = reader.u16(at + 6); + if (auxiliaryCount === 0 || aux < 20 || offset + aux + 8 > table.size) + throw new BinaryFormatFailure( + "malformed", + "Version definition name entry leaves its section", + ); + let auxiliary = offset + aux; + const versionName = readElfString( + names, + reader.u32(table.offset + auxiliary), ); - const at = table.offset + offset; - const versionIndex = reader.u16(at + 4) & 0x7fff; - const aux = reader.u32(at + 12); - const next = reader.u32(at + 16); - if (aux < 20 || offset + aux + 8 > table.size) - throw new BinaryFormatFailure( - "malformed", - "Version definition name entry leaves its section", - ); - definitions.set( - versionIndex, - readElfString(nameBytes, reader.u32(at + aux)), - ); - if (next === 0) break; - offset += next; + addName(versionIndex, versionName); + for (let item = 0; item < auxiliaryCount; item++) { + if (auxiliary + 8 > table.size) + throw new BinaryFormatFailure( + "malformed", + "Version definition auxiliary chain leaves its section", + ); + if (item + 1 < auxiliaryCount) { + const auxiliaryNext = reader.u32(table.offset + auxiliary + 4); + if (auxiliaryNext < 8) + throw new BinaryFormatFailure( + "malformed", + "Version definition auxiliary chain ends before its declared count", + ); + auxiliary += auxiliaryNext; + } + } + if (index + 1 < verdef.info) { + if (next < 20) + throw new BinaryFormatFailure( + "malformed", + "Version definition chain ends before its declared count", + ); + offset += next; + } + } + } catch (cause) { + issues.push({ + code: "version_unresolved", + location: hex(verdef.offset), + message: `Version definitions: ${failureMessage(cause)}`, + }); } - } catch (cause) { - issues.push({ - code: "version_unresolved", - location: hex(verdef.offset), - message: `Version definitions: ${failureMessage(cause)}`, - }); } - return definitions; + + const verneed = sections.find(({ type }) => type === SHT_GNU_VERNEED); + if (verneed !== undefined) { + try { + const { table, names } = versionSectionData( + reader, + sections, + verneed, + "Version requirements", + ); + let offset = 0; + for (let record = 0; record < verneed.info; record++) { + if (offset + 16 > table.size) + throw new BinaryFormatFailure( + "malformed", + "Version requirement chain leaves its section", + ); + const at = table.offset + offset; + const auxiliaryCount = reader.u16(at + 2); + const auxiliaryOffset = reader.u32(at + 8); + const next = reader.u32(at + 12); + if (auxiliaryCount > 0 && auxiliaryOffset < 16) + throw new BinaryFormatFailure( + "malformed", + "Version requirement auxiliary chain starts inside its header", + ); + let auxiliary = offset + auxiliaryOffset; + for (let item = 0; item < auxiliaryCount; item++) { + if (auxiliary + 16 > table.size) + throw new BinaryFormatFailure( + "malformed", + "Version requirement auxiliary chain leaves its section", + ); + const auxAt = table.offset + auxiliary; + const versionIndex = reader.u16(auxAt + 6) & 0x7fff; + const auxiliaryNext = reader.u32(auxAt + 12); + try { + addName(versionIndex, readElfString(names, reader.u32(auxAt + 8))); + } catch (cause) { + issues.push({ + code: "version_unresolved", + location: hex(verneed.offset + BigInt(auxiliary)), + message: `Version requirement name: ${failureMessage(cause)}`, + }); + } + if (item + 1 < auxiliaryCount) { + if (auxiliaryNext < 16) + throw new BinaryFormatFailure( + "malformed", + "Version requirement auxiliary chain ends before its declared count", + ); + auxiliary += auxiliaryNext; + } + } + if (record + 1 < verneed.info) { + if (next < 16) + throw new BinaryFormatFailure( + "malformed", + "Version requirement chain ends before its declared count", + ); + offset += next; + } + } + } catch (cause) { + issues.push({ + code: "version_unresolved", + location: hex(verneed.offset), + message: `Version requirements: ${failureMessage(cause)}`, + }); + } + } + return namesByIndex; }; /** Read defined exported entries of `.dynsym` with their symbol versions. */ @@ -404,7 +523,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { }); } } - const definitions = readVersionDefinitions(reader, sections, issues); + const versionNames = readVersionNames(reader, sections, issues); const unresolvedVersions = new Set(); const retain = createRetention(); const exports: ElfExport[] = []; @@ -431,14 +550,13 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => { const versionIndex = raw & 0x7fff; if (versionIndex === 0) continue; if (versionIndex >= 2) { - const defined = definitions.get(versionIndex); - const name = defined === undefined || defined === "" ? null : defined; + const name = versionNames.get(versionIndex) ?? null; 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)}, has no named version definition`, + message: `Version index ${String(versionIndex)}, first used by symbol ${String(index)}, has no unambiguous version name`, }); } version = { diff --git a/tests/acceptance/applications/nativeExportsVersionRequirements.test.ts b/tests/acceptance/applications/nativeExportsVersionRequirements.test.ts new file mode 100644 index 0000000000..764b3dade4 --- /dev/null +++ b/tests/acceptance/applications/nativeExportsVersionRequirements.test.ts @@ -0,0 +1,44 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { nativeExportsSchema } from "../../../src/domain/native/nativeExports.js"; +import { elfExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const contract = toolContract("inspect_native_exports"); + +cliTest( + "reports defined ELF symbols named by the version-requirement table", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-elf-version-requirements-"); + const path = join(root, "copy-relocated-exports"); + const bytes = elfExportImage({ + symbols: [{ name: "stdout", type: 1, value: 0x28268n, version: 3 }], + versionRequirements: [{ index: 3, name: "GLIBC_2.2.5" }], + }); + await writeFile(path, bytes); + + const result = await cli.run({ + arguments: ["inspect-native-exports", path, "--json"], + }); + expect(result.exitCode).toBe(0); + const evidence = contract.outputSchema.parse(result.json); + expect(nativeExportsSchema.parse(evidence.normalized_result)).toMatchObject( + { + format: "elf", + coverage: { status: "complete", exports: 1 }, + issues: [], + exports: [ + { + name: "stdout", + value: "0x28268", + version: { index: 3, name: "GLIBC_2.2.5", hidden: false }, + }, + ], + }, + ); + }, + 30_000, +); diff --git a/tests/fixtures/binaryDiagnostics/nativeExports.ts b/tests/fixtures/binaryDiagnostics/nativeExports.ts index 7bada8dc7f..123a9e4501 100644 --- a/tests/fixtures/binaryDiagnostics/nativeExports.ts +++ b/tests/fixtures/binaryDiagnostics/nativeExports.ts @@ -122,9 +122,114 @@ export interface ElfFixtureSymbol { readonly nameOffset?: number; } +export interface ElfFixtureVersionRequirement { + readonly index: number; + readonly name: string; +} + +const elfStringHash = (value: string): number => { + let hash = 0; + for (const character of value) { + hash = (hash << 4) + character.charCodeAt(0); + const high = hash & 0xf0000000; + if (high !== 0) hash ^= high >>> 24; + hash &= ~high; + } + return hash >>> 0; +}; + +const writeVersionRequirementTable = (input: { + readonly offset: number; + readonly requirements: readonly ElfFixtureVersionRequirement[]; + readonly nameOffsets: readonly number[]; + readonly fileOffset: number; + readonly writeU16: (offset: number, value: number) => void; + readonly writeU32: (offset: number, value: number) => void; +}): void => { + const { offset, requirements, nameOffsets, fileOffset, writeU16, writeU32 } = + input; + if (requirements.length === 0) return; + writeU16(offset, 1); + writeU16(offset + 2, requirements.length); + writeU32(offset + 4, fileOffset); + writeU32(offset + 8, 16); + requirements.forEach((requirement, index) => { + const at = offset + 16 + index * 16; + writeU32(at, elfStringHash(requirement.name)); + writeU16(at + 6, requirement.index); + writeU32(at + 8, nameOffsets[index]!); + writeU32(at + 12, index + 1 < requirements.length ? 16 : 0); + }); +}; + +const writeVersionDefinitionTable = (input: { + readonly offset: number; + readonly definitions: readonly string[]; + readonly nameOffsets: readonly number[]; + readonly truncated: boolean; + readonly writeU16: (offset: number, value: number) => void; + readonly writeU32: (offset: number, value: number) => void; +}): void => { + const { offset, definitions, nameOffsets, truncated, writeU16, writeU32 } = + input; + definitions.forEach((_, index) => { + const at = offset + index * 28; + writeU16(at, 1); + writeU16(at + 2, index === 0 ? 1 : 0); + writeU16(at + 4, index + 1); + writeU16(at + 6, 1); + writeU32(at + 12, 20); + writeU32( + at + 16, + truncated && index === 0 ? 0 : index + 1 === definitions.length ? 0 : 28, + ); + writeU32(at + 20, nameOffsets[index]!); + }); +}; + +const writeElfSymbol = (input: { + readonly bytes: Buffer; + readonly symbol: ElfFixtureSymbol; + readonly index: number; + readonly at: number; + readonly wide: boolean; + readonly nameOffset: number; + readonly writeU16: (offset: number, value: number) => void; + readonly writeU32: (offset: number, value: number) => void; + readonly writeWord: (offset: number, value: bigint) => void; +}): void => { + const { + bytes, + symbol, + index, + at, + wide, + nameOffset, + writeU16, + writeU32, + writeWord, + } = input; + const info = ((symbol.binding ?? 1) << 4) | (symbol.type ?? 2); + const value = symbol.value ?? BigInt(0x1000 + index * 16); + writeU32(at, nameOffset); + if (wide) { + bytes[at + 4] = info; + bytes[at + 5] = symbol.visibility ?? 0; + writeU16(at + 6, symbol.section ?? 1); + writeWord(at + 8, value); + writeWord(at + 16, symbol.size ?? 16n); + } else { + writeWord(at + 4, value); + writeWord(at + 8, symbol.size ?? 16n); + bytes[at + 12] = info; + bytes[at + 13] = symbol.visibility ?? 0; + writeU16(at + 14, symbol.section ?? 1); + } +}; + /** - * Minimal ELF shared object with `.dynsym`, `.dynstr`, `.gnu.version` and - * `.gnu.version_d`, laid out in order. Version definitions are indexed from 1. + * Minimal ELF image with `.dynsym`, `.dynstr`, `.gnu.version`, definitions and + * requirements, laid out in order. */ export const elfExportImage = (options: { readonly bits?: 32 | 64; @@ -132,8 +237,13 @@ export const elfExportImage = (options: { readonly machine?: number; readonly symbols: readonly ElfFixtureSymbol[]; readonly versionDefinitions?: readonly string[]; + readonly versionRequirements?: readonly ElfFixtureVersionRequirement[]; + /** End the definition chain before its declared record count. */ + readonly truncatedVersionDefinitions?: boolean; /** Declare a `.gnu.version` table that is one entry short. */ readonly truncatedVersions?: boolean; + /** Shorten the GNU version-requirement section by one byte. */ + readonly truncatedVersionRequirements?: boolean; /** Point `.gnu.version` past the end of the file. */ readonly versionsOutsideFile?: boolean; /** Declare a zero-byte `.dynsym`, without its null entry. */ @@ -148,6 +258,7 @@ export const elfExportImage = (options: { const sectionWidth = wide ? 64 : 40; const symbols = [null, ...options.symbols]; const definitions = options.versionDefinitions ?? []; + const requirements = options.versionRequirements ?? []; const dynstr: string[] = ["\0"]; let dynstrBytes = 1; @@ -162,11 +273,13 @@ export const elfExportImage = (options: { return offset; }; const definitionNames = definitions.map(intern); + const requirementNames = requirements.map(({ name }) => intern(name)); + const requirementFile = requirements.length === 0 ? 0 : intern("libc.so.6"); 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"; + "\0.dynsym\0.dynstr\0.gnu.version\0.gnu.version_d\0.gnu.version_r\0.shstrtab\0"; let cursor = 0x100; const place = (size: number): number => { const offset = cursor; @@ -177,8 +290,11 @@ export const elfExportImage = (options: { const dynstrAt = place(dynstrBytes); const versymAt = place(symbols.length * 2); const verdefAt = place(definitions.length * 28); + const verneedAt = place( + requirements.length === 0 ? 0 : 16 + requirements.length * 16, + ); const shstrtabAt = place(shstrtab.length); - const sectionAt = place(6 * sectionWidth); + const sectionAt = place(7 * sectionWidth); const bytes = Buffer.alloc(cursor); const u16 = (offset: number, value: number) => @@ -204,39 +320,38 @@ export const elfExportImage = (options: { if (options.sectionHeaders === false) return bytes; 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 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, value); - word(at + 16, symbol.size ?? 16n); - } else { - word(at + 4, value); - word(at + 8, symbol.size ?? 16n); - bytes[at + 12] = info; - bytes[at + 13] = other; - u16(at + 14, symbol.section ?? 1); - } + if (symbol !== null) + writeElfSymbol({ + bytes, + symbol, + index, + at: dynsymAt + index * symbolWidth, + wide, + nameOffset: symbolNames[index]!, + writeU16: u16, + writeU32: u32, + writeWord: word, + }); }); Buffer.from(dynstr.join(""), "latin1").copy(bytes, dynstrAt); symbols.forEach((symbol, index) => u16(versymAt + index * 2, symbol?.version ?? (index === 0 ? 0 : 1)), ); - definitions.forEach((_, index) => { - const at = verdefAt + index * 28; - u16(at, 1); - u16(at + 2, index === 0 ? 1 : 0); - u16(at + 4, index + 1); - u16(at + 6, 1); - u32(at + 12, 20); - u32(at + 16, index === definitions.length - 1 ? 0 : 28); - u32(at + 20, definitionNames[index]!); + writeVersionDefinitionTable({ + offset: verdefAt, + definitions, + nameOffsets: definitionNames, + truncated: options.truncatedVersionDefinitions === true, + writeU16: u16, + writeU32: u32, + }); + writeVersionRequirementTable({ + offset: verneedAt, + requirements, + nameOffsets: requirementNames, + fileOffset: requirementFile, + writeU16: u16, + writeU32: u32, }); bytes.write(shstrtab, shstrtabAt, "latin1"); const sections = elfSectionRows(options, { @@ -249,6 +364,8 @@ export const elfExportImage = (options: { versym: symbols.length * 2, verdefAt, definitions: definitions.length, + verneedAt, + requirements: requirements.length, shstrtabAt, shstrtab: shstrtab.length, end: cursor, @@ -292,6 +409,8 @@ const elfSectionRows = ( readonly versym: number; readonly verdefAt: number; readonly definitions: number; + readonly verneedAt: number; + readonly requirements: number; readonly shstrtabAt: number; readonly shstrtab: number; readonly end: number; @@ -331,6 +450,16 @@ const elfSectionRows = ( layout.definitions, 0, ], + [ + 45, + 0x6ffffffe, + layout.verneedAt, + (layout.requirements === 0 ? 0 : 16 + layout.requirements * 16) - + (options.truncatedVersionRequirements === true ? 1 : 0), + 2, + layout.requirements === 0 ? 0 : 1, + 0, + ], ] satisfies ElfSectionRow[])), - [45, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0], + [60, 3, layout.shstrtabAt, layout.shstrtab, 0, 0, 0], ];