fix(exports): preserve ELF name byte identity

This commit is contained in:
morluto committed 2026-10-27 12:17:29 +08:00
1 parent 1cd82597da
commit 14dd07e740
4 files changed
+199 -24

No files matched your search

+4 -19
View File
@@ -7,7 +7,6 @@ import {
readMappedCString,
} from "../metadata/BinaryContainer.js";
import { architectureForElfMachine } from "../metadata/ElfImage.js";
import { isUtf8 } from "node:buffer";
import { readElfStringBytes } from "../metadata/ElfStringTable.js";
import { readElfSymbol } from "../../domain/elfSymbol.js";
import {
@@ -17,6 +16,7 @@ import {
import { readPeImage } from "../metadata/PeImage.js";
import {
MAX_NATIVE_EXPORT_ROWS,
decodeElfExportName,
type ElfExport,
type NativeExportIssue,
type NativeExports,
@@ -304,22 +304,7 @@ const versionSectionData = (
};
/** Version names by index, read from `.gnu.version_d` and `.gnu.version_r`. */
type DecodedElfString = {
readonly name: string;
readonly name_bytes_base64: string | null;
readonly name_utf8_decodable: boolean;
};
const decodeElfString = (bytes: Buffer): DecodedElfString => {
const utf8 = isUtf8(bytes);
return {
name: bytes.toString(utf8 ? "utf8" : "latin1"),
name_bytes_base64: bytes.some((byte) => byte >= 0x80)
? bytes.toString("base64")
: null,
name_utf8_decodable: utf8,
};
};
type DecodedElfString = ReturnType<typeof decodeElfExportName>;
const readVersionNames = (
reader: BinaryReader,
@@ -331,7 +316,7 @@ const readVersionNames = (
// output rows. Replacing an index releases its previous decoded name.
let textBytes = 0;
const addName = (index: number, name: Buffer): void => {
const decoded = name.length === 0 ? null : decodeElfString(name);
const decoded = name.length === 0 ? null : decodeElfExportName(name);
const previous = namesByIndex.get(index);
const nextTextBytes =
textBytes -
@@ -641,7 +626,7 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
let rawNameBase64: string | null;
try {
const rawName = readElfStringBytes(names, symbol.name_offset);
const decodedName = decodeElfString(rawName);
const decodedName = decodeElfExportName(rawName);
nameUtf8Decodable = decodedName.name_utf8_decodable;
name = decodedName.name;
rawNameBase64 = decodedName.name_bytes_base64;
+109 -1
View File
@@ -118,7 +118,7 @@ it("qualifies projected names with exact encoding facts and normalizes historica
analysisViewRequestSchema.parse({
kind: "native-exports",
collection: "exports",
names: ["café"],
indices: [3],
fields: ["name"],
}),
);
@@ -131,6 +131,66 @@ it("qualifies projected names with exact encoding facts and normalizes historica
}
});
it("matches ELF UTF-8 identities without conflating Latin-1 display names or unknown encodings", () => {
const report = parseNativeExports(
elfExportImage({
symbols: [{ name: "café" }, { name: "café" }, { name: "same" }],
}),
artifact,
);
expect(
project(report, {
kind: "native-exports",
collection: "exports",
names: ["café"],
fields: ["name"],
}),
).toMatchObject({
items: [{ source_index: 0, name: "café", name_utf8_decodable: true }],
coverage: { total: 1 },
});
if (report.format !== "elf") throw new Error("Expected ELF fixture");
const historical = {
...report,
exports: report.exports.map(
({ name_bytes_base64: _bytes, name_utf8_decodable: _utf8, ...row }) =>
row,
),
};
expect(
project(historical, {
kind: "native-exports",
collection: "exports",
names: ["café"],
}),
).toMatchObject({
items: [],
unknowns: [expect.stringContaining("lack retained encoding facts")],
});
expect(
project(historical, {
kind: "native-exports",
collection: "exports",
names: [],
}),
).toMatchObject({ items: [], unknowns: [] });
const missingBytes = {
...report,
exports: report.exports.map((row) => ({ ...row, name_bytes_base64: null })),
};
expect(
project(missingBytes, {
kind: "native-exports",
collection: "exports",
names: ["café", "same"],
fields: [],
}),
).toMatchObject({
items: [{ source_index: 2 }],
unknowns: [expect.stringContaining("lack retained encoding facts")],
});
});
it("does not fill historical name defaults into a projection that omits names", () => {
const result = project(elf(), {
kind: "native-exports",
@@ -146,6 +206,54 @@ it("does not fill historical name defaults into a projection that omits names",
expect(view_digest).toBe(digestCanonicalValue(content, "Analysis view"));
});
it.each(["Y2Fmw6k", "Y2Fmw6l=", "not base64"])(
"rejects untrustworthy imported ELF name bytes %s before selection",
(encoded) => {
for (const target of ["symbol", "version"]) {
const report = elf();
const row = report.exports[0]!;
if (target === "symbol") row.name_bytes_base64 = encoded;
else if (typeof row.version === "object" && row.version !== null)
row.version.name_bytes_base64 = encoded;
const result = projectAnalysisView(
parent(report),
analysisViewRequestSchema.parse({
kind: "native-exports",
collection: "exports",
names: ["missing"],
}),
);
expect(result.ok).toBe(false);
}
},
);
it.each(["text", "encoding"])(
"rejects contradictory ELF %s facts at the source owner",
(fact) => {
for (const target of ["symbol", "version"]) {
const report = elf();
const row = report.exports[0]!;
const name = target === "symbol" ? row : row.version;
if (name === null || typeof name !== "object")
throw new Error("Expected named ELF row");
name.name_bytes_base64 = Buffer.from(name.name!).toString("base64");
if (fact === "text") name.name = "different";
else name.name_utf8_decodable = false;
expect(
projectAnalysisView(
parent(report),
analysisViewRequestSchema.parse({
kind: "native-exports",
collection: "exports",
names: ["missing"],
}),
).ok,
).toBe(false);
}
},
);
it("preserves PE ordinal-only, forwarder, alias and same-address distinctions", () => {
const report = parseNativeExports(
peExportImage({