mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
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:
1 parent
b7ebccf8d3
commit
8b3902d377
12 files changed
+526
-41
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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 => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
Vendored
+86
@@ -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: "" });
|
||||
|
||||
Reference in new issue
Block a user