Files
rea/src/rust/RustBinaryImage.ts
T
morluto c19c2d6723 fix(native): separate container facts from Go metadata admission
Move ELF, PE and Mach-O container parsing into a shared native owner. Go alone selects and validates Go build-info sections, so unrelated malformed Go metadata no longer blocks Rust dependency or IL2CPP inspection. Remove the superseded Go-specific container representation and migrate all consumers. Adapted from draft PR #5510 without asserting human PR readiness.

Validation: 51 compiled CLI/MCP acceptance tests across Rust, Go and IL2CPP; 186 shared-reader tests; full check:ci passed. Three public synthetic-file probes reproduce the baseline Rust refusal and validate the fixed SDK-advertised MCP output, artifact digests and unchanged bytes. The exact zero-address nonallocated ELF case now succeeds for Rust while Go still reports invalid_format. No target execution or real compiler-artifact qualification was performed; no performance improvement is claimed.

Closes #3398
2026-10-21 11:58:15 +08:00

182 lines
5.7 KiB
TypeScript

import {
BinaryFormatFailure,
BinaryResourceFailure,
type ImageSection,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import type {
RustAuditableMetadata,
RustBinary,
} from "../domain/rust/rustBinary.js";
import {
readAuditableSection,
RustAuditableFormatFailure,
RustAuditableResourceFailure,
} from "./RustAuditableReader.js";
const RUSTC_PREFIX = Buffer.from("/rustc/");
const HASH_BYTES = 40;
/** One toolchain normally leaves one hash; a small cap bounds retained output. */
const MAX_RUSTC_HASHES = 64;
/** Container identity plus optional dependency metadata and inferred rustc commits. */
export interface RustBinaryImage {
readonly format: RustBinary["format"];
readonly architecture: string;
readonly bits: 32 | 64;
readonly byte_order: RustBinary["byte_order"];
readonly auditable_metadata: RustAuditableMetadata | null;
readonly rustc_commit: RustBinary["rustc_commit"];
}
/** Read dependency metadata from container sections without executing the image. */
export const readRustBinaryImage = (bytes: Buffer): RustBinaryImage => {
const container = readContainer(bytes);
const dependency = dependencySection(container.format, container.sections);
return {
format: container.format,
architecture: container.architecture,
bits: container.bits,
byte_order: container.byte_order,
auditable_metadata:
dependency === null
? null
: readDependency(bytes, container.format, dependency),
rustc_commit: rustcCommit(bytes, container.sections),
};
};
const readContainer = (bytes: Buffer) => {
try {
return readBinaryContainer(bytes);
} catch (cause) {
if (cause instanceof BinaryFormatFailure)
throw new RustAuditableFormatFailure(cause.kind, cause.message, {
cause,
});
if (cause instanceof BinaryResourceFailure)
throw new RustAuditableResourceFailure(
{
resource: "memory",
reported_limits: { maximum_bytes: cause.maximum_bytes },
},
cause.message,
{
cause,
},
);
throw cause;
}
};
const dependencySection = (
format: RustBinaryImage["format"],
sections: readonly ImageSection[],
): ImageSection | null => {
const expected = format === "macho" ? "__DATA,.dep-v0" : ".dep-v0";
const matches = sections.filter((section) => section.name === expected);
if (matches.length > 1)
throw new RustAuditableFormatFailure(
"malformed",
"Image contains more than one dependency section",
);
const section = matches[0];
if (section === undefined) return null;
if (section.compressed)
throw new RustAuditableFormatFailure(
"unsupported",
"Compressed dependency sections are unsupported",
);
if (!section.file_backed)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency section is not file-backed",
);
return section;
};
const readDependency = (
bytes: Buffer,
format: RustBinaryImage["format"],
section: ImageSection,
): RustAuditableMetadata =>
readAuditableSection(
format === "macho" ? "__DATA,.dep-v0" : ".dep-v0",
section.offset,
bytes.subarray(section.offset, section.offset + section.size),
);
const rustcCommit = (
bytes: Buffer,
sections: readonly ImageSection[],
): RustBinaryImage["rustc_commit"] => {
const hashes: { hash: string; file_offset: number }[] = [];
const seen = new Set<string>();
// Overlapping declared sections are scanned once, so total work stays
// bounded by the file size however many sections reference a range.
for (const range of mergedReadOnlyRanges(sections))
collectHashes(bytes, range, seen, hashes);
return hashes.length === 0 ? null : { evidence_kind: "inferred", hashes };
};
const mergedReadOnlyRanges = (
sections: readonly ImageSection[],
): { readonly offset: number; readonly end: number }[] => {
const ranges = sections
.filter((section) => section.read_only && section.size > 0)
.map((section) => ({
offset: section.offset,
end: section.offset + section.size,
}))
.sort((left, right) => left.offset - right.offset);
const merged: { offset: number; end: number }[] = [];
for (const range of ranges) {
const last = merged.at(-1);
if (last !== undefined && range.offset <= last.end)
last.end = Math.max(last.end, range.end);
else merged.push({ ...range });
}
return merged;
};
const collectHashes = (
bytes: Buffer,
range: { readonly offset: number; readonly end: number },
seen: Set<string>,
hashes: { hash: string; file_offset: number }[],
): void => {
const region = bytes.subarray(range.offset, range.end);
let cursor = 0;
while (cursor < region.length) {
const at = region.indexOf(RUSTC_PREFIX, cursor);
if (at < 0) return;
const start = at + RUSTC_PREFIX.length;
const hash = hashAt(region, start);
if (hash !== null && !seen.has(hash)) {
if (seen.size >= MAX_RUSTC_HASHES)
throw new RustAuditableResourceFailure(
{
resource: "capacity",
reported_limits: { maximum_rustc_hashes: MAX_RUSTC_HASHES },
},
`Read-only data contains more than ${MAX_RUSTC_HASHES} distinct rustc commit paths.`,
);
seen.add(hash);
hashes.push({ hash, file_offset: range.offset + at });
}
cursor = at + 1;
}
};
const hashAt = (region: Buffer, start: number): string | null => {
const end = start + HASH_BYTES;
if (end >= region.length || region[end] !== 0x2f) return null;
for (let index = start; index < end; index += 1) {
const byte = region[index] ?? 0;
const hex =
(byte >= 0x30 && byte <= 0x39) || (byte >= 0x61 && byte <= 0x66);
if (!hex) return null;
}
return region.toString("latin1", start, end);
};