mirror of
https://github.com/morluto/rea
synced 2026-10-28 23:11:11 -04:00
d4415444c4
Separate AMDGPU processors from feature-qualified note identities. Read Apple hidden ARM64 table rows once and preserve visibility through artifact, dylib and fuse consumers. Keep Objective-C++ template commas within type attributes and incomplete metadata unknown. Validated provider/parser and production property regressions, Electron CLI/MCP consumers, and native Apple lipo -hideARM64 output through the production slice reader and public artifact inspection. LLVM execution remains unverified. Fixes #6806 Fixes #6799 Fixes #6801
1664 lines
49 KiB
TypeScript
1664 lines
49 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
decodeChainedPointer,
|
|
parsePointerFixups,
|
|
} from "./AppleMachoFixups.js";
|
|
import { parsePropertyAttributes } from "./AppleObjcProperties.js";
|
|
import type { Segment } from "./AppleMachoSelection.js";
|
|
|
|
const BASE = 0x100000000n;
|
|
const IMPORTS = [
|
|
{
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
binding_source: "chained",
|
|
library_ordinal: 1,
|
|
library: "/usr/lib/libobjc.A.dylib",
|
|
weak: false,
|
|
addend: 0n,
|
|
},
|
|
{
|
|
symbol: "_free",
|
|
binding_source: "chained",
|
|
library_ordinal: 2,
|
|
library: "/usr/lib/libSystem.B.dylib",
|
|
weak: true,
|
|
addend: 4n,
|
|
},
|
|
] as const;
|
|
|
|
const field = (value: bigint, from: number): bigint => value << BigInt(from);
|
|
|
|
describe("chained pointer decoding", () => {
|
|
it("decodes DYLD_CHAINED_PTR_64 and _64_OFFSET rebases and binds", () => {
|
|
const rebase = 0x4000n | field(0x80n, 36) | field(3n, 51);
|
|
expect(decodeChainedPointer(BASE + 0x4000n, 2, BASE, IMPORTS)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x4000n,
|
|
});
|
|
expect(decodeChainedPointer(rebase, 6, BASE, IMPORTS)).toEqual({
|
|
kind: "rebase",
|
|
target: (0x80n << 56n) + BASE + 0x4000n,
|
|
});
|
|
const bind = 1n | field(2n, 24) | field(1n, 63);
|
|
expect(decodeChainedPointer(bind, 6, BASE, IMPORTS)).toEqual({
|
|
kind: "bind",
|
|
symbol: "_free",
|
|
binding_source: "chained",
|
|
library_ordinal: 2,
|
|
library: "/usr/lib/libSystem.B.dylib",
|
|
weak: true,
|
|
addend: 6n,
|
|
});
|
|
});
|
|
|
|
it("decodes arm64e plain, authenticated and 24-bit binds", () => {
|
|
expect(decodeChainedPointer(0x8000n, 9, BASE, IMPORTS)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x8000n,
|
|
});
|
|
expect(decodeChainedPointer(BASE + 0x8000n, 1, BASE, IMPORTS)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x8000n,
|
|
});
|
|
const authRebase = 0x8000n | field(0x1234n, 32) | field(1n, 63);
|
|
expect(decodeChainedPointer(authRebase, 1, BASE, IMPORTS)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x8000n,
|
|
});
|
|
const authBind = 0n | field(1n, 62) | field(1n, 63);
|
|
expect(decodeChainedPointer(authBind, 9, BASE, IMPORTS)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
addend: 0n,
|
|
});
|
|
const bind24 = 1n | field(0x7ffffn, 32) | field(1n, 62);
|
|
expect(decodeChainedPointer(bind24, 12, BASE, IMPORTS)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_free",
|
|
addend: 3n,
|
|
});
|
|
expect(() =>
|
|
decodeChainedPointer(field(1n, 62) | 9n, 9, BASE, IMPORTS),
|
|
).toThrow("Chained bind ordinal 9 has no import");
|
|
expect(() => decodeChainedPointer(1n, 3, BASE, IMPORTS)).toThrow(
|
|
"Unsupported chained pointer format 3",
|
|
);
|
|
});
|
|
});
|
|
|
|
/** Build `LC_DYLD_CHAINED_FIXUPS` data with one fixup segment and the given imports. */
|
|
const chainedData = (format: 1 | 2 | 3): Buffer => {
|
|
const symbols = Buffer.from("\0_OBJC_CLASS_$_NSObject\0_free\0");
|
|
const importSize = format === 3 ? 16 : format === 2 ? 8 : 4;
|
|
const imports = Buffer.alloc(importSize * 2);
|
|
const entry = (
|
|
index: number,
|
|
{
|
|
ordinal,
|
|
weak,
|
|
nameOffset,
|
|
addend,
|
|
}: {
|
|
ordinal: number;
|
|
weak: boolean;
|
|
nameOffset: number;
|
|
addend: number;
|
|
},
|
|
) => {
|
|
const at = index * importSize;
|
|
if (format === 3) {
|
|
imports.writeBigUInt64LE(
|
|
BigInt(ordinal & 0xffff) |
|
|
(BigInt(weak ? 1 : 0) << 16n) |
|
|
(BigInt(nameOffset) << 32n),
|
|
at,
|
|
);
|
|
imports.writeBigInt64LE(BigInt(addend), at + 8);
|
|
} else {
|
|
imports.writeUInt32LE(
|
|
((nameOffset << 9) | ((weak ? 1 : 0) << 8) | (ordinal & 0xff)) >>> 0,
|
|
at,
|
|
);
|
|
if (format === 2) imports.writeInt32LE(addend, at + 4);
|
|
}
|
|
};
|
|
entry(0, { ordinal: 1, weak: false, nameOffset: 1, addend: 0 });
|
|
entry(1, { ordinal: 2, weak: true, nameOffset: 24, addend: 7 });
|
|
// starts_in_image: two segments, only the second has fixups.
|
|
const starts = Buffer.alloc(12 + 24);
|
|
starts.writeUInt32LE(2, 0);
|
|
starts.writeUInt32LE(0, 4);
|
|
starts.writeUInt32LE(12, 8);
|
|
starts.writeUInt32LE(24, 12);
|
|
starts.writeUInt16LE(0x4000, 16);
|
|
starts.writeUInt16LE(6, 18);
|
|
starts.writeBigUInt64LE(0x4000n, 20);
|
|
starts.writeUInt16LE(1, 32);
|
|
starts.writeUInt16LE(8, 34);
|
|
const header = Buffer.alloc(28);
|
|
header.writeUInt32LE(0, 0);
|
|
header.writeUInt32LE(28, 4);
|
|
header.writeUInt32LE(28 + starts.length, 8);
|
|
header.writeUInt32LE(28 + starts.length + imports.length, 12);
|
|
header.writeUInt32LE(2, 16);
|
|
header.writeUInt32LE(format, 20);
|
|
header.writeUInt32LE(0, 24);
|
|
const data = Buffer.alloc(0x8000);
|
|
Buffer.concat([header, starts, imports, symbols]).copy(data);
|
|
data.writeBigUInt64LE(1n | field(1n, 63), 0x4008);
|
|
return data;
|
|
};
|
|
|
|
const SEGMENTS: Segment[] = [
|
|
{ address: BASE, size: 0x4000n, offset: 0, executable: true },
|
|
{ address: BASE + 0x4000n, size: 0x4000n, offset: 0x4000, executable: false },
|
|
];
|
|
|
|
const legacyRebaseInput = (stream: Buffer) => {
|
|
const data = Buffer.alloc(0x8000);
|
|
stream.copy(data, 0x7000);
|
|
return {
|
|
data,
|
|
fixups: parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: null,
|
|
binds: [],
|
|
rebases: [{ offset: 0x7000, size: stream.length }],
|
|
dylibs: [],
|
|
},
|
|
[{ address: BASE, size: 0x2000n, offset: 0, executable: false }],
|
|
BASE,
|
|
),
|
|
};
|
|
};
|
|
|
|
it("marks only opcode-selected pointer slots rebased, including target zero", () => {
|
|
// SET_TYPE(pointer), SET_SEGMENT(offset 0x100), DO once,
|
|
// ADD_ADDR(0x10), DO once, DONE.
|
|
const { data, fixups } = legacyRebaseInput(
|
|
Buffer.from([0x11, 0x20, 0x80, 0x02, 0x51, 0x30, 0x10, 0x51, 0]),
|
|
);
|
|
data.writeBigUInt64LE(BASE + 0x200n, 0x100);
|
|
data.writeBigUInt64LE(0n, 0x118);
|
|
|
|
expect(fixups.decode(BASE + 0x100n, BASE + 0x200n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x200n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x118n, 0n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x110n, 0n)).toEqual({
|
|
kind: "raw",
|
|
target: 0n,
|
|
});
|
|
expect(fixups).toMatchObject({
|
|
kind: "dyld-info",
|
|
examined: 1,
|
|
decoded: 1,
|
|
unreadRebaseBytes: 0,
|
|
});
|
|
});
|
|
|
|
it("applies count-and-skip opcodes to their emitted pointer locations", () => {
|
|
// SET_TYPE(pointer), SET_SEGMENT(offset 0x100), DO 1 skipping 8,
|
|
// DO_REBASE_ADD_ADDR(8), DO once, DONE. The selected slots are 0x100, 0x110,
|
|
// and 0x120; the skipped words remain raw.
|
|
const { fixups } = legacyRebaseInput(
|
|
Buffer.from([
|
|
0x11, 0x20, 0x80, 0x02, 0x80, 0x01, 0x08, 0x70, 0x08, 0x51, 0,
|
|
]),
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x100n, 0x200n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x200n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x110n, 0x210n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x210n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x120n, 0x220n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x220n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x108n, 0n)).toEqual({
|
|
kind: "raw",
|
|
target: 0n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x118n, 0n)).toEqual({
|
|
kind: "raw",
|
|
target: 0n,
|
|
});
|
|
});
|
|
|
|
it("retains known prefix rebases and leaves unmatched locations unknown on an invalid tail", () => {
|
|
const { fixups } = legacyRebaseInput(
|
|
Buffer.from([0x11, 0x20, 0x80, 0x02, 0x51, 0xf0]),
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x100n, BASE + 0x200n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x200n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x108n, BASE + 0x300n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: BASE + 0x300n,
|
|
reason: expect.stringContaining("Unsupported rebase opcode 0xf0"),
|
|
});
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "rebase_stream_decode_failed" },
|
|
]);
|
|
});
|
|
|
|
it("does not treat 32-bit text rebases as 64-bit metadata pointers", () => {
|
|
const { fixups } = legacyRebaseInput(
|
|
Buffer.from([0x12, 0x20, 0x80, 0x02, 0x51, 0]),
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x100n, BASE + 0x200n)).toMatchObject({
|
|
kind: "unsupported",
|
|
reason: "Legacy non-pointer rebase is not a 64-bit metadata pointer",
|
|
});
|
|
expect(fixups.decode(BASE + 0x108n, BASE + 0x300n)).toMatchObject({
|
|
kind: "raw",
|
|
target: BASE + 0x300n,
|
|
});
|
|
});
|
|
|
|
it("retains unaligned pointer locations accepted by dyld's rebase state checks", () => {
|
|
const { fixups } = legacyRebaseInput(
|
|
Buffer.from([0x11, 0x20, 0x81, 0x02, 0x51, 0]),
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x101n, 0x1234n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x1234n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x100n, 0n)).toEqual({
|
|
kind: "raw",
|
|
target: 0n,
|
|
});
|
|
});
|
|
|
|
it("rejects count expansion outside the selected file-backed segment without expanding it", () => {
|
|
const { fixups } = legacyRebaseInput(
|
|
Buffer.from([0x11, 0x20, 0x00, 0x60, 0x80, 0x80, 0x80, 0x80, 0x10, 0x00]),
|
|
);
|
|
|
|
expect(fixups.unreadRebaseBytes).toBeGreaterThan(0);
|
|
expect(fixups.decode(BASE + 0x100n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
reason: expect.stringContaining("leaves its file-backed segment"),
|
|
});
|
|
});
|
|
|
|
it("bounds cumulative work across repeated full-range runs and retains the prefix", () => {
|
|
const data = Buffer.alloc(0x10000);
|
|
const repeatedRun = Buffer.from([
|
|
0x11, // SET_TYPE(pointer)
|
|
0x20,
|
|
0, // SET_SEGMENT_AND_OFFSET(0, 0)
|
|
0x60,
|
|
0x80,
|
|
0x40, // DO_REBASE_ULEB_TIMES(8192)
|
|
]);
|
|
const stream = Buffer.concat([
|
|
...Array.from({ length: 100 }, () => repeatedRun),
|
|
Buffer.from([0]),
|
|
]);
|
|
stream.copy(data);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: null,
|
|
binds: [],
|
|
rebases: [{ offset: 0, size: stream.length }],
|
|
dylibs: [],
|
|
},
|
|
[
|
|
{
|
|
address: BASE,
|
|
size: BigInt(data.length),
|
|
offset: 0,
|
|
executable: false,
|
|
},
|
|
],
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups.decoded).toBe(0);
|
|
expect(fixups.failures.join("\n")).toContain("source-derived budget");
|
|
expect(fixups.decode(BASE + 0x100n, 0x1234n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x1234n,
|
|
});
|
|
expect(fixups.decode(BASE + 0x101n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: 0n,
|
|
});
|
|
});
|
|
|
|
it("shares the expansion budget across separate rebase streams", () => {
|
|
const data = Buffer.alloc(0x10000);
|
|
const stream = Buffer.from([0x11, 0x20, 0, 0x60, 0x80, 0x40, 0]);
|
|
const controlData = Buffer.alloc(0x10000);
|
|
stream.copy(controlData);
|
|
const singleRun = parsePointerFixups(
|
|
controlData,
|
|
{
|
|
chained: null,
|
|
binds: [],
|
|
rebases: [{ offset: 0, size: stream.length }],
|
|
dylibs: [],
|
|
},
|
|
[
|
|
{
|
|
address: BASE,
|
|
size: BigInt(controlData.length),
|
|
offset: 0,
|
|
executable: false,
|
|
},
|
|
],
|
|
BASE,
|
|
);
|
|
expect(singleRun).toMatchObject({ examined: 1, decoded: 1, failures: [] });
|
|
|
|
const rebases = Array.from({ length: 17 }, (_, index) => {
|
|
const offset = index * 16;
|
|
stream.copy(data, offset);
|
|
return { offset, size: stream.length };
|
|
});
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: null,
|
|
binds: [],
|
|
rebases,
|
|
dylibs: [],
|
|
},
|
|
[
|
|
{
|
|
address: BASE,
|
|
size: BigInt(data.length),
|
|
offset: 0,
|
|
executable: false,
|
|
},
|
|
],
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups).toMatchObject({ examined: 17, decoded: 15 });
|
|
expect(fixups.failures.join("\n")).toContain("source-derived budget");
|
|
expect(fixups.decode(BASE + 0x100n, 0x1234n)).toEqual({
|
|
kind: "rebase",
|
|
target: 0x1234n,
|
|
});
|
|
});
|
|
|
|
describe("incomplete legacy bind applicability", () => {
|
|
it("keeps unmatched and known-rebase slots unknown when a bind tail is malformed", () => {
|
|
const data = Buffer.alloc(0x2000);
|
|
const bind = Buffer.from([0x10, 0x40, 0x78, 0, 0x70, 0, 0xf0, 0x90]);
|
|
const rebase = Buffer.from([0x11, 0x20, 0x80, 0x20, 0x51, 0]);
|
|
bind.copy(data, 0);
|
|
rebase.copy(data, 16);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: bind.length, stream: "bind" }],
|
|
rebases: [{ offset: 16, size: rebase.length }],
|
|
dylibs: [],
|
|
},
|
|
[
|
|
{
|
|
address: BASE,
|
|
size: BigInt(data.length),
|
|
offset: 0,
|
|
executable: false,
|
|
},
|
|
],
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x1000n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: 0n,
|
|
reason: expect.stringContaining("could overlap this rebase location"),
|
|
});
|
|
expect(fixups.decode(BASE + 0x1008n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: 0n,
|
|
reason: expect.stringContaining("pointer-fixup applicability"),
|
|
});
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "bind_stream_decode_failed" },
|
|
]);
|
|
});
|
|
|
|
it("retains a positive bind prefix while leaving unmatched slots unknown", () => {
|
|
const opcodes = Buffer.from([
|
|
0x11,
|
|
0x40,
|
|
...Buffer.from("_x\0"),
|
|
0x71,
|
|
0x08,
|
|
0x90,
|
|
0xf0,
|
|
]);
|
|
const fixups = parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: ["/library-1"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x4008n, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_x",
|
|
});
|
|
expect(fixups.decode(BASE + 0x4010n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: 0n,
|
|
});
|
|
});
|
|
|
|
it("keeps unmatched chained-image slots unknown when a legacy bind tail fails", () => {
|
|
const chained = chainedData(1);
|
|
const bind = Buffer.from([0xf0, 0x90]);
|
|
const data = Buffer.concat([chained, bind]);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: chained.length },
|
|
binds: [{ offset: chained.length, size: bind.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups.decode(BASE + 0x4010n, 0n)).toMatchObject({
|
|
kind: "unknown",
|
|
raw: 0n,
|
|
reason: expect.stringContaining("pointer-fixup applicability"),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("independent chained segment coverage", () => {
|
|
it("counts successfully decoded chained segments alongside an unsupported segment", () => {
|
|
const starts = 28;
|
|
const startsSize = 4 + 2 * 4;
|
|
const firstInfo = starts + startsSize;
|
|
const secondInfo = firstInfo + 24;
|
|
const imports = secondInfo + 24;
|
|
const data = Buffer.alloc(0x3000);
|
|
data.writeUInt32LE(starts, 4);
|
|
data.writeUInt32LE(imports, 8);
|
|
data.writeUInt32LE(imports, 12);
|
|
data.writeUInt32LE(1, 20);
|
|
data.writeUInt32LE(2, starts);
|
|
data.writeUInt32LE(firstInfo - starts, starts + 4);
|
|
data.writeUInt32LE(secondInfo - starts, starts + 8);
|
|
for (const [info, format, segmentOffset] of [
|
|
[firstInfo, 6, 0],
|
|
[secondInfo, 99, 0x1000],
|
|
] as const) {
|
|
data.writeUInt32LE(24, info);
|
|
data.writeUInt16LE(0x1000, info + 4);
|
|
data.writeUInt16LE(format, info + 6);
|
|
data.writeBigUInt64LE(BigInt(segmentOffset), info + 8);
|
|
data.writeUInt16LE(1, info + 20);
|
|
data.writeUInt16LE(8, info + 22);
|
|
}
|
|
const segments: Segment[] = [
|
|
{ address: BASE, size: 0x1000n, offset: 0x1000, executable: false },
|
|
{
|
|
address: BASE + 0x1000n,
|
|
size: 0x1000n,
|
|
offset: 0x2000,
|
|
executable: false,
|
|
},
|
|
];
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: imports },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
segments,
|
|
BASE,
|
|
);
|
|
|
|
expect(fixups).toMatchObject({ examined: 2, decoded: 1 });
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "unsupported_pointer_format" },
|
|
]);
|
|
expect(fixups.decode(BASE + 8n, 0n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("pointer fixup tables", () => {
|
|
it("reports unsupported pointer formats even when no pointer is visited", () => {
|
|
const data = chainedData(1);
|
|
data.writeUInt16LE(99, 46);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: data.length },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.failures).toContain("Unsupported chained pointer format 99");
|
|
});
|
|
|
|
it("keeps unread LC_DYLD_INFO rebase bytes when chained fixups are present", () => {
|
|
const chained = chainedData(1);
|
|
const data = Buffer.concat([chained, Buffer.from([0xf0])]);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: chained.length },
|
|
binds: [],
|
|
rebases: [{ offset: chained.length, size: 1 }],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups).toMatchObject({
|
|
kind: "chained",
|
|
unreadRebaseBytes: 1,
|
|
});
|
|
});
|
|
|
|
it("does not decode unreferenced imports while constructing fixup facts", () => {
|
|
const data = chainedData(1);
|
|
// The second import's name leaves the pool. Preserve the first import
|
|
// and defer that distinct failure until its pointer is examined.
|
|
data.writeUInt32LE(0xffff_ff00, data.readUInt32LE(8) + 4);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups).toMatchObject({
|
|
examined: 1,
|
|
decoded: 1,
|
|
unreadRebaseBytes: 0,
|
|
});
|
|
expect(fixups.decode(BASE + 0x4008n, field(1n, 63))).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
});
|
|
expect(fixups.issues).toEqual([]);
|
|
expect(fixups.decoded).toBe(1);
|
|
expect(fixups.decode(BASE + 0x4008n, 1n | field(1n, 63))).toMatchObject({
|
|
kind: "unsupported",
|
|
});
|
|
expect(fixups.decoded).toBe(0);
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "chained_pointer_decode_failed" },
|
|
]);
|
|
});
|
|
|
|
for (const format of [1, 2, 3] as const)
|
|
it(`reads chained import format ${format}`, () => {
|
|
const data = chainedData(format);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: data.length },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/usr/lib/libobjc.A.dylib", "/usr/lib/libSystem.B.dylib"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups).toMatchObject({
|
|
kind: "chained",
|
|
formats: ["DYLD_CHAINED_PTR_64_OFFSET"],
|
|
});
|
|
expect(fixups.decode(BASE + 0x4008n, 1n | field(1n, 63))).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_free",
|
|
library: "/usr/lib/libSystem.B.dylib",
|
|
weak: true,
|
|
addend: format === 1 ? 0n : 7n,
|
|
});
|
|
// Pointers outside fixup segments are stored plainly.
|
|
expect(fixups.decode(BASE + 0x10n, 0x1234n)).toEqual({
|
|
kind: "raw",
|
|
target: 0x1234n,
|
|
});
|
|
});
|
|
});
|
|
describe("parsed chained pointer membership", () => {
|
|
it("parses a dense chain once and reuses its membership for every lookup", () => {
|
|
const data = chainedData(1);
|
|
const count = 512;
|
|
const raw = Array.from(
|
|
{ length: count },
|
|
(_, index) => BigInt(index + 1) | field(index + 1 < count ? 2n : 0n, 51),
|
|
);
|
|
for (const [index, pointer] of raw.entries())
|
|
data.writeBigUInt64LE(pointer, 0x4008 + index * 8);
|
|
const read = vi.spyOn(data, "readBigUInt64LE");
|
|
try {
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
const readsAfterParse = read.mock.calls.length;
|
|
for (const [index, pointer] of raw.entries())
|
|
expect(
|
|
fixups.decode(BASE + 0x4008n + BigInt(index * 8), pointer),
|
|
).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + BigInt(index + 1),
|
|
});
|
|
expect(read.mock.calls.length).toBe(readsAfterParse);
|
|
expect(fixups.issues).toEqual([]);
|
|
} finally {
|
|
read.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("preserves byte-exact unaligned producer locations", () => {
|
|
const data = chainedData(1);
|
|
data.writeUInt16LE(9, 62);
|
|
data.writeBigUInt64LE(0x1234n | field(2n, 51), 0x4009);
|
|
data.writeBigUInt64LE(0x4321n, 0x4011);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{ chained: { offset: 0, size: 128 }, binds: [], rebases: [], dylibs: [] },
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.decode(BASE + 0x4009n, data.readBigUInt64LE(0x4009))).toEqual(
|
|
{
|
|
kind: "rebase",
|
|
target: BASE + 0x1234n,
|
|
},
|
|
);
|
|
expect(fixups.decode(BASE + 0x4011n, data.readBigUInt64LE(0x4011))).toEqual(
|
|
{
|
|
kind: "rebase",
|
|
target: BASE + 0x4321n,
|
|
},
|
|
);
|
|
const plain = data.readBigUInt64LE(0x400b);
|
|
expect(fixups.decode(BASE + 0x400bn, plain)).toEqual({
|
|
kind: "raw",
|
|
target: plain,
|
|
});
|
|
});
|
|
|
|
it.each(["shared", "exhausted"])(
|
|
"preserves physical page facts with %s retention",
|
|
(mode) => {
|
|
const data = Buffer.alloc(0x8000);
|
|
const count = 5;
|
|
const starts = 28;
|
|
const table = starts + 4 + count * 4;
|
|
const imports = table + count * 24;
|
|
data.writeUInt32LE(starts, 4);
|
|
data.writeUInt32LE(imports, 8);
|
|
data.writeUInt32LE(imports, 12);
|
|
data.writeUInt32LE(1, 20);
|
|
data.writeUInt32LE(count, starts);
|
|
const segments: Segment[] = [];
|
|
for (let index = 0; index < count; index++) {
|
|
const info = table + index * 24;
|
|
const address = BASE + BigInt((index + 1) * 0x4000);
|
|
data.writeUInt32LE(info - starts, starts + 4 + index * 4);
|
|
data.writeUInt32LE(24, info);
|
|
data.writeUInt16LE(0x4000, info + 4);
|
|
data.writeUInt16LE(6, info + 6);
|
|
data.writeBigUInt64LE(address - BASE, info + 8);
|
|
data.writeUInt16LE(1, info + 20);
|
|
data.writeUInt16LE(8, info + 22);
|
|
segments.push({
|
|
address,
|
|
size: BigInt(0x4000 - (mode === "exhausted" ? index * 8 : 0)),
|
|
offset: 0x4000 + (mode === "exhausted" ? index * 8 : 0),
|
|
executable: false,
|
|
});
|
|
data.writeBigUInt64LE(
|
|
0x1234n,
|
|
0x4008 + (mode === "exhausted" ? index * 8 : 0),
|
|
);
|
|
}
|
|
const reads = vi.spyOn(data, "readBigUInt64LE");
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: imports },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
segments,
|
|
BASE,
|
|
);
|
|
for (const segment of segments)
|
|
expect(fixups.decode(segment.address + 8n, 0x1234n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x1234n,
|
|
});
|
|
expect(fixups.issues).toEqual([]);
|
|
expect(reads.mock.calls.filter(([at]) => at === 0x4008)).toHaveLength(1);
|
|
if (mode === "exhausted") {
|
|
// Five different physical views exceed ceil(32KiB / 8) bitmap bytes.
|
|
// The final view remains readable, with recomputation instead of rejection.
|
|
expect(reads.mock.calls.filter(([at]) => at === 0x4028)).toHaveLength(
|
|
2,
|
|
);
|
|
}
|
|
reads.mockRestore();
|
|
},
|
|
);
|
|
});
|
|
|
|
/** Small malformed carriers exercise expansion without allocating exploit-sized data. */
|
|
const aliasedStarts = (format: number, count = 32, pages = 128) => {
|
|
const data = Buffer.alloc(0x8000);
|
|
const starts = 28;
|
|
const info = starts + 4 + count * 4;
|
|
const end = info + 22 + pages * 2;
|
|
data.writeUInt32LE(starts, 4);
|
|
data.writeUInt32LE(end, 8);
|
|
data.writeUInt32LE(end, 12);
|
|
data.writeUInt32LE(1, 20);
|
|
data.writeUInt32LE(count, starts);
|
|
data.writeUInt32LE(22 + pages * 2, info);
|
|
data.writeUInt16LE(0x1000, info + 4);
|
|
data.writeUInt16LE(format, info + 6);
|
|
data.writeBigUInt64LE(0x4000n, info + 8);
|
|
data.writeUInt16LE(pages, info + 20);
|
|
const segments = Array.from({ length: count }, (_, index) => {
|
|
data.writeUInt32LE(info - starts, starts + 4 + index * 4);
|
|
return {
|
|
address: BASE + 0x4000n,
|
|
size: 0x4000n,
|
|
offset: 0x4000,
|
|
executable: false,
|
|
};
|
|
});
|
|
return {
|
|
data,
|
|
segments,
|
|
pageStarts: info + 22,
|
|
commands: {
|
|
chained: { offset: 0, size: end },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
};
|
|
};
|
|
|
|
describe("bounded chained page facts", () => {
|
|
it("does not expand unsupported non-NONE aliased declarations", () => {
|
|
const { data, segments, pageStarts, commands } = aliasedStarts(99);
|
|
const reads = vi.spyOn(data, "readUInt16LE");
|
|
const fixups = parsePointerFixups(data, commands, segments, BASE);
|
|
// All 128 encoded starts are zero (not NONE); the former per-segment maps
|
|
// read 32 * 128 starts before any bitmap budget could apply.
|
|
expect(
|
|
reads.mock.calls.filter(([at]) => at !== undefined && at >= pageStarts),
|
|
).toHaveLength(0);
|
|
expect(fixups.formats).toEqual(["format-99"]);
|
|
expect(fixups.issues).toHaveLength(32);
|
|
expect(fixups.decode(BASE + 0x4000n, 0n)).toEqual({
|
|
kind: "unsupported",
|
|
reason: "Unsupported chained pointer format 99",
|
|
});
|
|
reads.mockRestore();
|
|
});
|
|
|
|
it("bounds failed alias diagnostics and leaves later pages independently queryable", () => {
|
|
const { data, segments, pageStarts, commands } = aliasedStarts(6);
|
|
data.writeBigUInt64LE(field(0xfffn, 51), 0x4000);
|
|
data.writeBigUInt64LE(0x1234n, 0x5000);
|
|
const reads = vi.spyOn(data, "readUInt16LE");
|
|
const fixups = parsePointerFixups(data, commands, segments, BASE);
|
|
expect(
|
|
reads.mock.calls.filter(([at]) => at !== undefined && at >= pageStarts),
|
|
).toHaveLength(1);
|
|
expect(fixups.issues).toEqual([
|
|
{
|
|
facet: "pointer_fixups",
|
|
code: "chained_pointer_walk_failed",
|
|
message: "Chained pointer link leaves its file-backed page",
|
|
location: "0x100004000",
|
|
},
|
|
]);
|
|
expect(fixups.decode(BASE + 0x4000n, data.readBigUInt64LE(0x4000))).toEqual(
|
|
{
|
|
kind: "rebase",
|
|
target: BASE,
|
|
},
|
|
);
|
|
expect(fixups.decode(BASE + 0x5000n, 0x1234n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE + 0x1234n,
|
|
});
|
|
reads.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("chained metadata retention", () => {
|
|
it("bounds cache records even when malformed facts have no bitmap bytes", () => {
|
|
const count = 8;
|
|
const pages = 4;
|
|
const { data, commands } = aliasedStarts(6, count, pages);
|
|
const starts = 28;
|
|
const table = starts + 4 + count * 4;
|
|
const infoSize = 22 + pages * 2;
|
|
const end = table + count * infoSize;
|
|
data.writeUInt32LE(end, 8);
|
|
data.writeUInt32LE(end, 12);
|
|
const segments = Array.from({ length: count }, (_, index) => {
|
|
const info = table + index * infoSize;
|
|
data.copy(data, info, table, table + infoSize);
|
|
data.writeUInt32LE(info - starts, starts + 4 + index * 4);
|
|
const address = BASE + BigInt((index + 1) * 0x4000);
|
|
data.writeBigUInt64LE(address - BASE, info + 8);
|
|
for (let page = 0; page < pages; page++)
|
|
data.writeUInt16LE(0x8000 + index, info + 22 + page * 2);
|
|
return { address, offset: 0x4000, size: 0x4000n, executable: false };
|
|
});
|
|
const retained: unknown[] = [];
|
|
const set = Map.prototype.set;
|
|
const writes = vi.spyOn(Map.prototype, "set").mockImplementation(function (
|
|
this: Map<unknown, unknown>,
|
|
key,
|
|
value,
|
|
) {
|
|
if (typeof value === "object" && value !== null && "locations" in value)
|
|
retained.push(value);
|
|
return set.call(this, key, value);
|
|
});
|
|
try {
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{ ...commands, chained: { offset: 0, size: end } },
|
|
segments,
|
|
BASE,
|
|
);
|
|
expect(fixups.issues).toHaveLength(count);
|
|
expect(fixups.issues.map(({ location }) => location)).toEqual(
|
|
segments.map(({ address }) => `0x${address.toString(16)}`),
|
|
);
|
|
for (const segment of segments)
|
|
for (let page = 0; page < pages; page++)
|
|
expect(
|
|
fixups.decode(segment.address + BigInt(page * 0x1000), 0n),
|
|
).toMatchObject({ kind: "unsupported" });
|
|
} finally {
|
|
writes.mockRestore();
|
|
}
|
|
// All facts have zero bitmap bytes. Entry count must independently bound
|
|
// 8 * 4 distinct physical start/format/pageEnd keys to the derived 24 slots.
|
|
expect(retained).toHaveLength(Math.ceil(data.length / 0x1000) + 2 * count);
|
|
});
|
|
});
|
|
|
|
describe("chained pointer membership", () => {
|
|
it("retains a valid later page after an earlier chain fails", () => {
|
|
const data = chainedData(1);
|
|
data.copyWithin(66, 64, 128);
|
|
data.writeUInt32LE(66, 8);
|
|
data.writeUInt32LE(74, 12);
|
|
data.writeUInt32LE(26, 40);
|
|
data.writeUInt16LE(0x1000, 44);
|
|
data.writeUInt16LE(2, 60);
|
|
data.writeUInt16LE(8, 64);
|
|
data.writeBigUInt64LE(field(0xfffn, 51), 0x4008);
|
|
data.writeBigUInt64LE(0x1234n, 0x5008);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 160 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "chained_pointer_walk_failed" },
|
|
]);
|
|
expect(fixups.decode(BASE + 0x5008n, data.readBigUInt64LE(0x5008))).toEqual(
|
|
{ kind: "rebase", target: BASE + 0x1234n },
|
|
);
|
|
});
|
|
it.each([2, 6, 1, 9, 12])(
|
|
"decodes only chain members for pointer format %i",
|
|
(pointerFormat) => {
|
|
const data = chainedData(1);
|
|
data.writeUInt16LE(pointerFormat, 46);
|
|
const generic = pointerFormat === 2 || pointerFormat === 6;
|
|
const bindBit = generic ? 63 : 62;
|
|
data.writeBigUInt64LE(field(4n, 51) | field(1n, bindBit), 0x4008);
|
|
const second = generic ? 0x4018 : 0x4028;
|
|
data.writeBigUInt64LE(1n | field(1n, bindBit), second);
|
|
const plain = BASE + 0x1234n;
|
|
data.writeBigUInt64LE(plain, 0x4010);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.issues).toEqual([]);
|
|
expect(
|
|
fixups.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008)),
|
|
).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
});
|
|
expect(
|
|
fixups.decode(BASE + BigInt(second), data.readBigUInt64LE(second)),
|
|
).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_free",
|
|
});
|
|
expect(fixups.decode(BASE + 0x4010n, plain)).toEqual({
|
|
kind: "raw",
|
|
target: plain,
|
|
});
|
|
expect(fixups.decode(BASE + 0x4030n, 0n)).toEqual({
|
|
kind: "raw",
|
|
target: 0n,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("keeps NONE pages and pages absent from the table plain, and decodes a chained zero", () => {
|
|
const data = chainedData(1);
|
|
data.writeBigUInt64LE(0n, 0x4008);
|
|
const commands = {
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
} as const;
|
|
const fixups = parsePointerFixups(data, commands, SEGMENTS, BASE);
|
|
expect(fixups.decode(BASE + 0x4008n, 0n)).toEqual({
|
|
kind: "rebase",
|
|
target: BASE,
|
|
});
|
|
data.writeUInt16LE(0xffff, 62);
|
|
data.writeUInt16LE(99, 46);
|
|
data.writeUInt16LE(0x1000, 44);
|
|
const unknown = parsePointerFixups(data, commands, SEGMENTS, BASE);
|
|
expect(unknown.decode(BASE + 0x4008n, BASE + 8n)).toEqual({
|
|
kind: "raw",
|
|
target: BASE + 8n,
|
|
});
|
|
expect(unknown.decode(BASE + 0x5008n, BASE + 16n)).toEqual({
|
|
kind: "raw",
|
|
target: BASE + 16n,
|
|
});
|
|
});
|
|
});
|
|
describe("malformed chained pointer coverage", () => {
|
|
it("locates a failed later page without discarding established pointer facts", () => {
|
|
const data = chainedData(1);
|
|
data.copyWithin(66, 64, 128);
|
|
data.writeUInt32LE(66, 8);
|
|
data.writeUInt32LE(74, 12);
|
|
data.writeUInt32LE(26, 40);
|
|
data.writeUInt16LE(0x1000, 44);
|
|
data.writeUInt16LE(2, 60);
|
|
data.writeUInt16LE(8, 64);
|
|
data.writeBigUInt64LE(0x1234n, 0x4008);
|
|
data.writeBigUInt64LE(field(0xfffn, 51), 0x5008);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{ chained: { offset: 0, size: 160 }, binds: [], rebases: [], dylibs: [] },
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.issues).toEqual([
|
|
{
|
|
facet: "pointer_fixups",
|
|
code: "chained_pointer_walk_failed",
|
|
message: "Chained pointer link leaves its file-backed page",
|
|
location: "0x100005000",
|
|
},
|
|
]);
|
|
expect(fixups.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008))).toEqual(
|
|
{
|
|
kind: "rebase",
|
|
target: BASE + 0x1234n,
|
|
},
|
|
);
|
|
expect(fixups.decode(BASE + 0x5008n, data.readBigUInt64LE(0x5008))).toEqual(
|
|
{
|
|
kind: "rebase",
|
|
target: BASE,
|
|
},
|
|
);
|
|
});
|
|
|
|
it.each(["link", "multi"])(
|
|
"reports malformed %s chains without claiming complete fixups",
|
|
(malformed) => {
|
|
const data = chainedData(1);
|
|
if (malformed === "link")
|
|
data.writeBigUInt64LE(field(0xfffn, 51), 0x4008);
|
|
else data.writeUInt16LE(0x8000, 62);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.issues).toMatchObject([
|
|
{ code: "chained_pointer_walk_failed" },
|
|
]);
|
|
if (malformed === "link")
|
|
expect(
|
|
fixups.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008)),
|
|
).toEqual({ kind: "rebase", target: BASE });
|
|
expect(fixups.decode(BASE + 0x4010n, 0n)).toMatchObject({
|
|
kind: "unsupported",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[0, 1, "header version"],
|
|
[4, 126, "starts table"],
|
|
[8, 63, "starts table"],
|
|
[40, 22, "page starts"],
|
|
[48, 0x1000, "segment offset"],
|
|
] as const)(
|
|
"rejects malformed chained structure at byte %i",
|
|
(at, value, message) => {
|
|
const data = chainedData(1);
|
|
data.writeUInt32LE(value, at);
|
|
expect(() =>
|
|
parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 128 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
),
|
|
).toThrow(message);
|
|
},
|
|
);
|
|
});
|
|
describe("chained import library identity", () => {
|
|
it.each([1, 2, 3] as const)(
|
|
"retains valid positive and special ordinals and rejects invalid ones for format %i",
|
|
(format) => {
|
|
const data = chainedData(format);
|
|
const importAt = data.readUInt32LE(8);
|
|
const libraries = ["/library-1"];
|
|
const writeOrdinal = (ordinal: number) => {
|
|
if (format === 3)
|
|
data.writeBigUInt64LE(
|
|
(data.readBigUInt64LE(importAt) & ~0xffffn) |
|
|
BigInt(ordinal & 0xffff),
|
|
importAt,
|
|
);
|
|
else
|
|
data.writeUInt32LE(
|
|
(data.readUInt32LE(importAt) & ~0xff) | (ordinal & 0xff),
|
|
importAt,
|
|
);
|
|
};
|
|
for (const ordinal of [1, 0, -1, -2, -3]) {
|
|
writeOrdinal(ordinal);
|
|
data.writeBigUInt64LE(field(1n, 63), 0x4008);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 160 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: libraries,
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(
|
|
fixups.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008)),
|
|
).toMatchObject({
|
|
kind: "bind",
|
|
library_ordinal: ordinal,
|
|
library: ordinal === 1 ? libraries[0] : null,
|
|
});
|
|
}
|
|
writeOrdinal(2);
|
|
const invalidPositive = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 160 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: libraries,
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(
|
|
invalidPositive.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008)),
|
|
).toMatchObject({
|
|
kind: "unsupported",
|
|
reason: "Dylib ordinal 2 exceeds the 1 declared dependencies",
|
|
});
|
|
writeOrdinal(-4);
|
|
const invalidSpecial = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: { offset: 0, size: 160 },
|
|
binds: [],
|
|
rebases: [],
|
|
dylibs: libraries,
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(
|
|
invalidSpecial.decode(BASE + 0x4008n, data.readBigUInt64LE(0x4008)),
|
|
).toMatchObject({
|
|
kind: "unsupported",
|
|
reason: "Unsupported dylib ordinal -4",
|
|
});
|
|
},
|
|
);
|
|
});
|
|
describe("legacy pointer fixup streams", () => {
|
|
it("interprets legacy bind, weak-bind and lazy-bind opcodes", () => {
|
|
const opcodes = Buffer.from([
|
|
0x11, // SET_DYLIB_ORDINAL_IMM 1
|
|
0x40,
|
|
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
|
|
0x51, // SET_TYPE_IMM pointer
|
|
0x71,
|
|
0x08, // SET_SEGMENT_AND_OFFSET segment 1 + 8
|
|
0x90, // DO_BIND
|
|
0xc0,
|
|
0x02,
|
|
0x08, // DO_BIND_ULEB_TIMES_SKIPPING_ULEB 2 times, skip 8
|
|
0x00,
|
|
]);
|
|
const lazy = Buffer.from([
|
|
0x71,
|
|
0x30,
|
|
0x12,
|
|
0x40,
|
|
...Buffer.from("_free\0"),
|
|
0x90,
|
|
0x00,
|
|
0x71,
|
|
0x38,
|
|
0x12,
|
|
0x40,
|
|
...Buffer.from("_malloc\0"),
|
|
0x90,
|
|
0x00,
|
|
]);
|
|
const data = Buffer.concat([opcodes, lazy]);
|
|
const fixups = parsePointerFixups(
|
|
data,
|
|
{
|
|
chained: null,
|
|
binds: [
|
|
{ offset: 0, size: opcodes.length, stream: "bind" },
|
|
{ offset: opcodes.length, size: lazy.length, stream: "lazy" },
|
|
],
|
|
rebases: [],
|
|
dylibs: ["/usr/lib/libobjc.A.dylib", "/usr/lib/libSystem.B.dylib"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.kind).toBe("dyld-info");
|
|
expect(fixups.failures).toEqual([]);
|
|
for (const offset of [0x08n, 0x10n, 0x20n])
|
|
expect(fixups.decode(BASE + 0x4000n + offset, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
library: "/usr/lib/libobjc.A.dylib",
|
|
});
|
|
expect(fixups.decode(BASE + 0x4038n, 0n)).toMatchObject({
|
|
symbol: "_malloc",
|
|
library: "/usr/lib/libSystem.B.dylib",
|
|
});
|
|
expect(fixups.decode(BASE + 0x4018n, 0x99n)).toEqual({
|
|
kind: "raw",
|
|
target: 0x99n,
|
|
});
|
|
});
|
|
|
|
it("wraps a negative DO_BIND_ADD_ADDR_ULEB step and keeps decoding", () => {
|
|
// The unsigned 64-bit encoding of -8 revisits the current pointer slot.
|
|
const back = [0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01];
|
|
const opcodes = Buffer.from([
|
|
0x11, // SET_DYLIB_ORDINAL_IMM 1
|
|
0x40,
|
|
...Buffer.from("__ZdlPv\0"),
|
|
0x71,
|
|
0x08, // SET_SEGMENT_AND_OFFSET segment 1 + 8
|
|
0xa0,
|
|
...back, // DO_BIND_ADD_ADDR_ULEB -8
|
|
0x90, // DO_BIND the same slot again
|
|
0x12, // SET_DYLIB_ORDINAL_IMM 2
|
|
0x40,
|
|
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
|
|
0x90, // DO_BIND the next slot
|
|
0x00,
|
|
]);
|
|
const fixups = parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: ["/usr/lib/libc++.1.dylib", "/usr/lib/libobjc.A.dylib"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.failures).toEqual([]);
|
|
expect(fixups.decode(BASE + 0x4010n, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
library: "/usr/lib/libobjc.A.dylib",
|
|
});
|
|
expect(fixups.decode(BASE + 0x4018n, 0x99n)).toEqual({
|
|
kind: "raw",
|
|
target: 0x99n,
|
|
});
|
|
});
|
|
|
|
it("retains weak-bind stream provenance independently from its default ordinal", () => {
|
|
const opcodes = Buffer.from([
|
|
0x40,
|
|
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
|
|
0x71,
|
|
0x18,
|
|
0x90,
|
|
0x00,
|
|
]);
|
|
const fixups = parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length, stream: "weak" }],
|
|
rebases: [],
|
|
dylibs: [],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups.decode(BASE + 0x4018n, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
binding_source: "weak",
|
|
library_ordinal: 0,
|
|
library: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("legacy bind operand decoding", () => {
|
|
const addendStream = (addend: readonly number[]) =>
|
|
Buffer.from([
|
|
0x11, // SET_DYLIB_ORDINAL_IMM 1
|
|
0x40,
|
|
...Buffer.from("_x\0"),
|
|
0x71,
|
|
0x08, // SET_SEGMENT_AND_OFFSET segment 1 + 8
|
|
0x60,
|
|
...addend, // SET_ADDEND_SLEB
|
|
0x00,
|
|
]);
|
|
const parseBinds = (opcodes: Buffer) =>
|
|
parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: ["/usr/lib/libSystem.B.dylib"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
|
|
it.each([
|
|
{ addend: 0n, sleb: [0x00] },
|
|
{ addend: -1n, sleb: [0x7f] },
|
|
{ addend: -8n, sleb: [0x78] },
|
|
{ addend: 128n, sleb: [0x80, 0x01] },
|
|
{ addend: -129n, sleb: [0xff, 0x7e] },
|
|
// Ten-byte operands at the signed 64-bit limits are still valid.
|
|
{ addend: -(2n ** 63n), sleb: [...Array<number>(9).fill(0x80), 0x7f] },
|
|
{ addend: 2n ** 63n - 1n, sleb: [...Array<number>(9).fill(0xff), 0x00] },
|
|
// Redundant sign extension still belongs to the operand, not the opcode stream.
|
|
{ addend: 0n, sleb: [...Array<number>(11).fill(0x80), 0x00] },
|
|
{ addend: -1n, sleb: [...Array<number>(11).fill(0xff), 0x7f] },
|
|
])(
|
|
"applies a SET_ADDEND_SLEB addend of $addend to the following bind",
|
|
({ addend, sleb }) => {
|
|
const opcodes = addendStream(sleb);
|
|
opcodes[opcodes.length - 1] = 0x90; // DO_BIND
|
|
const fixups = parseBinds(Buffer.concat([opcodes, Buffer.from([0x00])]));
|
|
expect(fixups.failures).toEqual([]);
|
|
expect(fixups.decode(BASE + 0x4008n, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_x",
|
|
addend,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
// Ten continuation bytes, then 0x90. dyld keeps reading 0x90 as part of
|
|
// the operand (bit 7 is set) and binds nothing; it must not become DO_BIND.
|
|
{
|
|
name: "opcode-like tail",
|
|
operand: [...Array<number>(10).fill(0x80), 0x90, 0x00],
|
|
},
|
|
{
|
|
name: "positive signed overflow",
|
|
operand: [...Array<number>(9).fill(0x80), 0x01],
|
|
},
|
|
{
|
|
name: "negative signed overflow",
|
|
operand: [...Array<number>(9).fill(0xff), 0x7e],
|
|
},
|
|
{
|
|
name: "nonzero zero-extension",
|
|
operand: [...Array<number>(10).fill(0x80), 0x01],
|
|
},
|
|
{
|
|
name: "nonnegative sign-extension",
|
|
operand: [...Array<number>(10).fill(0xff), 0x00],
|
|
},
|
|
])("rejects an oversized SET_ADDEND_SLEB with $name", ({ operand }) => {
|
|
const opcodes = addendStream(operand);
|
|
const fixups = parseBinds(opcodes);
|
|
expect(fixups).toMatchObject({ examined: 1, decoded: 0 });
|
|
expect(fixups.failures.join("\n")).toContain(
|
|
"Oversized bind opcode operand",
|
|
);
|
|
expect(fixups.decode(BASE + 0x4008n, 0x99n)).not.toMatchObject({
|
|
kind: "bind",
|
|
});
|
|
});
|
|
|
|
it("does not read beyond the declared stream to terminate an addend", () => {
|
|
const opcodes = addendStream([0x80]);
|
|
const fixups = parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length - 1, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: ["/library-1", "/library-2"],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(fixups).toMatchObject({ examined: 1, decoded: 0 });
|
|
expect(fixups.failures).toEqual([
|
|
"bind bind opcodes: Truncated bind opcode operand",
|
|
]);
|
|
});
|
|
|
|
const addressStream = (operand: readonly number[]) =>
|
|
Buffer.from([
|
|
0x40,
|
|
...Buffer.from("_x\0"),
|
|
0x71,
|
|
0x08, // SET_SEGMENT_AND_OFFSET segment 1 + 8
|
|
0x80,
|
|
...operand, // ADD_ADDR_ULEB
|
|
0x90,
|
|
0x00, // DO_BIND, DONE
|
|
]);
|
|
|
|
it.each([
|
|
{ operand: [...Array<number>(9).fill(0xff), 0x01], offset: 0x4007n },
|
|
{ operand: [...Array<number>(11).fill(0x80), 0x00], offset: 0x4008n },
|
|
])(
|
|
"preserves unsigned 64-bit address deltas and zero padding: $offset",
|
|
({ operand, offset }) => {
|
|
const fixups = parseBinds(addressStream(operand));
|
|
expect(fixups.failures).toEqual([]);
|
|
expect(fixups.decode(BASE + offset, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_x",
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
name: "tenth-byte overflow",
|
|
operand: [...Array<number>(9).fill(0x80), 0x02],
|
|
},
|
|
{
|
|
name: "nonzero zero-extension",
|
|
operand: [...Array<number>(10).fill(0x80), 0x01],
|
|
},
|
|
])("rejects unsigned address deltas with $name", ({ operand }) => {
|
|
const fixups = parseBinds(addressStream(operand));
|
|
expect(fixups).toMatchObject({ examined: 1, decoded: 0 });
|
|
expect(fixups.failures).toEqual([
|
|
"bind bind opcodes: Oversized bind opcode operand",
|
|
]);
|
|
expect(fixups.decode(BASE + 0x4008n, 0n).kind).not.toBe("bind");
|
|
});
|
|
});
|
|
|
|
describe("Objective-C property attributes", () => {
|
|
it("parses type, ownership, accessors and quoted class names", () => {
|
|
expect(
|
|
parsePropertyAttributes(
|
|
"title",
|
|
'T@"NSString<NSCopying, NSSecureCoding>",C,N,GcustomTitle,V_title',
|
|
),
|
|
).toEqual({
|
|
name: "title",
|
|
type_encoding: '@"NSString<NSCopying, NSSecureCoding>"',
|
|
atomicity: "nonatomic",
|
|
attributes: [
|
|
{ name: "T", value: '@"NSString<NSCopying, NSSecureCoding>"' },
|
|
{ name: "C", value: "" },
|
|
{ name: "N", value: "" },
|
|
{ name: "G", value: "customTitle" },
|
|
{ name: "V", value: "_title" },
|
|
],
|
|
is_readonly: false,
|
|
getter: "customTitle",
|
|
setter: null,
|
|
});
|
|
expect(parsePropertyAttributes("count", "Tq,R")).toMatchObject({
|
|
type_encoding: "q",
|
|
is_readonly: true,
|
|
atomicity: "atomic",
|
|
});
|
|
expect(parsePropertyAttributes("count", "Tq,R,N").atomicity).toBe(
|
|
"nonatomic",
|
|
);
|
|
expect(parsePropertyAttributes("count", "").atomicity).toBeNull();
|
|
expect(parsePropertyAttributes("count", "R").atomicity).toBeNull();
|
|
});
|
|
|
|
it("keeps template argument commas inside an Objective-C++ type", () => {
|
|
expect(
|
|
parsePropertyAttributes(
|
|
"position",
|
|
"T{vector<float, float, float>=},N,V_position",
|
|
),
|
|
).toMatchObject({
|
|
type_encoding: "{vector<float, float, float>=}",
|
|
atomicity: "nonatomic",
|
|
attributes: [
|
|
{ name: "T", value: "{vector<float, float, float>=}" },
|
|
{ name: "N", value: "" },
|
|
{ name: "V", value: "_position" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("establishes no defaults from an unfinished quoted class name", () => {
|
|
// `,N,R` here lie inside the unterminated quote, so they are not flags.
|
|
expect(parsePropertyAttributes("title", 'T@"NSString,N,R')).toMatchObject({
|
|
type_encoding: '@"NSString,N,R',
|
|
atomicity: null,
|
|
is_readonly: null,
|
|
});
|
|
expect(parsePropertyAttributes("title", 'N,R,T@"NSString')).toMatchObject({
|
|
atomicity: "nonatomic",
|
|
is_readonly: true,
|
|
});
|
|
});
|
|
|
|
it("keeps defaults unknown when a template argument is unfinished", () => {
|
|
expect(
|
|
parsePropertyAttributes("position", "T{vector<float, float,N,R"),
|
|
).toMatchObject({
|
|
type_encoding: "{vector<float, float,N,R",
|
|
atomicity: null,
|
|
is_readonly: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
it("preserves classic special ordinals and returns unsupported at invalid bind locations", () => {
|
|
const parseOrdinal = (ordinal: number, dylibs: readonly string[]) => {
|
|
const immediate = ordinal < 0 ? ordinal + 16 : ordinal;
|
|
const opcodes = Buffer.from([
|
|
ordinal > 0 ? 0x10 | ordinal : 0x30 | immediate,
|
|
0x40,
|
|
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
|
|
0x71,
|
|
0x08,
|
|
0x90,
|
|
0x00,
|
|
]);
|
|
return parsePointerFixups(
|
|
opcodes,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: opcodes.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs,
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
};
|
|
for (const ordinal of [0, -1, -2, -3]) {
|
|
const fixups = parseOrdinal(ordinal, []);
|
|
expect(fixups.decode(BASE + 0x4008n, 0n)).toMatchObject({
|
|
kind: "bind",
|
|
symbol: "_OBJC_CLASS_$_NSObject",
|
|
library_ordinal: ordinal,
|
|
library: null,
|
|
});
|
|
}
|
|
for (const [ordinal, dylibs, reason] of [
|
|
[1, [], "Dylib ordinal 1 exceeds the 0 declared dependencies"],
|
|
[-4, [], "Unsupported dylib ordinal -4"],
|
|
] as const) {
|
|
const fixups = parseOrdinal(ordinal, dylibs);
|
|
expect(fixups.decode(BASE + 0x4008n, 0n)).toEqual({
|
|
kind: "unsupported",
|
|
reason,
|
|
});
|
|
}
|
|
const oversized = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
|
|
let remaining = oversized;
|
|
const encoded: number[] = [];
|
|
while (remaining >= 0x80n) {
|
|
encoded.push(Number(remaining & 0x7fn) | 0x80);
|
|
remaining >>= 7n;
|
|
}
|
|
encoded.push(Number(remaining));
|
|
const oversizedStream = Buffer.from([
|
|
0x20,
|
|
...encoded,
|
|
0x40,
|
|
...Buffer.from("_OBJC_CLASS_$_NSObject\0"),
|
|
0x71,
|
|
0x08,
|
|
0x90,
|
|
0x00,
|
|
]);
|
|
const oversizedFixups = parsePointerFixups(
|
|
oversizedStream,
|
|
{
|
|
chained: null,
|
|
binds: [{ offset: 0, size: oversizedStream.length, stream: "bind" }],
|
|
rebases: [],
|
|
dylibs: [],
|
|
},
|
|
SEGMENTS,
|
|
BASE,
|
|
);
|
|
expect(oversizedFixups.decode(BASE + 0x4008n, 0n)).toEqual({
|
|
kind: "unsupported",
|
|
reason: `Dylib ordinal ${oversized} exceeds the safe integer range`,
|
|
});
|
|
});
|