mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(native): preserve mapping and literal coverage
This commit is contained in:
@@ -1121,6 +1121,21 @@ const stringsDimension = (
|
||||
).dimensions.find(({ dimension }) => dimension === "strings_names");
|
||||
|
||||
describe("function comparison Swift string literals", () => {
|
||||
it("retains literal comparisons when legacy dossiers omit facet declarations", () => {
|
||||
const legacy = (base: "0x1000" | "0x2000", value: string) => {
|
||||
const result = withLiterals(base, [value]);
|
||||
delete result.facet_availability;
|
||||
return result;
|
||||
};
|
||||
const left = legacy("0x1000", "SleepDisabled");
|
||||
expect(
|
||||
stringsDimension("1", left, "2", legacy("0x2000", "SleepDisabled")),
|
||||
).toMatchObject({ status: "unchanged" });
|
||||
expect(
|
||||
stringsDimension("1", left, "2", legacy("0x2000", "Holding awake")),
|
||||
).toMatchObject({ status: "changed" });
|
||||
});
|
||||
|
||||
it("treats differing decoded literals as a strings and names change", () => {
|
||||
expect(
|
||||
stringsDimension(
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
normalizeCfg,
|
||||
referencedStringProjection,
|
||||
sorted,
|
||||
swiftLiteralCoverage,
|
||||
swiftLiteralSitesConflict,
|
||||
} from "./functionComparisonNormalization.js";
|
||||
import type {
|
||||
@@ -101,7 +102,9 @@ const facetLimitations = (
|
||||
strings_names: [
|
||||
[
|
||||
"strings_names",
|
||||
dossier.referenced_strings.length + dossier.referenced_names.length,
|
||||
dossier.referenced_strings.length +
|
||||
dossier.referenced_names.length +
|
||||
(dossier.swift_string_literals?.length ?? 0),
|
||||
],
|
||||
],
|
||||
cfg: [["cfg", dossier.basic_blocks.length]],
|
||||
@@ -515,15 +518,6 @@ const groupStringsByOffset = (
|
||||
return groups;
|
||||
};
|
||||
|
||||
const swiftLiteralCoverage = (
|
||||
snapshot: FunctionSnapshot,
|
||||
): "absent" | "complete" | "partial" =>
|
||||
(snapshot.dossier.swift_string_literal_omissions ?? []).length > 0
|
||||
? "partial"
|
||||
: snapshot.dossier.swift_string_literals === undefined
|
||||
? "absent"
|
||||
: "complete";
|
||||
|
||||
const compareReferences = (
|
||||
compared: FunctionRelationshipComparison,
|
||||
context: ComparisonContext,
|
||||
|
||||
@@ -191,9 +191,18 @@ export const swiftLiteralSitesConflict = (
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Omitted literal inventory differs from an observed empty inventory. */
|
||||
export const swiftLiteralCoverage = (
|
||||
snapshot: FunctionSnapshot,
|
||||
): "absent" | "complete" | "partial" =>
|
||||
(snapshot.dossier.swift_string_literal_omissions ?? []).length > 0
|
||||
? "partial"
|
||||
: snapshot.dossier.swift_string_literals === undefined
|
||||
? "absent"
|
||||
: "complete";
|
||||
|
||||
const swiftLiteralSites = (snapshot: FunctionSnapshot) => {
|
||||
const base = snapshot.dossier.procedure.address;
|
||||
const omissions = snapshot.dossier.swift_string_literal_omissions ?? [];
|
||||
const site = (sourceAddress: string, registers: readonly string[]) => {
|
||||
const offset = relativeAddress(sourceAddress, base);
|
||||
return offset === null ? null : canonicalJson([offset, registers]);
|
||||
@@ -211,9 +220,7 @@ const swiftLiteralSites = (snapshot: FunctionSnapshot) => {
|
||||
].sort(compareUnicodeCodePoints),
|
||||
);
|
||||
}
|
||||
const complete =
|
||||
snapshot.dossier.swift_string_literals !== undefined &&
|
||||
omissions.length === 0;
|
||||
const complete = swiftLiteralCoverage(snapshot) === "complete";
|
||||
return { values, complete };
|
||||
};
|
||||
|
||||
|
||||
@@ -133,6 +133,106 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
/** File ranges remain inside the padded source while VM ranges may alias. */
|
||||
const mappingSegment = (
|
||||
address: bigint,
|
||||
fileOffset: number,
|
||||
fileSize: number,
|
||||
memorySize = fileSize,
|
||||
): Buffer => {
|
||||
const command = Buffer.alloc(72);
|
||||
command.writeUInt32LE(0x19);
|
||||
command.writeUInt32LE(command.length, 4);
|
||||
command.writeBigUInt64LE(address, 24);
|
||||
command.writeBigUInt64LE(BigInt(memorySize), 32);
|
||||
command.writeBigUInt64LE(BigInt(fileOffset), 40);
|
||||
command.writeBigUInt64LE(BigInt(fileSize), 48);
|
||||
command.writeUInt32LE(1, 60);
|
||||
return command;
|
||||
};
|
||||
|
||||
const mappingImage = (commands: readonly Buffer[]): Buffer => {
|
||||
const bytes = Buffer.alloc(512);
|
||||
Buffer.from(machoImage({ commands })).copy(bytes);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
describe("Mach-O metadata span mapping", () => {
|
||||
it("rejects a conflicting alias covering only the end of a pointer", () => {
|
||||
const layout = parseMachoLayout(
|
||||
mappingImage([
|
||||
mappingSegment(0x1000n, 0, 512),
|
||||
mappingSegment(0x1104n, 400, 4),
|
||||
]),
|
||||
"arm64",
|
||||
);
|
||||
expect(layout.offset(0x1100n, 4)).toBe(256);
|
||||
expect(() => layout.offset(0x1100n, 8)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it("maps adjacent ranges and duplicate aliases with the same file translation", () => {
|
||||
const layout = parseMachoLayout(
|
||||
mappingImage([
|
||||
mappingSegment(0x1000n, 0, 128),
|
||||
mappingSegment(0x1080n, 128, 128),
|
||||
mappingSegment(0x107cn, 124, 8),
|
||||
]),
|
||||
"arm64",
|
||||
);
|
||||
expect(layout.offset(0x107cn, 8)).toBe(124);
|
||||
});
|
||||
|
||||
it("rejects file aliases into another segment's zero-filled memory", () => {
|
||||
const layout = parseMachoLayout(
|
||||
mappingImage([
|
||||
mappingSegment(0x1000n, 0, 128, 256),
|
||||
mappingSegment(0x1080n, 256, 8),
|
||||
]),
|
||||
"arm64",
|
||||
);
|
||||
expect(() => layout.offset(0x1080n, 8)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it.each([-1, 0.5, Number.POSITIVE_INFINITY])(
|
||||
"rejects an invalid requested span size %s",
|
||||
(size) => {
|
||||
const layout = parseMachoLayout(
|
||||
mappingImage([mappingSegment(0x1000n, 0, 512)]),
|
||||
"arm64",
|
||||
);
|
||||
expect(() => layout.offset(0x1000n, size)).toThrow(RangeError);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps dispatch coverage partial when a class-list pointer has a partial alias", () => {
|
||||
const main = Buffer.concat([
|
||||
mappingSegment(0x1000n, 0, 512),
|
||||
Buffer.alloc(80),
|
||||
]);
|
||||
main.writeUInt32LE(main.length, 4);
|
||||
main.writeUInt32LE(1, 64);
|
||||
main.write("__objc_classlist", 72, "ascii");
|
||||
main.writeBigUInt64LE(0x1100n, 72 + 32);
|
||||
main.writeBigUInt64LE(8n, 72 + 40);
|
||||
const result = decodeAppleDispatchMetadata(
|
||||
mappingImage([main, mappingSegment(0x1104n, 400, 4)]),
|
||||
10,
|
||||
{ path: "/aliased", sha256: "fixture" },
|
||||
);
|
||||
expect(result.objc_classes).toEqual([]);
|
||||
expect(result.coverage).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
status: "partial",
|
||||
reason: expect.stringContaining(
|
||||
"Unmapped or ambiguous metadata pointer",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("native Mach-O slice selection", () => {
|
||||
it("rejects misaligned load commands", () => {
|
||||
expect(() =>
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
readMachoLoadCommandDeclarations,
|
||||
} from "../domain/apple/machoLoadCommands.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
createFileMappingIndex,
|
||||
type ZeroFillMapping,
|
||||
} from "./metadata/BinaryContainer.js";
|
||||
|
||||
import type { FixupCommands } from "./AppleMachoFixups.js";
|
||||
import { decodeMachoFixedName, decodeMachoName } from "./AppleMachoNames.js";
|
||||
@@ -197,6 +202,7 @@ export const parseMachoLayout = (
|
||||
slice,
|
||||
sliceEnd,
|
||||
segments: [],
|
||||
zeroFills: [],
|
||||
sections: [],
|
||||
dylibs: [],
|
||||
binds: [],
|
||||
@@ -220,19 +226,27 @@ export const parseMachoLayout = (
|
||||
const complete = framing.finish();
|
||||
if (!complete.ok) throw new RangeError(complete.error);
|
||||
const { segments, sections, chained, binds, rebases, dylibs } = state;
|
||||
const mappings = createFileMappingIndex(
|
||||
segments.map((segment) => ({
|
||||
address: segment.address,
|
||||
offset: segment.offset,
|
||||
size: Number(segment.size),
|
||||
})),
|
||||
state.zeroFills,
|
||||
);
|
||||
const offset = (address: bigint, size = 1): number => {
|
||||
const matches = segments.filter(
|
||||
(segment) =>
|
||||
address >= segment.address &&
|
||||
address + BigInt(size) <= segment.address + segment.size,
|
||||
);
|
||||
if (matches.length !== 1)
|
||||
try {
|
||||
return mappings.offset(address, size);
|
||||
} catch (cause) {
|
||||
// Metadata consumers use RangeError to retain partial facts when a
|
||||
// pointer is unavailable. Mapping interpretation belongs to the shared
|
||||
// image reader, including aliases that only overlap part of the span.
|
||||
if (!(cause instanceof BinaryFormatFailure)) throw cause;
|
||||
throw new RangeError(
|
||||
`Unmapped or ambiguous metadata pointer ${hex(address)}`,
|
||||
{ cause },
|
||||
);
|
||||
const segment = matches[0];
|
||||
if (segment === undefined) throw new RangeError("Missing metadata segment");
|
||||
return segment.offset + Number(address - segment.address);
|
||||
}
|
||||
};
|
||||
return {
|
||||
segments,
|
||||
@@ -261,6 +275,7 @@ interface LayoutState {
|
||||
readonly slice: number;
|
||||
readonly sliceEnd: number;
|
||||
readonly segments: Segment[];
|
||||
readonly zeroFills: ZeroFillMapping[];
|
||||
readonly sections: Section[];
|
||||
readonly dylibs: FixupCommands["dylibs"][number][];
|
||||
readonly binds: FixupCommands["binds"][number][];
|
||||
@@ -290,6 +305,8 @@ const collectSegment = (
|
||||
if (!parsed.ok) throw new RangeError("Truncated segment command");
|
||||
const {
|
||||
address,
|
||||
memorySize,
|
||||
nameBytes,
|
||||
fileSize,
|
||||
fileOffset,
|
||||
protection,
|
||||
@@ -304,6 +321,11 @@ const collectSegment = (
|
||||
offset: state.slice + Number(fileOffset),
|
||||
executable: (protection & 4) !== 0,
|
||||
});
|
||||
if (memorySize > fileSize && decodeMachoFixedName(nameBytes) !== "__PAGEZERO")
|
||||
state.zeroFills.push({
|
||||
address: address + fileSize,
|
||||
size: memorySize - fileSize,
|
||||
});
|
||||
if (fileOffset === 0n && fileSize > 0n) state.baseAddress ??= address;
|
||||
if (headerSize + count * 80 > size)
|
||||
throw new RangeError("Truncated section table");
|
||||
|
||||
Reference in New Issue
Block a user