Files
rea/src/native/AppleMachoFixups.test.ts
T
morluto d4415444c4 fix(native): retain processor, hidden-slice and property type facts
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
2026-10-24 01:00:14 +08:00

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`,
});
});