mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
3b0ea590fc
* fix(native): honor Mach-O chained pointer locations and library ordinals Walk declared page starts and next links before decoding chained pointers, preserving plain pointers between fixups, zero-offset rebases, and known facts in malformed chains. Interpret positive dylib ordinals with dyld's special-ordinal cutoff so valid dependency provenance is retained. Give the Swift verification fixture an explicit module name so link-mode filenames do not produce invalid identifiers. Validation: 46 focused regressions; pre-fix reproductions in an isolated copy; real macOS CLI/MCP verification of 10 compiled Objective-C/Swift, arm64/arm64e, chained/legacy and symbols/stripped fixture variants. * perf(native): reuse parsed Mach-O chain membership * test(native): validate parsed-chain repair against local gates * test(native): pin chained fixup page failure provenance * fix(native): bound parsed chained page facts and metadata * test(server): pin shared partial Evidence provenance --------- Co-authored-by: baron <richstyles@gmail.com>
403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { decodeAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
|
|
|
|
const base = 0x100000000n;
|
|
const fixture = (relative = false) => {
|
|
const bytes = Buffer.alloc(4096);
|
|
const u32 = (offset: number, value: number) =>
|
|
bytes.writeUInt32LE(value, offset);
|
|
const ptr = (offset: number, target: number) =>
|
|
bytes.writeBigUInt64LE(base + BigInt(target), offset);
|
|
u32(0, 0xfeedfacf);
|
|
u32(4, 0x0100000c);
|
|
u32(16, 1);
|
|
u32(20, 152);
|
|
u32(32, 0x19);
|
|
u32(36, 152);
|
|
bytes.write("__DATA", 40);
|
|
ptr(56, 0);
|
|
bytes.writeBigUInt64LE(4096n, 64);
|
|
bytes.writeBigUInt64LE(4096n, 80);
|
|
u32(92, 5);
|
|
u32(96, 1);
|
|
bytes.write("__objc_classlist", 104);
|
|
bytes.write("__DATA", 120);
|
|
ptr(136, 0x180);
|
|
bytes.writeBigUInt64LE(8n, 144);
|
|
u32(152, 0x180);
|
|
ptr(0x180, 0x200);
|
|
ptr(0x200, 0x280);
|
|
ptr(0x220, 0x300);
|
|
ptr(0x2a0, 0x380);
|
|
u32(0x308, 16);
|
|
ptr(0x318, 0x450);
|
|
ptr(0x320, 0x480);
|
|
ptr(0x330, 0x500);
|
|
ptr(0x328, 0x700);
|
|
ptr(0x340, 0x600);
|
|
u32(0x380, 1);
|
|
ptr(0x398, 0x450);
|
|
bytes.write("Fixture\0", 0x450);
|
|
bytes.write("performAction:\0", 0x460);
|
|
u32(0x480, relative ? 0xc000000c : 24);
|
|
u32(0x484, 1);
|
|
if (relative) {
|
|
bytes.writeInt32LE(0x460 - 0x488, 0x488);
|
|
bytes.writeInt32LE(0x5c0 - 0x48c, 0x48c);
|
|
bytes.writeInt32LE(0x700 - 0x490, 0x490);
|
|
} else {
|
|
ptr(0x488, 0x460);
|
|
ptr(0x490, 0x5c0);
|
|
ptr(0x498, 0x700);
|
|
}
|
|
u32(0x500, 32);
|
|
u32(0x504, 1);
|
|
ptr(0x508, 0x580);
|
|
ptr(0x510, 0x5a0);
|
|
ptr(0x518, 0x5c0);
|
|
u32(0x580, 8);
|
|
bytes.write("state\0", 0x5a0);
|
|
bytes.write("i\0", 0x5c0);
|
|
u32(0x600, 16);
|
|
u32(0x604, 3);
|
|
ptr(0x608, 0x640);
|
|
ptr(0x610, 0x650);
|
|
ptr(0x618, 0x660);
|
|
ptr(0x620, 0x670);
|
|
ptr(0x628, 0x680);
|
|
ptr(0x630, 0x690);
|
|
bytes.write("atomicValue\0", 0x640);
|
|
bytes.write("Tq\0", 0x650);
|
|
bytes.write("nonatomicValue\0", 0x660);
|
|
bytes.write("Tq,N\0", 0x670);
|
|
bytes.write("unknownValue\0", 0x680);
|
|
bytes.writeBigUInt64LE(1n, 0x700);
|
|
ptr(0x708, 0x740);
|
|
ptr(0x748, 0x900);
|
|
ptr(0x758, 0x800);
|
|
ptr(0x768, 0x840);
|
|
u32(0x780, 72);
|
|
u32(0x800, 24);
|
|
u32(0x804, 1);
|
|
ptr(0x808, 0x920);
|
|
u32(0x840, 24);
|
|
u32(0x844, 1);
|
|
ptr(0x848, 0x940);
|
|
bytes.write("FixtureProtocol\0", 0x900);
|
|
bytes.write("requiredAction:\0", 0x920);
|
|
bytes.write("optionalAction:\0", 0x940);
|
|
return bytes;
|
|
};
|
|
const provenance = { path: "/fixture/app", sha256: "a".repeat(64) };
|
|
describe("chained Apple dispatch binary metadata", () => {
|
|
it("keeps absolute metadata pointers between declared chained fixups", () => {
|
|
const bytes = fixture();
|
|
bytes.writeUInt32LE(2, 16);
|
|
bytes.writeUInt32LE(168, 20);
|
|
bytes.writeUInt32LE(0x80000034, 184);
|
|
bytes.writeUInt32LE(16, 188);
|
|
bytes.writeUInt32LE(0xa00, 192);
|
|
bytes.writeUInt32LE(64, 196);
|
|
// Only the class-list pointer needs rebasing. All remaining metadata
|
|
// pointers carry their stored absolute values, outside the single chain.
|
|
bytes.writeBigUInt64LE(0x200n, 0x180);
|
|
bytes.writeUInt32LE(28, 0xa04);
|
|
bytes.writeUInt32LE(60, 0xa08);
|
|
bytes.writeUInt32LE(60, 0xa0c);
|
|
bytes.writeUInt32LE(1, 0xa14);
|
|
bytes.writeUInt32LE(1, 0xa1c);
|
|
bytes.writeUInt32LE(8, 0xa20);
|
|
bytes.writeUInt32LE(24, 0xa24);
|
|
bytes.writeUInt16LE(0x1000, 0xa28);
|
|
bytes.writeUInt16LE(6, 0xa2a);
|
|
bytes.writeUInt16LE(1, 0xa38);
|
|
bytes.writeUInt16LE(0x180, 0xa3a);
|
|
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
|
expect(result.objc_classes[0]).toMatchObject({ name: "Fixture" });
|
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
|
selector: "performAction:",
|
|
implementation_address: "0x100000700",
|
|
});
|
|
expect(
|
|
result.coverage.find(({ facet }) => facet === "pointer_fixups"),
|
|
).toMatchObject({ status: "complete" });
|
|
});
|
|
});
|
|
describe("Apple dispatch binary metadata", () => {
|
|
it.each([false, true])(
|
|
"decodes stripped absolute/relative method lists (relative=%s) with exact offsets",
|
|
(relative) => {
|
|
const result = decodeAppleDispatchMetadata(
|
|
fixture(relative),
|
|
100,
|
|
provenance,
|
|
);
|
|
expect(result.objc_classes).toMatchObject([
|
|
{
|
|
name: "Fixture",
|
|
instance_size: 16,
|
|
location: { address: "0x100000200", file_offset: 512 },
|
|
},
|
|
{ name: "Fixture", is_meta_class: true },
|
|
]);
|
|
expect(result.objc_dispatch_implementations).toMatchObject([
|
|
{
|
|
selector: "performAction:",
|
|
implementation_address: "0x100000700",
|
|
location: { file_offset: 0x488 },
|
|
decode: { status: "decoded" },
|
|
},
|
|
]);
|
|
expect(result.objc_ivars).toMatchObject([
|
|
{
|
|
name: "state",
|
|
type_encoding: "i",
|
|
offset: 8,
|
|
location: { file_offset: 0x508 },
|
|
},
|
|
]);
|
|
expect(result.objc_classes[0]?.properties).toEqual([
|
|
{
|
|
name: "atomicValue",
|
|
type_encoding: "q",
|
|
attributes: [{ name: "T", value: "q" }],
|
|
atomicity: "atomic",
|
|
is_readonly: false,
|
|
getter: null,
|
|
setter: null,
|
|
},
|
|
{
|
|
name: "nonatomicValue",
|
|
type_encoding: "q",
|
|
attributes: [
|
|
{ name: "T", value: "q" },
|
|
{ name: "N", value: "" },
|
|
],
|
|
atomicity: "nonatomic",
|
|
is_readonly: false,
|
|
getter: null,
|
|
setter: null,
|
|
},
|
|
{
|
|
name: "unknownValue",
|
|
type_encoding: null,
|
|
attributes: [],
|
|
atomicity: null,
|
|
is_readonly: null,
|
|
getter: null,
|
|
setter: null,
|
|
},
|
|
]);
|
|
expect(result.objc_classes[0]).toMatchObject({
|
|
is_meta_class: false,
|
|
is_root_class: false,
|
|
ivar_count: 1,
|
|
});
|
|
expect(result.objc_classes[1]).toMatchObject({
|
|
is_meta_class: true,
|
|
is_root_class: false,
|
|
ivar_count: null,
|
|
});
|
|
expect(result.objc_classes[0]?.methods[0]).toMatchObject({
|
|
is_required: null,
|
|
is_optional: null,
|
|
});
|
|
expect(result.objc_protocols[0]).toMatchObject({
|
|
name: "FixtureProtocol",
|
|
methods: [
|
|
{
|
|
selector: "requiredAction:",
|
|
is_required: true,
|
|
is_optional: false,
|
|
},
|
|
],
|
|
optional_methods: [
|
|
{
|
|
selector: "optionalAction:",
|
|
is_required: false,
|
|
is_optional: true,
|
|
},
|
|
],
|
|
});
|
|
},
|
|
);
|
|
it("keeps unsupported pointers and record truncation explicit", () => {
|
|
const bytes = fixture();
|
|
bytes.writeBigUInt64LE(0xffffffffffffffffn, 0x498);
|
|
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
|
implementation_address: null,
|
|
decode: { status: "partial" },
|
|
});
|
|
const truncated = decodeAppleDispatchMetadata(fixture(), 1, provenance);
|
|
expect(truncated.coverage[0]?.reason).toContain("max_records_reached");
|
|
expect(
|
|
truncated.coverage.find(
|
|
({ facet }) => facet === "objc_properties_categories",
|
|
),
|
|
).toMatchObject({ status: "partial", reason: "max_records_reached" });
|
|
});
|
|
it("rejects malformed command and section boundaries", () => {
|
|
const bytes = fixture();
|
|
bytes.writeUInt32LE(7, 36);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"command size",
|
|
);
|
|
expect(() =>
|
|
decodeAppleDispatchMetadata(Buffer.alloc(10), 100, provenance),
|
|
).toThrow("Truncated");
|
|
});
|
|
});
|
|
|
|
describe("universal Apple dispatch metadata", () => {
|
|
it.each([false, true])("decodes selected FAT slices (fat64=%s)", (fat64) => {
|
|
const thin = fixture();
|
|
const offset = 256;
|
|
const bytes = Buffer.alloc(offset + thin.length);
|
|
bytes.writeUInt32BE(fat64 ? 0xcafebabf : 0xcafebabe, 0);
|
|
bytes.writeUInt32BE(1, 4);
|
|
bytes.writeUInt32BE(0x0100000c, 8);
|
|
if (fat64) {
|
|
bytes.writeBigUInt64BE(BigInt(offset), 16);
|
|
bytes.writeBigUInt64BE(BigInt(thin.length), 24);
|
|
} else {
|
|
bytes.writeUInt32BE(offset, 16);
|
|
bytes.writeUInt32BE(thin.length, 20);
|
|
}
|
|
thin.copy(bytes, offset);
|
|
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
|
expect(result.objc_classes[0]).toMatchObject({
|
|
name: "Fixture",
|
|
location: { address: "0x100000200", file_offset: offset + 512 },
|
|
});
|
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
|
implementation_address: "0x100000700",
|
|
location: { file_offset: offset + 0x488 },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("FAT64 dispatch slice validation", () => {
|
|
const wrapped = () => {
|
|
const thin = fixture();
|
|
const bytes = Buffer.alloc(256 + thin.length);
|
|
bytes.writeUInt32BE(0xcafebabf, 0);
|
|
bytes.writeUInt32BE(1, 4);
|
|
bytes.writeUInt32BE(0x0100000c, 8);
|
|
bytes.writeBigUInt64BE(256n, 16);
|
|
bytes.writeBigUInt64BE(BigInt(thin.length), 24);
|
|
thin.copy(bytes, 256);
|
|
return bytes;
|
|
};
|
|
|
|
it("rejects a truncated architecture table", () => {
|
|
expect(() =>
|
|
decodeAppleDispatchMetadata(wrapped().subarray(0, 32), 100, provenance),
|
|
).toThrow("Malformed FAT architecture table");
|
|
});
|
|
|
|
it.each([16, 24])(
|
|
"checks the full 64-bit slice extent at field %i",
|
|
(field) => {
|
|
const bytes = wrapped();
|
|
bytes.writeBigUInt64BE(0x100000000n, field);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"FAT architecture 0 slice range extends beyond the file",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("rejects ambiguous matching architectures", () => {
|
|
const bytes = wrapped();
|
|
bytes.writeUInt32BE(2, 4);
|
|
bytes.copy(bytes, 40, 8, 40);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"Ambiguous FAT architecture slice",
|
|
);
|
|
});
|
|
|
|
it("rejects a missing selected architecture", () => {
|
|
expect(() =>
|
|
decodeAppleDispatchMetadata(wrapped(), 100, provenance, "x86_64"),
|
|
).toThrow("Requested FAT architecture is absent");
|
|
});
|
|
});
|
|
|
|
describe("byte-swapped universal Apple dispatch metadata", () => {
|
|
const wrapped = (fat64: boolean) => {
|
|
const thin = fixture();
|
|
const offset = 256;
|
|
const bytes = Buffer.alloc(offset + thin.length);
|
|
bytes.writeUInt32LE(fat64 ? 0xcafebabf : 0xcafebabe, 0);
|
|
bytes.writeUInt32LE(1, 4);
|
|
bytes.writeUInt32LE(0x0100000c, 8);
|
|
if (fat64) {
|
|
bytes.writeBigUInt64LE(BigInt(offset), 16);
|
|
bytes.writeBigUInt64LE(BigInt(thin.length), 24);
|
|
} else {
|
|
bytes.writeUInt32LE(offset, 16);
|
|
bytes.writeUInt32LE(thin.length, 20);
|
|
}
|
|
thin.copy(bytes, offset);
|
|
return bytes;
|
|
};
|
|
|
|
it.each([false, true])(
|
|
"decodes selected little-endian FAT slices (fat64=%s)",
|
|
(fat64) => {
|
|
const result = decodeAppleDispatchMetadata(
|
|
wrapped(fat64),
|
|
100,
|
|
provenance,
|
|
);
|
|
expect(result.objc_classes[0]).toMatchObject({
|
|
name: "Fixture",
|
|
location: { address: "0x100000200", file_offset: 256 + 512 },
|
|
});
|
|
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
|
implementation_address: "0x100000700",
|
|
location: { file_offset: 256 + 0x488 },
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([6, 16])(
|
|
"states the header byte order when its architecture table is truncated at %i bytes",
|
|
(length) => {
|
|
expect(() =>
|
|
decodeAppleDispatchMetadata(
|
|
wrapped(false).subarray(0, length),
|
|
100,
|
|
provenance,
|
|
),
|
|
).toThrow(
|
|
"Malformed FAT architecture table (FAT header byte order: little-endian)",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("validates the selected thin slice's own byte order", () => {
|
|
const bytes = wrapped(false);
|
|
bytes.writeUInt32BE(0xfeedfacf, 256);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"Only little-endian 64-bit Mach-O metadata is supported",
|
|
);
|
|
});
|
|
|
|
it("rejects a 32-bit slice payload inside a little-endian container", () => {
|
|
const bytes = wrapped(false);
|
|
bytes.writeUInt32BE(0xfeedface, 256);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"Unsupported 32-bit Mach-O metadata for requested CPU type arm64 (FAT header byte order: little-endian)",
|
|
);
|
|
});
|
|
|
|
it("checks the full little-endian FAT64 slice extent", () => {
|
|
const bytes = wrapped(true);
|
|
bytes.writeBigUInt64LE(0x100000000n, 16);
|
|
expect(() => decodeAppleDispatchMetadata(bytes, 100, provenance)).toThrow(
|
|
"FAT architecture 0 slice range extends beyond the file (FAT header byte order: little-endian)",
|
|
);
|
|
});
|
|
});
|