Files
rea/src/native/metadata/BinaryContainer.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

289 lines
8.4 KiB
TypeScript

/** Distinguish corrupt recognized binaries from containers outside parser coverage. */
export class BinaryFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
) {
super(message);
this.name = "BinaryFormatFailure";
}
}
/** Resource boundary exceeded while decoding metadata or native container structure. */
export class BinaryResourceFailure extends Error {
constructor(
readonly boundary: "build-info" | "structure" | "search",
readonly maximum_bytes: number,
message: string,
) {
super(message);
this.name = "BinaryResourceFailure";
}
}
/** One file-backed virtual range; addresses stay exact until mapped to file offsets. */
export interface FileMapping {
readonly address: bigint;
readonly offset: number;
readonly size: number;
}
/** Declared zero-initialized virtual memory with no corresponding file bytes. */
export interface ZeroFillMapping {
readonly address: bigint;
readonly size: bigint;
}
/** Native loader region facts; language-specific fallback selection belongs to consumers. */
export interface LoadRegion {
readonly mapping: FileMapping;
readonly memory_size: bigint;
readonly read: boolean;
readonly write: boolean;
readonly execute: boolean;
/** Raw Mach-O VM_PROT masks preserve bits not represented by booleans. */
readonly protection_masks?: {
readonly current: number;
readonly maximum: number;
};
}
/** 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;
readonly read_only: boolean;
readonly compressed: boolean;
} & (
| {
readonly format: "elf";
readonly type: number;
readonly address: bigint;
readonly declared_range: {
readonly offset: bigint;
readonly size: bigint;
};
}
| {
readonly format: "pe";
readonly virtual_address: number;
readonly virtual_size: number;
readonly raw_size: number;
readonly characteristics: number;
}
| {
readonly format: "macho";
readonly section_name: string;
readonly type: number;
readonly address: bigint;
readonly declared_range: {
readonly offset: bigint;
readonly size: bigint;
};
readonly segment_mapping: FileMapping | null;
readonly segment_unmapped: boolean;
}
);
/** Native container facts, without language metadata search policy. */
export interface BinaryContainer {
readonly format: "elf" | "pe" | "macho";
readonly architecture: string;
readonly bits: 32 | 64;
readonly byte_order: "little" | "big";
/** Declared PE preferred base, when available. */
readonly image_base?: bigint;
/** Mach-O load-command observation, when available. */
readonly macho_chained_fixups?: boolean;
readonly mappings: readonly FileMapping[];
readonly load_regions?: readonly LoadRegion[];
/** File-backed ranges the native loader marks executable. */
readonly executable_mappings?: readonly FileMapping[];
readonly zero_fills: readonly ZeroFillMapping[];
readonly sections: readonly ImageSection[];
}
/** Reader that checks every structural range before decoding numeric fields. */
export class BinaryReader {
constructor(
readonly bytes: Buffer,
readonly little: boolean,
) {}
range(offset: number, size: number, label: string): Buffer {
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
offset < 0 ||
size < 0 ||
offset > this.bytes.length ||
size > this.bytes.length - offset
)
throw new BinaryFormatFailure(
"malformed",
`${label} range is truncated or outside the file`,
);
return this.bytes.subarray(offset, offset + size);
}
u16(offset: number): number {
const value = this.range(offset, 2, "16-bit field");
return this.little ? value.readUInt16LE() : value.readUInt16BE();
}
u32(offset: number): number {
const value = this.range(offset, 4, "32-bit field");
return this.little ? value.readUInt32LE() : value.readUInt32BE();
}
word(offset: number, bits: 32 | 64): bigint {
if (bits === 32) return BigInt(this.u32(offset));
const value = this.range(offset, 8, "64-bit field");
return this.little ? value.readBigUInt64LE() : value.readBigUInt64BE();
}
fileRange(offset: bigint, size: bigint, label: string): FileMapping {
if (
offset > BigInt(this.bytes.length) ||
size > BigInt(this.bytes.length) - offset
)
throw new BinaryFormatFailure(
"malformed",
`${label} range is outside the file`,
);
const result = { address: 0n, offset: Number(offset), size: Number(size) };
this.range(result.offset, result.size, label);
return result;
}
table(offset: bigint, count: number, width: number, label: string): number {
const size = BigInt(count) * BigInt(width);
if (size > 16n * 1024n * 1024n)
throw new BinaryResourceFailure(
"structure",
16 * 1024 * 1024,
`${label} exceeds the 16 MiB structural decoding budget`,
);
return this.fileRange(offset, size, label).offset;
}
}
/** Admit a declared file span, or mark it unbacked when it leaves the file. */
export const declaredFileRange = (
fileBytes: number,
offset: bigint,
size: bigint,
): {
readonly offset: number;
readonly size: number;
readonly file_backed: boolean;
} => {
if (
offset < 0n ||
size < 0n ||
offset > BigInt(Number.MAX_SAFE_INTEGER) ||
size > BigInt(Number.MAX_SAFE_INTEGER) ||
offset > BigInt(fileBytes) ||
size > BigInt(fileBytes) - offset
)
return { offset: 0, size: 0, file_backed: false };
return {
offset: Number(offset),
size: Number(size),
file_backed: size > 0n,
};
};
/** Map a virtual span without losing precision or selecting an ambiguous alias. */
export const mappedFileOffset = (
mappings: readonly FileMapping[],
address: bigint,
size: number,
zeroFills: readonly ZeroFillMapping[] = [],
): number => {
let found: number | undefined;
for (const mapping of mappings) {
const delta = address - mapping.address;
if (
delta < 0n ||
delta > BigInt(mapping.size) ||
BigInt(size) > BigInt(mapping.size) - delta
)
continue;
const offset = mapping.offset + Number(delta);
if (found !== undefined && found !== offset)
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file mappings",
);
found = offset;
}
if (found === undefined)
throw new BinaryFormatFailure(
"malformed",
"Virtual address or string range is not file mapped",
);
for (const zeroFill of zeroFills) {
if (spansOverlap(address, BigInt(size), zeroFill.address, zeroFill.size))
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
const end = address + BigInt(size);
for (const mapping of mappings) {
const start = address > mapping.address ? address : mapping.address;
const mappingEnd = mapping.address + BigInt(mapping.size);
const overlapEnd = end < mappingEnd ? end : mappingEnd;
if (start >= overlapEnd) continue;
if (
BigInt(found) + start - address !==
BigInt(mapping.offset) + start - mapping.address
)
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file mappings",
);
}
return found;
};
/** Recognize a wholly zero-filled span while refusing even partial file aliases. */
export const isZeroFilledSpan = (
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
address: bigint,
size: number,
): boolean => {
const bytes = BigInt(size);
if (
!zeroFills.some(
(mapping) =>
address >= mapping.address &&
address + bytes <= mapping.address + mapping.size,
)
)
return false;
for (const mapping of mappings) {
if (spansOverlap(address, bytes, mapping.address, BigInt(mapping.size)))
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
return true;
};
const spansOverlap = (
address: bigint,
size: bigint,
otherAddress: bigint,
otherSize: bigint,
): boolean =>
size > 0n &&
otherSize > 0n &&
address < otherAddress + otherSize &&
otherAddress < address + size;