mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
c19c2d6723
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
182 lines
5.7 KiB
TypeScript
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);
|
|
};
|