* fix(native): retain selected Mach-O slice identity * fix(native)!: own Mach-O load commands in binary reader * fix(native): preserve malformed Mach-O and image base limits * fix(native): preserve leading BOM in Mach-O names * test(native): update Mach-O provenance verification * fix(native): retain completed Mach-O inspection on cleanup failure * fix: bound Mach-O projected section output * fix(native): own retained Mach-O command bytes * fix(native): retain Mach-O outcomes on final cancellation * fix(cli): preserve caller path in partial evidence * fix: reserve Mach-O projection envelope budget
581 lines
19 KiB
TypeScript
581 lines
19 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { createHash } from "node:crypto";
|
|
import { readMachoCommandNames } from "./MachoCommandNames.js";
|
|
import { projectMachoBinary } from "./MachoBinaryProjection.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("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");
|
|
});
|