mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
1 parent
5d0d243a45
commit
22df16186f
6 files changed
+104
-5
No files matched your search
@@ -332,7 +332,14 @@ 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. A versioned
|
||||
and `binding`. ELF string tables do not declare a text encoding. Names that
|
||||
contain non-ASCII bytes are decoded as UTF-8 when those bytes form valid
|
||||
UTF-8; `name_utf8_decodable` records whether the non-ASCII name bytes are
|
||||
valid UTF-8 and `name_bytes_base64` retains those exact bytes (`null` for
|
||||
ASCII names). When they are not valid UTF-8, `name` uses a byte-preserving
|
||||
Latin-1 mapping and the raw bytes remain available. Older retained results
|
||||
report null for the newly added encoding fields. `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
|
||||
|
||||
@@ -49,6 +49,10 @@ const peExportSchema = z.strictObject({
|
||||
*/
|
||||
const elfExportSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
/** Exact non-ASCII name bytes; ELF string tables do not declare text encoding. */
|
||||
name_bytes_base64: z.string().nullable().default(null),
|
||||
/** Whether `name` was decoded as UTF-8; null means this field was not recorded in older Evidence. */
|
||||
name_utf8_decodable: z.boolean().nullable().default(null),
|
||||
/** `st_value` as stored; on 32-bit ARM, bit 0 of a function marks Thumb code. */
|
||||
value: address,
|
||||
size: z.string().regex(/^[0-9]+$/u),
|
||||
|
||||
@@ -518,6 +518,30 @@ describe("export inventory review regressions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ELF symbol-name text", () => {
|
||||
it("decodes UTF-8 names and retains exact bytes", () => {
|
||||
const result = parse(elfExportImage({ symbols: [{ name: "café" }] }));
|
||||
expect(result.format).toBe("elf");
|
||||
if (result.format !== "elf") throw new TypeError("expected ELF result");
|
||||
expect(result.exports[0]).toMatchObject({
|
||||
name: "café",
|
||||
name_bytes_base64: "Y2Fmw6k=",
|
||||
name_utf8_decodable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains invalid UTF-8 bytes without replacement", () => {
|
||||
const result = parse(elfExportImage({ symbols: [{ name: "\u00ff" }] }));
|
||||
expect(result.format).toBe("elf");
|
||||
if (result.format !== "elf") throw new TypeError("expected ELF result");
|
||||
expect(result.exports[0]).toMatchObject({
|
||||
name: "ÿ",
|
||||
name_bytes_base64: "/w==",
|
||||
name_utf8_decodable: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses formats other than PE and ELF", () => {
|
||||
const macho = Buffer.alloc(64);
|
||||
macho.writeUInt32LE(0xfeedfacf, 0);
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
mappedFileOffset,
|
||||
} from "../metadata/BinaryContainer.js";
|
||||
import { architectureForElfMachine } from "../metadata/ElfImage.js";
|
||||
import { readElfString, readElfSymbol } from "../metadata/ElfSymbol.js";
|
||||
import { isUtf8 } from "node:buffer";
|
||||
import {
|
||||
readElfString,
|
||||
readElfStringBytes,
|
||||
readElfSymbol,
|
||||
} from "../metadata/ElfSymbol.js";
|
||||
import {
|
||||
readNativeDirectoryFromBuffer,
|
||||
type NativeSection,
|
||||
@@ -561,8 +566,12 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
}
|
||||
}
|
||||
let name: string;
|
||||
let rawName: Buffer;
|
||||
let nameUtf8Decodable: boolean;
|
||||
try {
|
||||
name = readElfString(names, symbol.name_offset);
|
||||
rawName = readElfStringBytes(names, symbol.name_offset);
|
||||
nameUtf8Decodable = isUtf8(rawName);
|
||||
name = rawName.toString(nameUtf8Decodable ? "utf8" : "latin1");
|
||||
} catch (cause) {
|
||||
issues.push({
|
||||
code: "name_unreadable",
|
||||
@@ -579,9 +588,18 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
retain(name, typeof version === "object" ? (version?.name ?? null) : null);
|
||||
const rawNameBase64 = rawName.some((byte) => byte >= 0x80)
|
||||
? rawName.toString("base64")
|
||||
: null;
|
||||
retain(
|
||||
name,
|
||||
typeof version === "object" ? (version?.name ?? null) : null,
|
||||
rawNameBase64,
|
||||
);
|
||||
exports.push({
|
||||
name,
|
||||
name_bytes_base64: rawNameBase64,
|
||||
name_utf8_decodable: nameUtf8Decodable,
|
||||
value: hex(symbol.value),
|
||||
size: symbol.size.toString(),
|
||||
type: ELF_TYPES[symbol.type] ?? "other",
|
||||
|
||||
@@ -19,6 +19,11 @@ export const readElfSymbol = (
|
||||
};
|
||||
};
|
||||
export const readElfString = (bytes: Buffer, offset: number): string => {
|
||||
return readElfStringBytes(bytes, offset).toString("latin1");
|
||||
};
|
||||
|
||||
/** Read exact string-table bytes; ELF does not prescribe a character encoding. */
|
||||
export const readElfStringBytes = (bytes: Buffer, offset: number): Buffer => {
|
||||
if (offset >= bytes.length)
|
||||
throw new BinaryFormatFailure(
|
||||
"malformed",
|
||||
@@ -32,5 +37,5 @@ export const readElfString = (bytes: Buffer, offset: number): string => {
|
||||
"malformed",
|
||||
"ELF string is unterminated or exceeds the string budget",
|
||||
);
|
||||
return bytes.toString("latin1", offset, offset + end);
|
||||
return bytes.subarray(offset, offset + end);
|
||||
};
|
||||
@@ -54,6 +54,47 @@ describe("native export stable filesystem boundary", () => {
|
||||
await service.close();
|
||||
});
|
||||
|
||||
it("keeps ELF symbol bytes while exposing valid UTF-8 text", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-unicode-");
|
||||
const path = join(root, "libfixture.so");
|
||||
const bytes = elfExportImage({
|
||||
symbols: [{ name: "café" }, { name: "\u00ff" }],
|
||||
});
|
||||
await writeFile(path, bytes);
|
||||
const service = createNativeExportsService();
|
||||
const result = await service.inspect({ path });
|
||||
if (!result.ok) throw new Error(JSON.stringify(result.error));
|
||||
expect(result.value.normalized_result).toMatchObject({
|
||||
format: "elf",
|
||||
exports: [
|
||||
{
|
||||
name: "café",
|
||||
name_bytes_base64: "Y2Fmw6k=",
|
||||
name_utf8_decodable: true,
|
||||
},
|
||||
{
|
||||
name: "ÿ",
|
||||
name_bytes_base64: "/w==",
|
||||
name_utf8_decodable: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
const olderRows = JSON.parse(
|
||||
JSON.stringify(result.value.normalized_result),
|
||||
) as { exports: Record<string, unknown>[] };
|
||||
for (const row of olderRows.exports) {
|
||||
delete row.name_bytes_base64;
|
||||
delete row.name_utf8_decodable;
|
||||
}
|
||||
expect(nativeExportsSchema.parse(olderRows)).toMatchObject({
|
||||
exports: [
|
||||
{ name: "café", name_bytes_base64: null, name_utf8_decodable: null },
|
||||
{ name: "ÿ", name_bytes_base64: null, name_utf8_decodable: null },
|
||||
],
|
||||
});
|
||||
await service.close();
|
||||
});
|
||||
|
||||
it("returns typed input, unsupported, budget and cancellation failures", async () => {
|
||||
const root = await createTestTempDirectory("rea-native-export-failures-");
|
||||
const path = join(root, "fixture.dll");
|
||||
|
||||
Reference in new issue
Block a user