fix(native): own complete metadata mappings and ambiguity

Resolve Go pointer descriptors and strings across complete, consistent file-backed spans, including PE headers and adjacent ELF load ranges. Reuse the canonical image-mapping selection for native exports. Preserve existing gaps, alias, zero-fill and capacity refusals.

Forward parsed zero-fill ownership through IL2CPP relocation tables, destinations and mapped-span checks. Distinguish missing spans from ambiguous ownership in the shared mapping failure: unresolved relocation targets may remain unknown, while ambiguous raw registration bytes cannot be reused as a resolved count.

Validation: parser, provider, service, native-export, IL2CPP and public CLI/MCP regressions cover PE32/PE32+ headers, split ELF descriptors/contents, relocation count conflicts, ordinary unresolved targets and unchanged inputs. Authored carriers establish static parsing, not Windows loading or actual legacy Go linker emission.

Fixes #6831
Fixes #6830
This commit is contained in:
morluto committed 2026-10-24 04:21:56 +08:00
1 parent b7ebccf8d3
commit 8b3902d377
12 files changed
+526 -41

No files matched your search

+12
View File
@@ -116,6 +116,18 @@ including pointer-backed strings. Legacy pointer-backed fields are decoded at
the width declared by Go's build-info record, independently of the image's ELF
class. It does not scan arbitrary byte strings as compiler metadata.
Legacy string descriptors and contents may use the declared file-backed PE
headers. A descriptor or string may also cross adjacent loader mappings when
their virtual ranges cover every requested byte and identify one contiguous
file range. Gaps, conflicting file aliases and nonempty zero-fill overlap remain
invalid. Discovery policy and the aggregate 1 MiB string-content limit are
unchanged. These are static reads over REA's native mapping facts; they do not
establish Windows/Linux loading or parity with Go's standard reader, whose
[Go 1.24.0 ELF/PE readers](https://github.com/golang/go/blob/go1.24.0/src/debug/buildinfo/buildinfo.go#L445)
read within individual segments or sections. The historical two-word string
descriptor follows the
[Go 1.17.13 reader](https://github.com/golang/go/blob/go1.17.13/src/cmd/go/internal/version/version.go#L212).
A universal (FAT) Mach-O needs `architecture` (`x86`, `x86_64`, `arm`
or `arm64`; CLI `--architecture`). REA reads its own FAT table, without `lipo`,
and validates the selected slice's range, alignment and Mach-O header CPU
+2
View File
@@ -304,6 +304,8 @@ Scans yield cooperatively in bounded batches. Name-address retention and reverse
ELF RELATIVE writes into zero-fill are kept as a private virtual-pointer overlay. Unresolved supported-width symbol relocations, including RELA PLT entries, remain unknown only for the bytes their ELF ABI relocation type covers; an adjacent 32-bit relocation does not make a separate 64-bit method slot unknown. Stored zeros never establish no-body. COPY, TLS descriptor, REL, packed APS2 and other unsupported relocation encodings fail closed. Input bytes are unchanged.
ELF relocation tables and file-backed destinations may span adjacent load ranges when every range agrees on the file translation. Any overlapping zero-fill declaration makes those bytes ambiguous; REA refuses the table or relocation destination instead of choosing the stored file bytes. Missing file bytes remain distinct from conflicting owners: supported unresolved relocations outside file-backed memory retain their unknown overlay, while ambiguous destinations refuse linking. Registration coverage uses the same file/zero-fill ownership checks as pointer reads.
Native address queries batch exact64-bit hexadecimal `addresses` and `states` (`code`, `no-body`, `unknown`), intersecting indices and pagination. Numerically equal address spellings match; original row indices and every shared-body alternative survive:
```json
+14 -18
View File
@@ -3,9 +3,11 @@ import {
BinaryResourceFailure,
BinaryReader,
mappedFileOffset,
imageFileMappings,
isZeroFilledSpan,
type BinaryContainer,
type FileMapping,
type ZeroFillMapping,
} from "../native/metadata/BinaryContainer.js";
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
import {
@@ -88,15 +90,11 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
: { section_names_limitation: sectionNamesLimitation }),
};
const { search, header } = selected;
const mappings = imageFileMappings(container);
const verifyMapping = (size: number): void => {
const address = search.address + BigInt(header - search.offset);
if (
mappedFileOffset(
container.mappings,
address,
size,
container.zero_fills,
) !== header
mappedFileOffset(mappings, address, size, container.zero_fills) !== header
)
throw new BinaryFormatFailure(
"malformed",
@@ -159,14 +157,16 @@ export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const pointerBits = ptrSize === 4 ? 32 : 64;
version = readPointerString(
reader,
container,
mappings,
container.zero_fills,
reader.word(header + 16, pointerBits),
pointerBits,
);
const moduleField = header + 16 + ptrSize;
module = readOptionalModuleString(
reader,
container,
mappings,
container.zero_fills,
reader.word(moduleField, pointerBits),
moduleField,
pointerBits,
@@ -346,11 +346,11 @@ const readInlineExtent = (
const readPointerString = (
reader: BinaryReader,
container: BinaryContainer,
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
address: bigint,
pointerBits: 32 | 64,
): LocatedString => {
const { mappings, zero_fills: zeroFills } = container;
const width = pointerBits / 8;
const header = mappedFileOffset(mappings, address, width * 2, zeroFills);
const valueAddress = reader.word(header, pointerBits);
@@ -375,19 +375,15 @@ const readPointerString = (
const readOptionalModuleString = (
reader: BinaryReader,
container: BinaryContainer,
mappings: readonly FileMapping[],
zeroFills: readonly ZeroFillMapping[],
address: bigint,
pointerField: number,
pointerBits: 32 | 64,
): LocatedString =>
isZeroFilledSpan(
container.mappings,
container.zero_fills,
address,
(pointerBits / 8) * 2,
)
isZeroFilledSpan(mappings, zeroFills, address, (pointerBits / 8) * 2)
? { bytes: Buffer.alloc(0), location: { offset: pointerField, bytes: 0 } }
: readPointerString(reader, container, address, pointerBits);
: readPointerString(reader, mappings, zeroFills, address, pointerBits);
/** Decode metadata text without replacing invalid bytes or stripping an initial BOM. */
export const observedUtf8 = (bytes: Buffer, label: string): string => {
+8 -7
View File
@@ -1,5 +1,5 @@
import {
BinaryFormatFailure,
BinaryMappingFailure,
mappedFileOffset,
isZeroFilledSpan,
type FileMapping,
@@ -124,14 +124,15 @@ function* dynamicTags(image: Buffer): Generator<void, Map<bigint, bigint>> {
}
const fileOffset = (
mappings: readonly FileMapping[],
source: RelocationSource,
address: bigint,
size: number,
): number | null => {
try {
return mappedFileOffset(mappings, address, size);
return mappedFileOffset(source.mappings, address, size, source.zeroFills);
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return null;
if (cause instanceof BinaryMappingFailure && cause.reason === "unmapped")
return null;
throw cause;
}
};
@@ -163,7 +164,7 @@ function* applyRelaTable(
size > BigInt(source.image.length)
)
throw malformed("invalid DT_RELASZ or DT_RELAENT");
const start = fileOffset(source.mappings, address, Number(size));
const start = fileOffset(source, address, Number(size));
if (start === null) throw malformed("RELA table is not file-backed");
for (let at = start; at < start + Number(size); at += 24) {
if ((at - start) % (24 * 4096) === 0) yield;
@@ -180,11 +181,11 @@ function* applyRelaTable(
output.unresolved.set(destination, Math.max(priorWidth ?? 0, width));
// Registration scans read this private image directly. Clear unknown
// fields so their stored bytes cannot masquerade as resolved pointers.
const target = fileOffset(source.mappings, destination, width);
const target = fileOffset(source, destination, width);
if (target !== null) output.bytes.fill(0, target, target + width);
continue;
}
const target = fileOffset(source.mappings, destination, 8);
const target = fileOffset(source, destination, 8);
const value = source.image.readBigInt64LE(at + 16) & 0xffffffffffffffffn;
if (target !== null) output.bytes.writeBigUInt64LE(value, target);
else if (
+6 -1
View File
@@ -237,7 +237,12 @@ export const readUInt32 = (
export const isMapped = (image: NativeImage, address: bigint, size: number) => {
try {
mappedFileOffset(image.container.mappings, address, size);
mappedFileOffset(
image.container.mappings,
address,
size,
image.container.zero_fills,
);
return true;
} catch (cause) {
if (cause instanceof BinaryFormatFailure) return false;
+2 -4
View File
@@ -3,6 +3,7 @@ import {
BinaryReader,
BinaryResourceFailure,
mappedFileOffset,
imageFileMappings,
} from "../metadata/BinaryContainer.js";
import { architectureForElfMachine } from "../metadata/ElfImage.js";
import { isUtf8 } from "node:buffer";
@@ -109,10 +110,7 @@ const parsePeExports = (bytes: Buffer, artifact: Artifact): NativeExports => {
],
};
const reader = new BinaryReader(bytes, true);
const mappings =
image.pe_header_mapping === undefined
? image.mappings
: [image.pe_header_mapping, ...image.mappings];
const mappings = imageFileMappings(image);
const map = (rva: number, size: number): number =>
mappedFileOffset(mappings, base + BigInt(rva), size, image.zero_fills);
const cString = (rva: number): string => {
+60 -11
View File
@@ -9,6 +9,16 @@ export class BinaryFormatFailure extends Error {
}
}
/** A missing file span is a candidate miss; conflicting owners are malformed. */
export class BinaryMappingFailure extends BinaryFormatFailure {
constructor(
readonly reason: "unmapped" | "ambiguous",
message: string,
) {
super("malformed", message);
}
}
/** Resource boundary exceeded while decoding metadata or native container structure. */
export class BinaryResourceFailure extends Error {
constructor(
@@ -121,6 +131,14 @@ export interface BinaryContainer {
readonly sections: readonly ImageSection[];
}
/** All declared file-backed image memory, including PE headers at the image base. */
export const imageFileMappings = (
container: BinaryContainer,
): readonly FileMapping[] =>
container.pe_header_mapping === undefined
? container.mappings
: [container.pe_header_mapping, ...container.mappings];
/** Reader that checks every structural range before decoding numeric fields. */
export class BinaryReader {
constructor(
@@ -212,7 +230,7 @@ export const declaredFileRange = (
};
};
/** Map a virtual span without losing precision or selecting an ambiguous alias. */
/** Map a contiguous file span without gaps or conflicting loader mappings. */
export const mappedFileOffset = (
mappings: readonly FileMapping[],
address: bigint,
@@ -230,21 +248,52 @@ export const mappedFileOffset = (
continue;
const offset = mapping.offset + Number(delta);
if (found !== undefined && found !== offset)
throw new BinaryFormatFailure(
"malformed",
throw new BinaryMappingFailure(
"ambiguous",
"Virtual address has ambiguous file mappings",
);
found = offset;
}
if (found === undefined && size > 0) {
// Keep the common single-mapping path allocation-free. A split span needs
// ordered intervals to prove that every byte is backed, even when native
// mappings arrive out of address order.
const end = address + BigInt(size);
const spans = mappings
.filter((mapping) =>
spansOverlap(
address,
BigInt(size),
mapping.address,
BigInt(mapping.size),
),
)
.sort((left, right) =>
left.address < right.address
? -1
: left.address > right.address
? 1
: 0,
);
let covered = address;
for (const mapping of spans) {
if (mapping.address > covered) break;
const mappingEnd = mapping.address + BigInt(mapping.size);
if (mappingEnd > covered) covered = mappingEnd;
}
const first = spans[0];
if (covered >= end && first !== undefined)
found = first.offset + Number(address - first.address);
}
if (found === undefined)
throw new BinaryFormatFailure(
"malformed",
throw new BinaryMappingFailure(
"unmapped",
"Virtual address or string range is not file mapped",
);
for (const zeroFill of zeroFills) {
if (spansOverlap(address, BigInt(size), zeroFill.address, zeroFill.size))
throw new BinaryFormatFailure(
"malformed",
throw new BinaryMappingFailure(
"ambiguous",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
@@ -258,8 +307,8 @@ export const mappedFileOffset = (
BigInt(found) + start - address !==
BigInt(mapping.offset) + start - mapping.address
)
throw new BinaryFormatFailure(
"malformed",
throw new BinaryMappingFailure(
"ambiguous",
"Virtual address has ambiguous file mappings",
);
}
@@ -284,8 +333,8 @@ export const isZeroFilledSpan = (
return false;
for (const mapping of mappings) {
if (spansOverlap(address, bytes, mapping.address, BigInt(mapping.size)))
throw new BinaryFormatFailure(
"malformed",
throw new BinaryMappingFailure(
"ambiguous",
"Virtual address has ambiguous file and zero-fill mappings",
);
}
@@ -0,0 +1,79 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { evidenceResultOf } from "../../src/contracts/toolOutputSchemaPrimitives.js";
import { goBinarySchema } from "../../src/domain/go/goBinary.js";
import {
createGoCrossLoadPointerFixture,
createGoPeHeaderPointerFixture,
} from "../fixtures/go/image.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest, runFileCli } from "../support/cli/cliFixture.js";
cliTest(
"retains header-backed PE and cross-load ELF pointer metadata through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-go-pointer-mappings-");
const { client, call } = await connectLocalToolsMcp();
const cases = [
...([32, 64] as const).flatMap((bits) =>
(["contents", "descriptors"] as const).map((placement) => ({
name: `pe${bits}-header-${placement}`,
fixture: createGoPeHeaderPointerFixture(bits, placement),
})),
),
...(["contents", "descriptor"] as const).map((placement) => ({
name: `elf-cross-load-${placement}`,
fixture: createGoCrossLoadPointerFixture(placement),
})),
];
for (const { name, fixture } of cases) {
const path = join(root, name);
await writeFile(path, fixture.bytes);
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = evidenceResultOf(goBinarySchema).parse(result.json);
expect(evidence.normalized_result.build_info).toMatchObject({
encoding: "pointer",
go_version: "go1.26.0",
go_version_bytes_base64: Buffer.from("go1.26.0").toString("base64"),
module_bytes_base64: fixture.moduleBytes.toString("base64"),
module: { path: "example.com/tool/cmd/tool" },
version_location: { offset: fixture.versionOffset, bytes: 8 },
module_location: {
offset: fixture.moduleOffset,
bytes: fixture.moduleBytes.length,
},
});
expect(evidence.subject?.digest?.sha256).toBe(
createHash("sha256").update(fixture.bytes).digest("hex"),
);
const response = await call("inspect_go_binary", { path });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
expect(response.structuredContent).toEqual(evidence);
expect(await readFile(path)).toEqual(fixture.bytes);
}
const damaged = createGoCrossLoadPointerFixture("contents");
damaged.bytes.writeBigUInt64LE(0x11001n, 200); // A one-byte virtual gap.
const path = join(root, "elf-cross-load-gap");
await writeFile(path, damaged.bytes);
const rejected = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(rejected.exitCode).toBe(1);
expect(rejected.json).toMatchObject({
code: "invalid_request",
details: { issues: [{ path: ["path"], reason: "invalid_format" }] },
});
const response = await call("inspect_go_binary", { path });
expect(parseMcpToolError(response)).toEqual({ error: rejected.json });
await client.ping();
},
60000,
);
+18
View File
@@ -12,6 +12,7 @@ import { readMachoImage } from "../../../src/native/metadata/MachoImage.js";
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
import {
createGoBinaryFixture,
createGoPeHeaderPointerFixture,
createGoSplitElfFixture,
createGoOverlappingElfFixture,
GO_MODULE_TEXT,
@@ -264,6 +265,23 @@ it("refuses mapped pointer lengths outside the file instead of rounding 64-bit a
expect(() => readGoBinaryImage(length.bytes)).toThrowError(/limit|length/i);
});
it.each(["file bytes", "zero-fill"])(
"refuses a header-backed PE module descriptor contradicted by %s",
(alias) => {
const { bytes } = createGoPeHeaderPointerFixture(64, "descriptors");
const section = 152 + bytes.readUInt16LE(148) + 40;
bytes.writeUInt16LE(2, 134);
bytes.write(".alias", section, "ascii");
bytes.writeUInt32LE(16, section + 8);
bytes.writeUInt32LE(528, section + 12);
if (alias === "file bytes") {
bytes.writeUInt32LE(16, section + 16);
bytes.writeUInt32LE(1536, section + 20);
}
expect(() => readGoBinaryImage(bytes)).toThrowError(/ambiguous/i);
},
);
it("refuses overlong or truncated varints before decoding or allocating strings", () => {
const fixture = createGoBinaryFixture();
fixture.bytes.fill(
@@ -31,6 +31,58 @@ it("does not confuse adjacent mappings with overlapping byte spans", () => {
expect(mappedFileOffset([selected, adjacent], 164n, 0)).toBe(80);
});
it("maps a split span across unsorted adjacent and agreeing overlapping ranges", () => {
expect(
mappedFileOffset(
[
{ address: 164n, offset: 80, size: 32 },
{ address: 156n, offset: 72, size: 12 },
selected,
],
156n,
24,
),
).toBe(72);
});
it.each([
{ address: 165n, offset: 81, size: 32 }, // One virtual byte is unbacked.
{ address: 164n, offset: 96, size: 32 }, // Virtual adjacency, different file translation.
])(
"refuses a split span with a gap or discontinuous file bytes",
(adjacent) => {
expect(() => mappedFileOffset([selected, adjacent], 156n, 24)).toThrowError(
/mapped|ambiguous/i,
);
},
);
it("checks aliases and zero-fill inside the later part of a split span", () => {
const adjacent = { address: 164n, offset: 80, size: 32 };
expect(() =>
mappedFileOffset(
[selected, adjacent, { address: 170n, offset: 4, size: 4 }],
156n,
24,
),
).toThrowError(/ambiguous/i);
expect(() =>
mappedFileOffset([selected, adjacent], 156n, 24, [
{ address: 170n, size: 4n },
]),
).toThrowError(/ambiguous/i);
});
it("keeps ambiguous zero-length endpoints invalid", () => {
expect(() =>
mappedFileOffset(
[selected, { address: 164n, offset: 96, size: 32 }],
164n,
0,
),
).toThrowError(/ambiguous/i);
});
it.each([
{ address: 132n, size: 4n },
{ address: 112n, size: 12n },
@@ -0,0 +1,187 @@
import { expect, it } from "vitest";
import { linkIl2cppBinary } from "../../../src/il2cpp/Il2cppBinaryFile.js";
import {
isMapped,
openImageSteps,
readPointer,
readSlot,
} from "../../../src/il2cpp/Il2cppBinaryImage.js";
import { readIl2cppMetadataForLinking } from "../../../src/il2cpp/Il2cppMetadataFile.js";
import {
createElfFixture,
createIl2cppBinaryFixture,
FIXTURE_BINARY_BASE,
} from "../../fixtures/il2cpp/binary.js";
import { createLinkedIl2cppMetadataFixture } from "../../fixtures/il2cpp/metadata.js";
const { metadata, link: facts } = readIl2cppMetadataForLinking(
createLinkedIl2cppMetadataFixture().bytes,
);
const link = (bytes: Buffer) => linkIl2cppBinary(bytes, metadata, facts);
const open = (bytes: Buffer) => {
const steps = openImageSteps(bytes, undefined);
let step = steps.next();
while (!step.done) step = steps.next();
return step.value;
};
/** Replace PT_LOAD declarations, keeping the original PT_DYNAMIC and payload. */
const withLoadRanges = (
bytes: Buffer,
ranges: readonly {
readonly offset: number;
readonly size: number;
readonly zeroFill?: number;
}[],
): Buffer => {
const headers: Buffer[] = [];
const start = Number(bytes.readBigUInt64LE(32));
const width = bytes.readUInt16LE(54);
for (let index = 0; index < bytes.readUInt16LE(56); index++) {
const at = start + index * width;
if (bytes.readUInt32LE(at) !== 1)
headers.push(bytes.subarray(at, at + width));
}
for (const range of ranges) {
const header = Buffer.alloc(width);
header.writeUInt32LE(1);
header.writeUInt32LE(7, 4);
header.writeBigUInt64LE(BigInt(range.offset), 8);
const address = FIXTURE_BINARY_BASE + BigInt(range.offset);
header.writeBigUInt64LE(address, 16);
header.writeBigUInt64LE(address, 24);
header.writeBigUInt64LE(BigInt(range.size), 32);
header.writeBigUInt64LE(BigInt(range.size + (range.zeroFill ?? 0)), 40);
header.writeBigUInt64LE(1n, 48);
headers.push(header);
}
const result = Buffer.concat([bytes, ...headers]);
result.writeBigUInt64LE(BigInt(bytes.length), 32);
result.writeUInt16LE(headers.length, 56);
return result;
};
it.each([183, 62])(
"links agreeing split ELF RELA spans without changing input bytes (machine %s)",
(machine) => {
const original = createElfFixture(createIl2cppBinaryFixture(), { machine });
// Split the first Elf64_Rela after its destination and half its info word.
const split = withLoadRanges(original, [
{ offset: 0, size: 0x100c },
{ offset: 0x100c, size: original.length - 0x100c },
]);
const snapshot = Buffer.from(split);
expect(link(split)).toEqual(link(original));
expect(split).toEqual(snapshot);
},
);
it.each([false, true])(
"refuses a RELA addend claimed by file bytes and zero-fill (split %s)",
(split) => {
const original = createElfFixture(createIl2cppBinaryFixture());
const bytes = withLoadRanges(original, [
...(split
? [
{ offset: 0, size: 0x100c },
{ offset: 0x100c, size: original.length - 0x100c },
]
: [{ offset: 0, size: original.length }]),
{ offset: 0x1010, size: 0, zeroFill: 8 },
]);
const snapshot = Buffer.from(bytes);
expect(() => link(bytes)).toThrow(
expect.objectContaining({
reason: "malformed-container",
message: expect.stringContaining(
"ambiguous file and zero-fill mappings",
),
}),
);
expect(bytes).toEqual(snapshot);
},
);
it("refuses a RELATIVE destination claimed by both file bytes and zero-fill", () => {
const original = createElfFixture(createIl2cppBinaryFixture());
const bytes = withLoadRanges(original, [
{ offset: 0, size: original.length },
{ offset: 0x304, size: 0, zeroFill: 4 },
]);
expect(() => link(bytes)).toThrow(
expect.objectContaining({
reason: "malformed-container",
message: expect.stringContaining("ambiguous file and zero-fill mappings"),
}),
);
});
it("refuses an unresolved count relocation with conflicting file/zero-fill owners", () => {
const original = createElfFixture(createIl2cppBinaryFixture(), {
unresolvedRelocations: [{ offset: 0x600, kind: 258 }], // R_AARCH64_ABS32
});
const bytes = withLoadRanges(original, [
{ offset: 0, size: original.length },
{ offset: 0x600, size: 0, zeroFill: 4 },
]);
const snapshot = Buffer.from(bytes);
expect(() => link(bytes)).toThrow(
expect.objectContaining({
reason: "malformed-container",
message: expect.stringContaining("ambiguous file and zero-fill mappings"),
}),
);
expect(bytes).toEqual(snapshot);
});
it("clears a file-backed unresolved count before scanning registration candidates", () => {
const control = createElfFixture(createIl2cppBinaryFixture());
const bytes = createElfFixture(createIl2cppBinaryFixture(), {
unresolvedRelocations: [{ offset: 0x600, kind: 258 }],
});
const snapshot = Buffer.from(bytes);
expect(link(control).code_gen_modules_field).toBe(
FIXTURE_BINARY_BASE + 0x600n,
);
expect(() => link(bytes)).toThrow(
expect.objectContaining({ reason: "code-registration-not-found" }),
);
expect(bytes).toEqual(snapshot);
});
it.each([false, true])(
"retains unrelated unresolved targets without file bytes (zero-fill %s)",
(zeroFill) => {
const control = createElfFixture(createIl2cppBinaryFixture());
const original = createElfFixture(createIl2cppBinaryFixture(), {
unresolvedRelocations: [{ offset: 0x2000, kind: 258 }],
});
const bytes = withLoadRanges(original, [
{ offset: 0, size: original.length },
...(zeroFill ? [{ offset: 0x2000, size: 0, zeroFill: 4 }] : []),
]);
expect(link(bytes)).toEqual(link(control));
expect(readSlot(open(bytes), FIXTURE_BINARY_BASE + 0x2000n)).toBe(
"unknown",
);
},
);
it("keeps mapped-span admission consistent with pointer reads across zero-fill aliases", () => {
const original = createElfFixture(createIl2cppBinaryFixture());
const ranges = [
{ offset: 0, size: 0xc08 },
{ offset: 0xc08, size: original.length - 0xc08 },
];
for (const conflict of [false, true]) {
const bytes = withLoadRanges(original, [
...ranges,
...(conflict ? [{ offset: 0xc08, size: 0, zeroFill: 4 }] : []),
]);
const image = open(bytes);
const address = FIXTURE_BINARY_BASE + 0xc04n;
expect(isMapped(image, address, 8)).toBe(!conflict);
expect(readPointer(image, address)).toBe(conflict ? null : 0n);
expect(isMapped(image, FIXTURE_BINARY_BASE + 0xc00n, 4)).toBe(true);
}
});
+86
View File
@@ -141,6 +141,92 @@ export const createGoBinaryFixture = (
return { bytes, headerOffset, versionOffset, moduleOffset, moduleBytes };
};
/** Legacy strings in declared PE header memory, with the record still in .data. */
export const createGoPeHeaderPointerFixture = (
bits: 32 | 64,
placement: "contents" | "descriptors",
) => {
const fixture = createGoBinaryFixture({
format: "pe",
bits,
encoding: "pointer",
});
const { bytes, headerOffset } = fixture;
const width = bits / 8;
const word = (offset: number, value: number) => {
if (bits === 64) bytes.writeBigUInt64LE(BigInt(value), offset);
else bytes.writeUInt32LE(value, offset);
};
const section = 152 + bytes.readUInt16LE(148);
bytes.writeUInt16LE(0x22, 150);
bytes.writeUInt32LE(4096, 184); // SectionAlignment.
bytes.writeUInt32LE(512, 188); // FileAlignment.
bytes.writeUInt32LE(8192, 208); // SizeOfImage.
bytes.writeUInt32LE(1024, 212); // SizeOfHeaders.
bytes.writeUInt32LE(4096, section + 12);
const sectionAddress = (offset: number) => 0x11000 + offset - 1024;
word(headerOffset + 16, sectionAddress(1536));
word(headerOffset + 16 + width, sectionAddress(1536 + width * 2));
word(1536, sectionAddress(fixture.versionOffset));
word(1536 + width * 2, sectionAddress(fixture.moduleOffset));
if (placement === "descriptors") {
bytes.copy(bytes, 512, 1536, 1536 + width * 4);
word(headerOffset + 16, 0x10200);
word(headerOffset + 16 + width, 0x10200 + width * 2);
return fixture;
}
const versionOffset = 512;
const moduleOffset = 528;
bytes.copy(
bytes,
versionOffset,
fixture.versionOffset,
fixture.versionOffset + 8,
);
fixture.moduleBytes.copy(bytes, moduleOffset);
word(1536, 0x10000 + versionOffset);
word(1536 + width * 2, 0x10000 + moduleOffset);
return { ...fixture, versionOffset, moduleOffset };
};
/** A pointer string or its two-word descriptor crossing adjacent ELF PT_LOADs. */
export const createGoCrossLoadPointerFixture = (
placement: "contents" | "descriptor",
) => {
const fixture = createGoBinaryFixture({ encoding: "pointer" });
const bytes = Buffer.alloc(8192);
fixture.bytes.copy(bytes);
bytes.writeUInt16LE(2, 56);
bytes.writeBigUInt64LE(4096n, 160);
bytes.writeBigUInt64LE(4096n, 168);
bytes.writeBigUInt64LE(4096n, 176);
bytes.writeUInt32LE(1, 184);
bytes.writeUInt32LE(6, 188);
bytes.writeBigUInt64LE(4096n, 192);
bytes.writeBigUInt64LE(0x11000n, 200);
bytes.writeBigUInt64LE(4096n, 216);
bytes.writeBigUInt64LE(4096n, 224);
bytes.writeBigUInt64LE(4096n, 232);
bytes.writeBigUInt64LE(32n, 416); // Named discovery section contains only the record.
const moduleOffset = 5000;
fixture.moduleBytes.copy(bytes, moduleOffset);
bytes.writeBigUInt64LE(0x10000n + BigInt(moduleOffset), 1552);
if (placement === "descriptor") {
bytes.copy(bytes, 4088, 1536, 1552);
bytes.writeBigUInt64LE(0x10ff8n, fixture.headerOffset + 16);
return { ...fixture, bytes, moduleOffset };
}
const versionOffset = 4092;
bytes.copy(
bytes,
versionOffset,
fixture.versionOffset,
fixture.versionOffset + 8,
);
bytes.writeBigUInt64LE(0x10ffcn, 1536);
return { ...fixture, bytes, versionOffset, moduleOffset };
};
/** Build an ELF inline record with a selected aggregate string-content size. */
export const createGoInlineAggregateFixture = (contentBytes: number) => {
const fixture = createGoBinaryFixture({ moduleText: "" });