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
This commit is contained in:
morluto
2026-10-21 11:58:15 +08:00
parent 08f4ba99ce
commit c19c2d6723
19 changed files with 876 additions and 420 deletions
+50 -85
View File
@@ -1,25 +1,14 @@
import {
GoBinaryFormatFailure,
GoBinaryResourceFailure,
GoBinaryReader,
goUtf8,
BinaryFormatFailure,
BinaryResourceFailure,
BinaryReader,
mappedFileOffset,
isZeroFilledSpan,
type GoBinaryContainer,
type GoFileMapping,
} from "./GoBinaryContainer.js";
import {
classifyFatContainerIdentity,
readFatContainerHeader,
} from "../domain/apple/machoContainer.js";
import { readGoElfImage } from "./GoElfImage.js";
import { readGoMachoImage } from "./GoMachoImage.js";
import { readGoPeImage } from "./GoPeImage.js";
export {
GoBinaryFormatFailure,
GoBinaryResourceFailure,
} from "./GoBinaryContainer.js";
type BinaryContainer,
type FileMapping,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import { goBuildInfoSearch } from "./GoBuildInfoSearch.js";
const MAGIC = Buffer.from("ff20476f206275696c64696e663a", "hex");
const MODULE_START = Buffer.from("3077af0c9274080241e1c107e6d618e6", "hex");
@@ -56,19 +45,19 @@ export interface GoBinaryImage {
/** Inspect bounded executable bytes using native mappings, without loading or executing code. */
export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const container = readGoBinaryContainer(bytes);
const container = readBinaryContainer(bytes);
const { format, architecture, bits, byte_order } = container;
let selected: { search: GoFileMapping; header: number } | null = null;
let selected: { search: FileMapping; header: number } | null = null;
const searched = new Set<string>();
let remainingSearchBytes = MAX_HEADER_SEARCH_BYTES;
for (const search of container.search) {
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.
const identity = `${search.offset}:${search.size}:${search.address % 16n}`;
if (searched.has(identity)) continue;
searched.add(identity);
if (search.size > remainingSearchBytes)
throw new GoBinaryResourceFailure(
throw new BinaryResourceFailure(
"search",
MAX_HEADER_SEARCH_BYTES,
"Go build-info discovery exceeds the cumulative 256 MiB header-search budget",
@@ -93,22 +82,22 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
container.zero_fills,
) !== header
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info bytes disagree with their virtual mapping",
);
};
verifyMapping(32);
const reader = new GoBinaryReader(bytes, byte_order === "little");
const reader = new BinaryReader(bytes, byte_order === "little");
const ptrSize = bytes[header + 14];
const flags = bytes[header + 15] ?? 0;
if ((flags & ~3) !== 0)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"unsupported",
"Go build-info flags contain an unsupported format bit",
);
if ((flags & 2) === 0 && (flags & 1) !== (byte_order === "big" ? 1 : 0))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info byte order disagrees with the image",
);
@@ -125,12 +114,12 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
verifyMapping(module.location.offset + module.location.bytes - header);
} else {
if (ptrSize !== 4 && ptrSize !== 8)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info pointer width must be 4 or 8",
);
if (ptrSize * 8 !== bits)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info pointer width disagrees with the image",
);
@@ -149,13 +138,13 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
);
}
if (version.location.bytes + module.location.bytes > MAX_BUILD_INFO_BYTES)
throw new GoBinaryResourceFailure(
throw new BinaryResourceFailure(
"build-info",
MAX_BUILD_INFO_BYTES,
"Go build-info strings exceed the aggregate 1 MiB limit",
);
if (version.bytes.length === 0)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info compiler version is empty",
);
@@ -168,7 +157,7 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
!module.bytes.subarray(-16).equals(MODULE_END) ||
module.bytes[module.bytes.length - 17] !== 10
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go module string framing is invalid",
);
@@ -194,9 +183,9 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const observedText = (bytes: Buffer): string | null => {
try {
return goUtf8(bytes, "Go build string");
return observedUtf8(bytes, "Go build string");
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return null;
if (cause instanceof BinaryFormatFailure) return null;
throw cause;
}
};
@@ -206,45 +195,7 @@ interface LocatedString {
readonly location: GoStringLocation;
}
/** Identify an ELF, PE, or thin Mach-O image and its named file sections. */
export const readGoBinaryContainer = (bytes: Buffer): GoBinaryContainer => {
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
return readGoElfImage(bytes);
if (bytes.subarray(0, 2).equals(Buffer.from("MZ")))
return readGoPeImage(bytes);
if (bytes.length >= 4) {
const magic = bytes.readUInt32BE();
if ([0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca].includes(magic)) {
const fatHeader = readFatContainerHeader(bytes, bytes.length);
if (fatHeader.state === "malformed")
throw new GoBinaryFormatFailure("malformed", fatHeader.reason);
const identity =
fatHeader.state === "parsed"
? classifyFatContainerIdentity(fatHeader, (offset) => {
if (offset >= bytes.length) return undefined;
if (offset + 4 > bytes.length) return null;
return bytes.readUInt32BE(offset);
})
: undefined;
// An invalid FAT-shaped table is not proof of a Mach-O container: JVM
// constant-pool bytes can share both the magic and a known CPU value.
const confirmedFatMachO = identity?.state === "mach-o";
if (confirmedFatMachO)
throw new GoBinaryFormatFailure(
"unsupported",
"Universal/fat Mach-O containers require selecting a thin slice",
);
}
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
return readGoMachoImage(bytes);
}
throw new GoBinaryFormatFailure(
"unsupported",
"Only ELF, PE, and thin Mach-O native images are supported",
);
};
const findHeader = (bytes: Buffer, search: GoFileMapping): number | null => {
const findHeader = (bytes: Buffer, search: FileMapping): number | null => {
if (search.size === 0) return null;
const end = search.offset + search.size;
const selected = bytes.subarray(search.offset, end);
@@ -256,7 +207,7 @@ const findHeader = (bytes: Buffer, search: GoFileMapping): number | null => {
const address = search.address + BigInt(candidate - search.offset);
if (address % 16n === 0n) {
if (end - candidate < 32)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info header is truncated",
);
@@ -268,34 +219,34 @@ const findHeader = (bytes: Buffer, search: GoFileMapping): number | null => {
};
const readInlineString = (
reader: GoBinaryReader,
reader: BinaryReader,
offset: number,
remaining: number,
): LocatedString => {
let length = 0n;
for (let index = 0; index < 10; index++) {
if (index >= remaining)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info varint is truncated",
);
const byte = reader.bytes[offset + index] ?? 0;
if (index === 9 && byte > 1)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info varint overflows 64 bits",
);
length |= BigInt(byte & 127) << BigInt(index * 7);
if ((byte & 128) === 0) {
if (length > BigInt(MAX_BUILD_INFO_BYTES))
throw new GoBinaryResourceFailure(
throw new BinaryResourceFailure(
"build-info",
MAX_BUILD_INFO_BYTES,
"Go build-info string length exceeds the 1 MiB limit",
);
const size = Number(length);
if (size > remaining - index - 1)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info string range is truncated",
);
@@ -306,15 +257,15 @@ const readInlineString = (
};
}
}
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Go build-info varint is unterminated",
);
};
const readPointerString = (
reader: GoBinaryReader,
container: GoBinaryContainer,
reader: BinaryReader,
container: BinaryContainer,
address: bigint,
): LocatedString => {
const { mappings, zero_fills: zeroFills, bits } = container;
@@ -323,7 +274,7 @@ const readPointerString = (
const valueAddress = reader.word(header, bits);
const length = reader.word(header + width, bits);
if (length > BigInt(MAX_BUILD_INFO_BYTES))
throw new GoBinaryResourceFailure(
throw new BinaryResourceFailure(
"build-info",
MAX_BUILD_INFO_BYTES,
"Go build-info pointer string length exceeds the 1 MiB limit",
@@ -341,8 +292,8 @@ const readPointerString = (
};
const readOptionalModuleString = (
reader: GoBinaryReader,
container: GoBinaryContainer,
reader: BinaryReader,
container: BinaryContainer,
address: bigint,
pointerField: number,
): LocatedString =>
@@ -354,3 +305,17 @@ const readOptionalModuleString = (
)
? { bytes: Buffer.alloc(0), location: { offset: pointerField, bytes: 0 } }
: readPointerString(reader, container, address);
/** Decode metadata text without replacing invalid bytes or stripping an initial BOM. */
export const observedUtf8 = (bytes: Buffer, label: string): string => {
try {
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes,
);
} catch (cause) {
throw new BinaryFormatFailure(
"malformed",
`${label} contains malformed UTF-8: ${String(cause)}`,
);
}
};
+6 -6
View File
@@ -30,11 +30,11 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok, type Result } from "../domain/result.js";
import { readGoBinaryImage } from "./GoBinaryImage.js";
import {
readGoBinaryImage,
GoBinaryFormatFailure,
GoBinaryResourceFailure,
} from "./GoBinaryImage.js";
BinaryFormatFailure,
BinaryResourceFailure,
} from "../native/metadata/BinaryContainer.js";
import { parseGoModuleBytes, parseGoModuleText } from "./GoModuleText.js";
/** Bundled format reader identity; embedded compiler versions are separate observed facts. */
@@ -57,7 +57,7 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof GoBinaryResourceFailure)
if (cause instanceof BinaryResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
cause.boundary === "search" ? "cpu" : "memory",
@@ -75,7 +75,7 @@ const failureForPrimary = (cause: unknown, path: string): AnalysisError => {
),
},
);
if (cause instanceof GoBinaryFormatFailure)
if (cause instanceof BinaryFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, {
cause,
+137
View File
@@ -0,0 +1,137 @@
import {
BinaryFormatFailure,
BinaryReader,
mappedFileOffset,
type BinaryContainer,
type FileMapping,
} from "../native/metadata/BinaryContainer.js";
const fallbackSearchRanges = (
container: BinaryContainer,
): readonly FileMapping[] => {
if (container.format === "elf")
return (container.load_regions ?? [])
.filter((region) => region.write && !region.execute)
.map((region) => region.mapping);
if (container.format === "macho")
return (container.load_regions ?? [])
.filter(
(region) =>
region.mapping.address !== 0n &&
region.protection_masks?.current === 3 &&
region.protection_masks.maximum === 3,
)
.slice(0, 1)
.map((region) => region.mapping);
if (container.image_base === undefined) return [];
const section = container.sections.find(
(candidate) =>
candidate.format === "pe" &&
candidate.virtual_address !== 0 &&
candidate.raw_size !== 0 &&
(candidate.characteristics & ~0x00600000) >>> 0 === 0xc0000040,
);
return section?.format === "pe"
? [
{
address: container.image_base + BigInt(section.virtual_address),
offset: section.offset,
size: section.size,
},
]
: [];
};
/** Go owns build-info section selection and validity; native readers only own container facts. */
export const goBuildInfoSearch = (
bytes: Buffer,
container: BinaryContainer,
): readonly FileMapping[] => {
const reader = new BinaryReader(bytes, container.byte_order === "little");
const named: FileMapping[] = [];
for (const section of container.sections) {
if (section.format === "elf" && section.name === ".go.buildinfo") {
if (named.length !== 0)
throw new BinaryFormatFailure(
"malformed",
"ELF contains ambiguous build-info sections",
);
if (section.type !== 1)
throw new BinaryFormatFailure(
"malformed",
"ELF build-info section is not file-backed data",
);
if (section.compressed)
throw new BinaryFormatFailure(
"unsupported",
"Compressed ELF build-info sections are unsupported",
);
const search = {
...reader.fileRange(
section.declared_range.offset,
section.declared_range.size,
"ELF build-info section",
),
address: section.address,
};
if (
mappedFileOffset(
container.mappings,
search.address,
search.size,
container.zero_fills,
) !== search.offset
)
throw new BinaryFormatFailure(
"malformed",
"ELF build-info section disagrees with its load mapping",
);
named.push(search);
} else if (
section.format === "macho" &&
section.section_name === "__go_buildinfo"
) {
if (section.segment_unmapped)
throw new BinaryFormatFailure(
"malformed",
"Mach-O build-info section is in an unmapped segment",
);
if (section.segment_mapping === null)
throw new BinaryFormatFailure(
"malformed",
"Mach-O build-info section is not file mapped",
);
if ([1, 0xc, 0x12].includes(section.type))
throw new BinaryFormatFailure(
"malformed",
"Mach-O build-info section is zero-fill data",
);
const search = {
...reader.fileRange(
section.declared_range.offset,
section.declared_range.size,
"Mach-O build-info section",
),
address: section.address,
};
if (
mappedFileOffset(
[section.segment_mapping],
search.address,
search.size,
) !== search.offset
)
throw new BinaryFormatFailure(
"malformed",
"Mach-O build-info section disagrees with its segment",
);
named.push(search);
}
}
if (named.length > 1)
throw new BinaryFormatFailure(
"malformed",
"Mach-O has ambiguous build-info sections",
);
return named.length > 0 ? named : fallbackSearchRanges(container);
};
+1 -1
View File
@@ -7,7 +7,7 @@ it("rejects a 12-byte load command in a 64-bit Go Mach-O image", () => {
readGoBinaryImage(Buffer.from(machoImageWithUnknownCommand(true, 12))),
).toThrow(
expect.objectContaining({
name: "GoBinaryFormatFailure",
name: "BinaryFormatFailure",
kind: "malformed",
}),
);
+10 -10
View File
@@ -1,10 +1,10 @@
import {
GoBinaryFormatFailure,
BinaryFormatFailure,
mappedFileOffset,
isZeroFilledSpan,
type GoFileMapping,
type GoZeroFillMapping,
} from "../go/GoBinaryContainer.js";
type FileMapping,
type ZeroFillMapping,
} from "../native/metadata/BinaryContainer.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
const DT_RELA = 7n,
@@ -26,8 +26,8 @@ interface RelocatedImage {
}
interface RelocationSource {
readonly image: Buffer;
readonly mappings: readonly GoFileMapping[];
readonly zeroFills: readonly GoZeroFillMapping[];
readonly mappings: readonly FileMapping[];
readonly zeroFills: readonly ZeroFillMapping[];
readonly relative: number;
readonly unknownPointerKinds: ReadonlySet<number>;
}
@@ -101,14 +101,14 @@ function* dynamicTags(image: Buffer): Generator<void, Map<bigint, bigint>> {
}
const fileOffset = (
mappings: readonly GoFileMapping[],
mappings: readonly FileMapping[],
address: bigint,
size: number,
): number | null => {
try {
return mappedFileOffset(mappings, address, size);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return null;
if (cause instanceof BinaryFormatFailure) return null;
throw cause;
}
};
@@ -178,8 +178,8 @@ function* applyRelaTable(
/** Materialize relative pointers in a private copy, preserving unknown and .bss facts. */
export function* applyElfRelativeRelocationsSteps(
image: Buffer,
mappings: readonly GoFileMapping[],
zeroFills: readonly GoZeroFillMapping[],
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
): Generator<void, RelocatedImage> {
if (image.length < 64) throw malformed("ELF header is truncated");
const kinds = RELOCATION_KINDS.get(image.readUInt16LE(18));
+17 -17
View File
@@ -3,13 +3,13 @@ import {
validateFatSlice,
} from "../domain/apple/machoContainer.js";
import {
GoBinaryFormatFailure,
GoBinaryResourceFailure,
BinaryFormatFailure,
BinaryResourceFailure,
mappedFileOffset,
isZeroFilledSpan,
type GoBinaryContainer,
} from "../go/GoBinaryContainer.js";
import { readGoBinaryContainer } from "../go/GoBinaryImage.js";
type BinaryContainer,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import { applyElfRelativeRelocationsSteps } from "./ElfRelocations.js";
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
const MACHO_CPU = new Map([
@@ -64,7 +64,7 @@ const selectImage = (
export interface NativeImage {
readonly bytes: Buffer;
readonly container: GoBinaryContainer;
readonly container: BinaryContainer;
readonly imageBase: bigint;
readonly unresolvedPointerSlots: ReadonlySet<bigint>;
readonly zeroFillPointers: ReadonlyMap<bigint, bigint>;
@@ -75,16 +75,16 @@ export function* openImageSteps(
architecture: string | undefined,
): Generator<void, NativeImage> {
let image = selectImage(bytes, architecture);
let container: GoBinaryContainer;
let container: BinaryContainer;
try {
container = readGoBinaryContainer(image);
container = readBinaryContainer(image);
} catch (cause) {
if (cause instanceof GoBinaryResourceFailure)
if (cause instanceof BinaryResourceFailure)
throw new Il2cppBinaryFormatFailure(
"native-index-capacity",
cause.message,
);
if (cause instanceof GoBinaryFormatFailure)
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure(
cause.kind === "unsupported"
? "unsupported-container"
@@ -121,7 +121,7 @@ export function* openImageSteps(
container.zero_fills,
);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure)
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
throw cause;
}
@@ -172,7 +172,7 @@ export const readPointer = (
);
return image.bytes.readBigUInt64LE(offset);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return null;
if (cause instanceof BinaryFormatFailure) return null;
throw cause;
}
};
@@ -195,7 +195,7 @@ export const readUInt32 = (
),
);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return null;
if (cause instanceof BinaryFormatFailure) return null;
throw cause;
}
};
@@ -205,7 +205,7 @@ export const isMapped = (image: NativeImage, address: bigint, size: number) => {
mappedFileOffset(image.container.mappings, address, size);
return true;
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return false;
if (cause instanceof BinaryFormatFailure) return false;
throw cause;
}
};
@@ -262,7 +262,7 @@ export const hasPointerArrayCoverage = (
);
}
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return false;
if (cause instanceof BinaryFormatFailure) return false;
throw cause;
}
cursor += BigInt(size);
@@ -284,7 +284,7 @@ export const isExecutable = (image: NativeImage, address: bigint): boolean => {
mappedFileOffset(image.container.executable_mappings ?? [], address, 1)
);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure) return false;
if (cause instanceof BinaryFormatFailure) return false;
throw cause;
}
};
@@ -312,7 +312,7 @@ export const readSlot = (
try {
return isZeroFilled(image, address, 8) ? 0n : readPointer(image, address);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure)
if (cause instanceof BinaryFormatFailure)
throw new Il2cppBinaryFormatFailure("malformed-container", cause.message);
throw cause;
}
@@ -1,69 +1,112 @@
/** Distinguish corrupt recognized binaries from containers outside parser coverage. */
export class GoBinaryFormatFailure extends Error {
export class BinaryFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
) {
super(message);
this.name = "GoBinaryFormatFailure";
this.name = "BinaryFormatFailure";
}
}
/** Resource boundary exceeded while decoding metadata or native container structure. */
export class GoBinaryResourceFailure extends Error {
export class BinaryResourceFailure extends Error {
constructor(
readonly boundary: "build-info" | "structure" | "search",
readonly maximum_bytes: number,
message: string,
) {
super(message);
this.name = "GoBinaryResourceFailure";
this.name = "BinaryResourceFailure";
}
}
/** One file-backed virtual range; addresses stay exact until mapped to file offsets. */
export interface GoFileMapping {
export interface FileMapping {
readonly address: bigint;
readonly offset: number;
readonly size: number;
}
/** Declared zero-initialized virtual memory with no corresponding file bytes. */
export interface GoZeroFillMapping {
export interface ZeroFillMapping {
readonly address: bigint;
readonly size: bigint;
}
/** One named file section. Invalid declared ranges stay visible with `file_backed: false`. */
export interface GoImageSection {
/** 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 and its ordered producer-defined build-info search ranges. */
export interface GoBinaryContainer {
/** 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 preferred base when the container supplies one (PE). */
/** Declared PE preferred base, when available. */
readonly image_base?: bigint;
/** Mach-O load-command observation; omitted for other formats. */
/** Mach-O load-command observation, when available. */
readonly macho_chained_fixups?: boolean;
readonly mappings: readonly GoFileMapping[];
/** File-backed ranges the native loader marks executable; omitted if unobserved. */
readonly executable_mappings?: readonly GoFileMapping[];
readonly zero_fills: readonly GoZeroFillMapping[];
readonly sections: readonly GoImageSection[];
readonly search: readonly GoFileMapping[];
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 GoBinaryReader {
export class BinaryReader {
constructor(
readonly bytes: Buffer,
readonly little: boolean,
@@ -78,7 +121,7 @@ export class GoBinaryReader {
offset > this.bytes.length ||
size > this.bytes.length - offset
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
`${label} range is truncated or outside the file`,
);
@@ -101,12 +144,12 @@ export class GoBinaryReader {
return this.little ? value.readBigUInt64LE() : value.readBigUInt64BE();
}
fileRange(offset: bigint, size: bigint, label: string): GoFileMapping {
fileRange(offset: bigint, size: bigint, label: string): FileMapping {
if (
offset > BigInt(this.bytes.length) ||
size > BigInt(this.bytes.length) - offset
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
`${label} range is outside the file`,
);
@@ -118,7 +161,7 @@ export class GoBinaryReader {
table(offset: bigint, count: number, width: number, label: string): number {
const size = BigInt(count) * BigInt(width);
if (size > 16n * 1024n * 1024n)
throw new GoBinaryResourceFailure(
throw new BinaryResourceFailure(
"structure",
16 * 1024 * 1024,
`${label} exceeds the 16 MiB structural decoding budget`,
@@ -155,10 +198,10 @@ export const declaredFileRange = (
/** Map a virtual span without losing precision or selecting an ambiguous alias. */
export const mappedFileOffset = (
mappings: readonly GoFileMapping[],
mappings: readonly FileMapping[],
address: bigint,
size: number,
zeroFills: readonly GoZeroFillMapping[] = [],
zeroFills: readonly ZeroFillMapping[] = [],
): number => {
let found: number | undefined;
for (const mapping of mappings) {
@@ -171,20 +214,20 @@ export const mappedFileOffset = (
continue;
const offset = mapping.offset + Number(delta);
if (found !== undefined && found !== offset)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file mappings",
);
found = offset;
}
if (found === undefined)
throw new GoBinaryFormatFailure(
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 GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file and zero-fill mappings",
);
@@ -199,7 +242,7 @@ export const mappedFileOffset = (
BigInt(found) + start - address !==
BigInt(mapping.offset) + start - mapping.address
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file mappings",
);
@@ -209,8 +252,8 @@ export const mappedFileOffset = (
/** Recognize a wholly zero-filled span while refusing even partial file aliases. */
export const isZeroFilledSpan = (
mappings: readonly GoFileMapping[],
zeroFills: readonly GoZeroFillMapping[],
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
address: bigint,
size: number,
): boolean => {
@@ -225,7 +268,7 @@ export const isZeroFilledSpan = (
return false;
for (const mapping of mappings) {
if (spansOverlap(address, bytes, mapping.address, BigInt(mapping.size)))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Virtual address has ambiguous file and zero-fill mappings",
);
@@ -243,17 +286,3 @@ const spansOverlap = (
otherSize > 0n &&
address < otherAddress + otherSize &&
otherAddress < address + size;
/** Decode metadata text without replacing invalid bytes or stripping an initial BOM. */
export const goUtf8 = (bytes: Buffer, label: string): string => {
try {
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes,
);
} catch (cause) {
throw new GoBinaryFormatFailure(
"malformed",
`${label} contains malformed UTF-8: ${String(cause)}`,
);
}
};
@@ -1,22 +1,22 @@
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
BinaryFormatFailure,
BinaryReader,
type BinaryContainer,
type FileMapping,
type ImageSection,
type LoadRegion,
type ZeroFillMapping,
} from "./BinaryContainer.js";
import { readElfFixedHeader } from "../../domain/elfFixedHeader.js";
/** Read ELF build-info sections and file-backed program mappings. */
export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
/** Read ELF sections and file-backed program mappings without interpreting language metadata. */
export const readElfImage = (bytes: Buffer): BinaryContainer => {
const fixedHeader = readElfFixedHeader(bytes);
if (!fixedHeader.ok)
throw new GoBinaryFormatFailure("malformed", fixedHeader.error);
throw new BinaryFormatFailure("malformed", fixedHeader.error);
const { elfClass: bits, headerSize } = fixedHeader.value;
const reader = new GoBinaryReader(
const reader = new BinaryReader(
bytes,
fixedHeader.value.byteOrder === "little",
);
@@ -32,7 +32,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
(phCount !== 0 && phOffset < BigInt(headerSize)) ||
(shOffset !== 0n && shOffset < BigInt(headerSize))
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF header table overlaps the native header",
);
@@ -41,7 +41,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
(shCount === 0 || phCount === 0xffff || namesIndex === 0xffff)
) {
if (shWidth < (wide ? 64 : 40))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF extended section header width is invalid",
);
@@ -51,14 +51,14 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
"ELF section zero",
).offset;
if (reader.u32(first + 4) !== 0)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF initial section for extended numbering must have SHT_NULL type",
);
if (shCount === 0) {
const count = reader.word(first + (wide ? 32 : 20), bits);
if (count < 0xff00n || count > BigInt(Number.MAX_SAFE_INTEGER))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF extended section count is invalid",
);
@@ -67,7 +67,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
if (phCount === 0xffff) {
phCount = reader.u32(first + (wide ? 44 : 28));
if (phCount < 0xffff)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF extended program count is below its reserved range",
);
@@ -75,7 +75,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
if (namesIndex === 0xffff) {
namesIndex = reader.u32(first + (wide ? 40 : 24));
if (namesIndex < 0xff00)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF extended section name index is below its reserved range",
);
@@ -84,13 +84,10 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
shOffset === 0n &&
(shCount !== 0 || namesIndex !== 0 || phCount === 0xffff)
) {
throw new GoBinaryFormatFailure(
"malformed",
"ELF section table is missing",
);
throw new BinaryFormatFailure("malformed", "ELF section table is missing");
}
if (phCount !== 0 && phWidth < (wide ? 56 : 32))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF program header width is invalid",
);
@@ -100,11 +97,11 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
phWidth,
"ELF program headers",
);
const mappings: GoFileMapping[] = [];
const executableMappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const fallback: GoFileMapping[] = [];
const mappings: FileMapping[] = [];
const loadRegions: LoadRegion[] = [];
const executableMappings: FileMapping[] = [];
const zeroFills: ZeroFillMapping[] = [];
const sections: ImageSection[] = [];
for (let index = 0; index < phCount; index++) {
const offset = phStart + index * phWidth;
if (reader.u32(offset) !== 1) continue;
@@ -114,7 +111,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
const size = reader.word(offset + (wide ? 32 : 16), bits);
const memorySize = reader.word(offset + (wide ? 40 : 20), bits);
if (size > memorySize || address + memorySize > 1n << BigInt(bits))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF load segment sizes or addresses are invalid",
);
@@ -126,16 +123,21 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
};
if (mapping.size !== 0) {
mappings.push(mapping);
loadRegions.push({
mapping,
memory_size: memorySize,
read: (flags & 4) !== 0,
write: (flags & 2) !== 0,
execute: (flags & 1) !== 0,
});
if ((flags & 1) !== 0) executableMappings.push(mapping);
}
if (memorySize > size)
zeroFills.push({ address: address + size, size: memorySize - size });
if (mapping.size !== 0 && (flags & 3) === 2) fallback.push(mapping);
}
let search: GoFileMapping | null = null;
if (shCount !== 0) {
if (shWidth < (wide ? 64 : 40) || namesIndex >= shCount)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF section width or name-table index is invalid",
);
@@ -148,7 +150,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
if (namesIndex !== 0) {
const namesHeader = shStart + namesIndex * shWidth;
if (reader.u32(namesHeader + 4) !== 3)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF section name table is not a string table",
);
@@ -161,7 +163,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
const offset = shStart + index * shWidth;
const nameOffset = reader.u32(offset);
if (nameOffset >= names.size)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"ELF section name offset is outside its string table",
);
@@ -174,73 +176,29 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
wide,
header: offset,
});
// Comparing the complete fixed name avoids quadratic scans through malicious string tables.
const expectedName = Buffer.from(".go.buildinfo\0");
if (
expectedName.length > names.size - nameOffset ||
!bytes
.subarray(
names.offset + nameOffset,
names.offset + nameOffset + expectedName.length,
)
.equals(expectedName)
)
continue;
if (search !== null)
throw new GoBinaryFormatFailure(
"malformed",
"ELF contains ambiguous build-info sections",
);
if (reader.u32(offset + 4) !== 1)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section is not file-backed data",
);
if ((reader.word(offset + 8, bits) & 0x800n) !== 0n)
throw new GoBinaryFormatFailure(
"unsupported",
"Compressed ELF build-info sections are unsupported",
);
const address = reader.word(offset + (wide ? 16 : 12), bits);
search = {
...reader.fileRange(
reader.word(offset + (wide ? 24 : 16), bits),
reader.word(offset + (wide ? 32 : 20), bits),
"ELF build-info section",
),
address,
};
if (
mappedFileOffset(mappings, address, search.size, zeroFills) !==
search.offset
)
throw new GoBinaryFormatFailure(
"malformed",
"ELF build-info section disagrees with its load mapping",
);
}
}
}
return {
format: "elf",
architecture: goArchitectureForElfMachine(fixedHeader.value.machine),
architecture: architectureForElfMachine(fixedHeader.value.machine),
bits,
byte_order: reader.little ? "little" : "big",
mappings,
load_regions: loadRegions,
executable_mappings: executableMappings,
zero_fills: zeroFills,
sections,
search: search === null ? fallback : [search],
};
};
const recordElfSection = (
sections: GoImageSection[],
sections: ImageSection[],
input: {
readonly bytes: Buffer;
readonly names: { readonly offset: number; readonly size: number };
readonly nameOffset: number;
readonly reader: GoBinaryReader;
readonly reader: BinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
@@ -299,18 +257,22 @@ const elfImageSection = ({
header,
name,
}: {
readonly reader: GoBinaryReader;
readonly reader: BinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
}): GoImageSection => {
}): ImageSection => {
const type = reader.u32(header + 4);
const flags = reader.word(header + 8, bits);
const declared_range = {
offset: reader.word(header + (wide ? 24 : 16), bits),
size: reader.word(header + (wide ? 32 : 20), bits),
};
const declared = declaredFileRange(
reader.bytes.length,
reader.word(header + (wide ? 24 : 16), bits),
reader.word(header + (wide ? 32 : 20), bits),
declared_range.offset,
declared_range.size,
);
// SHT_NOBITS offsets and sizes do not describe stored bytes.
const range =
@@ -323,13 +285,22 @@ const elfImageSection = ({
(flags & 1n) === 0n &&
(flags & 4n) === 0n &&
(flags & 2n) !== 0n;
return { name, ...range, read_only: readOnly, compressed };
return {
format: "elf",
name,
type,
address: reader.word(header + (wide ? 16 : 12), bits),
declared_range,
...range,
read_only: readOnly,
compressed,
};
};
const SHT_NOBITS = 8;
/** Go build-info vocabulary, including families outside REA target admission. */
const goArchitectureForElfMachine = (machine: number): string => {
/** Map ELF machine identifiers to stable architecture names. */
const architectureForElfMachine = (machine: number): string => {
const names = new Map([
[3, "386"],
[8, "mips"],
@@ -0,0 +1,50 @@
import { expect, it } from "vitest";
import { readGoBinaryImage } from "../../go/GoBinaryImage.js";
import { createRustImageWithGoSection } from "../../../tests/fixtures/rust/image.js";
import { openImageSteps } from "../../il2cpp/Il2cppBinaryImage.js";
import { readBinaryContainer } from "./readBinaryContainer.js";
it.each(["unmapped", "zero-fill", "duplicate", "out-of-file"] as const)(
"keeps IL2CPP admission independent of a Mach-O Go build-info %s failure",
(fault) => {
const goNamed = createRustImageWithGoSection("macho", fault);
const control = Buffer.from(goNamed);
const sectionName = Buffer.from("__other\0".padEnd(16, "\0"), "ascii");
for (let index = 0; index < (fault === "duplicate" ? 2 : 1); index++)
sectionName.copy(control, 408 + index * 152);
expect(() => readGoBinaryImage(goNamed)).toThrow(/build-info|mapped/i);
expect(readGoBinaryImage(control).build_info).toBeNull();
expect(readBinaryContainer(goNamed).load_regions).toEqual(
readBinaryContainer(control).load_regions,
);
const image = openImageSteps(goNamed, undefined).next();
expect(image.done).toBe(true);
if (!image.done)
throw new Error("container admission unexpectedly yielded");
expect(image.value.bytes).toBe(goNamed);
expect(image.value.container).toMatchObject({
format: "macho",
macho_chained_fixups: false,
});
expect(image.value.container.executable_mappings?.length).toBeGreaterThan(
0,
);
expect(
image.value.container.sections.filter(
(section) =>
section.format === "macho" &&
section.section_name === "__go_buildinfo",
),
).toHaveLength(fault === "duplicate" ? 2 : 1);
const controlImage = openImageSteps(control, undefined).next();
expect(controlImage.done).toBe(true);
if (!controlImage.done)
throw new Error("control container admission unexpectedly yielded");
expect(controlImage.value.container.mappings).toEqual(
image.value.container.mappings,
);
},
);
@@ -1,21 +1,21 @@
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
import { machoLoadCommandAlignment } from "../domain/apple/machoContainer.js";
BinaryFormatFailure,
BinaryReader,
type BinaryContainer,
type FileMapping,
type ImageSection,
type LoadRegion,
type ZeroFillMapping,
} from "./BinaryContainer.js";
import { machoLoadCommandAlignment } from "../../domain/apple/machoContainer.js";
/** Read thin Mach-O images, retaining the slice's exact file-backed mappings. */
export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
export const readMachoImage = (bytes: Buffer): BinaryContainer => {
const magic = bytes.readUInt32BE(0);
const little = magic === 0xcefaedfe || magic === 0xcffaedfe;
const bits = magic === 0xfeedfacf || magic === 0xcffaedfe ? 64 : 32;
const reader = new GoBinaryReader(bytes, little);
const reader = new BinaryReader(bytes, little);
const headerSize = bits === 64 ? 32 : 28;
reader.range(0, headerSize, "Mach-O header");
const count = reader.u32(16);
@@ -27,18 +27,17 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
"Mach-O load commands",
);
if (count > Math.floor(commandBytes / 8))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O command count exceeds its load-command range",
);
const mappings: GoFileMapping[] = [];
const executableMappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const named: GoFileMapping[] = [];
let fallback: GoFileMapping | null = null;
let offset = commandStart;
const mappings: FileMapping[] = [];
const loadRegions: LoadRegion[] = [];
const executableMappings: FileMapping[] = [];
const zeroFills: ZeroFillMapping[] = [];
const sections: ImageSection[] = [];
let chainedFixups = false;
let offset = commandStart;
const end = commandStart + commandBytes;
for (let index = 0; index < count; index++) {
const command = reader.u32(offset);
@@ -48,7 +47,7 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
size % machoLoadCommandAlignment(bits === 64) !== 0 ||
size > end - offset
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O load command size is invalid or truncated",
);
@@ -56,14 +55,14 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
if (command === 1 || command === 0x19) {
const wide = command === 0x19;
if (wide !== (bits === 64))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O segment command does not match the header width",
);
const segmentHeader = wide ? 72 : 56;
const sectionWidth = wide ? 80 : 68;
if (size < segmentHeader)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O segment header is truncated",
);
@@ -79,12 +78,12 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
(!unmapped && fileSize > memorySize) ||
address + memorySize > 1n << BigInt(bits)
)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O segment addresses or sizes are invalid",
);
if (sectionCount > Math.floor((size - segmentHeader) / sectionWidth))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O section table exceeds its segment command",
);
@@ -101,6 +100,17 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
);
if (mapping !== undefined && name !== "__PAGEZERO") {
mappings.push(mapping);
loadRegions.push({
mapping,
memory_size: memorySize,
read: (protection & 1) !== 0,
write: (protection & 2) !== 0,
execute: (protection & 4) !== 0,
protection_masks: {
current: protection,
maximum: maxProtection,
},
});
if ((protection & 4) !== 0) executableMappings.push(mapping);
}
if (memorySize > fileSize && name !== "__PAGEZERO")
@@ -108,14 +118,6 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
address: address + fileSize,
size: memorySize - fileSize,
});
if (
fallback === null &&
mapping !== undefined &&
address !== 0n &&
protection === 3 &&
maxProtection === 3
)
fallback = mapping;
for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
const section = offset + segmentHeader + sectionIndex * sectionWidth;
const sectionName = fixedName(
@@ -129,6 +131,9 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
header: section,
name: `${fixedName(reader.range(section + 16, 16, "Mach-O section segment"))},${sectionName}`,
protection,
sectionName,
segmentMapping: mapping ?? null,
segmentUnmapped: unmapped,
unmapped: unmapped || mapping === undefined,
segmentAddress: address,
segmentMemorySize: memorySize,
@@ -136,56 +141,15 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
segmentFileSize: fileSize,
}),
);
if (sectionName !== "__go_buildinfo") continue;
if (unmapped)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is in an unmapped segment",
);
if (mapping === undefined)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is not file mapped",
);
const sectionFlags = reader.u32(section + (wide ? 64 : 56));
if ([1, 0xc, 0x12].includes(sectionFlags & 0xff))
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section is zero-fill data",
);
const sectionAddress = reader.word(section + 32, bits);
const sectionSize = reader.word(section + (wide ? 40 : 36), bits);
const sectionOffset = BigInt(reader.u32(section + (wide ? 48 : 40)));
const info = {
...reader.fileRange(
sectionOffset,
sectionSize,
"Mach-O build-info section",
),
address: sectionAddress,
};
if (
mappedFileOffset([mapping], sectionAddress, info.size) !== info.offset
)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O build-info section disagrees with its segment",
);
named.push(info);
}
}
offset += size;
}
if (offset !== end)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"Mach-O load-command byte count does not match its commands",
);
if (named.length > 1)
throw new GoBinaryFormatFailure(
"malformed",
"Mach-O has ambiguous build-info sections",
);
const cpu = reader.u32(4);
const names = new Map([
[7, "386"],
@@ -202,10 +166,10 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
bits,
byte_order: little ? "little" : "big",
mappings,
load_regions: loadRegions,
executable_mappings: executableMappings,
zero_fills: zeroFills,
sections,
search: named.length > 0 ? named : fallback === null ? [] : [fallback],
};
};
@@ -216,24 +180,30 @@ const machoImageSection = ({
header,
name,
protection,
sectionName,
segmentMapping,
segmentUnmapped,
unmapped,
segmentAddress,
segmentMemorySize,
segmentFileOffset,
segmentFileSize,
}: {
readonly reader: GoBinaryReader;
readonly reader: BinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
readonly protection: number;
readonly sectionName: string;
readonly segmentMapping: FileMapping | null;
readonly segmentUnmapped: boolean;
readonly unmapped: boolean;
readonly segmentAddress: bigint;
readonly segmentMemorySize: bigint;
readonly segmentFileOffset: bigint;
readonly segmentFileSize: bigint;
}): GoImageSection => {
}): ImageSection => {
const flags = reader.u32(header + (wide ? 64 : 56));
const zeroFill = [1, 0xc, 0x12].includes(flags & 0xff);
const sectionAddress = reader.word(header + 32, bits);
@@ -251,11 +221,7 @@ const machoImageSection = ({
sectionSize <= segmentFileSize - fileDelta;
const range = unmapped
? { offset: 0, size: 0, file_backed: false }
: declaredFileRange(
reader.bytes.length,
BigInt(reader.u32(header + (wide ? 48 : 40))),
reader.word(header + (wide ? 40 : 36), bits),
);
: declaredFileRange(reader.bytes.length, sectionOffset, sectionSize);
const fileBacked =
range.file_backed && !zeroFill && inSegmentMemory && inSegmentFile;
const readOnly =
@@ -264,7 +230,14 @@ const machoImageSection = ({
(protection & 2) === 0 &&
(flags & 0x80000400) === 0;
return {
format: "macho",
name,
section_name: sectionName,
type: flags & 0xff,
address: sectionAddress,
declared_range: { offset: sectionOffset, size: sectionSize },
segment_mapping: segmentMapping,
segment_unmapped: segmentUnmapped,
...range,
file_backed: fileBacked,
read_only: readOnly,
@@ -1,10 +1,10 @@
import { expect, it } from "vitest";
import { readGoBinaryImage } from "./GoBinaryImage.js";
import { readGoPeImage } from "./GoPeImage.js";
import { readGoBinaryImage } from "../../go/GoBinaryImage.js";
import { readPeImage } from "./PeImage.js";
import {
createGoBinaryFixture,
setGoPeVirtualSize,
} from "../../tests/fixtures/go/image.js";
} from "../../../tests/fixtures/go/image.js";
it.each([32, 64] as const)(
"uses raw-size fallback for PE%d Go build-info and keeps RVA mappings intact",
@@ -15,7 +15,7 @@ it.each([32, 64] as const)(
expect(expected).not.toBeNull();
setGoPeVirtualSize(fixture.bytes, 0);
const container = readGoPeImage(fixture.bytes);
const container = readPeImage(fixture.bytes);
expect(container.sections[0]).toMatchObject({
name: ".data",
offset: 1024,
@@ -27,16 +27,35 @@ it.each([32, 64] as const)(
offset: 1024,
size: 2048,
});
expect(container.search[0]).toMatchObject({
address: 0x10400n,
offset: 1024,
size: 2048,
expect(container.sections[0]).toMatchObject({
virtual_address: 0x400,
virtual_size: 0,
raw_size: 2048,
characteristics: 0xc0000040,
});
expect(container).not.toHaveProperty("data_ranges");
expect(container.image_base).toBe(0x10000n);
expect(container.executable_mappings).toEqual([]);
expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected);
}
},
);
it("keeps Go fallback candidates when a PE section has no printable name", () => {
const fixture = createGoBinaryFixture({ format: "pe", bits: 64 });
const expected = readGoBinaryImage(fixture.bytes).build_info;
const pe = fixture.bytes.readUInt32LE(0x3c);
const section = pe + 24 + fixture.bytes.readUInt16LE(pe + 20);
fixture.bytes.fill(0, section, section + 8);
const container = readPeImage(fixture.bytes);
expect(container.sections[0]).toMatchObject({
name: "",
characteristics: 0xc0000040,
});
expect(readGoBinaryImage(fixture.bytes).build_info).toEqual(expected);
});
it.each(["inline", "pointer"] as const)(
"does not search PE raw alignment padding beyond a nonzero VirtualSize (%s)",
(encoding) => {
@@ -51,13 +70,19 @@ it.each(["inline", "pointer"] as const)(
fixture.bytes.fill(0, fixture.headerOffset, fixture.headerOffset + 512);
setGoPeVirtualSize(fixture.bytes, 512);
const container = readGoPeImage(fixture.bytes);
const container = readPeImage(fixture.bytes);
expect(container.sections[0]).toMatchObject({
size: 512,
file_backed: true,
});
expect(container.mappings[0]).toMatchObject({ size: 2048, offset: 1024 });
expect(container.search[0]).toMatchObject({ size: 512, offset: 1024 });
expect(container.sections[0]).toMatchObject({
size: 512,
offset: 1024,
virtual_size: 512,
raw_size: 2048,
});
expect(container).not.toHaveProperty("data_ranges");
expect(readGoBinaryImage(fixture.bytes).build_info).toBeNull();
},
);
@@ -1,38 +1,41 @@
import { observeMzWindowsHeader, parseDosMzHeader } from "../domain/dosMz.js";
import {
observeMzWindowsHeader,
parseDosMzHeader,
} from "../../domain/dosMz.js";
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
BinaryFormatFailure,
BinaryReader,
type BinaryContainer,
type FileMapping,
type ImageSection,
type ZeroFillMapping,
} from "./BinaryContainer.js";
const peHeaderOffset = (reader: GoBinaryReader): number => {
const peHeaderOffset = (reader: BinaryReader): number => {
const windowsHeader = observeMzWindowsHeader(reader.bytes);
if (windowsHeader.state !== "declared") {
const dos = parseDosMzHeader(reader.bytes);
if (!dos.ok) throw new GoBinaryFormatFailure("malformed", dos.error);
throw new GoBinaryFormatFailure(
if (!dos.ok) throw new BinaryFormatFailure("malformed", dos.error);
throw new BinaryFormatFailure(
"unsupported",
"DOS MZ images are outside the supported ELF, PE and thin Mach-O formats",
);
}
const { offset: pe } = windowsHeader;
if (pe < 64)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE signature overlaps the DOS header",
);
reader.range(pe, 24, "PE signature and COFF header");
if (["NE", "LE", "LX"].includes(windowsHeader.signature))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"unsupported",
`${windowsHeader.signature} images are outside the supported ELF, PE and thin Mach-O formats`,
);
if (windowsHeader.signature !== "PE")
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE signature is invalid or overlaps the DOS header",
);
@@ -40,41 +43,41 @@ const peHeaderOffset = (reader: GoBinaryReader): number => {
};
const optionalHeaderMagic = (
reader: GoBinaryReader,
reader: BinaryReader,
offset: number,
size: number,
): number => {
reader.range(offset, size, "PE optional header");
if (size < 2)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE optional header is missing its complete magic field",
);
return reader.u16(offset);
};
/** Read PE32/PE32+ native image mappings and the linker data section. */
export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
const reader = new GoBinaryReader(bytes, true);
/** Read PE32/PE32+ native image mappings and section facts. */
export const readPeImage = (bytes: Buffer): BinaryContainer => {
const reader = new BinaryReader(bytes, true);
const pe = peHeaderOffset(reader);
const optional = pe + 24;
const optionalSize = reader.u16(pe + 20);
const magic = optionalHeaderMagic(reader, optional, optionalSize);
const bits = magic === 0x10b ? 32 : magic === 0x20b ? 64 : null;
if (bits === null)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"unsupported",
"Only PE32 and PE32+ image optional headers are supported",
);
if (optionalSize < (bits === 64 ? 112 : 96))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE optional header is truncated",
);
const directoriesOffset = bits === 64 ? 112 : 96;
const directories = reader.u32(optional + directoriesOffset - 4);
if (directories * 8 > optionalSize - directoriesOffset)
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE data directories extend beyond the optional header",
);
@@ -86,11 +89,10 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
40,
"PE sections",
);
const mappings: GoFileMapping[] = [];
const executableMappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
let search: GoFileMapping | null = null;
const mappings: FileMapping[] = [];
const executableMappings: FileMapping[] = [];
const zeroFills: ZeroFillMapping[] = [];
const sections: ImageSection[] = [];
for (let index = 0; index < count; index++) {
const entry = start + index * 40;
const sectionName = peSectionName(
@@ -103,19 +105,21 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
const characteristics = reader.u32(entry + 36);
// SizeOfRawData includes file-alignment padding. VirtualSize is the content.
const contentSize = virtualSize === 0 ? size : Math.min(virtualSize, size);
if (sectionName.length > 0)
sections.push(
peImageSection({
reader,
name: sectionName,
size: contentSize,
offset,
characteristics,
}),
);
sections.push(
peImageSection({
reader,
name: sectionName,
size: contentSize,
offset,
characteristics,
virtualAddress,
virtualSize,
rawSize: size,
}),
);
const address = imageBase + BigInt(virtualAddress);
if (address + BigInt(Math.max(size, virtualSize)) > 1n << BigInt(bits))
throw new GoBinaryFormatFailure(
throw new BinaryFormatFailure(
"malformed",
"PE section virtual address overflows its address width",
);
@@ -135,15 +139,6 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
address: address + BigInt(size),
size: BigInt(virtualSize - size),
});
// Match debug/buildinfo's first initialized, readable, writable data section.
if (
search === null &&
virtualAddress !== 0 &&
size !== 0 &&
(characteristics & ~0x00600000) >>> 0 === 0xc0000040
) {
search = { ...mapping, size: contentSize };
}
}
const machine = reader.u16(pe + 4);
const names = new Map([
@@ -164,7 +159,6 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
executable_mappings: executableMappings,
zero_fills: zeroFills,
sections,
search: search === null ? [] : [search],
};
};
@@ -179,13 +173,19 @@ const peImageSection = ({
size,
offset,
characteristics,
virtualAddress,
virtualSize,
rawSize,
}: {
readonly reader: GoBinaryReader;
readonly reader: BinaryReader;
readonly name: string;
readonly size: number;
readonly offset: number;
readonly characteristics: number;
}): GoImageSection => {
readonly virtualAddress: number;
readonly virtualSize: number;
readonly rawSize: number;
}): ImageSection => {
const range = declaredFileRange(
reader.bytes.length,
BigInt(offset),
@@ -197,5 +197,15 @@ const peImageSection = ({
(characteristics & 0x80000000) === 0 &&
(characteristics & 0x20000000) === 0 &&
(characteristics & 0x40) !== 0;
return { name, ...range, read_only: readOnly, compressed: false };
return {
format: "pe",
name,
virtual_address: virtualAddress,
virtual_size: virtualSize,
raw_size: rawSize,
characteristics,
...range,
read_only: readOnly,
compressed: false,
};
};
@@ -0,0 +1,48 @@
import {
BinaryFormatFailure,
type BinaryContainer,
} from "./BinaryContainer.js";
import {
classifyFatContainerIdentity,
readFatContainerHeader,
} from "../../domain/apple/machoContainer.js";
import { readElfImage } from "./ElfImage.js";
import { readPeImage } from "./PeImage.js";
import { readMachoImage } from "./MachoImage.js";
/** Identify an ELF, PE, or thin Mach-O image and its named file sections. */
export const readBinaryContainer = (bytes: Buffer): BinaryContainer => {
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
return readElfImage(bytes);
if (bytes.subarray(0, 2).equals(Buffer.from("MZ"))) return readPeImage(bytes);
if (bytes.length >= 4) {
const magic = bytes.readUInt32BE();
if ([0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca].includes(magic)) {
const fatHeader = readFatContainerHeader(bytes, bytes.length);
if (fatHeader.state === "malformed")
throw new BinaryFormatFailure("malformed", fatHeader.reason);
const identity =
fatHeader.state === "parsed"
? classifyFatContainerIdentity(fatHeader, (offset) => {
if (offset >= bytes.length) return undefined;
if (offset + 4 > bytes.length) return null;
return bytes.readUInt32BE(offset);
})
: undefined;
// An invalid FAT-shaped table is not proof of a Mach-O container: JVM
// constant-pool bytes can share both the magic and a known CPU value.
const confirmedFatMachO = identity?.state === "mach-o";
if (confirmedFatMachO)
throw new BinaryFormatFailure(
"unsupported",
"Universal/fat Mach-O containers require selecting a thin slice",
);
}
if ([0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe].includes(magic))
return readMachoImage(bytes);
}
throw new BinaryFormatFailure(
"unsupported",
"Only ELF, PE, and thin Mach-O native images are supported",
);
};
+13 -13
View File
@@ -1,9 +1,9 @@
import {
GoBinaryFormatFailure,
GoBinaryResourceFailure,
type GoImageSection,
} from "../go/GoBinaryContainer.js";
import { readGoBinaryContainer } from "../go/GoBinaryImage.js";
BinaryFormatFailure,
BinaryResourceFailure,
type ImageSection,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import type {
RustAuditableMetadata,
RustBinary,
@@ -48,13 +48,13 @@ export const readRustBinaryImage = (bytes: Buffer): RustBinaryImage => {
const readContainer = (bytes: Buffer) => {
try {
return readGoBinaryContainer(bytes);
return readBinaryContainer(bytes);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure)
if (cause instanceof BinaryFormatFailure)
throw new RustAuditableFormatFailure(cause.kind, cause.message, {
cause,
});
if (cause instanceof GoBinaryResourceFailure)
if (cause instanceof BinaryResourceFailure)
throw new RustAuditableResourceFailure(
{
resource: "memory",
@@ -71,8 +71,8 @@ const readContainer = (bytes: Buffer) => {
const dependencySection = (
format: RustBinaryImage["format"],
sections: readonly GoImageSection[],
): GoImageSection | null => {
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)
@@ -98,7 +98,7 @@ const dependencySection = (
const readDependency = (
bytes: Buffer,
format: RustBinaryImage["format"],
section: GoImageSection,
section: ImageSection,
): RustAuditableMetadata =>
readAuditableSection(
format === "macho" ? "__DATA,.dep-v0" : ".dep-v0",
@@ -108,7 +108,7 @@ const readDependency = (
const rustcCommit = (
bytes: Buffer,
sections: readonly GoImageSection[],
sections: readonly ImageSection[],
): RustBinaryImage["rustc_commit"] => {
const hashes: { hash: string; file_offset: number }[] = [];
const seen = new Set<string>();
@@ -120,7 +120,7 @@ const rustcCommit = (
};
const mergedReadOnlyRanges = (
sections: readonly GoImageSection[],
sections: readonly ImageSection[],
): { readonly offset: number; readonly end: number }[] => {
const ranges = sections
.filter((section) => section.read_only && section.size > 0)
+94
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { deflateSync } from "node:zlib";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
@@ -7,6 +8,8 @@ import { toolContract } from "../../src/contracts/toolContracts.js";
import { machoImageWithUnknownCommand } from "../../src/artifacts/apple/MachoImage.fixture.js";
import {
createRustImage,
createRustImageWithGoSection,
RUSTC_HASH,
rustDependencyPayload,
} from "../fixtures/rust/image.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
@@ -229,3 +232,94 @@ cliTest(
expect(await readFile(path)).toEqual(bytes);
},
);
cliTest(
"keeps Rust evidence independent of Go section errors through public CLI and stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-go-section-");
const { client, call } = await connectLocalToolsMcp();
for (const [format, fault, code] of [
["elf", "compressed", "unsupported_target"],
["elf", "zero-address", "invalid_request"],
["macho", "zero-fill", "invalid_request"],
] as const) {
const bytes = createRustImageWithGoSection(format, fault);
if (fault === "zero-address") {
const sectionHeader = 512;
expect(bytes.readUInt32LE(sectionHeader + 4)).toBe(1);
expect(bytes.readBigUInt64LE(sectionHeader + 8)).toBe(0n);
expect(bytes.readBigUInt64LE(sectionHeader + 16)).toBe(0n);
expect(bytes.readBigUInt64LE(sectionHeader + 24)).toBe(3072n);
expect(bytes.readBigUInt64LE(sectionHeader + 32)).toBe(16n);
const nameOffset = bytes.readUInt32LE(sectionHeader);
expect(
bytes.toString(
"ascii",
768 + nameOffset,
768 + nameOffset + ".go.buildinfo".length,
),
).toBe(".go.buildinfo");
}
const path = join(root, `mixed.${format}`);
await writeFile(path, bytes);
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,
);
expect(evidence.normalized_result).toMatchObject({
artifact: {
path,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
},
format,
auditable_metadata: {
file_offset: 1024,
root_package_index: 1,
packages: [
{ name: "itoa", version: "1.0.18" },
{ name: "sample", version: "0.1.0", root: true },
],
dependency_edges: [{ from_index: 1, to_index: 0 }],
},
rustc_commit: {
evidence_kind: "inferred",
hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }],
},
});
const response = await call("inspect_rust_binary", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
const go = await cli.run({
arguments: ["inspect-go-binary", path, "--json"],
});
expect(go.exitCode).toBe(1);
expect(go.json).toMatchObject({
code,
details: { operation: "inspect_go_binary" },
});
if (fault === "zero-address")
expect(go.json).toMatchObject({
details: {
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
const goResponse = await call("inspect_go_binary", { path });
expect(parseMcpToolError(goResponse).error).toMatchObject({
code,
details: { operation: "inspect_go_binary" },
});
if (fault === "zero-address")
expect(parseMcpToolError(goResponse).error).toMatchObject({
details: {
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
}
await client.ping();
},
30000,
);
+41 -7
View File
@@ -5,11 +5,11 @@ import {
fatImage,
machoImage,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
import {
readGoBinaryImage,
GoBinaryResourceFailure,
} from "../../../src/go/GoBinaryImage.js";
import { readGoMachoImage } from "../../../src/go/GoMachoImage.js";
import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js";
import { goBuildInfoSearch } from "../../../src/go/GoBuildInfoSearch.js";
import { BinaryResourceFailure } from "../../../src/native/metadata/BinaryContainer.js";
import { readMachoImage } from "../../../src/native/metadata/MachoImage.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import {
createGoBinaryFixture,
createGoSplitElfFixture,
@@ -17,9 +17,43 @@ import {
GO_MODULE_TEXT,
} from "../../fixtures/go/image.js";
it("preserves exact Mach-O protection masks for Go fallback selection", () => {
const { bytes } = createGoBinaryFixture({
format: "macho",
sectionless: true,
});
const firstSegment = 32;
const secondSegment = 104;
bytes.writeUInt32LE(2, 16);
bytes.writeUInt32LE(144, 20);
bytes.writeUInt32LE(11, firstSegment + 56);
bytes.writeUInt32LE(11, firstSegment + 60);
bytes.writeUInt32LE(0x19, secondSegment);
bytes.writeUInt32LE(72, secondSegment + 4);
bytes.write("__DATA2", secondSegment + 8, "ascii");
bytes.writeBigUInt64LE(0x20000n, secondSegment + 24);
bytes.writeBigUInt64LE(BigInt(bytes.length), secondSegment + 32);
bytes.writeBigUInt64LE(0n, secondSegment + 40);
bytes.writeBigUInt64LE(BigInt(bytes.length), secondSegment + 48);
bytes.writeUInt32LE(3, secondSegment + 56);
bytes.writeUInt32LE(3, secondSegment + 60);
const container = readBinaryContainer(bytes);
expect(
container.load_regions?.map((region) => region.protection_masks),
).toEqual([
{ current: 11, maximum: 11 },
{ current: 3, maximum: 3 },
]);
expect(goBuildInfoSearch(bytes, container)).toMatchObject([
{ address: 0x20000n, offset: 0, size: bytes.length },
]);
expect(readGoBinaryImage(bytes).build_info?.go_version).toBe("go1.26.0");
});
it("bounds cumulative header searches across overlapping ELF file spans", () => {
const bytes = createGoOverlappingElfFixture();
expect(() => readGoBinaryImage(bytes)).toThrow(GoBinaryResourceFailure);
expect(() => readGoBinaryImage(bytes)).toThrow(BinaryResourceFailure);
expect(() => readGoBinaryImage(bytes)).toThrow(/256 MiB.*search/);
});
@@ -633,7 +667,7 @@ it("does not mark Mach-O section bytes outside their owning segment as read-only
fixture.bytes.writeBigUInt64LE(0x10c00n, section + 32);
fixture.bytes.writeBigUInt64LE(128n, section + 40);
fixture.bytes.writeUInt32LE(3072, section + 48);
const image = readGoMachoImage(fixture.bytes);
const image = readMachoImage(fixture.bytes);
expect(image.sections[0]).toMatchObject({
file_backed: false,
read_only: false,
+1 -1
View File
@@ -1,5 +1,5 @@
import { expect, it } from "vitest";
import { mappedFileOffset } from "../../../src/go/GoBinaryContainer.js";
import { mappedFileOffset } from "../../../src/native/metadata/BinaryContainer.js";
const selected = { address: 100n, offset: 16, size: 64 };
@@ -2,11 +2,14 @@ import { expect, it } from "vitest";
import { fat } from "../../../src/domain/binaryTarget.fixture.js";
import {
createRustImage,
createRustImageWithGoSection,
createRustHashOverflowImage,
RUSTC_HASH,
rustDependencyPayload,
} from "../../fixtures/rust/image.js";
import { readRustBinaryImage } from "../../../src/rust/RustBinaryImage.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js";
import { RustAuditableFormatFailure } from "../../../src/rust/RustAuditableReader.js";
const packages = [
@@ -139,3 +142,48 @@ it("refuses read-only data with more distinct rustc paths than it retains", () =
}),
);
});
it.each([
["elf", "compressed"],
["elf", "unmapped"],
["elf", "zero-fill"],
["elf", "duplicate"],
["elf", "out-of-file"],
["elf", "zero-address"],
["macho", "unmapped"],
["macho", "zero-fill"],
["macho", "duplicate"],
["macho", "out-of-file"],
] as const)(
"retains Rust evidence beside an unrelated %s %s Go section",
(format, fault) => {
const bytes = createRustImageWithGoSection(format, fault);
const control = Buffer.from(bytes);
const sectionOffset = format === "elf" ? 768 + 11 : 408;
const sectionName = Buffer.from(
(format === "elf" ? ".other\0" : "__other\0").padEnd(
format === "elf" ? 13 : 16,
"\0",
),
"ascii",
);
if (format === "elf") {
const nameOffset = Number(bytes.readBigUInt64LE(320 + 32));
sectionName.copy(control, 768 + nameOffset);
} else {
sectionName.copy(control, sectionOffset);
}
expect(readBinaryContainer(bytes).load_regions).toEqual(
readBinaryContainer(control).load_regions,
);
expect(() => readGoBinaryImage(bytes)).toThrow(/build-info|mapped/i);
expect(readRustBinaryImage(bytes)).toMatchObject({
format,
auditable_metadata: { file_offset: 1024, packages },
rustc_commit: {
evidence_kind: "inferred",
hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }],
},
});
},
);
+72
View File
@@ -261,3 +261,75 @@ export const createRustHashOverflowImage = (): Buffer => {
}
return bytes;
};
/** Valid Rust evidence beside an independently invalid Go metadata section. */
export const createRustImageWithGoSection = (
format: "elf" | "macho",
fault:
| "compressed"
| "unmapped"
| "zero-fill"
| "duplicate"
| "out-of-file"
| "zero-address",
): Buffer => {
const bytes = createRustImage({ format });
const count = fault === "duplicate" ? 2 : 1;
if (format === "elf") {
const nameOffset = Number(bytes.readBigUInt64LE(320 + 32));
const name = Buffer.from(".go.buildinfo\0");
name.copy(bytes, 768 + nameOffset);
bytes.writeBigUInt64LE(BigInt(nameOffset + name.length), 320 + 32);
bytes.writeUInt16LE(4 + count, 60);
for (let index = 0; index < count; index++) {
const header = 512 + index * 64;
bytes.writeUInt32LE(nameOffset, header);
bytes.writeUInt32LE(fault === "zero-fill" ? 8 : 1, header + 4);
bytes.writeBigUInt64LE(fault === "compressed" ? 0x800n : 0n, header + 8);
bytes.writeBigUInt64LE(
fault === "zero-address"
? 0n
: fault === "unmapped"
? 0x600000n
: 0x400c00n,
header + 16,
);
bytes.writeBigUInt64LE(
fault === "out-of-file" ? 4088n : 3072n,
header + 24,
);
bytes.writeBigUInt64LE(fault === "zero-address" ? 16n : 32n, header + 32);
if (fault === "zero-address")
Buffer.from("additional data\0", "ascii").copy(bytes, 3072);
}
} else {
if (fault === "compressed") throw new Error("Not a Mach-O section flag");
if (fault === "zero-address") throw new Error("Not an ELF section address");
bytes.writeUInt32LE(2 + count, 16);
bytes.writeUInt32LE((2 + count) * 152, 20);
for (let index = 0; index < count; index++) {
const command = 336 + index * 152;
const little: RustImageWrite["little"] = (offset, value, width) => {
if (width === 16) bytes.writeUInt16LE(value, offset);
else if (width === 32) bytes.writeUInt32LE(value, offset);
else bytes.writeBigUInt64LE(BigInt(value), offset);
};
writeMachoSegment({
bytes,
little,
command,
segment: "__EXTRA",
section: "__go_buildinfo",
body: Buffer.alloc(32),
fileOffset: 3072,
protection: fault === "unmapped" ? 0 : 3,
flags: fault === "zero-fill" ? 1 : 0,
});
if (fault === "out-of-file") bytes.writeUInt32LE(4088, command + 72 + 48);
bytes.writeBigUInt64LE(0x200000000n, command + 24);
bytes.writeBigUInt64LE(0x200000000n, command + 72 + 32);
if (fault === "unmapped") bytes.writeBigUInt64LE(0n, command + 32);
}
}
return bytes;
};