mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
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:
+50
-85
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,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 }],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Vendored
+72
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user