fix(parsers): retain independent facts and boundary identity

Share bounded ELF section-name decoding across native and note readers; retain read-only ranges and coverage reasons when names are unknown. Preserve ELF symbol-version bytes, dyld-bound method uncertainty, partial gRPC-Web trailers, and per-parent model identity checks. Compare GPU signatures as bytes and require a PTX module header. Share selected-path errno classification across EVM and BEAM.

Validation: 397 shared-parser tests passed (one existing skip), 5 MCP schema tests passed, 14 EVM error-mapping tests passed, and 23 protobuf CLI/MCP matrix cases passed across focused runs. Build, typecheck/lint, docs, formatting, and 2134-file discovery passed. Full EVM provider workflow remains locally unverified on Darwin arm64; format controls are synthetic.

Fixes #6738
Fixes #6737
Fixes #6745
Fixes #6740
Fixes #6749
Fixes #6747
Fixes #6739
This commit is contained in:
morluto
2026-10-23 22:18:43 +08:00
parent a68cfd7db0
commit 99d2d7ad22
45 changed files with 1209 additions and 176 deletions
+6 -1
View File
@@ -341,7 +341,12 @@ count and SHA-256.
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"`
`.gnu.version_r`, the corresponding `name_bytes_base64` and
`name_utf8_decodable` facts, and the `hidden` flag from `.gnu.version`.
Version names use the same UTF-8-when-valid, otherwise byte-preserving
Latin-1 display mapping as symbol names; ELF does not declare either
string's text encoding. Older retained results report null for the added
version-name encoding fields. `"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.
+4 -2
View File
@@ -19,8 +19,10 @@ rea inspect-evm-interface ./runtime.bin raw --json
Initial real verification covers flat legacy bytecode on Linux x64 and unchanged
bundled EVMole 0.9.3. EOF-style EF00 containers return `unsupported_target`,
with the selected path and format reason. Provider/configuration failures retain
their distinct unavailable-provider or capability classification.
with the selected path and format reason. Selected-path resolution errors
`ENOENT`, `ENOTDIR`, `ELOOP`, and `ENAMETOOLONG` are `invalid_request` path
issues. Permission, resource, cancellation, provider, and cleanup failures retain
their distinct classifications.
The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`;
`REA_EVM_PRLIMIT_COMMAND` can select another absolute executable. REA installs
no system tools and changes no user configuration. [Upstream provenance and
+2 -1
View File
@@ -130,7 +130,8 @@ For ELF images without a named build-info section, discovery checks each
nonempty writable, non-executable load segment in program-header order. This
includes sectionless PIE images with metadata in a later data segment. Named
sections retain priority, and alignment, file mapping and record validation
remain required.
remain required. When ELF section names are unreadable, limitations report that
coverage gap; the fallback search policy is unchanged.
A named ELF build-info section selects the record's discovery range. For inline
encoding, the following version and module strings may continue past that
+7 -3
View File
@@ -36,9 +36,11 @@ HIP bundle recognition accepts both the legacy `amdgcn` triple and LLVM's
`amdgpu` subarchitecture triples, including declared generic processors such
as `gfx11-generic` and `gfx9-4-generic`. CUDA's legacy fatbinary compression
flag is retained as an unsupported codec with its encoded and decoded extents;
compressed bytes are not treated as PTX text. CCOB v1 has no stored total size,
so a following legacy bundle is located by the next `CCOB` signature and
zero-byte alignment padding remains part of the first container extent.
compressed bytes are not treated as PTX text. CCOB is recognized by the exact
four signature bytes `CCOB`; high-bit-corrupted bytes are not decoded as ASCII
and accepted as a bundle. CCOB v1 has no stored total size, so a following
legacy bundle is located by the next `CCOB` signature and zero-byte alignment
padding remains part of the first container extent.
`cuda_target_header` retains the OS ABI, normalized raw `e_flags` and exact
header location. CUDA OS ABI `0x33` and the observed `0x41` flags layout use
@@ -191,3 +193,5 @@ failures preserve earlier successfully parsed declarations and the original
failure reason. Standalone PTX recognition tries a small header first, then
continues within the existing 16 MiB text budget when leading comments or
whitespace obscure its directives; admitted full-file probe bytes are reused.
After those comments and whitespace, `.version` must be the first PTX module
token; later PTX-looking text does not identify an arbitrary binary as PTX.
+3 -1
View File
@@ -248,7 +248,9 @@ The result adds:
`rva` (address minus `image_base`), both hexadecimal strings. `state` is
`code` when the module slot holds an unambiguously mapped executable address, `no-body` when an observed slot is zero,
as for abstract, extern and most generic methods, and `unknown` when no type
range in any image holds the method or a symbol relocation leaves its slot unresolved.
range in any image holds the method, a symbol relocation leaves its slot
unresolved, or dyld will bind a Mach-O slot. The raw word in a dyld-bound
slot is not treated as the resolved address.
`address` and `rva` are `null` unless `state` is `code`. The summary counts
each state in `counts.method_address_states`. Ghidra seeds are written only
for `code` rows.
+6
View File
@@ -82,6 +82,12 @@ runtime dependency coverage. A missing section remains unknown rather than an
empty dependency set. Retained views describe the captured artifact digest;
they do not assert that the current file or running application is unchanged.
ELF section names are resolved through the section-header string table.
Zlib-compressed name tables are decoded within the native structural budget.
If a name table uses unsupported compression, the result reports that
limitation and leaves `.dep-v0` presence unknown rather than reporting the
section absent.
The inline Evidence result contains the artifact path, SHA-256, byte length,
container format, architecture, word size, and byte order. When a `.dep-v0`
section is present, `auditable_metadata` contains:
+4
View File
@@ -137,6 +137,10 @@ chunks. Its offsets refer to **base64-decoded body bytes**; other offsets refer
to retained body bytes. Every field has a body-scoped `field_id` derived from
its byte offset, a `frame_index`, and a `parent_field_id` for schema-driven
nested messages or groups. Repeated field numbers remain separate occurrences.
Trailer blocks are parsed as HTTP field bytes. Valid UTF-8 values are retained;
when a field contains opaque non-UTF-8 bytes, that field is omitted and the
trailer frame is partial, while other valid trailer fields remain available.
The retained body and frame byte range preserve the original opaque value.
Without a schema, varints and fixed-width values retain exact unsigned decimal
representations. Length-delimited UTF-8, nested-message and packed readings
+26
View File
@@ -90,6 +90,32 @@ export class ArtifactFileCleanupFailure<
}
}
/** Errnos raised while the caller-selected artifact path is being resolved. */
export type ArtifactSelectedPathErrno =
| "ENOENT"
| "ENOTDIR"
| "ELOOP"
| "ENAMETOOLONG";
/** Return only known path-resolution errnos; permissions and I/O remain distinct. */
export const artifactSelectedPathErrno = (
cause: unknown,
): ArtifactSelectedPathErrno | undefined => {
if (!(cause instanceof Error) || !("code" in cause)) return undefined;
switch (String(cause.code)) {
case "ENOENT":
return "ENOENT";
case "ENOTDIR":
return "ENOTDIR";
case "ELOOP":
return "ELOOP";
case "ENAMETOOLONG":
return "ENAMETOOLONG";
default:
return undefined;
}
};
/** Normalize the physical primary failure without replacing its exact cleanup owner. */
export const mapArtifactFileFailure = (
cause: unknown,
+4 -5
View File
@@ -161,11 +161,10 @@ export const decodeProtobufBody = async (
);
const facts = parsed.ok ? parsed.value : parsed.error;
frame.examined_bytes = facts.examined_bytes;
if (parsed.ok || parsed.error.kind !== "invalid-utf8")
frame.trailers = facts.trailers.map(({ name, value }) => ({
name: redact(name),
value: redact(value),
}));
frame.trailers = facts.trailers.map(({ name, value }) => ({
name: redact(name),
value: redact(value),
}));
if (!parsed.ok) {
frame.status = "partial";
frame.error = {
@@ -9,11 +9,19 @@ const source = {
};
const same = (value: string) => value;
const trailerFrame = (text: string) => {
const payload = Buffer.from(text, "utf8");
return trailerFrameBytes(Buffer.from(text, "utf8"));
};
const trailerFrameBytes = (payload: Uint8Array) => {
const frame = Buffer.alloc(5 + payload.length);
frame[0] = 128;
frame.writeUInt32BE(payload.length, 1);
payload.copy(frame, 5);
frame.set(payload, 5);
return frame;
};
const dataFrame = (payload: Uint8Array) => {
const frame = Buffer.alloc(5 + payload.length);
frame.writeUInt32BE(payload.length, 1);
frame.set(payload, 5);
return frame;
};
@@ -63,6 +71,55 @@ it("retains allowed whitespace, visible text, and valid UTF-8 obs-text", async (
expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text));
});
it.each(["grpc-web", "grpc-web-text"] as const)(
"keeps valid trailer fields around an opaque non-UTF-8 value in %s framing",
async (framing) => {
const trailer = Buffer.concat([
Buffer.from("grpc-status: 0\r\nx-note: caf", "ascii"),
Buffer.from([0xe9]),
Buffer.from("\r\nx-after: kept\r\n", "ascii"),
]);
const binary = Buffer.concat([
dataFrame(Buffer.from([0x08, 0x01])),
trailerFrameBytes(trailer),
]);
const input =
framing === "grpc-web-text"
? Buffer.from(binary.toString("base64"), "ascii")
: binary;
const result = await decodeProtobufBody(
input,
framing,
source,
same,
new ProtobufBudget(),
);
expect(result.status).toBe("partial");
expect(result.fields).toMatchObject([
{ number: 1, wire_type: "varint", value: "1" },
]);
expect(result.frames[1]).toMatchObject({
kind: "trailer",
offset: binary.length - trailer.length - 5,
payload_offset: binary.length - trailer.length,
bytes: trailer.length,
status: "partial",
examined_bytes: trailer.length,
error: {
reason:
"gRPC-web trailer field contains opaque non-UTF-8 bytes; that field was omitted",
},
trailers: [
{ name: "grpc-status", value: "0" },
{ name: "x-after", value: "kept" },
],
});
expect(result.examined_bytes).toBe(binary.length);
expect(binary.subarray(-trailer.length)).toEqual(trailer);
},
);
it.each(["Grpc-Status", "GRPC-STATUS"])(
"retains a valid prefix when the trailer name %s violates gRPC-Web casing",
async (name) => {
@@ -82,7 +139,7 @@ it.each(["Grpc-Status", "GRPC-STATUS"])(
},
);
it("leaves undecodable trailer fields unknown and preserves unexpected failures", async () => {
it("omits a trailer field with a non-ASCII name and preserves unexpected failures", async () => {
const input = trailerFrame("grpc-status: 0\r\n");
input[5] = 255;
const malformed = await decodeProtobufBody(
@@ -93,8 +150,8 @@ it("leaves undecodable trailer fields unknown and preserves unexpected failures"
new ProtobufBudget(),
);
expect(malformed.status).toBe("partial");
expect(malformed.frames[0]?.examined_bytes).toBe(0);
expect(malformed.frames[0]?.trailers).toBeUndefined();
expect(malformed.frames[0]?.examined_bytes).toBe("grpc-status: 0\r\n".length);
expect(malformed.frames[0]?.trailers).toEqual([]);
const failure = { kind: "controlled-redaction-failure" };
await expect(
+103 -32
View File
@@ -7,7 +7,7 @@ interface TrailerFacts {
}
type TrailerFailure = TrailerFacts & {
readonly kind: "invalid-utf8" | "invalid-name" | "invalid-value";
readonly kind: "invalid-utf8-value" | "invalid-name" | "invalid-value";
readonly reason: string;
};
@@ -16,45 +16,116 @@ export const parseGrpcWebTrailers = (
bytes: Uint8Array,
budget: ProtobufBudget,
): Result<TrailerFacts, TrailerFailure> => {
let text: string;
try {
text = new TextDecoder("utf-8", {
fatal: true,
ignoreBOM: true,
}).decode(bytes);
} catch (cause) {
if (!(cause instanceof TypeError)) throw cause;
return err({
kind: "invalid-utf8",
reason: "gRPC-web trailer header block is not valid UTF-8",
trailers: [],
examined_bytes: 0,
});
}
// The whole UTF-8 block has been examined even when field validation stops.
const facts: TrailerFacts = { trailers: [], examined_bytes: bytes.length };
for (const line of text.split("\r\n")) {
if (line === "") continue;
const colon = line.indexOf(":");
// Unlike ordinary HTTP/1 headers, in-body gRPC-Web names must be lower-case.
if (colon <= 0 || !/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(line.slice(0, colon)))
let decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
const trailers: TrailerFacts["trailers"] = [];
let examinedBytes = 0;
const facts = (): TrailerFacts => ({
trailers,
examined_bytes: examinedBytes,
});
let opaqueValueFailure: TrailerFailure | undefined;
for (let start = 0; start <= bytes.length;) {
let end = start;
while (
end + 1 < bytes.length &&
!(bytes[end] === 13 && bytes[end + 1] === 10)
)
end += 1;
const hasSeparator = end + 1 < bytes.length;
const lineEnd = hasSeparator ? end : bytes.length;
const lineExaminedBytes = hasSeparator ? end + 2 : lineEnd;
if (lineEnd === start) {
if (!hasSeparator) break;
examinedBytes = lineExaminedBytes;
start = end + 2;
continue;
}
let colon = start;
while (colon < lineEnd && bytes[colon] !== 58) colon += 1;
if (colon === start || colon === lineEnd)
return err({
...facts,
...facts(),
examined_bytes: lineExaminedBytes,
kind: "invalid-name",
reason:
"gRPC-web trailer header names must be lower-case HTTP field names",
});
const value = line.slice(colon + 1).replace(/^[ \t]+/, "");
// RFC 9110 permits HTAB but excludes other C0 controls and DEL.
if (/[\u0000-\u0008\u000a-\u001f\u007f]/u.test(value))
let name = "";
let validName = true;
for (let index = start; index < colon; index += 1) {
const byte = bytes[index]!;
if (!isTrailerNameByte(byte)) {
validName = false;
break;
}
name += String.fromCharCode(byte);
}
// Unlike ordinary HTTP/1 headers, in-body gRPC-Web names must be lower-case.
if (!validName)
return err({
...facts,
kind: "invalid-value",
...facts(),
examined_bytes: lineExaminedBytes,
kind: "invalid-name",
reason:
"gRPC-web trailer field value contains a forbidden control character",
"gRPC-web trailer header names must be lower-case HTTP field names",
});
let valueStart = colon + 1;
while (valueStart < lineEnd && [9, 32].includes(bytes[valueStart]!))
valueStart += 1;
// RFC 9110 permits HTAB but excludes other C0 controls and DEL.
for (let index = valueStart; index < lineEnd; index += 1) {
const byte = bytes[index]!;
if (byte <= 8 || (byte >= 10 && byte <= 31) || byte === 127)
return err({
...facts(),
examined_bytes: lineExaminedBytes,
kind: "invalid-value",
reason:
"gRPC-web trailer field value contains a forbidden control character",
});
}
let value: string;
try {
value = decoder.decode(bytes.subarray(valueStart, lineEnd));
} catch (cause) {
if (!(cause instanceof TypeError)) throw cause;
decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
opaqueValueFailure ??= {
...facts(),
kind: "invalid-utf8-value",
reason:
"gRPC-web trailer field contains opaque non-UTF-8 bytes; that field was omitted",
};
examinedBytes = lineExaminedBytes;
if (!hasSeparator) break;
start = end + 2;
continue;
}
budget.charge(512);
facts.trailers.push({ name: line.slice(0, colon), value });
trailers.push({ name, value });
examinedBytes = lineExaminedBytes;
if (!hasSeparator) break;
start = end + 2;
}
return ok(facts);
if (opaqueValueFailure !== undefined)
return err({
...opaqueValueFailure,
trailers,
examined_bytes: bytes.length,
});
return ok({ trailers, examined_bytes: bytes.length });
};
const isTrailerNameByte = (byte: number): boolean =>
(byte >= 97 && byte <= 122) ||
(byte >= 48 && byte <= 57) ||
byte === 33 ||
(byte >= 35 && byte <= 39) ||
byte === 42 ||
byte === 43 ||
(byte >= 45 && byte <= 46) ||
byte === 94 ||
byte === 95 ||
byte === 96 ||
byte === 124 ||
byte === 126;
+8 -4
View File
@@ -143,10 +143,13 @@ const read = (parent: AnalysisViewParent): Result<Checked, AnalysisError> => {
typeof key === "object" &&
isImmutableJsonSnapshot(key);
const cached = reusable ? checked.get(key) : undefined;
if (cached !== undefined) return ok(cached);
const parsed = parseAnalysisViewResult(modelArtifactSchema, parent);
if (!parsed.ok) return parsed;
const report = reusable ? (key as ModelArtifact) : parsed.value;
let report: ModelArtifact;
if (cached !== undefined) report = cached.report;
else {
const parsed = parseAnalysisViewResult(modelArtifactSchema, parent);
if (!parsed.ok) return parsed;
report = reusable ? (key as ModelArtifact) : parsed.value;
}
if (
parent.subject != null &&
(parent.subject.local_path !== report.artifact.path ||
@@ -162,6 +165,7 @@ const read = (parent: AnalysisViewParent): Result<Checked, AnalysisError> => {
},
]),
);
if (cached !== undefined) return ok(cached);
const summary = summarizeModel(report);
const {
artifact: _artifact,
+4
View File
@@ -64,6 +64,10 @@ const elfExportSchema = z.strictObject({
z.strictObject({
index: z.number().int().min(2).max(0x7fff),
name: z.string().min(1).nullable(),
/** Exact non-ASCII version-name bytes; ELF strings do not declare encoding. */
name_bytes_base64: z.string().nullable().default(null),
/** Null means older Evidence did not retain version-name encoding facts. */
name_utf8_decodable: z.boolean().nullable().default(null),
hidden: z.boolean(),
}),
]),
+4 -6
View File
@@ -2,6 +2,7 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
artifactFilePrimaryFailure,
artifactSelectedPathErrno,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../artifacts/ArtifactFileOperation.js";
@@ -163,16 +164,13 @@ const beamFailurePrimary = (cause: unknown, input: InspectErlangBeamInput) => {
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
cause,
});
if (
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
String(cause.code),
)
)
const pathErrno = artifactSelectedPathErrno(cause);
if (pathErrno !== undefined)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected BEAM file could not be read (${String(cause.code)}): ${input.path}.`,
message: `Selected BEAM file could not be read (${pathErrno}): ${input.path}.`,
},
]);
}
+40
View File
@@ -1,3 +1,6 @@
import { readFile, symlink } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { expect, it } from "vitest";
import { evmInterfaceFailure } from "./EvmoleFailures.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
@@ -187,3 +190,40 @@ it.each([
expect(result.cleanupResources).toEqual(["exact-owner"]);
}
});
it.skipIf(process.platform === "win32")(
"maps real selected-path failures before the EVM worker starts",
async () => {
const root = await createTestTempDirectory("rea-evm-path-mapping-");
await symlink("loop", join(root, "loop"));
for (const [code, path] of [
["ELOOP", join(root, "loop", "carrier.bin")],
["ENAMETOOLONG", join(root, "x".repeat(256))],
] as const) {
let source: unknown;
try {
await readFile(path);
throw new Error("Expected selected-path read failure");
} catch (cause) {
source = cause;
}
const failure = evmInterfaceFailure(source, "artifact-read", path);
expect(failure.cause).toBe(source);
expect(projectAnalysisError(failure)).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining(code),
},
],
},
});
expect(evmInterfaceFailure(source, "worker", path)._tag).toBe(
"ProviderAdapterError",
);
}
},
);
+7 -3
View File
@@ -1,6 +1,9 @@
import type { EvmWorkerLimits } from "./EvmWorkerLimits.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import {
artifactFileCancellationCause,
artifactSelectedPathErrno,
} from "../artifacts/ArtifactFileOperation.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
@@ -212,7 +215,8 @@ export const evmInterfaceFailure = (
cause.code,
{ cause, ...cleanupOptions },
);
if (cause.code === "ENOENT" || cause.code === "ENOTDIR")
const pathErrno = artifactSelectedPathErrno(cause);
if (pathErrno !== undefined)
return new AnalysisInputError(
"inspect_evm_interface",
{ cause, ...cleanupOptions },
@@ -220,7 +224,7 @@ export const evmInterfaceFailure = (
{
path: ["path"],
reason: "invalid_value",
message: `Selected bytecode carrier could not be read (${String(cause.code)}): ${path}.`,
message: `Selected bytecode carrier could not be read (${pathErrno}): ${path}.`,
},
],
);
+16 -1
View File
@@ -41,6 +41,8 @@ export interface GoBinaryImage {
readonly bits: 32 | 64;
readonly byte_order: "little" | "big";
readonly build_info: GoBuildInfo | null;
/** Native section-name coverage, retained for discovery limitations. */
readonly section_names_limitation?: string;
}
/** Inspect bounded executable bytes using native mappings, without loading or executing code. */
@@ -50,6 +52,7 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
let selected: { search: FileMapping; header: number } | null = null;
const searched = new Set<string>();
let remainingSearchBytes = MAX_HEADER_SEARCH_BYTES;
const sectionNamesLimitation = container.section_names_limitation;
for (const search of goBuildInfoSearch(bytes, container)) {
// Identical file spans and alignment inspect the same candidate bytes.
// Partially overlapping spans still consume a cumulative work budget.
@@ -70,7 +73,16 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
}
}
if (selected === null)
return { format, architecture, bits, byte_order, build_info: null };
return {
format,
architecture,
bits,
byte_order,
build_info: null,
...(sectionNamesLimitation === undefined
? {}
: { section_names_limitation: sectionNamesLimitation }),
};
const { search, header } = selected;
const verifyMapping = (size: number): void => {
const address = search.address + BigInt(header - search.offset);
@@ -175,6 +187,9 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
version_location: version.location,
module_location: module.location,
},
...(sectionNamesLimitation === undefined
? {}
: { section_names_limitation: sectionNamesLimitation }),
};
};
+8 -2
View File
@@ -160,6 +160,7 @@ const outputLimit = (path: string) =>
const limitationsFor = (
info: { go_version: string | null; module_text: string | null } | null,
module: GoModuleMetadata | null,
sectionNamesLimitation: string | undefined,
): string[] => [
"Embedded build metadata is observed file content, not proof of compiler identity, dependency authenticity or runtime behavior.",
"Only Go build information is inspected; function names, source mappings, type metadata and decompiled code are not recovered.",
@@ -174,6 +175,7 @@ const limitationsFor = (
"No recognized Go build-info record was found in the inspected image; absent metadata does not establish that the binary is not Go.",
]
: []),
...(sectionNamesLimitation === undefined ? [] : [sectionNamesLimitation]),
...(module !== null && module.main === null
? [
"Dependency coverage is unknown because no main-module record was decoded from the embedded module information, as in GOPATH-mode builds. Decoded dependency records remain visible; an empty list does not establish the absence of dependencies.",
@@ -253,6 +255,10 @@ export class GoBinaryProvider implements GoBinaryPort {
input.architecture,
);
const image = readGoBinaryImage(selected.bytes);
const {
section_names_limitation: sectionNamesLimitation,
...publicImage
} = image;
const info = inFile(image.build_info, selected.fileOffset);
if (
info !== null &&
@@ -273,7 +279,7 @@ export class GoBinaryProvider implements GoBinaryPort {
)
: parseGoModuleText(info.module_text);
const report = {
...image,
...publicImage,
artifact: {
path: input.path,
sha256: snapshot.sha256,
@@ -283,7 +289,7 @@ export class GoBinaryProvider implements GoBinaryPort {
? {}
: { selected_slice: selected.selection }),
build_info: info === null ? null : { ...info, module },
limitations: limitationsFor(info, module),
limitations: limitationsFor(info, module, sectionNamesLimitation),
};
if (Buffer.byteLength(JSON.stringify(report), "utf8") > OUTPUT_BYTES)
throw outputLimit(input.path);
+3
View File
@@ -15,6 +15,7 @@ import {
import { gpuObject } from "./GpuObject.js";
import { isAmdGpuBundleId, readAmdGpuBundleTarget } from "./AmdGpuTarget.js";
export const CLANG_BUNDLE_MAGIC = Buffer.from("__CLANG_OFFLOAD_BUNDLE__");
export const CCOB_MAGIC = Buffer.from("CCOB");
export const readClangBundleDirectory = async (
source: GpuSource,
base: number,
@@ -85,6 +86,8 @@ export const readClangBundleDirectory = async (
};
export const readCcobHeader = (bytes: Buffer, remaining: number) => {
if (bytes.length < 20) rejectGpu("CCOB header is truncated");
if (!bytes.subarray(0, CCOB_MAGIC.length).equals(CCOB_MAGIC))
rejectGpu("Invalid CCOB magic");
const version = bytes.readUInt16LE(4),
method = bytes.readUInt16LE(6);
let width: number, total: number, decoded: number, hash: bigint;
+4 -3
View File
@@ -1,4 +1,5 @@
import {
CCOB_MAGIC,
CLANG_BUNDLE_MAGIC,
readLegacyCcobTotal,
readClangBundleDirectory,
@@ -146,13 +147,13 @@ export class GpuInventory {
await this.#hip(source, depth);
return "hip-bundle";
}
if (prefix.subarray(0, 4).toString("ascii") === "CCOB") {
if (prefix.subarray(0, CCOB_MAGIC.length).equals(CCOB_MAGIC)) {
await this.#hip(source, depth);
return "hip-bundle";
}
if (
prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) ||
prefix.subarray(0, 2).toString("ascii") === "MZ"
(prefix[0] === 0x4d && prefix[1] === 0x5a)
) {
const elf = prefix[0] === 0x7f;
const directory = await readGpuNativeDirectory(
@@ -543,7 +544,7 @@ export class GpuInventory {
base = await nextGpuContainerOffset(source, directory.end);
continue;
}
if (prefix.subarray(0, 4).toString("ascii") !== "CCOB")
if (!prefix.subarray(0, CCOB_MAGIC.length).equals(CCOB_MAGIC))
rejectGpu("Unexpected bytes between Clang offload bundles");
const legacyTotal =
prefix.length >= 6 && prefix.readUInt16LE(4) === 1
+12 -9
View File
@@ -535,8 +535,7 @@ export const ptxTargetIdentity = (target: string): string =>
/** Resolve all module-scope targets through the highest numbered architecture. */
export const readPtxTarget = (text: string): PtxTargetFacts => {
const stream = tokens(text);
let version = false,
depth = 0,
let depth = 0,
target: string | null = null,
targetIdentity: string | null = null,
targetArchitecture = -1,
@@ -544,6 +543,15 @@ export const readPtxTarget = (text: string): PtxTargetFacts => {
unsupported = false,
malformed = false;
try {
const first = stream.next();
if (first.done || first.value.value !== ".version")
return { recognized: false, target: null, limitation: null };
const declaredVersion = stream.next();
if (
declaredVersion.done ||
ptxVersion(declaredVersion.value.value) === null
)
return { recognized: false, target: null, limitation: null };
for (const token of stream) {
if (token.value === "{") {
depth++;
@@ -554,12 +562,7 @@ export const readPtxTarget = (text: string): PtxTargetFacts => {
continue;
}
if (depth !== 0) continue;
if (token.value === ".version") {
const value = stream.next();
version = !value.done && ptxVersion(value.value.value) !== null;
continue;
}
if (token.value !== ".target" || !version) continue;
if (token.value !== ".target") continue;
const value = stream.next();
if (value.done) {
unsupported = true;
@@ -593,7 +596,7 @@ export const readPtxTarget = (text: string): PtxTargetFacts => {
if (!(cause instanceof BinaryFormatFailure)) throw cause;
malformed = true;
}
const recognized = version && target !== null;
const recognized = target !== null;
const limitation = !recognized
? null
: unsupported
+53 -3
View File
@@ -10,6 +10,14 @@ import {
type BinaryContainer,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import {
parsePointerFixups,
type PointerFixups,
} from "../native/AppleMachoFixups.js";
import {
parseMachoLayout,
UnsupportedMachoMetadataFormatError,
} from "../native/AppleMachoSelection.js";
import { applyElfRelativeRelocationsSteps } from "./ElfRelocations.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
const MACHO_CPU = new Map([
@@ -68,6 +76,8 @@ export interface NativeImage {
readonly imageBase: bigint;
readonly unresolvedPointerSlots: ReadonlyMap<bigint, number>;
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
/** Mach-O loader fixups, used when interpreting method pointer slots. */
readonly machoPointerFixups?: PointerFixups;
}
export function* openImageSteps(
@@ -131,6 +141,30 @@ export function* openImageSteps(
"unsupported-pointer-encoding",
"Mach-O chained fixups are not decoded",
);
let machoPointerFixups: PointerFixups | undefined;
if (container.format === "macho") {
try {
const layout = parseMachoLayout(image, container.architecture);
machoPointerFixups = parsePointerFixups(
image,
layout.fixups,
layout.segments,
layout.baseAddress,
);
} catch (cause) {
if (cause instanceof UnsupportedMachoMetadataFormatError)
throw new Il2cppBinaryFormatFailure(
"unsupported-container",
cause.message,
);
if (cause instanceof RangeError || cause instanceof TypeError)
throw new Il2cppBinaryFormatFailure(
"malformed-container",
cause.message,
);
throw cause;
}
}
if (container.format === "pe" && container.image_base === undefined)
throw new Il2cppBinaryFormatFailure(
"malformed-container",
@@ -153,6 +187,7 @@ export function* openImageSteps(
imageBase,
unresolvedPointerSlots: relocated?.unresolved ?? new Map<bigint, number>(),
zeroFillPointers: relocated?.zeroFillPointers ?? new Map<bigint, bigint>(),
...(machoPointerFixups === undefined ? {} : { machoPointerFixups }),
};
}
@@ -307,10 +342,25 @@ export const readSlot = (
address: bigint,
): bigint | "unknown" | null => {
if (hasUnresolvedPointer(image, address)) return "unknown";
const virtual = image.zeroFillPointers.get(address);
if (virtual !== undefined) return virtual;
try {
return isZeroFilled(image, address, 8) ? 0n : readPointer(image, address);
const virtual = image.zeroFillPointers.get(address);
let raw: bigint | null;
if (virtual !== undefined) raw = virtual;
else if (isZeroFilled(image, address, 8)) raw = 0n;
else raw = readPointer(image, address);
if (raw === null) return null;
if (image.machoPointerFixups !== undefined) {
const resolution = image.machoPointerFixups.decode(address, raw);
if (resolution.kind === "bind" || resolution.kind === "unknown")
return "unknown";
if (resolution.kind === "unsupported")
throw new Il2cppBinaryFormatFailure(
"unsupported-pointer-encoding",
resolution.reason,
);
return resolution.target;
}
return raw;
} catch (cause) {
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
+26 -35
View File
@@ -1,5 +1,4 @@
import { createHash } from "node:crypto";
import { inflateSync } from "node:zlib";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
import {
@@ -14,12 +13,11 @@ import {
elfObjectTypeName,
gnuNoteTypeName,
} from "../../domain/native/elfNotes.js";
import { decodeElfSectionNameTable } from "../metadata/ElfSectionNameTable.js";
const PT_NOTE = 4;
const SHT_NOTE = 7;
const SHT_STRTAB = 3;
const SHF_COMPRESSED = 0x800;
const ELFCOMPRESS_ZLIB = 1;
const HEX_DESCRIPTOR_LIMIT = 64;
interface TextBudget {
@@ -335,39 +333,32 @@ export const parseElfNotes = (
return fail("ELF section name table is not a string table.");
const strOff = reader.word(namesHeader + (wide ? 24 : 16));
const strSize = reader.word(namesHeader + (wide ? 32 : 20));
shstrtab = reader.slice(strOff, strSize);
if ((reader.word(namesHeader + 8) & SHF_COMPRESSED) !== 0) {
const compressionHeaderBytes = wide ? 24 : 12;
if (strSize < compressionHeaderBytes)
return fail("ELF compressed section name table header is truncated.");
const compressionType = reader.u32(strOff);
if (compressionType !== ELFCOMPRESS_ZLIB) {
shstrtab = null;
sectionNameLimitation = `ELF section names are unavailable because compression type ${String(compressionType)} is unsupported.`;
} else {
const uncompressedSize = reader.word(strOff + (wide ? 8 : 4));
if (uncompressedSize > input.max_file_bytes - bytes.length)
throw new ArtifactReaderFailure(
"limit",
`ELF source and compressed section names exceed the ${String(input.max_file_bytes)}-byte memory budget.`,
{ boundary: "memory" },
);
try {
shstrtab = inflateSync(shstrtab.subarray(compressionHeaderBytes), {
maxOutputLength: Math.max(1, uncompressedSize),
});
} catch (cause: unknown) {
return fail(
`ELF compressed section name table could not be decompressed: ${cause instanceof Error ? cause.message : "invalid zlib data"}.`,
);
}
if (shstrtab.length !== uncompressedSize)
return fail(
"ELF compressed section name table size is inconsistent.",
);
text.limit = bytes.length + shstrtab.length;
}
const rawNames = reader.slice(strOff, strSize);
const flags = reader.word(namesHeader + 8);
const decoded = decodeElfSectionNameTable(
rawNames,
BigInt(flags),
header.elfClass,
reader.little,
input.max_file_bytes - bytes.length,
);
if (decoded.state === "malformed") return fail(decoded.message);
if (decoded.state === "limit")
throw new ArtifactReaderFailure(
"limit",
`ELF source and compressed section names exceed the ${String(input.max_file_bytes)}-byte memory budget.`,
{ boundary: "memory" },
);
if (decoded.state === "unsupported") {
shstrtab = null;
sectionNameLimitation = `ELF section names are unavailable because compression type ${String(decoded.compressionType)} is unsupported.`;
} else {
shstrtab = decoded.bytes;
if (decoded.compressed) text.limit = bytes.length + shstrtab.length;
}
} else if (shnum !== 0) {
sectionNameLimitation =
"ELF section names are unavailable because no section-name string table is declared.";
}
const tables: ElfNotes["tables"] = [];
const parsed: ReturnType<typeof toEntry>[] = [];
+105 -6
View File
@@ -279,13 +279,25 @@ describe("ELF dynamic symbol exports", () => {
name: "versioned_fn",
type: "function",
binding: "global",
version: { index: 2, name: "VER_1", hidden: false },
version: {
index: 2,
name: "VER_1",
name_bytes_base64: null,
name_utf8_decodable: true,
hidden: false,
},
},
{
name: "old_fn",
type: "function",
binding: "global",
version: { index: 3, name: "VER_0", hidden: true },
version: {
index: 3,
name: "VER_0",
name_bytes_base64: null,
name_utf8_decodable: true,
hidden: true,
},
},
{ name: "weak_obj", type: "object", binding: "weak", version: null },
{ name: "ifunc_sym", type: "ifunc", binding: "global", version: null },
@@ -315,7 +327,9 @@ describe("ELF dynamic symbol exports", () => {
exports: [{ name: "be_fn", value: "0x10000", size: "8" }],
});
});
});
describe("ELF export version and name coverage", () => {
it("marks versions unknown when the version table does not fit", () => {
const result = parse(
elfExportImage({
@@ -346,8 +360,20 @@ describe("ELF dynamic symbol exports", () => {
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual([
{ index: 7, name: null, hidden: false },
{ index: 7, name: null, hidden: false },
{
index: 7,
name: null,
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
{
index: 7,
name: null,
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
]);
expect(result.issues).toHaveLength(1);
});
@@ -494,7 +520,13 @@ describe("ELF version requirements", () => {
expect(result.exports).toMatchObject([
{
name: "defined_version",
version: { index: 2, name: null, hidden: false },
version: {
index: 2,
name: null,
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
},
]);
expect(result.coverage.status).toBe("partial");
@@ -572,7 +604,15 @@ describe("export inventory review regressions", () => {
expect(
result.format === "elf" && result.exports.map(({ version }) => version),
).toEqual([{ index: 2, name: null, hidden: false }]);
).toEqual([
{
index: 2,
name: null,
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
]);
expect(result.issues).toMatchObject([{ code: "version_unresolved" }]);
expect(result.coverage.status).toBe("partial");
});
@@ -609,6 +649,65 @@ describe("ELF symbol-name text", () => {
name_utf8_decodable: false,
});
});
it.each([
{
source: "definition",
result: () =>
parse(
elfExportImage({
symbols: [{ name: "defined", version: 2 }],
versionDefinitions: ["libfixture.so", "libcafé.so"],
}),
),
versionIndex: 2,
},
{
source: "requirement",
result: () =>
parse(
elfExportImage({
symbols: [{ name: "required", version: 3 }],
versionRequirements: [{ index: 3, name: "libcafé.so" }],
}),
),
versionIndex: 3,
},
])(
"decodes UTF-8 version names from the $source table",
({ result, versionIndex }) => {
const parsed = result();
expect(parsed.format).toBe("elf");
if (parsed.format !== "elf") throw new TypeError("expected ELF result");
expect(parsed.exports[0]?.version).toEqual({
index: versionIndex,
name: "libcafé.so",
name_bytes_base64: "bGliY2Fmw6kuc28=",
name_utf8_decodable: true,
hidden: false,
});
expect(parsed.issues).toEqual([]);
expect(parsed.coverage.status).toBe("complete");
},
);
it("retains invalid UTF-8 version-name bytes with a byte-preserving display", () => {
const result = parse(
elfExportImage({
symbols: [{ name: "defined", version: 2 }],
versionDefinitions: ["libfixture.so", "\u00ff"],
}),
);
expect(result.format).toBe("elf");
if (result.format !== "elf") throw new TypeError("expected ELF result");
expect(result.exports[0]?.version).toEqual({
index: 2,
name: "ÿ",
name_bytes_base64: "/w==",
name_utf8_decodable: false,
hidden: false,
});
});
});
it("refuses formats other than PE and ELF", () => {
+39 -21
View File
@@ -6,11 +6,7 @@ import {
} from "../metadata/BinaryContainer.js";
import { architectureForElfMachine } from "../metadata/ElfImage.js";
import { isUtf8 } from "node:buffer";
import {
readElfString,
readElfStringBytes,
readElfSymbol,
} from "../metadata/ElfSymbol.js";
import { readElfStringBytes, readElfSymbol } from "../metadata/ElfSymbol.js";
import {
readNativeDirectoryFromBuffer,
type NativeSection,
@@ -334,14 +330,31 @@ 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,
};
};
const readVersionNames = (
reader: BinaryReader,
sections: readonly NativeSection[],
issues: NativeExportIssue[],
): Map<number, string | null> => {
const namesByIndex = new Map<number, string | null>();
const addName = (index: number, name: string): void => {
namesByIndex.set(index, name || null);
): Map<number, DecodedElfString | null> => {
const namesByIndex = new Map<number, DecodedElfString | null>();
const addName = (index: number, name: Buffer): void => {
namesByIndex.set(index, name.length === 0 ? null : decodeElfString(name));
};
const verneed = sections.find(({ type }) => type === SHT_GNU_VERNEED);
if (verneed !== undefined) {
@@ -379,7 +392,10 @@ const readVersionNames = (
const versionIndex = reader.u16(auxAt + 6) & 0x7fff;
const auxiliaryNext = reader.u32(auxAt + 12);
try {
addName(versionIndex, readElfString(names, reader.u32(auxAt + 8)));
addName(
versionIndex,
readElfStringBytes(names, reader.u32(auxAt + 8)),
);
} catch (cause) {
issues.push({
code: "version_unresolved",
@@ -441,7 +457,7 @@ const readVersionNames = (
"Version definition name entry leaves its section",
);
let auxiliary = offset + aux;
const versionName = readElfString(
const versionName = readElfStringBytes(
names,
reader.u32(table.offset + auxiliary),
);
@@ -600,8 +616,8 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
const versionIndex = raw & 0x7fff;
if (versionIndex === 0) continue;
if (versionIndex >= 2) {
const name = versionNames.get(versionIndex) ?? null;
if (name === null && !unresolvedVersions.has(versionIndex)) {
const decodedName = versionNames.get(versionIndex) ?? null;
if (decodedName === null && !unresolvedVersions.has(versionIndex)) {
unresolvedVersions.add(versionIndex);
issues.push({
code: "version_unresolved",
@@ -611,18 +627,22 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
}
version = {
index: versionIndex,
name,
name: decodedName?.name ?? null,
name_bytes_base64: decodedName?.name_bytes_base64 ?? null,
name_utf8_decodable: decodedName?.name_utf8_decodable ?? null,
hidden: (raw & 0x8000) !== 0,
};
}
}
let name: string;
let rawName: Buffer;
let nameUtf8Decodable: boolean;
let rawNameBase64: string | null;
try {
rawName = readElfStringBytes(names, symbol.name_offset);
nameUtf8Decodable = isUtf8(rawName);
name = rawName.toString(nameUtf8Decodable ? "utf8" : "latin1");
const rawName = readElfStringBytes(names, symbol.name_offset);
const decodedName = decodeElfString(rawName);
nameUtf8Decodable = decodedName.name_utf8_decodable;
name = decodedName.name;
rawNameBase64 = decodedName.name_bytes_base64;
} catch (cause) {
issues.push({
code: "name_unreadable",
@@ -639,13 +659,11 @@ const parseElfExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
});
continue;
}
const rawNameBase64 = rawName.some((byte) => byte >= 0x80)
? rawName.toString("base64")
: null;
retain(
name,
typeof version === "object" ? (version?.name ?? null) : null,
rawNameBase64,
typeof version === "object" ? (version?.name_bytes_base64 ?? null) : null,
);
exports.push({
name,
+6 -1
View File
@@ -50,7 +50,6 @@ export interface LoadRegion {
/** Parsed section facts; consumer metadata validity is separate from native structure. */
export type ImageSection = {
readonly name: string;
readonly offset: number;
readonly size: number;
readonly file_backed: boolean;
@@ -59,6 +58,8 @@ export type ImageSection = {
} & (
| {
readonly format: "elf";
/** Null when the ELF name table is unavailable or this name is unreadable. */
readonly name: string | null;
readonly type: number;
readonly address: bigint;
readonly declared_range: {
@@ -68,6 +69,7 @@ export type ImageSection = {
}
| {
readonly format: "pe";
readonly name: string;
readonly virtual_address: number;
readonly virtual_size: number;
readonly raw_size: number;
@@ -75,6 +77,7 @@ export type ImageSection = {
}
| {
readonly format: "macho";
readonly name: string;
readonly section_name: string;
readonly type: number;
readonly address: bigint;
@@ -107,6 +110,8 @@ export interface BinaryContainer {
}[];
/** Mach-O load-command observation, when available. */
readonly macho_chained_fixups?: boolean;
/** Section-name lookup was incomplete; absence by name is not established. */
readonly section_names_limitation?: string;
readonly mappings: readonly FileMapping[];
readonly load_regions?: readonly LoadRegion[];
/** File-backed ranges the native loader marks executable. */
+5 -2
View File
@@ -67,7 +67,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => {
zeroFills.push({ address: address + size, size: memorySize - size });
}
for (const section of directory.sections) {
if (section.name === "" || section.name_state === "unavailable") continue;
if (section.name === "") continue;
const range = declaredFileRange(bytes.length, section.offset, section.size);
const file_backed = section.type !== 8 && range.file_backed;
const compressed = (section.flags & 0x800n) !== 0n;
@@ -81,7 +81,7 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => {
if (section.name === null && !read_only) continue;
sections.push({
format: "elf",
name: section.name ?? "",
name: section.name,
type: section.type,
address: section.address,
declared_range: { offset: section.offset, size: section.size },
@@ -101,6 +101,9 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => {
executable_mappings: executableMappings,
zero_fills: zeroFills,
sections,
...(directory.section_names_limitation === undefined
? {}
: { section_names_limitation: directory.section_names_limitation }),
};
};
@@ -0,0 +1,73 @@
import { inflateSync } from "node:zlib";
const SHF_COMPRESSED = 0x800n;
const ELFCOMPRESS_ZLIB = 1;
export type ElfSectionNameTableResult =
| {
readonly state: "available";
readonly bytes: Buffer;
readonly compressed: boolean;
}
| { readonly state: "unsupported"; readonly compressionType: number }
| { readonly state: "malformed"; readonly message: string }
| {
readonly state: "limit";
readonly requiredBytes: bigint;
readonly maximumBytes: number;
};
/** Decode the ELF section-name table once for consumers that need its names. */
export const decodeElfSectionNameTable = (
bytes: Buffer,
flags: bigint,
bits: 32 | 64,
little: boolean,
maximumDecodedBytes: number,
): ElfSectionNameTableResult => {
if ((flags & SHF_COMPRESSED) === 0n)
return { state: "available", bytes, compressed: false };
const headerBytes = bits === 64 ? 24 : 12;
if (bytes.length < headerBytes)
return {
state: "malformed",
message: "ELF compressed section-name table header is truncated",
};
const compressionType = little
? bytes.readUInt32LE(0)
: bytes.readUInt32BE(0);
if (compressionType !== ELFCOMPRESS_ZLIB)
return { state: "unsupported", compressionType };
const declaredSize =
bits === 64
? little
? bytes.readBigUInt64LE(8)
: bytes.readBigUInt64BE(8)
: BigInt(little ? bytes.readUInt32LE(4) : bytes.readUInt32BE(4));
if (declaredSize > BigInt(maximumDecodedBytes))
return {
state: "limit",
requiredBytes: declaredSize,
maximumBytes: maximumDecodedBytes,
};
const expected = Number(declaredSize);
let decoded: Buffer;
try {
decoded = inflateSync(bytes.subarray(headerBytes), {
maxOutputLength: Math.max(1, expected),
});
} catch (cause: unknown) {
return {
state: "malformed",
message: `ELF compressed section-name table could not be decompressed: ${cause instanceof Error ? cause.message : "invalid zlib data"}`,
};
}
if (decoded.length !== expected)
return {
state: "malformed",
message: "ELF compressed section-name table size is inconsistent",
};
return { state: "available", bytes: decoded, compressed: true };
};
+55 -23
View File
@@ -5,6 +5,7 @@ import {
BinaryResourceFailure,
declaredFileRange,
} from "./BinaryContainer.js";
import { decodeElfSectionNameTable } from "./ElfSectionNameTable.js";
/** A borrowed positional source. Drivers own admission, cancellation and file identity. */
export interface NativeReadRequest {
@@ -33,6 +34,7 @@ export interface NativeSectionDirectory {
readonly machine: number;
readonly elf_os_abi: number | null;
readonly flags: number;
readonly section_names_limitation?: string;
readonly program_offset: bigint;
readonly program_count: number;
readonly program_width: number;
@@ -138,6 +140,7 @@ export function* readElfSectionDirectory(
);
}
const sections: NativeSection[] = [];
let section_names_limitation: string | undefined;
if (count !== 0) {
if (sectionWidth < (wide ? 64 : 40) || namesIndex >= count)
reject("ELF section width or name-table index is invalid");
@@ -154,11 +157,31 @@ export function* readElfSectionDirectory(
const h = namesIndex * sectionWidth;
if (table.u32(h + 4) !== 3)
reject("ELF section name table is not a string table");
names = yield request(
const rawNames = yield request(
fileSize,
table.word(h + (wide ? 24 : 16), bits),
table.word(h + (wide ? 32 : 20), bits),
);
const decoded = decodeElfSectionNameTable(
rawNames,
table.word(h + 8, bits),
bits,
little,
Math.max(0, STRUCTURE_BYTES - rawNames.length),
);
if (decoded.state === "malformed") return reject(decoded.message);
if (decoded.state === "limit")
throw new BinaryResourceFailure(
"structure",
STRUCTURE_BYTES,
`Decoded ELF section names require ${String(decoded.requiredBytes)} bytes, above the ${String(decoded.maximumBytes)}-byte remaining structural budget`,
);
if (decoded.state === "unsupported")
section_names_limitation = `ELF section names are unavailable because compression type ${String(decoded.compressionType)} is unsupported`;
else names = decoded.bytes;
} else if (count > 1) {
section_names_limitation =
"ELF section names are unavailable because no section-name string table is declared";
}
const decodedNames = new Map<
number,
@@ -171,30 +194,36 @@ export function* readElfSectionDirectory(
let name: string | null = null;
let name_state: NativeSection["name_state"] = "unavailable";
if (names !== null) {
if (nameOffset >= names.length)
reject("ELF section name offset is outside its string table");
let decoded = decodedNames.get(nameOffset);
if (decoded === undefined) {
const window = names.subarray(
nameOffset,
Math.min(nameOffset + 65536, names.length),
);
const end = window.indexOf(0);
nameBytes += end < 0 ? window.length : end + 1;
if (nameBytes > STRUCTURE_BYTES)
throw new BinaryResourceFailure(
"structure",
STRUCTURE_BYTES,
"Decoded ELF section names exceed the 16 MiB structural budget",
if (nameOffset >= names.length) {
section_names_limitation ??=
"ELF section names are incomplete because a name offset is outside its string table";
} else {
let decoded = decodedNames.get(nameOffset);
if (decoded === undefined) {
const window = names.subarray(
nameOffset,
Math.min(nameOffset + 65536, names.length),
);
decoded = {
name: end < 0 ? null : window.toString("latin1", 0, end),
state: end < 0 ? "truncated" : "known",
};
decodedNames.set(nameOffset, decoded);
const end = window.indexOf(0);
nameBytes += end < 0 ? window.length : end + 1;
if (nameBytes > STRUCTURE_BYTES)
throw new BinaryResourceFailure(
"structure",
STRUCTURE_BYTES,
"Decoded ELF section names exceed the 16 MiB structural budget",
);
decoded = {
name: end < 0 ? null : window.toString("latin1", 0, end),
state: end < 0 ? "truncated" : "known",
};
if (end < 0)
section_names_limitation ??=
"ELF section names are unavailable because a name is not NUL-terminated";
decodedNames.set(nameOffset, decoded);
}
name_state = decoded.state;
name = decoded.name;
}
name_state = decoded.state;
name = decoded.name;
}
const type = table.u32(h + 4);
const offset = table.word(h + (wide ? 24 : 16), bits);
@@ -224,6 +253,9 @@ export function* readElfSectionDirectory(
little,
machine,
flags: r.u32(wide ? 48 : 36),
...(section_names_limitation === undefined
? {}
: { section_names_limitation }),
program_offset,
program_count,
program_width,
+4
View File
@@ -28,6 +28,7 @@ export interface RustBinaryImage {
readonly byte_order: RustBinary["byte_order"];
readonly auditable_metadata: RustAuditableMetadata | null;
readonly rustc_commit: RustBinary["rustc_commit"];
readonly section_names_limitation?: string;
}
/** Read dependency metadata from container sections without executing the image. */
@@ -44,6 +45,9 @@ export const readRustBinaryImage = (bytes: Buffer): RustBinaryImage => {
? null
: readDependency(bytes, container.format, dependency),
rustc_commit: rustcCommit(bytes, container),
...(container.section_names_limitation === undefined
? {}
: { section_names_limitation: container.section_names_limitation }),
};
};
+17 -2
View File
@@ -101,7 +101,12 @@ export class RustBinaryProvider implements RustBinaryPort {
selected.fileOffset,
);
const report = {
...image,
format: image.format,
architecture: image.architecture,
bits: image.bits,
byte_order: image.byte_order,
auditable_metadata: image.auditable_metadata,
rustc_commit: image.rustc_commit,
artifact: {
path: input.path,
sha256: snapshot.sha256,
@@ -172,11 +177,21 @@ const limitationsFor = (
"Embedded dependency metadata is file content. It does not authenticate package names, versions, sources, or the compiler that produced the binary.",
"Only the .dep-v0 dependency section and read-only compiler path strings are inspected. Functions, types, and runtime behavior are not recovered.",
"Resource guards bound the selected file to 256 MiB, a dependency section to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the section size, and the complete report to 8 MiB. Package and dependency counts are admitted against that output budget before schema cloning, graph objects, or reverse-edge allocation.",
...(image.auditable_metadata === null
...(image.section_names_limitation === undefined
? []
: [image.section_names_limitation]),
...(image.auditable_metadata === null &&
image.section_names_limitation === undefined
? [
"No .dep-v0 section was found. Absence of .dep-v0 does not mean the binary has no dependencies.",
]
: []),
...(image.auditable_metadata === null &&
image.section_names_limitation !== undefined
? [
"The presence of .dep-v0 could not be determined because ELF section names are unavailable.",
]
: []),
...unknownFormatLimitation(image.auditable_metadata?.format_version),
...(image.auditable_metadata?.packages.some((pkg) => pkg.kind === null)
? [
@@ -42,3 +42,44 @@ cliTest(
},
30_000,
);
cliTest(
"preserves UTF-8 GNU version names and their exact bytes",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-elf-version-utf8-");
const path = join(root, "versioned.so");
await writeFile(
path,
elfExportImage({
symbols: [{ name: "plain", version: 2 }],
versionDefinitions: ["libfixture.so", "libcafé.so"],
}),
);
const result = await cli.run({
arguments: ["inspect-native-exports", path, "--json"],
});
expect(result.exitCode, result.stderr).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: "plain",
version: {
index: 2,
name: "libcafé.so",
name_bytes_base64: "bGliY2Fmw6kuc28=",
name_utf8_decodable: true,
hidden: false,
},
},
],
},
);
},
30_000,
);
@@ -33,6 +33,7 @@ import {
import {
createElfFixture,
createIl2cppBinaryFixture,
createIl2cppDyldBindFixture,
createUniversalFixture,
FIXTURE_BINARY_BASE,
FIXTURE_METHOD_ADDRESSES,
@@ -852,6 +853,52 @@ cliTest(
30000,
);
cliTest(
"keeps a dyld-bound method slot unresolved through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-il2cpp-dyld-bind-");
const path = join(root, "global-metadata.dat");
const binaryPath = join(root, "GameAssembly.dylib");
await writeFile(path, createLinkedIl2cppMetadataFixture().bytes);
await writeFile(binaryPath, createIl2cppDyldBindFixture(0n));
const result = await runFileCli(cli, {
arguments: [
"inspect-il2cpp-metadata",
path,
"--binary",
binaryPath,
"--json",
],
});
expect(result.exitCode, result.stderr).toBe(0);
const report = parseCliMetadata(result.json);
expect(report.method_addresses[1]).toEqual({
state: "unknown",
address: null,
rva: null,
});
expect(report.method_addresses[4]).toEqual({
state: "no-body",
address: null,
rva: null,
});
const { client, call } = await connectLocalToolsMcp();
const response = await call("inspect_il2cpp_metadata", {
path,
binary_path: binaryPath,
detail: "complete",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(
parseCliMetadata(response.structuredContent).method_addresses,
).toEqual(report.method_addresses);
await client.ping();
},
30000,
);
cliTest(
"keeps nested IL2CPP generic arguments on their declaring type components",
async ({ cli }) => {
+52
View File
@@ -8,6 +8,7 @@ import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { machoImageWithUnknownCommand } from "../../src/artifacts/apple/MachoImage.fixture.js";
import {
compressRustElfSectionNames,
createRustImage,
createRustImageWithGoSection,
RUSTC_HASH,
@@ -258,6 +259,57 @@ cliTest("returns null metadata when .dep-v0 is absent", async ({ cli }) => {
);
});
cliTest(
"reads compressed ELF names and keeps unsupported compression unknown",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-rust-elf-compressed-names-",
);
for (const fixture of [
{ name: "zlib", type: 1, supported: true },
{ name: "unknown", type: 2, supported: false },
]) {
const path = join(root, `${fixture.name}.elf`);
await writeFile(
path,
compressRustElfSectionNames(
createRustImage({ format: "elf" }),
fixture.type,
),
);
const result = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_rust_binary").outputSchema.parse(
result.json,
);
const report = rustBinarySchema.parse(evidence.normalized_result);
if (fixture.supported) {
expect(report.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
packages: [
{ name: "itoa", version: "1.0.18" },
{ name: "sample", version: "0.1.0", root: true },
],
dependency_edges: [{ from_index: 1, to_index: 0 }],
});
} else {
expect(report.auditable_metadata).toBeNull();
expect(report.limitations.join("\n")).toContain(
"ELF section names are unavailable because compression type 2 is unsupported",
);
expect(report.limitations.join("\n")).toContain(
"The presence of .dep-v0 could not be determined",
);
expect(report.limitations.join("\n")).not.toContain(
"No .dep-v0 section was found",
);
}
}
},
);
cliTest(
"keeps arm64_32 in Rust CLI Evidence when dependency metadata is absent",
async ({ cli }) => {
+16 -1
View File
@@ -559,6 +559,12 @@ captureTest(
const trailerCases = [
["valid", "grpc-status", "0", "complete"],
[
"opaque",
"grpc-status",
"0\r\nx-note: caf\u00e9\r\nx-after: kept",
"partial",
],
["uppercase", "Grpc-Status", "0", "partial"],
["nul", "grpc-status", "0\u0000bad", "partial"],
["lf", "grpc-status", "0\nbad", "partial"],
@@ -582,7 +588,10 @@ captureTest.for(
async ({ framing, name, headerName, value, status }, { cli, mcp }) => {
const root = await createTestTempDirectory("rea-grpc-web-trailers-");
const path = join(root, `${framing}-${name}.har`);
const trailer = Buffer.from(`${headerName}: ${value}\r\n`);
const trailer = Buffer.from(
`${headerName}: ${value}\r\n`,
name === "opaque" ? "latin1" : "utf8",
);
const body = Buffer.concat([
protobufFrame(Buffer.from([8, 1])),
protobufFrame(trailer, 128),
@@ -618,6 +627,12 @@ captureTest.for(
],
fields: [{ number: 1, wire_type: "varint", value: "1" }],
});
if (name === "opaque") {
expect(report.records[0]?.protobuf?.[0]?.frames[1]?.trailers).toEqual([
{ name: "grpc-status", value: "0" },
{ name: "x-after", value: "kept" },
]);
}
const reply = await mcp.call("inspect_web_network_capture", {
capture_path: path,
format: "har",
@@ -1,5 +1,5 @@
import { createHash } from "node:crypto";
import { access, readFile, writeFile } from "node:fs/promises";
import { access, readFile, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { z } from "zod";
@@ -562,6 +562,41 @@ it.runIf(!unsupportedHost)(
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
},
);
it.runIf(!unsupportedHost)(
"classifies selected carrier path-resolution errnos as invalid input",
async () => {
const { root } = await fixture();
const loop = join(root, "loop");
await symlink("loop", loop);
const cases = [
{ code: "ELOOP", path: join(loop, "carrier.bin") },
{ code: "ENAMETOOLONG", path: join(root, "x".repeat(256)) },
] as const;
const service = new EvmInterfaceService(new EvmoleInterfaceProvider({}));
try {
for (const { code, path } of cases) {
const result = await service.inspect({ path, encoding: "raw" });
if (result.ok) throw new Error(`Expected ${code} path failure`);
expect(result.error._tag).toBe("AnalysisInputError");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["path"],
reason: "invalid_value",
message: expect.stringContaining(code),
},
],
},
});
}
} finally {
await service.close();
}
},
);
it.runIf(unsupportedHost)(
"reports unverified host coverage before acquiring a provider",
async () => {
@@ -53,12 +53,15 @@ describe("native export stable filesystem boundary", () => {
}
await service.close();
});
});
describe("native export encoding and failures", () => {
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" }],
symbols: [{ name: "café", version: 2 }, { name: "\u00ff" }],
versionDefinitions: ["libfixture.so", "libcafé.so"],
});
await writeFile(path, bytes);
const service = createNativeExportsService();
@@ -71,6 +74,12 @@ describe("native export stable filesystem boundary", () => {
name: "café",
name_bytes_base64: "Y2Fmw6k=",
name_utf8_decodable: true,
version: {
index: 2,
name: "libcafé.so",
name_bytes_base64: "bGliY2Fmw6kuc28=",
name_utf8_decodable: true,
},
},
{
name: "ÿ",
@@ -85,10 +94,26 @@ describe("native export stable filesystem boundary", () => {
for (const row of olderRows.exports) {
delete row.name_bytes_base64;
delete row.name_utf8_decodable;
const version = row.version;
if (version !== null && typeof version === "object") {
Reflect.deleteProperty(version, "name_bytes_base64");
Reflect.deleteProperty(version, "name_utf8_decodable");
}
}
expect(nativeExportsSchema.parse(olderRows)).toMatchObject({
exports: [
{ name: "café", name_bytes_base64: null, name_utf8_decodable: null },
{
name: "café",
name_bytes_base64: null,
name_utf8_decodable: null,
version: {
index: 2,
name: "libcafé.so",
name_bytes_base64: null,
name_utf8_decodable: null,
hidden: false,
},
},
{ name: "ÿ", name_bytes_base64: null, name_utf8_decodable: null },
],
});
@@ -99,6 +99,28 @@ it("keeps the header-search budget distinct from a worker CPU limit", async () =
expect(projected.remediation.action).not.toMatch(/observed signal/u);
});
it("reports unreadable ELF names alongside the no-record limitation", async () => {
const bytes = createGoBinaryFixture({ omitBuildInfo: true }).bytes;
const nameTableHeader = 256 + 64;
bytes.writeBigUInt64LE(0x800n, nameTableHeader + 8);
bytes.writeBigUInt64LE(24n, nameTableHeader + 32);
bytes.writeUInt32LE(2, 768);
const root = await createTestTempDirectory("rea-go-unknown-section-names-");
const path = join(root, "unknown-names.elf");
await writeFile(path, bytes);
const result = await new GoBinaryProvider().inspect({ path });
if (!result.ok) throw result.error;
expect(result.value.build_info).toBeNull();
expect(result.value.limitations).toContain(
"No recognized Go build-info record was found in the inspected image; absent metadata does not establish that the binary is not Go.",
);
expect(result.value.limitations).toContain(
"ELF section names are unavailable because compression type 2 is unsupported",
);
expect(result.value).not.toHaveProperty("section_names_limitation");
});
it.skipIf(process.platform === "win32")(
"rejects a symlink without replacing its selected file identity",
async () => {
+44
View File
@@ -706,6 +706,50 @@ it("iterates mixed plain/CCOB bundles without counting flat records as nesting",
expect(result.value.kernels).toHaveLength(20);
expect(result.value.coverage.discovery).toBe("complete");
});
it("does not recognize a CCOB signature after its first byte is corrupted", async () => {
const bytes = ccobFixture(3, "zlib");
bytes[0] = bytes[0]! | 0x80;
const result = await inspect(bytes);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisUnsupportedTargetError" },
});
});
it("requires the PTX version directive before any module content", async () => {
const result = await inspect(Buffer.from(`unexpected-prefix ${ptxFixture}`));
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisUnsupportedTargetError" },
});
});
it("retains an embedded CCOB with a corrupted signature as unsupported", async () => {
const bytes = ccobFixture(3, "zlib");
bytes[0] = bytes[0]! | 0x80;
const result = await inspect(
elfFixture(62, [{ name: ".hip_fatbin", bytes }]),
);
if (!result.ok) throw result.error;
expect(result.value.coverage.discovery).toBe("partial");
expect(result.value.kernels).toEqual([]);
expect(result.value.objects).toContainEqual(
expect.objectContaining({
kind: "native-section",
inspection: "unsupported",
limitations: expect.arrayContaining([
"No supported CUDA/HIP container or PTX declarations were identified",
]),
}),
);
});
it("does not dispatch a high-bit-corrupted DOS signature to the PE parser", async () => {
const bytes = peGpuFixture(Buffer.from(ptxFixture));
bytes[0] = bytes[0]! | 0x80;
const result = await inspect(bytes);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisUnsupportedTargetError" },
});
});
it("uses each legacy CCOB v1 stream boundary across alignment padding", async () => {
const first = ccobFixture(1, "zlib"),
second = ccobFixture(2, "zlib"),
@@ -8,6 +8,7 @@ import { readIl2cppMetadataForLinking } from "../../../src/il2cpp/Il2cppMetadata
import {
createElfFixture,
createIl2cppBinaryFixture,
createIl2cppDyldBindFixture,
createUniversalFixture,
createSplitPointerPeFixture,
FIXTURE_BINARY_BASE,
@@ -577,6 +578,22 @@ it("reports a symbol-relocated method slot as unknown rather than no-body", () =
expect(linked.method_addresses[0]).toEqual(expectedAddresses[0]);
});
it.each([0n, FIXTURE_BINARY_BASE + 0x210n])(
"keeps a dyld-bound method slot unknown regardless of its raw word %s",
(raw) => {
const linked = link(createIl2cppDyldBindFixture(raw));
expect(linked.method_addresses[1]).toEqual(expectedAddress("unknown"));
expect(linked.method_addresses[0]).toEqual(expectedAddresses[0]);
expect(linked.method_addresses[4]).toEqual(expectedAddress("no-body"));
},
);
it("keeps unbound Mach-O method words classified from their values", () => {
const linked = link(createIl2cppBinaryFixture());
expect(linked.method_addresses[1]).toEqual(expectedAddresses[1]);
expect(linked.method_addresses[4]).toEqual(expectedAddress("no-body"));
});
it.each([
["PC32", 2],
["ABS32", 10],
@@ -319,7 +319,12 @@ it("records a portable export inventory through MCP without a binary target", as
const root = await createTestTempDirectory("rea-native-exports-mcp-");
const path = join(root, "libfixture.so");
const bytes = elfExportImage({
symbols: [{ name: "exported_fn" }, { name: "hidden_fn", visibility: 2 }],
symbols: [
{ name: "exported_fn" },
{ name: "hidden_fn", visibility: 2 },
{ name: "versioned_fn", version: 2 },
],
versionDefinitions: ["libfixture.so", "libcafé.so"],
});
await writeFile(path, bytes);
const session = createTestBinarySession(() => {
@@ -348,6 +353,22 @@ it("records a portable export inventory through MCP without a binary target", as
name: "inspect_native_exports",
arguments: { path },
});
expect(
parseEvidence(response.structuredContent).normalized_result,
).toMatchObject({
format: "elf",
exports: [
{ name: "exported_fn", version: null },
{
name: "versioned_fn",
version: {
name: "libcafé.so",
name_bytes_base64: "bGliY2Fmw6kuc28=",
name_utf8_decodable: true,
},
},
],
});
assertRecordedMcpEvidence(
response,
"inspect_native_exports",
@@ -0,0 +1,78 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import { createModelArtifactService } from "../../../src/composition/model.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { freezeJsonSnapshot } from "../../../src/domain/immutableJson.js";
import { ggufFixture } from "../../fixtures/model/model.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it("checks each model parent subject independently of cached declarations", async () => {
const root = await createTestTempDirectory("rea-model-view-identity-");
const path = join(root, "selected.gguf");
await writeFile(path, ggufFixture().bytes);
const service = createModelArtifactService();
try {
const inspected = await service.inspect({
path,
hash: "full",
detail: "complete",
});
if (!inspected.ok) throw inspected.error;
const original = inspected.value;
if (original.subject === null) throw new Error("Missing full-file subject");
const report = freezeJsonSnapshot(original.normalized_result);
const parent = (name: string, targetPath: string, digest: string) =>
createEvidence(
{ path: targetPath, sha256: digest, format: "file" },
original.provider,
{
operation: original.operation,
parameters: { path, hash: "full", fixture: name },
result: report,
confidence: "observed",
limitations: original.limitations,
},
);
const digest = original.subject.digest.sha256;
const valid = parent("valid", path, digest);
const conflicting = [
parent("wrong-path", join(root, "other.gguf"), digest),
parent("wrong-digest", path, "00".repeat(32)),
];
const project = (record: typeof valid) =>
inspectAnalysisView(
{
source: {
kind: "retained-evidence",
evidence_id: record.evidence_id,
},
view: { kind: "summary" },
},
(id) => (id === record.evidence_id ? record : undefined),
);
for (const record of conflicting)
expect(record.normalized_result).toBe(report);
for (const phase of ["cold", "warm"]) {
for (const record of conflicting) {
expect(await project(record), phase).toMatchObject({
ok: false,
error: {
_tag: "AnalysisInputError",
issues: [{ path: ["source", "subject"], reason: "invalid_value" }],
},
});
}
const projected = await project(valid);
if (!projected.ok) throw projected.error;
expect(projected.value.subject).toEqual(original.subject);
expect(projected.value.normalized_result).toMatchObject({
artifact: { path, sha256: digest },
});
}
} finally {
await service.close();
}
});
@@ -4,6 +4,7 @@ import {
createRustImage,
createRustImageWithGoSection,
createRustHashOverflowImage,
compressRustElfSectionNames,
RUSTC_HASH,
rustDependencyPayload,
} from "../../fixtures/rust/image.js";
@@ -127,6 +128,43 @@ it("returns null metadata when the dependency section is absent", () => {
]);
});
it("reads dependency metadata through a compressed ELF section-name table", () => {
const image = readRustBinaryImage(
compressRustElfSectionNames(createRustImage({ format: "elf" })),
);
expect(image.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
file_offset: 1024,
packages,
dependency_edges: [{ from_index: 1, to_index: 0 }],
});
});
it("keeps dependency presence unknown for unsupported ELF name compression", () => {
const image = readRustBinaryImage(
compressRustElfSectionNames(createRustImage({ format: "elf" }), 2),
);
expect(image.auditable_metadata).toBeNull();
expect(image.section_names_limitation).toMatch(
/compression type 2 is unsupported/u,
);
expect(image.rustc_commit?.hashes).toEqual([
{ hash: RUSTC_HASH, file_offset: 2048 },
]);
});
it("keeps dependency presence unknown when its ELF name offset is unreadable", () => {
const bytes = createRustImage({ format: "elf" });
bytes.writeUInt32LE(0xffff, 384);
const image = readRustBinaryImage(bytes);
expect(image.auditable_metadata).toBeNull();
expect(image.section_names_limitation).toMatch(/outside its string table/u);
expect(image.rustc_commit?.hashes).toEqual([
{ hash: RUSTC_HASH, file_offset: 2048 },
]);
});
it("finds rustc paths in read-only ELF load data without section headers", () => {
const bytes = createRustImage({
format: "elf",
+21
View File
@@ -187,6 +187,27 @@ export const createIl2cppBinaryFixture = (
return bytes;
};
/** Add one classic dyld bind for a method slot, retaining its chosen raw word. */
export const createIl2cppDyldBindFixture = (raw: bigint): Buffer => {
const bytes = Buffer.from(createIl2cppBinaryFixture());
const command = 104;
const streamOffset = 0xf00;
const stream = Buffer.concat([
Buffer.from([0x3e, 0x40]),
Buffer.from("_FixtureMethod\0", "utf8"),
Buffer.from([0x51, 0x70, 0x80, 0x06, 0x90, 0x00]),
]);
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(120, 20);
bytes.writeUInt32LE(0x80000022, command);
bytes.writeUInt32LE(48, command + 4);
bytes.writeUInt32LE(streamOffset, command + 16);
bytes.writeUInt32LE(stream.length, command + 20);
stream.copy(bytes, streamOffset);
bytes.writeBigUInt64LE(raw, 0x300);
return bytes;
};
/** Wrap thin Mach-O slices in a 32-bit universal header, page aligned. */
export const createUniversalFixture = (
slices: readonly { readonly cpu: number; readonly bytes: Buffer }[],
+32
View File
@@ -17,6 +17,38 @@ export const rustDependencyPayload = (
},
): Buffer => deflateSync(Buffer.from(JSON.stringify(document)));
/** Compress the ELF section-name table without changing other sections. */
export const compressRustElfSectionNames = (
bytes: Buffer,
compressionType = 1,
): Buffer => {
const sectionOffset = Number(bytes.readBigUInt64LE(40));
const sectionWidth = bytes.readUInt16LE(58);
const namesIndex = bytes.readUInt16LE(62);
const nameHeader = sectionOffset + namesIndex * sectionWidth;
const namesOffset = Number(bytes.readBigUInt64LE(nameHeader + 24));
const namesSize = Number(bytes.readBigUInt64LE(nameHeader + 32));
const names = Buffer.from(
bytes.subarray(namesOffset, namesOffset + namesSize),
);
const compressed = deflateSync(names);
const compressionHeader = Buffer.alloc(24);
compressionHeader.writeUInt32LE(compressionType, 0);
compressionHeader.writeBigUInt64LE(BigInt(names.length), 8);
compressionHeader.writeBigUInt64LE(1n, 16);
compressionHeader.copy(bytes, namesOffset);
compressed.copy(bytes, namesOffset + compressionHeader.length);
bytes.writeBigUInt64LE(
bytes.readBigUInt64LE(nameHeader + 8) | 0x800n,
nameHeader + 8,
);
bytes.writeBigUInt64LE(
BigInt(compressionHeader.length + compressed.length),
nameHeader + 32,
);
return bytes;
};
export const RUSTC_HASH = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const RUSTC_PATH = `/rustc/${RUSTC_HASH}/library/core`;