Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/native/MachoBinaryProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ export const projectMachoBinary = (
decoded = true;
const owner = textFact(row.note.owner, true, commandLimitations);
fields.data_owner = owner.text;
fields.data_owner_storage_hex = Buffer.from(row.note.owner).toString(
"hex",
);
if (owner.bytesHex !== null) fields.data_owner_bytes_hex = owner.bytesHex;
fields.offset = rawNumber(row.note.offset, 16);
fields.size = rawNumber(row.note.size, 16);
Expand Down
92 changes: 92 additions & 0 deletions src/native/MachoCommandNames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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;
Expand Down Expand Up @@ -321,6 +322,97 @@ it("retains note and fileset records without treating names as framing", async (
).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);
Expand Down
15 changes: 15 additions & 0 deletions src/native/MachoCommandNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,8 @@ export const readMachoCommandNames = async (
};
}
let fileset: MachoCommandNames["commands"][number]["fileset"];
let filesetStringStartsAfterHeader = false;
let filesetStringEnd: number | undefined;
if (frame.command === 0x80000035) {
if (frame.size < 32)
throw new MachoBinaryFormatError("Mach-O fileset command is truncated");
Expand All @@ -641,6 +643,7 @@ export const readMachoCommandNames = async (
throw new MachoBinaryFormatError(
"Mach-O fileset entry string offset is invalid",
);
filesetStringStartsAfterHeader = offset === 32;
const length = Math.min(frame.size - offset, maximumStringBytes + 1);
const value = await exact(at + offset, length);
const end = value.indexOf(0);
Expand All @@ -657,6 +660,7 @@ export const readMachoCommandNames = async (
entryId: retain(value.subarray(0, end)),
reserved: view.getUint32(20, table.littleEndian),
};
filesetStringEnd = offset + end + 1;
}
let uuid: Uint8Array | undefined;
if (frame.command === 0x1b) {
Expand Down Expand Up @@ -723,6 +727,17 @@ export const readMachoCommandNames = async (
};
}
const fullyDecoded =
(linkerOptions !== undefined &&
frame.size ===
12 +
linkerOptions.reduce(
(total, option) => total + option.byteLength + 1,
0,
)) ||
(note !== undefined && frame.size === 40) ||
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
(fileset !== undefined &&
filesetStringStartsAfterHeader &&
filesetStringEnd === frame.size) ||
(fixedNames !== undefined && frame.size === 8 + fixedNames.length * 4) ||
(uuid !== undefined && frame.size === 24) ||
(entrypoint !== undefined && frame.size === 24) ||
Expand Down
45 changes: 45 additions & 0 deletions tests/boundary/cli/nativeMachoLinkerOptionsCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,50 @@ describe.skipIf(process.platform !== "darwin")(
]);
},
);
cliTest(
"does not report undecoded payload for an exact linker option command",
async ({ cli, processes }) => {
const directory = await createTestTempDirectory("rea-linker-option-");
const source = join(directory, "option.c");
const object = join(directory, "option.o");
await writeFile(
source,
[
'__asm__(".linker_option \\"-lz\\"");',
"int sample(void) { return 7; }",
"",
].join("\n"),
);
const compiled = await processes.run(
"/usr/bin/xcrun",
["clang", "-c", source, "-o", object],
{ timeoutMs: 30_000 },
);
expect(compiled.exitCode, compiled.stderr).toBe(0);
const result = await cli.run({
arguments: ["inspect-macho", object, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
const { load_commands: loadCommands } = evidenceSchema.parse(
result.json,
).normalized_result;
const option = loadCommands.items.find(
(command) => command.kind === "LC_LINKER_OPTION",
);
expect(option?.fields).toMatchObject({
cmd: "LC_LINKER_OPTION",
cmdsize: 16,
count: 1,
"string #1": "-lz",
});
expect(option?.fields).not.toHaveProperty("command_sha256");
expect(loadCommands.limitations).not.toEqual(
expect.arrayContaining([
expect.stringContaining("payload fields are not decoded"),
]),
);
},
);
},
);
Loading