fix(elf): follow loader version index precedence

VERDEF entries replace VERNEED names for shared indices, matching glibc lookup behavior.
This commit is contained in:
morluto committed 2026-10-23 17:26:11 +08:00
1 parent 446b284239
commit 642f6571ea
3 files changed
+77 -74

No files matched your search

+2 -1
View File
@@ -334,11 +334,12 @@ count and SHA-256.
`.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.
Definition names take precedence when both tables name the same index.
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 when no
unambiguous definition or requirement names the index, and `version` is
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`:
@@ -340,7 +340,7 @@ describe("ELF version requirements", () => {
});
});
it("keeps conflicting names for one version index unknown", () => {
it("uses a definition name when a requirement shares its index", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "ambiguous", version: 2 }],
@@ -350,10 +350,13 @@ describe("ELF version requirements", () => {
);
expect(result.exports).toMatchObject([
{ name: "ambiguous", version: { index: 2, name: null, hidden: false } },
{
name: "ambiguous",
version: { index: 2, name: "DEFINITION", hidden: false },
},
]);
expect(result.coverage.status).toBe("partial");
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
expect(result.coverage.status).toBe("complete");
expect(result.issues).toEqual([]);
});
it("retains exports and reports a truncated version-requirement chain", () => {
+68 -69
View File
@@ -290,76 +290,8 @@ const readVersionNames = (
): Map<number, string | null> => {
const namesByIndex = new Map<number, string | null>();
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);
namesByIndex.set(index, name || null);
};
const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF);
if (verdef !== undefined) {
try {
const { table, names } = versionSectionData(
reader,
sections,
verdef,
"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",
);
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),
);
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)}`,
});
}
}
const verneed = sections.find(({ type }) => type === SHT_GNU_VERNEED);
if (verneed !== undefined) {
try {
@@ -430,6 +362,73 @@ const readVersionNames = (
});
}
}
// GNU lookup installs requirement names first, then definition names.
const verdef = sections.find(({ type }) => type === SHT_GNU_VERDEF);
if (verdef !== undefined) {
try {
const { table, names } = versionSectionData(
reader,
sections,
verdef,
"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",
);
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),
);
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)}`,
});
}
}
return namesByIndex;
};