Files
reverse-engineering-anything/src/artifacts/apple/MachoLoadCommandReader.test.ts
T
morluto 90062c0922 fix(apple): trace legacy macOS dylibs with implicit platforms
Apply dyld’s implicit macOS compatibility rule only for a root executable with observed macOS platform and a supported candidate CPU type. Retain the absent platform fields as raw facts and expose the derivation on the candidate. Traverse the selected dylib and report missing required children; preserve unknown platform coverage and the later known fallback graph in other contexts.

Validation: 144 source/reader tests, 14 compiled CLI tests, 18 schema tests, full check:ci and docs:check passed. Four source-built app variants passed CLI and SDK-validated stdio MCP checks with unchanged fixture digests. No fixture executable or Apple dyld comparison was run. Independent review resolved a library-root authority gap before integration.

Sixteen cold/warm task-cost observations are retained locally with sampling limits; the catalog adds 66 bytes. Actual model tokens and maximum-sized inputs were not measured; no speedup is claimed.

Closes #5696
2026-10-21 12:04:29 +08:00

1666 lines
56 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdir, realpath, rename, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
CPU,
FILE_TYPE,
LC,
codeSignatureCommand,
buildVersionCommand,
dyldEnvironmentCommand,
dylibCommand,
dylibUseCommand,
fatImage,
lazyLoadDylibInfoCommand,
lazyLoadDylibInfoPayload,
machoImage,
machoImageWithLazyLoadDylibInfo,
segmentCommand,
readerOf,
rpathCommand,
versionMinCommand,
} from "./MachoImage.fixture.js";
import {
MAX_LAZY_INFO_DEPENDENCIES,
MAX_LAZY_INFO_SYMBOL_REFERENCES,
MAX_LOAD_COMMAND_BYTES,
hasMachoMagic,
readMachoImage,
} from "./MachoLoadCommandReader.js";
import { traceDylibResolution } from "./DylibResolutionReader.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js";
const read = (bytes: Uint8Array) =>
readMachoImage(readerOf(bytes), bytes.length);
// Legacy `LC_VERSION_MIN_*` commands carry no simulator flag: per dyld
// rules Intel images are simulator, ARM images are device (tvOS simulator
// is x86_64 only). A legacy simulator executable must not fail resolution
// against a modern simulator dylib.
it.each([
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.i386, platform: 7 },
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.x86_64, platform: 7 },
{ command: LC.VERSION_MIN_IPHONEOS, cpu: CPU.arm64, platform: 2 },
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.x86_64, platform: 8 },
{ command: LC.VERSION_MIN_TVOS, cpu: CPU.arm64, platform: 3 },
{ command: LC.VERSION_MIN_WATCHOS, cpu: CPU.i386, platform: 9 },
{ command: LC.VERSION_MIN_WATCHOS, cpu: CPU.arm64, platform: 4 },
{ command: LC.VERSION_MIN_MACOSX, cpu: CPU.x86_64, platform: 1 },
] as const)(
"classifies legacy command $command on $cpu.type as platform $platform",
async ({ command, cpu, platform }) => {
const facts = await read(
machoImage({ cpu, commands: [versionMinCommand(command)] }),
);
if (facts.status !== "parsed") throw new Error(facts.status);
expect(facts.slices[0]).toMatchObject({ platform, platforms: [platform] });
},
);
it("uses legacy simulator and device platform facts when resolving dylibs", async () => {
const root = await createTestTempDirectory("rea-macho-legacy-platform-");
const dylibPath = "Frameworks/libThing.dylib";
const installName = "@rpath/libThing.dylib";
const intelExecutable = machoImage({
cpu: CPU.x86_64,
commands: [
versionMinCommand(LC.VERSION_MIN_IPHONEOS),
rpathCommand("@executable_path/Frameworks"),
dylibCommand(LC.LOAD_DYLIB, installName),
],
});
const armExecutable = machoImage({
cpu: CPU.arm64,
commands: [
versionMinCommand(LC.VERSION_MIN_IPHONEOS),
rpathCommand("@executable_path/Frameworks"),
dylibCommand(LC.LOAD_DYLIB, installName),
],
});
const simulatorDylib = fatImage([
{
cpu: CPU.x86_64,
bytes: machoImage({
cpu: CPU.x86_64,
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(7),
dylibCommand(LC.ID_DYLIB, installName),
],
}),
},
{
cpu: CPU.arm64,
bytes: machoImage({
cpu: CPU.arm64,
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(7),
dylibCommand(LC.ID_DYLIB, installName),
],
}),
},
]);
await mkdir(join(root, "Frameworks"), { recursive: true });
await writeFile(join(root, "Intel"), intelExecutable);
await writeFile(join(root, "Arm"), armExecutable);
await writeFile(join(root, dylibPath), simulatorDylib);
const trace = async (target: string, architecture: "arm64" | "x86_64") => {
const bytes = target === "Intel" ? intelExecutable : armExecutable;
return traceDylibResolution({
rootPath: root,
targetPath: join(root, target),
targetSha256: createHash("sha256").update(bytes).digest("hex"),
enumerateRoots: false,
parameters: { roots: [target], architecture },
});
};
const intelTrace = await trace("Intel", "x86_64");
const armTrace = await trace("Arm", "arm64");
expect(intelTrace.edges).toContainEqual(
expect.objectContaining({
loader: "Intel",
resolution: expect.objectContaining({
status: "resolved",
image: dylibPath,
}),
}),
);
expect(armTrace.edges).toContainEqual(
expect.objectContaining({
loader: "Arm",
candidates: expect.arrayContaining([
expect.objectContaining({ outcome: "platform-mismatch" }),
]),
}),
);
});
it.each([false, true] as const)(
"applies macOS dyld's implicit platform to a platform-less dylib and traces its required child present=%s",
async (childPresent) => {
const root = await createTestTempDirectory("rea-dylib-platform-unknown-");
const executablePath = "Contents/MacOS/probe";
const dylibPath = "Contents/Frameworks/libA.dylib";
const fallbackPath = "Contents/Fallbacks/libA.dylib";
const childPath = "Contents/Frameworks/libMissing.dylib";
const fallbackChildPath = "Contents/Fallbacks/libFallbackOnly.dylib";
const executable = machoImage({
cpu: CPU.arm64,
commands: [
buildVersionCommand(1),
rpathCommand("@executable_path/../Frameworks"),
rpathCommand("@executable_path/../Fallbacks"),
dylibCommand(LC.LOAD_DYLIB, "@rpath/libA.dylib"),
],
});
const dylib = machoImage({
cpu: CPU.arm64,
fileType: FILE_TYPE.dylib,
commands: [
rpathCommand("@loader_path"),
dylibCommand(LC.LOAD_DYLIB, "@rpath/libMissing.dylib"),
],
});
const child = machoImage({
cpu: CPU.arm64,
fileType: FILE_TYPE.dylib,
commands: [buildVersionCommand(1)],
});
const fallback = machoImage({
cpu: CPU.arm64,
fileType: FILE_TYPE.dylib,
commands: [
rpathCommand("@loader_path"),
dylibCommand(LC.LOAD_DYLIB, "@rpath/libFallbackOnly.dylib"),
],
});
const fallbackChild = machoImage({
cpu: CPU.arm64,
fileType: FILE_TYPE.dylib,
commands: [buildVersionCommand(1)],
});
await mkdir(join(root, "Contents/MacOS"), { recursive: true });
await mkdir(join(root, "Contents/Frameworks"), { recursive: true });
await mkdir(join(root, "Contents/Fallbacks"), { recursive: true });
await writeFile(join(root, executablePath), executable);
await writeFile(join(root, dylibPath), dylib);
await writeFile(join(root, fallbackPath), fallback);
await writeFile(join(root, fallbackChildPath), fallbackChild);
if (childPresent) await writeFile(join(root, childPath), child);
const trace = await traceDylibResolution({
rootPath: root,
targetPath: join(root, executablePath),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: [executablePath] },
});
expect(
trace.images.find(({ path }) => path === executablePath)?.slices[0],
).toMatchObject({ platform: 1, platforms: [1] });
expect(
trace.images.find(({ path }) => path === dylibPath)?.slices[0],
).toMatchObject({ platform: null, platforms: [] });
expect(trace.edges[0]).toMatchObject({
loader: executablePath,
install_name: "@rpath/libA.dylib",
candidates: [
{
outcome: "resolved",
resolved_path: dylibPath,
platform_basis: "macos-dyld-implicit",
},
],
resolution: { status: "resolved", image: dylibPath },
});
expect(trace.edges[1]).toMatchObject({
loader: dylibPath,
install_name: "@rpath/libMissing.dylib",
resolution: childPresent
? { status: "resolved", image: childPath }
: { status: "unresolved", image: null },
loader_conditional: false,
});
expect(trace.images.map(({ path }) => path)).toEqual(
childPresent
? [executablePath, childPath, dylibPath].sort()
: [executablePath, dylibPath].sort(),
);
expect(trace.images.map(({ path }) => path)).not.toContain(fallbackPath);
expect(trace.images.map(({ path }) => path)).not.toContain(
fallbackChildPath,
);
expect(trace.coverage.status).toBe("complete");
expect(trace.limitations).toContainEqual(
expect.stringContaining(dylibPath),
);
expect(trace.findings.map(({ kind }) => kind)).toEqual(
childPresent ? [] : ["required-load-unresolved"],
);
},
);
describe("absolute dyld candidate paths at the filesystem reader boundary", () => {
it.each(["install-name", "rpath"] as const)(
"resolves an absolute in-root %s while preserving its source path",
async (kind) => {
const root = await createTestTempDirectory("rea-dylib-absolute-in-root-");
const canonicalRoot = await realpath(root);
const bridgePath = "Frameworks/libBridge.dylib";
const leafPath = "Frameworks/libLeaf.dylib";
const bridgeAbsolute = `${canonicalRoot}/${bridgePath}`;
const leafName = "@loader_path/libLeaf.dylib";
const executable = machoImage({
commands: [
buildVersionCommand(1),
...(kind === "rpath"
? [rpathCommand(`${canonicalRoot}/Frameworks`)]
: []),
dylibCommand(
LC.LOAD_DYLIB,
kind === "rpath" ? "@rpath/libBridge.dylib" : bridgeAbsolute,
),
],
});
const bridge = machoImage({
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, leafName),
],
});
const leaf = machoImage({
fileType: FILE_TYPE.dylib,
commands: [buildVersionCommand(1)],
});
await mkdir(join(root, "Frameworks"), { recursive: true });
await writeFile(join(root, "App"), executable);
await writeFile(bridgeAbsolute, bridge);
await writeFile(join(root, leafPath), leaf);
const trace = await traceDylibResolution({
rootPath: root,
targetPath: join(root, "App"),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: ["App"] },
});
expect(trace.edges[0]).toMatchObject({
install_name:
kind === "rpath" ? "@rpath/libBridge.dylib" : bridgeAbsolute,
candidates: [
{
path: bridgeAbsolute,
...(kind === "rpath"
? { rpath: `${canonicalRoot}/Frameworks` }
: {}),
outcome: "resolved",
resolved_path: bridgePath,
},
],
resolution: { status: "resolved", image: bridgePath },
});
expect(trace.edges).toContainEqual(
expect.objectContaining({
loader: bridgePath,
install_name: leafName,
resolution: { status: "resolved", image: leafPath },
}),
);
expect(trace.images.map(({ path }) => path)).toEqual([
"App",
bridgePath,
leafPath,
]);
},
);
it("keeps a genuinely external absolute candidate outside the analyzed root", async () => {
const root = await createTestTempDirectory("rea-dylib-absolute-outside-");
const canonicalRoot = await realpath(root);
const executable = machoImage({
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib"),
dylibCommand(LC.LOAD_DYLIB, `${canonicalRoot}/../outside.dylib`),
dylibCommand(
LC.LOAD_DYLIB,
`file://${canonicalRoot}/Frameworks/libBridge.dylib`,
),
],
});
await writeFile(join(root, "App"), executable);
const trace = await traceDylibResolution({
rootPath: root,
targetPath: join(root, "App"),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: ["App"] },
});
expect(trace.edges[0]).toMatchObject({
candidates: [
{
path: "/usr/lib/libSystem.B.dylib",
outcome: "outside-target",
resolved_path: null,
},
],
resolution: { status: "undetermined", image: null },
});
expect(trace.edges[1]).toMatchObject({
candidates: [{ outcome: "escapes-target", resolved_path: null }],
resolution: { status: "undetermined", image: null },
});
expect(trace.edges[2]).toMatchObject({
install_name: `file://${canonicalRoot}/Frameworks/libBridge.dylib`,
candidates: [
{
path: `file://${canonicalRoot}/Frameworks/libBridge.dylib`,
outcome: "undetermined",
resolved_path: null,
},
],
resolution: { status: "undetermined", image: null },
});
expect(trace.images.map(({ path }) => path)).toEqual(["App"]);
});
});
describe("absolute dyld candidate traversal", () => {
it("maps an absolute candidate spelled through the selected root symlink", async () => {
const root = await createTestTempDirectory(
"rea-dylib-absolute-root-alias-",
);
const aliasRoot = join(root, "bundle-alias");
const bridgePath = "Frameworks/libBridge.dylib";
const bridgeAbsolute = `${aliasRoot}/${bridgePath}`;
const executable = machoImage({
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, bridgeAbsolute),
],
});
const bridge = machoImage({
fileType: FILE_TYPE.dylib,
commands: [buildVersionCommand(1)],
});
await symlink(".", aliasRoot);
await mkdir(join(root, "Frameworks"), { recursive: true });
await writeFile(join(root, "App"), executable);
await writeFile(join(root, bridgePath), bridge);
const trace = await traceDylibResolution({
rootPath: aliasRoot,
targetPath: `${aliasRoot}/App`,
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: ["App"] },
});
expect(trace.edges[0]).toMatchObject({
install_name: bridgeAbsolute,
candidates: [
{
path: bridgeAbsolute,
outcome: "resolved",
resolved_path: bridgePath,
},
],
resolution: { status: "resolved", image: bridgePath },
});
});
it.skipIf(process.platform === "win32")(
"preserves symlink-then-parent traversal for an absolute in-root path",
async () => {
const root = await createTestTempDirectory("rea-dylib-absolute-symlink-");
const canonicalRoot = await realpath(root);
const bridgePath = "Frameworks/Versions/libBridge.dylib";
const decoyPath = "Frameworks/libBridge.dylib";
const sourcePath = `${canonicalRoot}/Frameworks/Current/../libBridge.dylib`;
const executable = machoImage({
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, sourcePath),
],
});
const bridge = machoImage({
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, "@loader_path/libLeaf.dylib"),
],
});
const leaf = machoImage({
fileType: FILE_TYPE.dylib,
commands: [buildVersionCommand(1)],
});
await mkdir(join(root, "Frameworks/Versions/A"), { recursive: true });
await symlink("Versions/A", join(root, "Frameworks/Current"));
await writeFile(join(root, "App"), executable);
await writeFile(join(root, bridgePath), bridge);
await writeFile(join(root, decoyPath), new Uint8Array([0, 1, 2]));
await writeFile(join(root, "Frameworks/Versions/libLeaf.dylib"), leaf);
const trace = await traceDylibResolution({
rootPath: root,
targetPath: join(root, "App"),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: ["App"] },
});
expect(trace.edges[0]).toMatchObject({
candidates: [
{
path: sourcePath,
outcome: "resolved",
resolved_path: bridgePath,
},
],
resolution: { status: "resolved", image: bridgePath },
});
expect(trace.edges).toContainEqual(
expect.objectContaining({
loader: bridgePath,
resolution: {
status: "resolved",
image: "Frameworks/Versions/libLeaf.dylib",
},
}),
);
},
);
});
it("selects matching arm64e subtype slices when tracing a mixed FAT image", async () => {
const root = await createTestTempDirectory("rea-macho-arm64e-subtypes-");
const executablePath = "Contents/MacOS/App";
const dylibPath = "Contents/MacOS/libVariant.dylib";
const installName = "@executable_path/libVariant.dylib";
const arm64eDependency = "@executable_path/libArm64e.dylib";
const arm64eX1Dependency = "@executable_path/libArm64eX1.dylib";
const executableSlice = (cpu: typeof CPU.arm64e | typeof CPU.arm64eX1) =>
machoImage({
cpu,
commands: [
buildVersionCommand(1),
dylibCommand(LC.LOAD_DYLIB, installName),
],
});
const executable = fatImage([
{
cpu: CPU.arm64eX1,
bytes: executableSlice(CPU.arm64eX1),
},
{
cpu: CPU.arm64e,
bytes: executableSlice(CPU.arm64e),
},
]);
const variantSlice = (
cpu: typeof CPU.arm64e | typeof CPU.arm64eX1,
dependencyName: string,
) =>
machoImage({
cpu,
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(1),
dylibCommand(LC.ID_DYLIB, installName),
dylibCommand(LC.LOAD_DYLIB, dependencyName),
],
});
const completeDylib = fatImage([
{
cpu: CPU.arm64e,
bytes: variantSlice(CPU.arm64e, arm64eDependency),
},
{
cpu: CPU.arm64eX1,
bytes: variantSlice(CPU.arm64eX1, arm64eX1Dependency),
},
]);
await mkdir(join(root, "Contents/MacOS"), { recursive: true });
await writeFile(join(root, executablePath), executable);
await writeFile(join(root, dylibPath), completeDylib);
const trace = () =>
traceDylibResolution({
rootPath: root,
targetPath: join(root, executablePath),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: [executablePath] },
});
const complete = await trace();
expect(
complete.edges
.filter(({ loader }) => loader === dylibPath)
.map(({ install_name: dependencyName }) => dependencyName),
).toEqual([arm64eX1Dependency, arm64eDependency]);
const onlyArm64e = fatImage([
{
cpu: CPU.arm64e,
bytes: variantSlice(CPU.arm64e, arm64eDependency),
},
]);
await writeFile(join(root, dylibPath), onlyArm64e);
const missingVariant = await trace();
expect(
missingVariant.edges
.filter(
({ loader, install_name: dependencyName }) =>
loader === executablePath && dependencyName === installName,
)
.map(({ candidates, resolution }) => [
candidates[0]?.outcome,
resolution.status,
]),
).toEqual([
["architecture-missing", "unresolved"],
["resolved", "resolved"],
]);
});
describe("Mach-O load command reader: decodes thin-image load commands", () => {
it("preserves leading U+FEFF in load-command strings", async () => {
const image = machoImage({
commands: [
dylibCommand(LC.ID_DYLIB, "\uFEFF@rpath/libself.dylib"),
dylibCommand(LC.LOAD_DYLIB, "\uFEFF@rpath/libother.dylib"),
rpathCommand("\uFEFF@loader_path/Frameworks"),
dyldEnvironmentCommand("\uFEFFDYLD_LIBRARY_PATH=/example"),
],
});
expect(await read(image)).toMatchObject({
status: "parsed",
slices: [
{
install_name: "\uFEFF@rpath/libself.dylib",
dependencies: [{ install_name: "\uFEFF@rpath/libother.dylib" }],
rpaths: ["\uFEFF@loader_path/Frameworks"],
dyld_environment: ["\uFEFFDYLD_LIBRARY_PATH=/example"],
},
],
});
});
it("decodes dylib loading commands of a thin image", async () => {
const image = machoImage({
fileType: FILE_TYPE.dylib,
commands: [
dylibCommand(LC.ID_DYLIB, "@rpath/Core.framework/Core"),
dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib", {
current: 0x050f0400,
compatibility: 0x10000,
}),
dylibCommand(LC.LOAD_WEAK_DYLIB, "@rpath/libweak.dylib"),
dylibCommand(LC.REEXPORT_DYLIB, "@loader_path/libre.dylib"),
dylibCommand(LC.LOAD_UPWARD_DYLIB, "@rpath/libup.dylib"),
dylibCommand(LC.LAZY_LOAD_DYLIB, "@rpath/liblazy.dylib"),
rpathCommand("@loader_path/Frameworks"),
rpathCommand("/usr/lib/swift"),
dyldEnvironmentCommand("DYLD_FRAMEWORK_PATH=@executable_path"),
codeSignatureCommand(),
],
});
const facts = await read(image);
expect(facts.status).toBe("parsed");
if (facts.status !== "parsed") return;
expect(facts.slices).toHaveLength(1);
const [slice] = facts.slices;
expect(slice).toMatchObject({
architecture: "arm64",
file_type: "dylib",
install_name: "@rpath/Core.framework/Core",
rpaths: ["@loader_path/Frameworks", "/usr/lib/swift"],
dyld_environment: ["DYLD_FRAMEWORK_PATH=@executable_path"],
code_signature_present: true,
});
expect(
slice?.dependencies.map(
({ command, install_name: name, weak, reexport, upward }) => [
command,
name,
weak,
reexport,
upward,
],
),
).toEqual([
["LC_LOAD_DYLIB", "/usr/lib/libSystem.B.dylib", false, false, false],
["LC_LOAD_WEAK_DYLIB", "@rpath/libweak.dylib", true, false, false],
["LC_REEXPORT_DYLIB", "@loader_path/libre.dylib", false, true, false],
["LC_LOAD_UPWARD_DYLIB", "@rpath/libup.dylib", false, false, true],
["LC_LAZY_LOAD_DYLIB", "@rpath/liblazy.dylib", false, false, false],
]);
expect(slice?.dependencies[0]).toMatchObject({
encoding: "dylib_command",
current_version: "1295.4.0",
compatibility_version: "1.0.0",
});
});
it("decodes dylib_use_command flags", async () => {
const facts = await read(
machoImage({
commands: [
dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libdelay.dylib", 0x8),
dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libweakup.dylib", 0x1 | 0x4),
dylibUseCommand(LC.LOAD_DYLIB, "@rpath/libre.dylib", 0x2),
],
}),
);
if (facts.status !== "parsed") throw new Error(facts.status);
expect(
facts.slices[0]?.dependencies.map(
({ encoding, weak, upward, reexport, delayed_init: delayed }) => ({
encoding,
weak,
upward,
reexport,
delayed,
}),
),
).toEqual([
{
encoding: "dylib_use_command",
weak: false,
upward: false,
reexport: false,
delayed: true,
},
{
encoding: "dylib_use_command",
weak: true,
upward: true,
reexport: false,
delayed: false,
},
{
encoding: "dylib_use_command",
weak: false,
upward: false,
reexport: true,
delayed: false,
},
]);
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO payload", () => {
it("reads install names from the linkedit payload in load-command order", async () => {
const payload = lazyLoadDylibInfoPayload({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
});
expect(payload).toHaveLength(56);
const image = machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
commandsBefore: [
dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib"),
],
commandsAfter: [dylibCommand(LC.LOAD_DYLIB, "@rpath/libafter.dylib")],
});
const facts = await read(image);
if (facts.status !== "parsed")
throw new Error(
facts.status === "not-mach-o" ? facts.status : facts.reason,
);
expect(
facts.slices[0]?.dependencies.map(
({ command, install_name: name, encoding, weak, current_version }) => ({
command,
name,
encoding,
weak,
current_version,
}),
),
).toEqual([
{
command: "LC_LOAD_DYLIB",
name: "/usr/lib/libSystem.B.dylib",
encoding: "dylib_command",
weak: false,
current_version: "1.0.0",
},
{
command: "LC_LAZY_LOAD_DYLIB_INFO",
name: "@rpath/libl.dylib",
encoding: "lazy_load_dylib_info",
weak: false,
current_version: null,
},
{
command: "LC_LOAD_DYLIB",
name: "@rpath/libafter.dylib",
encoding: "dylib_command",
weak: false,
current_version: "1.0.0",
},
]);
const weak = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flags: 0x1,
}),
);
if (weak.status !== "parsed")
throw new Error(weak.status === "not-mach-o" ? weak.status : weak.reason);
expect(weak.slices[0]?.dependencies[0]).toMatchObject({ weak: true });
const universal = await read(
fatImage([
{
cpu: CPU.arm64,
bytes: machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
}),
},
]),
);
if (universal.status !== "parsed")
throw new Error(
universal.status === "not-mach-o" ? universal.status : universal.reason,
);
expect(universal.slices[0]?.dependencies[0]?.install_name).toBe(
"@rpath/libl.dylib",
);
});
it("rejects a lazy-info payload that is truncated, mis-pointed, or has unknown flags", async () => {
const outside = machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
});
const view = new DataView(outside.buffer);
const commandAt = 32;
view.setUint32(commandAt + 8, outside.length, true);
expect((await read(outside)).status).toBe("malformed");
const badPayload = lazyLoadDylibInfoPayload({
path: "@rpath/libl.dylib",
});
const badPath = machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
});
const payloadAt = badPath.length - badPayload.length;
new DataView(badPath.buffer).setUint32(payloadAt, badPath.length, true);
expect(await read(badPath)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("load path"),
});
const unknownFlags = machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flags: 0x2,
});
expect(await read(unknownFlags)).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("unrecognized bits"),
});
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO image offsets", () => {
it("rejects flag and chain-start offsets outside the slice VM range", async () => {
const beyondPage = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flagImageOffset: 0x4001,
}),
);
expect(beyondPage).toMatchObject({
status: "malformed",
reason: expect.stringContaining("flag image offset 0x4001"),
});
const beyondChain = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
chainStartImageOffset: 0x4001,
}),
);
expect(beyondChain).toMatchObject({
status: "malformed",
reason: expect.stringContaining("chain start image offset 0x4001"),
});
const onPage = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flagImageOffset: 0x4000,
chainStartImageOffset: 0x4000,
}),
);
expect(onPage.status).toBe("parsed");
});
it("measures the VM range from __TEXT and ignores __LINKEDIT", async () => {
const text = segmentCommand({
name: "__TEXT",
vmaddr: 0x100000000n,
vmsize: 0x8000n,
});
const inside = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flagImageOffset: 0x8000,
commandsBefore: [text],
}),
);
expect(inside.status).toBe("parsed");
const outside = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
chainStartImageOffset: 0x8001,
commandsBefore: [text],
}),
);
expect(outside).toMatchObject({
status: "malformed",
reason: expect.stringContaining("slice VM range 0x8000"),
});
const linkedit = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
flagImageOffset: 0x4001,
commandsBefore: [
segmentCommand({
name: "__LINKEDIT",
vmaddr: 0n,
vmsize: 0x100000n,
}),
],
}),
);
expect(linkedit.status).toBe("malformed");
});
it("keeps a short segment parseable until a lazy-info command needs its VM range", async () => {
const short = new Uint8Array(16);
const view = new DataView(short.buffer);
view.setUint32(0, 0x19, true);
view.setUint32(4, short.length, true);
expect((await read(machoImage({ commands: [short] }))).status).toBe(
"parsed",
);
const withLazy = await read(
machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
commandsBefore: [short],
}),
);
expect(withLazy).toMatchObject({
status: "malformed",
reason: expect.stringContaining("slice VM range"),
});
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO payload budget", () => {
it("reads one shared payload range once", async () => {
const payload = lazyLoadDylibInfoPayload({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
});
const dataOffset = 32 + 16 + 16 + 16;
const image = machoImage({
commands: [
lazyLoadDylibInfoCommand(dataOffset, payload.length),
lazyLoadDylibInfoCommand(dataOffset, payload.length),
],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
let payloadReads = 0;
const facts = await readMachoImage(async (offset, length) => {
if (offset === image.length && length === payload.length)
payloadReads += 1;
return bytes.subarray(offset, offset + length);
}, bytes.length);
expect(payloadReads).toBe(1);
expect(facts).toMatchObject({
status: "parsed",
slices: [
{
dependencies: [
{ install_name: "@rpath/libl.dylib" },
{ install_name: "@rpath/libl.dylib" },
],
},
],
});
});
it("refuses distinct lazy-info payloads whose combined size exceeds the aggregate limit", async () => {
const payload = lazyLoadDylibInfoPayload({ path: "@rpath/libl.dylib" });
const dataOffset = 32 + 16 + 32;
const image = machoImage({
commands: [
lazyLoadDylibInfoCommand(dataOffset, payload.length),
lazyLoadDylibInfoCommand(0, MAX_LOAD_COMMAND_BYTES),
],
});
const bytes = new Uint8Array(MAX_LOAD_COMMAND_BYTES);
bytes.set(image);
bytes.set(payload, image.length);
expect(await read(bytes)).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("aggregate limit"),
});
});
it("does not reread a payload range contained in an earlier read", async () => {
const payload = lazyLoadDylibInfoPayload({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
});
const image = machoImage({
commands: [
lazyLoadDylibInfoCommand(0, payload.length + 8),
lazyLoadDylibInfoCommand(0, payload.length),
],
});
const bytes = new Uint8Array(image.length + payload.length + 8);
bytes.set(image);
bytes.set(payload, image.length);
const view = new DataView(bytes.buffer);
view.setUint32(32 + 8, image.length, true);
view.setUint32(48 + 8, image.length, true);
let payloadReads = 0;
const facts = await readMachoImage(async (offset, length) => {
if (offset === image.length) payloadReads += 1;
return bytes.subarray(offset, offset + length);
}, bytes.length);
expect(payloadReads).toBe(1);
expect(facts).toMatchObject({
status: "parsed",
slices: [
{
dependencies: [
{ install_name: "@rpath/libl.dylib" },
{ install_name: "@rpath/libl.dylib" },
],
},
],
});
});
it("parses distinct non-nested lazy-info payloads under the dependency limit", async () => {
const count = 8;
const payloadSize = 26;
const payload = new Uint8Array(payloadSize);
new DataView(payload.buffer).setUint32(0, 24, true);
new DataView(payload.buffer).setUint32(20, 24, true);
const dataOffset = 32 + 16 + count * 16;
const commands = Array.from({ length: count }, (_, index) =>
lazyLoadDylibInfoCommand(dataOffset + index * payloadSize, payloadSize),
);
const image = machoImage({ commands });
const bytes = new Uint8Array(image.length + count * payloadSize);
bytes.set(image);
for (let index = 0; index < count; index += 1)
bytes.set(payload, image.length + index * payloadSize);
const facts = await read(bytes);
if (facts.status !== "parsed")
throw new Error(
facts.status === "not-mach-o" ? facts.status : facts.reason,
);
expect(facts.slices[0]?.dependencies).toHaveLength(count);
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO dependency limit", () => {
it("retains one result per command until the dependency limit", async () => {
const payload = lazyLoadDylibInfoPayload({ path: "@rpath/libl.dylib" });
const imageFor = (count: number) => {
const dataOffset = 32 + 16 + count * 16;
const image = machoImage({
commands: Array.from({ length: count }, () =>
lazyLoadDylibInfoCommand(dataOffset, payload.length),
),
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
return bytes;
};
const atLimit = await read(imageFor(MAX_LAZY_INFO_DEPENDENCIES));
if (atLimit.status !== "parsed")
throw new Error(
atLimit.status === "not-mach-o" ? atLimit.status : atLimit.reason,
);
expect(atLimit.slices[0]?.dependencies).toHaveLength(
MAX_LAZY_INFO_DEPENDENCIES,
);
expect(await read(imageFor(MAX_LAZY_INFO_DEPENDENCIES + 1))).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("dependency limit"),
});
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO string bounds", () => {
it("does not reuse a cached path that extends past a shorter payload", async () => {
const payload = new Uint8Array(48);
const view = new DataView(payload.buffer);
const path = new TextEncoder().encode("@rpath/libl.dylib");
view.setUint32(0, 24, true);
view.setUint32(20, 24, true);
payload.set(path, 24);
const image = machoImage({
commands: [
lazyLoadDylibInfoCommand(0, payload.length),
lazyLoadDylibInfoCommand(0, 24),
],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
const patched = new DataView(bytes.buffer);
patched.setUint32(32 + 8, image.length, true);
patched.setUint32(48 + 8, image.length, true);
expect(await read(bytes)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("outside the payload"),
});
const truncated = machoImage({
commands: [
lazyLoadDylibInfoCommand(0, payload.length),
lazyLoadDylibInfoCommand(0, 40),
],
});
const truncatedBytes = new Uint8Array(truncated.length + payload.length);
truncatedBytes.set(truncated);
truncatedBytes.set(payload, truncated.length);
const truncatedView = new DataView(truncatedBytes.buffer);
truncatedView.setUint32(32 + 8, truncated.length, true);
truncatedView.setUint32(48 + 8, truncated.length, true);
expect(await read(truncatedBytes)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("unterminated"),
});
});
it("rejects a load path taken from the payload header", async () => {
const payload = new Uint8Array(24);
const view = new DataView(payload.buffer);
view.setUint32(0, 4, true);
view.setUint32(20, 24, true);
const image = machoImage({
commands: [lazyLoadDylibInfoCommand(0, payload.length)],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
new DataView(bytes.buffer).setUint32(32 + 8, image.length, true);
expect(await read(bytes)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("inside the payload header"),
});
const symbolPayload = lazyLoadDylibInfoPayload({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
});
new DataView(symbolPayload.buffer).setUint32(24, 4, true);
const symbolImage = machoImage({
commands: [lazyLoadDylibInfoCommand(0, symbolPayload.length)],
});
const symbolBytes = new Uint8Array(
symbolImage.length + symbolPayload.length,
);
symbolBytes.set(symbolImage);
symbolBytes.set(symbolPayload, symbolImage.length);
new DataView(symbolBytes.buffer).setUint32(
32 + 8,
symbolImage.length,
true,
);
expect(await read(symbolBytes)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("symbol 0"),
});
});
it("charges repeated and overlapping symbol scans against the string budget", async () => {
const path = new TextEncoder().encode("@rpath/libl.dylib");
const stringBytes = 64 * 1024;
const imageFor = (symbolCount: number, step: number) => {
const pathOffset = 24 + symbolCount * 4;
const stringOffset = pathOffset + path.length + 1;
const payload = new Uint8Array(stringOffset + stringBytes + 1);
const view = new DataView(payload.buffer);
view.setUint32(0, pathOffset, true);
view.setUint32(16, symbolCount, true);
view.setUint32(20, 24, true);
payload.set(path, pathOffset);
payload.fill(0x61, stringOffset, stringOffset + stringBytes);
for (let symbol = 0; symbol < symbolCount; symbol += 1)
view.setUint32(
24 + symbol * 4,
stringOffset + (step === 0 ? 0 : symbol % 8),
true,
);
const image = machoImage({
commands: [lazyLoadDylibInfoCommand(0, payload.length)],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
new DataView(bytes.buffer).setUint32(32 + 8, image.length, true);
return bytes;
};
expect(imageFor(2, 0).length).toBeLessThan(MAX_LOAD_COMMAND_BYTES);
expect(await read(imageFor(2, 0))).toMatchObject({
status: "parsed",
slices: [{ dependencies: [{ install_name: "@rpath/libl.dylib" }] }],
});
const over = Math.floor(MAX_LOAD_COMMAND_BYTES / (stringBytes + 1)) + 2;
expect(await read(imageFor(over, 0))).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("string validation"),
});
expect(await read(imageFor(over, 1))).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("string validation"),
});
});
});
const lazyInfoPayloadWithSymbolCount = (
symbolCount: number,
pathText: string,
): Uint8Array => {
const path = new TextEncoder().encode(pathText);
const pathOffset = 24 + symbolCount * 4;
// One byte after the path NUL, because a load-path terminator in the
// payload's final byte is unterminated.
const payload = new Uint8Array(pathOffset + path.length + 2);
const view = new DataView(payload.buffer);
view.setUint32(0, pathOffset, true);
view.setUint32(16, symbolCount, true);
view.setUint32(20, 24, true);
payload.set(path, pathOffset);
const emptySymbol = pathOffset + path.length;
for (let symbol = 0; symbol < symbolCount; symbol += 1)
view.setUint32(24 + symbol * 4, emptySymbol, true);
return payload;
};
const imageWithLazyPayload = (payload: Uint8Array): Uint8Array => {
const image = machoImage({
commands: [lazyLoadDylibInfoCommand(0, payload.length)],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
const view = new DataView(bytes.buffer);
view.setUint32(32 + 8, image.length, true);
view.setUint32(32 + 12, payload.length, true);
return bytes;
};
describe("LC_LAZY_LOAD_DYLIB_INFO symbol cardinality", () => {
it("stops before caching one string for every symbol offset", async () => {
const atLimit = lazyInfoPayloadWithSymbolCount(
MAX_LAZY_INFO_SYMBOL_REFERENCES,
"@rpath/libl.dylib",
);
expect(await read(imageWithLazyPayload(atLimit))).toMatchObject({
status: "parsed",
slices: [{ dependencies: [{ install_name: "@rpath/libl.dylib" }] }],
});
const overLimit = lazyInfoPayloadWithSymbolCount(
MAX_LAZY_INFO_SYMBOL_REFERENCES + 1,
"@rpath/libl.dylib",
);
expect(await read(imageWithLazyPayload(overLimit))).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("65536-reference limit"),
});
});
it("counts symbol references across distinct lazy-info payloads", async () => {
const first = lazyInfoPayloadWithSymbolCount(
MAX_LAZY_INFO_SYMBOL_REFERENCES,
"@rpath/libl.dylib",
);
const second = lazyInfoPayloadWithSymbolCount(1, "@rpath/libm.dylib");
const image = machoImage({
commands: [
lazyLoadDylibInfoCommand(0, first.length),
lazyLoadDylibInfoCommand(0, second.length),
],
});
const bytes = new Uint8Array(image.length + first.length + second.length);
bytes.set(image);
bytes.set(first, image.length);
bytes.set(second, image.length + first.length);
const view = new DataView(bytes.buffer);
view.setUint32(32 + 8, image.length, true);
view.setUint32(32 + 12, first.length, true);
view.setUint32(48 + 8, image.length + first.length, true);
view.setUint32(48 + 12, second.length, true);
expect(await read(bytes)).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("65536-reference limit"),
});
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO path terminator", () => {
it("rejects a load path whose NUL is the payload's final byte", async () => {
const payload = new Uint8Array(27);
const view = new DataView(payload.buffer);
view.setUint32(0, 24, true);
view.setUint32(20, 24, true);
payload[24] = 0x61;
const imageFor = (size: number) => {
const image = machoImage({
commands: [lazyLoadDylibInfoCommand(0, size)],
});
const bytes = new Uint8Array(image.length + payload.length);
bytes.set(image);
bytes.set(payload, image.length);
new DataView(bytes.buffer).setUint32(32 + 8, image.length, true);
new DataView(bytes.buffer).setUint32(32 + 12, size, true);
return bytes;
};
expect((await read(imageFor(payload.length))).status).toBe("parsed");
expect(await read(imageFor(26))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("unterminated"),
});
});
});
describe("LC_LAZY_LOAD_DYLIB_INFO resolution", () => {
it("reports the lazy edge and does not treat an absent image as a launch failure", async () => {
const root = await createTestTempDirectory("rea-lazy-info-");
const executablePath = "lazy";
const dylibPath = "lib/libl.dylib";
const executable = machoImageWithLazyLoadDylibInfo({
path: "@rpath/libl.dylib",
symbols: ["_lf"],
commandsBefore: [
buildVersionCommand(1),
rpathCommand("@executable_path/lib"),
dylibCommand(LC.LOAD_DYLIB, "/usr/lib/libSystem.B.dylib"),
],
});
const dylib = machoImage({
fileType: FILE_TYPE.dylib,
commands: [
buildVersionCommand(1),
dylibCommand(LC.ID_DYLIB, "@rpath/libl.dylib"),
dylibCommand(LC.LOAD_DYLIB, "@rpath/libdeep.dylib"),
],
});
await mkdir(join(root, "lib"));
await writeFile(join(root, executablePath), executable);
await writeFile(join(root, dylibPath), dylib);
const trace = () =>
traceDylibResolution({
rootPath: root,
targetPath: join(root, executablePath),
targetSha256: createHash("sha256").update(executable).digest("hex"),
enumerateRoots: false,
parameters: { roots: [executablePath] },
});
const present = await trace();
expect(present.coverage.status).toBe("complete");
expect(present.edges.map(({ install_name: name }) => name)).toEqual([
"/usr/lib/libSystem.B.dylib",
"@rpath/libl.dylib",
]);
expect(present.edges[1]).toMatchObject({
command: "LC_LAZY_LOAD_DYLIB_INFO",
encoding: "lazy_load_dylib_info",
current_version: null,
compatibility_version: null,
resolution: { status: "resolved", image: dylibPath },
});
expect(present.findings).toEqual([]);
await rename(join(root, dylibPath), `${join(root, dylibPath)}.moved`);
const absent = await trace();
expect(absent.coverage.status).toBe("complete");
expect(absent.findings.map(({ kind }) => kind)).toEqual([
"lazy-load-unresolved",
]);
expect(absent.edges[1]?.resolution.status).toBe("unresolved");
});
});
describe("Mach-O load command reader: reads and validates universal binary slices", () => {
it("reads every slice of FAT and FAT_64 containers in either byte order", async () => {
for (const [wide, littleEndian] of [
[false, false],
[true, false],
[false, true],
[true, true],
] as const) {
const facts = await read(
fatImage(
[
{
cpu: CPU.arm64e,
bytes: machoImage({
cpu: CPU.arm64e,
commands: [rpathCommand("@executable_path/a")],
}),
},
{
cpu: CPU.x86_64,
bytes: machoImage({
cpu: CPU.x86_64,
commands: [rpathCommand("@executable_path/b")],
}),
},
],
wide,
littleEndian,
),
);
if (facts.status !== "parsed") throw new Error(facts.status);
expect(
facts.slices.map(({ architecture, rpaths }) => [architecture, rpaths]),
).toEqual([
["arm64e", ["@executable_path/a"]],
["x86_64", ["@executable_path/b"]],
]);
}
});
it("retains physical slice identity and declared build platform", async () => {
const thin = machoImage({ commands: [buildVersionCommand(7)] });
const parsedThin = await read(thin);
expect(
parsedThin.status === "parsed" && parsedThin.slices[0],
).toMatchObject({
slice_offset: 0,
slice_size: thin.length,
cpu_type: CPU.arm64.type,
cpu_subtype: CPU.arm64.subtype,
fat_cpu_type: null,
fat_cpu_subtype: null,
platform: 7,
});
const fat = fatImage([{ cpu: CPU.arm64, bytes: thin }]);
const parsedFat = await read(fat);
expect(parsedFat.status === "parsed" && parsedFat.slices[0]).toMatchObject({
slice_offset: 4096,
slice_size: thin.length,
fat_cpu_type: CPU.arm64.type,
fat_cpu_subtype: CPU.arm64.subtype,
});
});
it("rejects FAT table/header identity mismatches and unaligned ranges", async () => {
const original = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]);
for (const [fieldOffset, value] of [
[8, CPU.x86_64.type],
[12, 99],
] as const) {
const mismatch = original.slice();
new DataView(mismatch.buffer).setUint32(fieldOffset, value, false);
expect(await read(mismatch)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("disagrees"),
});
}
const unaligned = original.slice();
new DataView(unaligned.buffer).setUint32(16, 4097, false);
expect(await read(unaligned)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("not aligned"),
});
});
it("preserves unknown CPU table facts when the embedded slice is Mach-O", async () => {
const cpu = { type: 0x7fffffff, subtype: 11 };
const facts = await read(fatImage([{ cpu, bytes: machoImage({ cpu }) }]));
expect(facts.status === "parsed" && facts.slices[0]).toMatchObject({
architecture: "cpu-7fffffff",
cpu_type: cpu.type,
cpu_subtype: cpu.subtype,
fat_cpu_type: cpu.type,
fat_cpu_subtype: cpu.subtype,
});
});
});
describe("Mach-O load command reader inputs", () => {
it("names 32-bit and non-Mach-O inputs without guessing", async () => {
const i386 = await read(
machoImage({ cpu: { type: 7, subtype: 3 }, wide: false }),
);
expect(i386.status === "parsed" && i386.slices[0]?.architecture).toBe(
"i386",
);
// A Java class file shares the FAT magic; version fields are not a slice count.
const javaClasses = [
javaClassFile(),
javaClassFile(4000),
javaClassFile(4000, true),
];
for (const javaClass of javaClasses) {
expect(hasMachoMagic(javaClass)).toBe(true);
expect(await read(javaClass)).toEqual({ status: "not-mach-o" });
}
const malformedFat = Buffer.alloc(28);
malformedFat.writeUInt32BE(0xcafebabe, 0);
malformedFat.writeUInt32BE(1, 4);
malformedFat.writeUInt32BE(0x0100000c, 8);
malformedFat.writeUInt32BE(4096, 16);
malformedFat.writeUInt32BE(32, 20);
malformedFat.writeUInt32BE(12, 24);
expect(await read(malformedFat)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("slice range extends beyond the file"),
});
expect(await read(new TextEncoder().encode("#!/bin/sh\n"))).toEqual({
status: "not-mach-o",
});
expect(await read(Uint8Array.from([0xcf, 0xfa]))).toEqual({
status: "not-mach-o",
});
// A recognized magic without room for a header is a truncated image.
expect(
await read(Uint8Array.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0x00])),
).toEqual({ status: "malformed", reason: "Mach-O header is truncated" });
});
it("keeps oversized CAFEBABE identity unknown when class payload is sparse", async () => {
const classPrefix = Buffer.from(javaClassFile(4000, true));
const payloadBytes = MAX_LOAD_COMMAND_BYTES + 1;
const attributeHeader = Buffer.alloc(6);
attributeHeader.writeUInt16BE(5, 0);
attributeHeader.writeUInt32BE(payloadBytes, 2);
classPrefix.writeUInt16BE(1, classPrefix.length - 2);
const prefix = Buffer.concat([classPrefix, attributeHeader]);
const size = prefix.length + payloadBytes;
const sparseReadAt = async (offset: number, length: number) => {
const result = Buffer.alloc(Math.min(length, size - offset));
if (offset < prefix.length)
prefix.copy(
result,
0,
offset,
Math.min(prefix.length, offset + length),
);
return result;
};
expect(await readMachoImage(sparseReadAt, size)).toEqual({
status: "unsupported",
reason: expect.stringContaining(
"class identity scan exceeds the 16 MiB limit",
),
});
});
});
describe("Mach-O load command reader failures", () => {
const withCommandSize = (size: number): Uint8Array => {
const image = machoImage({ commands: [rpathCommand("@loader_path")] });
new DataView(image.buffer).setUint32(32 + 4, size, true);
return image;
};
it("reports malformed command tables with the failing command", async () => {
expect(await read(withCommandSize(0))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("load command 0"),
});
expect(await read(withCommandSize(4096))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("invalid cmdsize"),
});
const extraCommand = machoImage({ commands: [rpathCommand("x")] });
new DataView(extraCommand.buffer).setUint32(16, 2, true);
expect(await read(extraCommand)).toMatchObject({
status: "malformed",
reason: "load command 1 starts beyond sizeofcmds",
});
});
it("requires 8-byte command alignment in 64-bit images", async () => {
// A command the reader does not decode; only its size is checked.
const command = new Uint8Array(12);
const view = new DataView(command.buffer);
view.setUint32(0, 0x7f000001, true);
view.setUint32(4, 12, true);
expect(await read(machoImage({ commands: [command] }))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("invalid cmdsize 12"),
});
expect(
await read(
machoImage({
cpu: { type: 7, subtype: 3 },
wide: false,
commands: [command],
}),
),
).toMatchObject({ status: "parsed" });
});
it("accepts FAT tables up to 128 records that name a Mach-O CPU", async () => {
const slices = Array.from({ length: 24 }, () => ({
cpu: CPU.arm64,
bytes: machoImage({}),
}));
const facts = await read(fatImage(slices));
expect(facts.status === "parsed" && facts.slices).toHaveLength(24);
// An empty table is not a Java class file, whose major version is nonzero.
for (const magic of [
[0xca, 0xfe, 0xba, 0xbe],
[0xbf, 0xba, 0xfe, 0xca],
])
expect(await read(Uint8Array.from([...magic, 0, 0, 0, 0]))).toEqual({
status: "malformed",
reason: "FAT header declares no architectures",
});
});
it("rejects string commands too short to hold their string offset", async () => {
const short = new Uint8Array(8);
const view = new DataView(short.buffer);
view.setUint32(0, LC.RPATH, true);
view.setUint32(4, 8, true);
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
status: "malformed",
reason: "load command 0 is too short to hold a string offset",
});
view.setUint32(0, LC.CODE_SIGNATURE, true);
expect(await read(machoImage({ commands: [short] }))).toMatchObject({
status: "malformed",
reason: "load command 0 is too short for LC_CODE_SIGNATURE",
});
});
it("rejects strings that are unterminated, misplaced or not UTF-8", async () => {
const unterminated = rpathCommand("abcd");
unterminated.fill(0x41, 12);
expect(await read(machoImage({ commands: [unterminated] }))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("unterminated"),
});
const misplaced = rpathCommand("abcd");
new DataView(misplaced.buffer).setUint32(8, 4, true);
expect(await read(machoImage({ commands: [misplaced] }))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("string offset 4"),
});
const invalid = rpathCommand("ab");
invalid[12] = 0xff;
expect(await read(machoImage({ commands: [invalid] }))).toMatchObject({
status: "malformed",
reason: expect.stringContaining("not UTF-8"),
});
});
it("refuses truncated, oversized, big-endian and out-of-range images", async () => {
const image = machoImage({ commands: [rpathCommand("@loader_path")] });
expect(await read(image.subarray(0, 20))).toMatchObject({
status: "malformed",
reason: "Mach-O header is truncated",
});
const oversized = machoImage({});
new DataView(oversized.buffer).setUint32(
20,
MAX_LOAD_COMMAND_BYTES + 8,
true,
);
expect(await read(oversized)).toMatchObject({ status: "unsupported" });
const beyond = machoImage({ commands: [rpathCommand("x")] });
new DataView(beyond.buffer).setUint32(20, beyond.length, true);
expect(await read(beyond)).toMatchObject({
status: "malformed",
reason: "load commands extend beyond the Mach-O slice",
});
const bigEndian = Uint8Array.from([0xfe, 0xed, 0xfa, 0xcf, 0, 0, 0, 0]);
expect(await read(bigEndian)).toMatchObject({ status: "unsupported" });
const fat = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]);
expect(await read(fat.subarray(0, 4100))).toMatchObject({
status: "malformed",
reason: "FAT architecture 0 slice range extends beyond the file",
});
});
it("preserves unsupported status for an unsupported FAT alignment exponent", async () => {
const fat = fatImage([{ cpu: CPU.arm64, bytes: machoImage({}) }]);
new DataView(fat.buffer).setUint32(24, 53, false);
expect(await read(fat)).toMatchObject({
status: "unsupported",
reason: expect.stringContaining("alignment exponent 53 is unsupported"),
});
});
});
it.each([true, false])(
"rejects unconsumed command bytes in wide=%s headers",
async (wide) => {
const bytes = machoImage({ wide, commands: [rpathCommand("ignored")] });
new DataView(bytes.buffer).setUint32(16, 0, true);
expect(await read(bytes)).toMatchObject({
status: "malformed",
reason: expect.stringContaining("sizeofcmds"),
});
},
);