mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
inspect-macho marked fully decoded LC_LINKER_OPTION, LC_NOTE and LC_FILESET_ENTRY commands as undecoded. Each now counts as decoded only when its payload is consumed exactly (the fileset string must start right after the 32-byte header and end at cmdsize), and the note keeps its full owner storage. Fixes #8935 Squashed from #8937 and rebased onto main. Co-Authored-By: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
673 lines
22 KiB
TypeScript
673 lines
22 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { createHash } from "node:crypto";
|
|
import { readMachoCommandNames } from "./MachoCommandNames.js";
|
|
import { projectMachoBinary } from "./MachoBinaryProjection.js";
|
|
import { machoImage, readerOf } from "../artifacts/apple/MachoImage.fixture.js";
|
|
|
|
it("owns retained command bytes independently of Buffer-backed read windows", async () => {
|
|
const paddedSize = 256 * 1024;
|
|
const image = Buffer.alloc(32 + paddedSize * 2 + 24);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(3, 16);
|
|
image.writeUInt32LE(paddedSize * 2 + 24, 20);
|
|
const rpath = 32;
|
|
image.writeUInt32LE(0x8000001c, rpath);
|
|
image.writeUInt32LE(paddedSize, rpath + 4);
|
|
image.writeUInt32LE(12, rpath + 8);
|
|
const fileset = rpath + paddedSize;
|
|
image.writeUInt32LE(0x80000035, fileset);
|
|
image.writeUInt32LE(paddedSize, fileset + 4);
|
|
image.writeUInt32LE(32, fileset + 24);
|
|
image.write("x", fileset + 32);
|
|
const uuid = fileset + paddedSize;
|
|
image.writeUInt32LE(0x1b, uuid);
|
|
image.writeUInt32LE(24, uuid + 4);
|
|
image.fill(0x42, uuid + 8, uuid + 24);
|
|
|
|
// Buffer.subarray and Buffer.slice both share the caller's backing store.
|
|
const result = await readMachoCommandNames(
|
|
async (offset, size) => image.subarray(offset, offset + size),
|
|
image.length,
|
|
paddedSize,
|
|
3,
|
|
);
|
|
const path = result.commands[0]!.commandString!;
|
|
const entry = result.commands[1]!.fileset!.entryId;
|
|
const id = result.commands[2]!.uuid!;
|
|
expect(path).toHaveLength(0);
|
|
expect([...entry]).toEqual([0x78]);
|
|
expect([...id]).toEqual(Array<number>(16).fill(0x42));
|
|
for (const retained of [path, entry, id]) {
|
|
expect(retained.buffer).not.toBe(image.buffer);
|
|
expect(retained.buffer.byteLength).toBe(retained.byteLength);
|
|
}
|
|
image.fill(0);
|
|
expect([...entry]).toEqual([0x78]);
|
|
expect([...id]).toEqual(Array<number>(16).fill(0x42));
|
|
});
|
|
|
|
it("retains newline and misleading field names from binary segment, section and dylib strings", async () => {
|
|
const name = Buffer.from("x\naddr 0x1");
|
|
const dylib = Buffer.from("lib\nLoad command 2\ncmd LC_SEGMENT_64\0");
|
|
const segmentSize = 152;
|
|
const dylibSize = (24 + dylib.length + 7) & ~7;
|
|
const image = Buffer.alloc(32 + segmentSize + dylibSize);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(2, 8);
|
|
image.writeUInt32LE(2, 12);
|
|
image.writeUInt32LE(2, 16);
|
|
image.writeUInt32LE(segmentSize + dylibSize, 20);
|
|
image.writeUInt32LE(0x200085, 24);
|
|
image.writeUInt32LE(0x19, 32);
|
|
image.writeUInt32LE(segmentSize, 36);
|
|
name.copy(image, 40);
|
|
image.writeBigUInt64LE(0x100002000n, 56);
|
|
image.writeBigUInt64LE(0x3000n, 64);
|
|
image.writeBigUInt64LE(0x4000n, 72);
|
|
image.writeBigUInt64LE(0x2000n, 80);
|
|
image.writeUInt32LE(7, 88);
|
|
image.writeUInt32LE(5, 92);
|
|
image.writeUInt32LE(1, 96);
|
|
image.writeUInt32LE(2, 100);
|
|
name.copy(image, 104);
|
|
Buffer.from("__DATA").copy(image, 120);
|
|
image.writeBigUInt64LE(0x100002100n, 136);
|
|
image.writeBigUInt64LE(0x88n, 144);
|
|
image.writeUInt32LE(0x4100, 152);
|
|
image.writeUInt32LE(4, 156);
|
|
image.writeUInt32LE(0x80000400, 168);
|
|
const dylibAt = 32 + segmentSize;
|
|
image.writeUInt32LE(0xc, dylibAt);
|
|
image.writeUInt32LE(dylibSize, dylibAt + 4);
|
|
image.writeUInt32LE(24, dylibAt + 8);
|
|
image.writeUInt32LE(123, dylibAt + 12);
|
|
image.writeUInt32LE(0x10002, dylibAt + 16);
|
|
image.writeUInt32LE(0x30004, dylibAt + 20);
|
|
dylib.copy(image, dylibAt + 24);
|
|
const reads: number[] = [];
|
|
const read = async (offset: number, size: number) => {
|
|
reads.push(size);
|
|
return image.subarray(offset, offset + size);
|
|
};
|
|
const result = await readMachoCommandNames(read, image.length, 128, 3);
|
|
expect(result.header).toEqual({
|
|
wide: true,
|
|
littleEndian: true,
|
|
cpuType: 0x0100000c,
|
|
cpuSubtype: 2,
|
|
fileType: 2,
|
|
flags: 0x200085,
|
|
});
|
|
expect(
|
|
Buffer.from(result.commands[0]!.segmentName!)
|
|
.subarray(0, name.length)
|
|
.equals(name),
|
|
).toBe(true);
|
|
expect(
|
|
Buffer.from(result.commands[0]!.sections![0]!.name)
|
|
.subarray(0, name.length)
|
|
.equals(name),
|
|
).toBe(true);
|
|
expect(Buffer.from(result.commands[1]!.commandString!).toString()).toBe(
|
|
"lib\nLoad command 2\ncmd LC_SEGMENT_64",
|
|
);
|
|
expect(result.commands[1]!.dylib).toEqual({
|
|
nameOffset: 24,
|
|
currentVersion: 0x10002,
|
|
compatibilityVersion: 0x30004,
|
|
encoding: { kind: "timestamp", timestamp: 123 },
|
|
});
|
|
expect(result.commands[1]!.rawSha256).toBe(
|
|
createHash("sha256")
|
|
.update(image.subarray(dylibAt, dylibAt + dylibSize))
|
|
.digest("hex"),
|
|
);
|
|
expect(result.commands[0]!.segment).toEqual({
|
|
address: 0x100002000n,
|
|
memorySize: 0x3000n,
|
|
fileOffset: 0x4000n,
|
|
fileSize: 0x2000n,
|
|
maxProtection: 7,
|
|
protection: 5,
|
|
flags: 2,
|
|
});
|
|
expect(result.commands[0]!.sections![0]).toMatchObject({
|
|
address: 0x100002100n,
|
|
size: 0x88n,
|
|
fileOffset: 0x4100,
|
|
alignmentExponent: 4,
|
|
flags: 0x80000400,
|
|
});
|
|
const projected = projectMachoBinary(result);
|
|
expect(projected.segments[0]?.sections[0]?.name).toBe(name.toString());
|
|
expect(projected.dependencies[0]?.path).toBe(
|
|
"lib\nLoad command 2\ncmd LC_SEGMENT_64",
|
|
);
|
|
expect(projected.commands).toHaveLength(2);
|
|
expect(Math.max(...reads)).toBeLessThan(image.length);
|
|
await expect(readMachoCommandNames(read, image.length, 4, 3)).rejects.toThrow(
|
|
"budget",
|
|
);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 128, 2),
|
|
).rejects.toThrow("records exceed");
|
|
});
|
|
|
|
it("retains alternate dylib flags and legacy platform versions", async () => {
|
|
const image = Buffer.alloc(32 + 32 + 16);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(2, 16);
|
|
image.writeUInt32LE(48, 20);
|
|
image.writeUInt32LE(0xc, 32);
|
|
image.writeUInt32LE(32, 36);
|
|
image.writeUInt32LE(28, 40);
|
|
image.writeUInt32LE(0x1a741800, 44);
|
|
image.writeUInt32LE(0x10002, 48);
|
|
image.writeUInt32LE(0x20003, 52);
|
|
image.writeUInt32LE(0x9, 56);
|
|
image.write("lib", 60, "ascii");
|
|
image.writeUInt32LE(0x24, 64);
|
|
image.writeUInt32LE(16, 68);
|
|
image.writeUInt32LE(0x000d0102, 72);
|
|
image.writeUInt32LE(0x000f0000, 76);
|
|
const result = await readMachoCommandNames(
|
|
async (offset, size) => image.subarray(offset, offset + size),
|
|
image.length,
|
|
32,
|
|
2,
|
|
);
|
|
expect(result.commands[0]?.dylib).toEqual({
|
|
nameOffset: 28,
|
|
currentVersion: 0x10002,
|
|
compatibilityVersion: 0x20003,
|
|
encoding: { kind: "use", flags: 9 },
|
|
});
|
|
expect(result.commands[1]?.legacyBuild).toEqual({
|
|
version: 0x000d0102,
|
|
sdk: 0x000f0000,
|
|
});
|
|
});
|
|
|
|
it("retains bounded linker options and dylinker strings with embedded framing", async () => {
|
|
const strings = Buffer.from("-lone\0\nLoad command 1\0");
|
|
const optionSize = (12 + strings.length + 7) & ~7;
|
|
const name = Buffer.from("dyld\nSection\0");
|
|
const nameSize = (12 + name.length + 7) & ~7;
|
|
const image = Buffer.alloc(32 + optionSize + nameSize);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(2, 16);
|
|
image.writeUInt32LE(optionSize + nameSize, 20);
|
|
image.writeUInt32LE(0x2d, 32);
|
|
image.writeUInt32LE(optionSize, 36);
|
|
image.writeUInt32LE(2, 40);
|
|
strings.copy(image, 44);
|
|
const at = 32 + optionSize;
|
|
image.writeUInt32LE(0xe, at);
|
|
image.writeUInt32LE(nameSize, at + 4);
|
|
image.writeUInt32LE(12, at + 8);
|
|
name.copy(image, at + 12);
|
|
const read = async (offset: number, size: number) =>
|
|
image.subarray(offset, offset + size);
|
|
const result = await readMachoCommandNames(read, image.length, 64, 4);
|
|
expect(
|
|
result.commands[0]?.linkerOptions?.map((value) =>
|
|
Buffer.from(value).toString(),
|
|
),
|
|
).toEqual(["-lone", "\nLoad command 1"]);
|
|
expect(Buffer.from(result.commands[1]!.commandString!).toString()).toBe(
|
|
"dyld\nSection",
|
|
);
|
|
await expect(readMachoCommandNames(read, image.length, 4, 4)).rejects.toThrow(
|
|
"bytes exceed",
|
|
);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 64, 3),
|
|
).rejects.toThrow("records exceed");
|
|
});
|
|
|
|
it("bounds aggregate linker option bytes across read windows", async () => {
|
|
const options = Buffer.from(`first\0${"x".repeat(4097)}\0`);
|
|
const size = (12 + options.length + 7) & ~7;
|
|
const image = Buffer.alloc(32 + size);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(1, 16);
|
|
image.writeUInt32LE(size, 20);
|
|
image.writeUInt32LE(0x2d, 32);
|
|
image.writeUInt32LE(size, 36);
|
|
image.writeUInt32LE(2, 40);
|
|
options.copy(image, 44);
|
|
const read = async (offset: number, length: number) =>
|
|
image.subarray(offset, offset + length);
|
|
const exact = await readMachoCommandNames(read, image.length, 4102, 3);
|
|
expect(
|
|
exact.commands[0]?.linkerOptions?.map((value) => value.byteLength),
|
|
).toEqual([5, 4097]);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 4101, 3),
|
|
).rejects.toThrow("bytes exceed the retained output budget");
|
|
});
|
|
|
|
it("reads many small linker options with linear source traffic", async () => {
|
|
const count = 10_000;
|
|
const commandSize = (12 + count * 2 + 7) & ~7;
|
|
const image = Buffer.alloc(32 + commandSize);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(1, 16);
|
|
image.writeUInt32LE(commandSize, 20);
|
|
image.writeUInt32LE(0x2d, 32);
|
|
image.writeUInt32LE(commandSize, 36);
|
|
image.writeUInt32LE(count, 40);
|
|
for (let index = 0; index < count; index++) image[44 + index * 2] = 65;
|
|
let requested = 0;
|
|
const result = await readMachoCommandNames(
|
|
async (offset, size) => {
|
|
requested += size;
|
|
return image.subarray(offset, offset + size);
|
|
},
|
|
image.length,
|
|
count,
|
|
count + 1,
|
|
);
|
|
expect(result.commands[0]?.linkerOptions).toHaveLength(count);
|
|
expect(requested).toBeLessThanOrEqual(image.length * 2 + 16);
|
|
});
|
|
|
|
it("retains note and fileset records without treating names as framing", async () => {
|
|
const image = Buffer.alloc(32 + 40 + 48);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(2, 16);
|
|
image.writeUInt32LE(88, 20);
|
|
image.writeUInt32LE(0x31, 32);
|
|
image.writeUInt32LE(40, 36);
|
|
image.write("x\nLoad command", 40);
|
|
image.writeBigUInt64LE(0x1234n, 56);
|
|
image.writeBigUInt64LE(0x55n, 64);
|
|
image.writeUInt32LE(0x80000035, 72);
|
|
image.writeUInt32LE(48, 76);
|
|
image.writeBigUInt64LE(0x1000n, 80);
|
|
image.writeBigUInt64LE(0x2000n, 88);
|
|
image.writeUInt32LE(32, 96);
|
|
image.writeUInt32LE(7, 100);
|
|
image.write("id\nSection", 104);
|
|
const read = async (offset: number, size: number) =>
|
|
image.subarray(offset, offset + size);
|
|
const result = await readMachoCommandNames(read, image.length, 32, 2);
|
|
expect(result.commands[0]?.note).toMatchObject({
|
|
offset: 0x1234n,
|
|
size: 0x55n,
|
|
});
|
|
expect(
|
|
Buffer.from(result.commands[0]!.note!.owner)
|
|
.toString("utf8")
|
|
.replace(/\0+$/u, ""),
|
|
).toBe("x\nLoad command");
|
|
expect(result.commands[1]?.fileset).toMatchObject({
|
|
address: 0x1000n,
|
|
fileOffset: 0x2000n,
|
|
reserved: 7,
|
|
});
|
|
expect(Buffer.from(result.commands[1]!.fileset!.entryId).toString()).toBe(
|
|
"id\nSection",
|
|
);
|
|
image.writeUInt32LE(31, 96);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 32, 2),
|
|
).rejects.toThrow("offset is invalid");
|
|
});
|
|
|
|
it("distinguishes fully decoded linker-option, note and fileset extents", async () => {
|
|
const command = (code: number, size: number) => {
|
|
const bytes = Buffer.alloc(size);
|
|
bytes.writeUInt32LE(code, 0);
|
|
bytes.writeUInt32LE(size, 4);
|
|
return bytes;
|
|
};
|
|
const linkerOptions = (size: number) => {
|
|
const bytes = command(0x2d, size);
|
|
bytes.writeUInt32LE(1, 8);
|
|
bytes.write("-lz\0", 12, "ascii");
|
|
return bytes;
|
|
};
|
|
const note = (size: number, owner = Buffer.alloc(16)) => {
|
|
const bytes = command(0x31, size);
|
|
owner.copy(bytes, 8, 0, 16);
|
|
return bytes;
|
|
};
|
|
const fileset = (size: number) => {
|
|
const bytes = command(0x80000035, size);
|
|
bytes.writeUInt32LE(32, 24);
|
|
bytes.write("entryid\0", 32, "ascii");
|
|
return bytes;
|
|
};
|
|
const filesetGap = () => {
|
|
const bytes = fileset(48);
|
|
bytes.writeUInt32LE(40, 24);
|
|
bytes.fill(0x5a, 32, 40);
|
|
bytes.write("entryid\0", 40, "ascii");
|
|
return bytes;
|
|
};
|
|
const owner = Buffer.from([0x78, 0x00, 0x79, ...Array<number>(13).fill(0)]);
|
|
const image = machoImage({
|
|
commands: [
|
|
linkerOptions(16),
|
|
linkerOptions(24),
|
|
note(40, owner),
|
|
note(48),
|
|
fileset(40),
|
|
fileset(48),
|
|
filesetGap(),
|
|
],
|
|
});
|
|
const binary = await readMachoCommandNames(
|
|
readerOf(image),
|
|
image.length,
|
|
1024,
|
|
16,
|
|
);
|
|
|
|
expect(binary.commands.map(({ fullyDecoded }) => fullyDecoded)).toEqual([
|
|
true,
|
|
false,
|
|
true,
|
|
false,
|
|
true,
|
|
false,
|
|
false,
|
|
]);
|
|
expect(binary.commands.map(({ rawSha256 }) => rawSha256 === null)).toEqual([
|
|
true,
|
|
false,
|
|
true,
|
|
false,
|
|
true,
|
|
false,
|
|
false,
|
|
]);
|
|
expect([...binary.commands[2]!.note!.owner]).toEqual([...owner]);
|
|
expect([...binary.commands[6]!.fileset!.entryId]).toEqual([
|
|
...Buffer.from("entryid"),
|
|
]);
|
|
const projection = projectMachoBinary(binary);
|
|
expect(projection.commands[2]?.fields.data_owner).toBe("x");
|
|
expect(projection.commands[2]?.fields.data_owner_storage_hex).toBe(
|
|
owner.toString("hex"),
|
|
);
|
|
expect(projection.commands[6]?.fields.entry_id).toBe("entryid");
|
|
expect(projection.commandLimitations).toEqual([
|
|
expect.stringContaining("payload fields are not decoded"),
|
|
]);
|
|
for (const index of [0, 2, 4])
|
|
expect(projection.commands[index]?.fields).not.toHaveProperty(
|
|
"command_sha256",
|
|
);
|
|
for (const index of [1, 3, 5, 6])
|
|
expect(projection.commands[index]?.fields.command_sha256).toMatch(
|
|
/^[a-f0-9]{64}$/u,
|
|
);
|
|
});
|
|
|
|
it("reads fixed command words in the selected slice byte order", async () => {
|
|
const image = Buffer.alloc(28 + 24 + 16);
|
|
image.writeUInt32BE(0xfeedface, 0);
|
|
image.writeUInt32BE(18, 4);
|
|
image.writeUInt32BE(2, 16);
|
|
image.writeUInt32BE(40, 20);
|
|
image.writeUInt32BE(2, 28);
|
|
image.writeUInt32BE(24, 32);
|
|
image.writeUInt32BE(128, 36);
|
|
image.writeUInt32BE(4, 40);
|
|
image.writeUInt32BE(256, 44);
|
|
image.writeUInt32BE(64, 48);
|
|
image.writeUInt32BE(0x1d, 52);
|
|
image.writeUInt32BE(16, 56);
|
|
image.writeUInt32BE(0x400, 60);
|
|
image.writeUInt32BE(0x80, 64);
|
|
const read = async (offset: number, size: number) =>
|
|
image.subarray(offset, offset + size);
|
|
const result = await readMachoCommandNames(read, image.length, 32, 2);
|
|
expect(result.commands[0]?.fixedFields).toEqual({
|
|
symoff: 128,
|
|
nsyms: 4,
|
|
stroff: 256,
|
|
strsize: 64,
|
|
});
|
|
expect(result.commands[1]?.fixedFields).toEqual({
|
|
dataoff: 0x400,
|
|
datasize: 0x80,
|
|
});
|
|
image.writeUInt32BE(20, 32);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 32, 2),
|
|
).rejects.toThrow("fixed command fields are truncated");
|
|
});
|
|
|
|
it("decodes 32-bit big-endian segment and section facts without reading payloads", async () => {
|
|
const image = Buffer.alloc(28 + 56 + 68);
|
|
image.writeUInt32BE(0xfeedface, 0);
|
|
image.writeUInt32BE(18, 4);
|
|
image.writeUInt32BE(1, 16);
|
|
image.writeUInt32BE(124, 20);
|
|
image.writeUInt32BE(1, 28);
|
|
image.writeUInt32BE(124, 32);
|
|
Buffer.from("__DATA").copy(image, 36);
|
|
image.writeUInt32BE(0x1000, 52);
|
|
image.writeUInt32BE(0x2000, 56);
|
|
image.writeUInt32BE(0x300, 60);
|
|
image.writeUInt32BE(0x400, 64);
|
|
image.writeUInt32BE(7, 68);
|
|
image.writeUInt32BE(3, 72);
|
|
image.writeUInt32BE(1, 76);
|
|
image.writeUInt32BE(4, 80);
|
|
Buffer.from("section\nname").copy(image, 84);
|
|
Buffer.from("__DATA").copy(image, 100);
|
|
image.writeUInt32BE(0x1100, 116);
|
|
image.writeUInt32BE(0x20, 120);
|
|
image.writeUInt32BE(0x310, 124);
|
|
image.writeUInt32BE(3, 128);
|
|
image.writeUInt32BE(0x80000000, 140);
|
|
const sizes: number[] = [];
|
|
const result = await readMachoCommandNames(
|
|
async (offset, size) => {
|
|
sizes.push(size);
|
|
return image.subarray(offset, offset + size);
|
|
},
|
|
image.length,
|
|
128,
|
|
2,
|
|
);
|
|
expect(result.commands[0]?.segment).toEqual({
|
|
address: 0x1000n,
|
|
memorySize: 0x2000n,
|
|
fileOffset: 0x300n,
|
|
fileSize: 0x400n,
|
|
maxProtection: 7,
|
|
protection: 3,
|
|
flags: 4,
|
|
});
|
|
expect(result.commands[0]?.sections?.[0]).toMatchObject({
|
|
address: 0x1100n,
|
|
size: 0x20n,
|
|
fileOffset: 0x310,
|
|
alignmentExponent: 3,
|
|
flags: 0x80000000,
|
|
});
|
|
expect(Math.max(...sizes)).toBeLessThan(image.length);
|
|
});
|
|
|
|
it("retains UUID, entrypoint and bounded build tool records", async () => {
|
|
const image = Buffer.alloc(32 + 24 + 24 + 32);
|
|
image.writeUInt32LE(0xfeedfacf, 0);
|
|
image.writeUInt32LE(0x0100000c, 4);
|
|
image.writeUInt32LE(3, 16);
|
|
image.writeUInt32LE(80, 20);
|
|
image.writeUInt32LE(0x1b, 32);
|
|
image.writeUInt32LE(24, 36);
|
|
Buffer.from("00112233445566778899aabbccddeeff", "hex").copy(image, 40);
|
|
image.writeUInt32LE(0x80000028, 56);
|
|
image.writeUInt32LE(24, 60);
|
|
image.writeBigUInt64LE(0x1234n, 64);
|
|
image.writeBigUInt64LE(0x4000n, 72);
|
|
image.writeUInt32LE(0x32, 80);
|
|
image.writeUInt32LE(32, 84);
|
|
image.writeUInt32LE(1, 88);
|
|
image.writeUInt32LE(0x000e0100, 92);
|
|
image.writeUInt32LE(0x000f0000, 96);
|
|
image.writeUInt32LE(1, 100);
|
|
image.writeUInt32LE(3, 104);
|
|
image.writeUInt32LE(0x01020300, 108);
|
|
const read = async (offset: number, size: number) =>
|
|
image.subarray(offset, offset + size);
|
|
const result = await readMachoCommandNames(read, image.length, 128, 4);
|
|
expect(Buffer.from(result.commands[0]!.uuid!).toString("hex")).toBe(
|
|
"00112233445566778899aabbccddeeff",
|
|
);
|
|
expect(result.commands[1]?.entrypoint).toEqual({
|
|
offset: 0x1234n,
|
|
stackSize: 0x4000n,
|
|
});
|
|
expect(result.commands[2]?.build).toEqual({
|
|
platform: 1,
|
|
minimumOs: 0x000e0100,
|
|
sdk: 0x000f0000,
|
|
tools: [{ tool: 3, version: 0x01020300 }],
|
|
});
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 128, 3),
|
|
).rejects.toThrow("records exceed");
|
|
image.writeUInt32LE(2, 100);
|
|
await expect(
|
|
readMachoCommandNames(read, image.length, 128, 4),
|
|
).rejects.toThrow("build tool table is truncated");
|
|
});
|
|
|
|
it.skipIf(process.platform !== "darwin")(
|
|
"reads an Apple linker generated multiline section from bounded ranges",
|
|
async () => {
|
|
const { mkdtemp, writeFile, rm, open } = await import("node:fs/promises");
|
|
const { join } = await import("node:path");
|
|
const { tmpdir } = await import("node:os");
|
|
const { execFileSync } = await import("node:child_process");
|
|
const root = await mkdtemp(join(tmpdir(), "rea-macho-names-"));
|
|
try {
|
|
const code = join(root, "main.c");
|
|
const payload = join(root, "payload");
|
|
const image = join(root, "target");
|
|
await writeFile(code, "int main(void) { return 0; }\n");
|
|
await writeFile(payload, "data");
|
|
execFileSync("/usr/bin/clang", [
|
|
"-arch",
|
|
"arm64",
|
|
"-o",
|
|
image,
|
|
code,
|
|
`-Wl,-sectcreate,__DATA,x\naddr 0x1,${payload}`,
|
|
]);
|
|
const handle = await open(image, "r");
|
|
try {
|
|
const stat = await handle.stat();
|
|
const result = await readMachoCommandNames(
|
|
async (offset, size) => {
|
|
const bytes = Buffer.alloc(size);
|
|
const read = await handle.read(bytes, 0, size, offset);
|
|
return bytes.subarray(0, read.bytesRead);
|
|
},
|
|
stat.size,
|
|
1024,
|
|
10000,
|
|
);
|
|
expect(
|
|
result.commands
|
|
.flatMap((command) => command.sections ?? [])
|
|
.some(
|
|
(section) =>
|
|
Buffer.from(section.name).subarray(0, 10).toString() ===
|
|
"x\naddr 0x1",
|
|
),
|
|
).toBe(true);
|
|
const parsed = projectMachoBinary(result);
|
|
expect(
|
|
parsed.segments
|
|
.flatMap((segment) => segment.sections)
|
|
.some((section) => section.name === "x\naddr 0x1"),
|
|
).toBe(true);
|
|
} finally {
|
|
await handle.close();
|
|
}
|
|
} finally {
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it("retains legacy on-disk dylib and fixed-library identities", async () => {
|
|
const commands: Buffer[] = [];
|
|
for (const id of [0x6, 0x7]) {
|
|
const command = Buffer.alloc(32);
|
|
command.writeUInt32LE(id, 0);
|
|
command.writeUInt32LE(command.length, 4);
|
|
command.writeUInt32LE(20, 8);
|
|
command.writeUInt32LE(7, 12);
|
|
command.writeUInt32LE(0x1000, 16);
|
|
command.write("lib\nX", 20);
|
|
commands.push(command);
|
|
}
|
|
const fvm = Buffer.alloc(24);
|
|
fvm.writeUInt32LE(9, 0);
|
|
fvm.writeUInt32LE(fvm.length, 4);
|
|
fvm.writeUInt32LE(16, 8);
|
|
fvm.writeUInt32LE(0x2000, 12);
|
|
fvm.write("file", 16);
|
|
commands.push(fvm);
|
|
const prebound = Buffer.alloc(32);
|
|
prebound.writeUInt32LE(0x10, 0);
|
|
prebound.writeUInt32LE(prebound.length, 4);
|
|
prebound.writeUInt32LE(20, 8);
|
|
prebound.writeUInt32LE(2, 12);
|
|
prebound.writeUInt32LE(28, 16);
|
|
prebound.write("lib", 20);
|
|
prebound[28] = 1;
|
|
commands.push(prebound);
|
|
const header = Buffer.alloc(32);
|
|
header.writeUInt32LE(0xfeedfacf, 0);
|
|
header.writeUInt32LE(0x0100000c, 4);
|
|
header.writeUInt32LE(commands.length, 16);
|
|
header.writeUInt32LE(
|
|
commands.reduce((size, command) => size + command.length, 0),
|
|
20,
|
|
);
|
|
const image = Buffer.concat([header, ...commands]);
|
|
const read = async (offset: number, size: number) =>
|
|
image.subarray(offset, offset + size);
|
|
const parsed = await readMachoCommandNames(read, image.length, 256, 4);
|
|
expect(Buffer.from(parsed.commands[0]!.commandString!).toString()).toBe(
|
|
"lib\nX",
|
|
);
|
|
expect(parsed.commands[0]!.fvmlib).toEqual({
|
|
minorVersion: 7,
|
|
headerAddress: 0x1000,
|
|
});
|
|
expect(parsed.commands[2]!.fvmfile).toEqual({ headerAddress: 0x2000 });
|
|
expect(parsed.commands[3]!.prebound).toMatchObject({
|
|
moduleCount: 2,
|
|
linkedModulesOffset: 28,
|
|
});
|
|
expect([...parsed.commands[3]!.prebound!.linkedModules]).toEqual([1]);
|
|
prebound.writeUInt32LE(33, 16);
|
|
const invalid = Buffer.concat([header, ...commands]);
|
|
await expect(
|
|
readMachoCommandNames(
|
|
async (offset, size) => invalid.subarray(offset, offset + size),
|
|
invalid.length,
|
|
256,
|
|
4,
|
|
),
|
|
).rejects.toThrow("bitmap exceeds");
|
|
});
|