rea/scripts/verify/apple/dyld-shared-cache.mjs

194 lines
5.7 KiB
JavaScript

import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { access, mkdtemp, realpath, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { DyldSharedCache } from "../../../dist/artifacts/apple/DyldSharedCacheReader.js";
import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
import {
buildMacosBundleFixture,
preflightMacosBundleFixture,
} from "../../fixtures/apple/macos-bundle.mjs";
const exec = promisify(execFile);
await preflightMacosBundleFixture();
try {
await exec("/usr/bin/xcrun", ["--find", "dyld_info"]);
} catch (cause) {
throw new Error(
"dyld shared cache verification requires dyld_info from Command Line Tools (xcrun --find dyld_info failed)",
{ cause },
);
}
/** The cache this host's dyld uses for native processes. */
const hostCache = async () => {
const names =
process.arch === "arm64"
? ["dyld_shared_cache_arm64e"]
: ["dyld_shared_cache_x86_64h", "dyld_shared_cache_x86_64"];
for (const directory of [
"/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld",
"/System/Library/dyld",
])
for (const name of names)
try {
await access(join(directory, name));
return join(directory, name);
} catch {
// Try the next documented location.
}
throw new Error("No dyld shared cache was found in the documented locations");
};
/** `dyld_info -all_dyld_cache -linked_dylibs`: path -> "attributes|load path" lines. */
const dyldInfoImages = async () => {
const { stdout } = await exec(
"/usr/bin/xcrun",
["dyld_info", "-all_dyld_cache", "-linked_dylibs"],
{ maxBuffer: 256 * 1024 * 1024 },
);
const images = new Map();
let current;
for (const line of stdout.split("\n")) {
const head = /^(\/.+) \[(\S+)\]:$/u.exec(line);
if (head !== null) {
current = [];
images.set(head[1], current);
continue;
}
const dependency = /^\s{8}(.*?)\s*(\/\S.*)$/u.exec(line);
if (dependency !== null && current !== undefined)
current.push(
`${dependency[1].split(/\s+/u).filter(Boolean).sort().join(",")}|${dependency[2]}`,
);
}
return images;
};
const attributes = (dependency) =>
[
dependency.reexport ? "re-export" : null,
dependency.weak ? "weak-link" : null,
dependency.upward ? "upward" : null,
dependency.delayed_init ? "delay-init" : null,
]
.filter((value) => value !== null)
.sort()
.join(",");
const cachePath = await hostCache();
const root = await mkdtemp(join(tmpdir(), "rea-dyld-cache-"));
try {
const expected = await dyldInfoImages();
const cache = await DyldSharedCache.open(cachePath);
let compared = 0;
try {
assert.deepEqual(
cache.images.map(({ path }) => path).sort(),
[...expected.keys()].sort(),
"image list differs from dyld_info",
);
for (const [path, dependencies] of expected) {
const read = await cache.imageFacts(path);
assert.equal(read?.facts.status, "parsed", path);
assert.deepEqual(
read.facts.slices[0].dependencies.map(
(dependency) =>
`${attributes(dependency)}|${dependency.install_name}`,
),
dependencies,
`${path} dependencies differ from dyld_info`,
);
compared += 1;
}
} finally {
await cache.close();
}
const inspected = await artifactCli("inspect-dyld-shared-cache", cachePath, [
"--image",
"/usr/lib/libSystem.B.dylib",
]);
assert.equal(inspected.images_total, expected.size);
assert.equal(inspected.coverage.status, "complete");
assert.equal(inspected.inspected_images[0].status, "parsed");
await withArtifactMcp(null, async (client) => {
const viaMcp = await artifactMcpResult(
client,
"inspect_dyld_shared_cache",
{
cache_path: cachePath,
images: ["/usr/lib/libSystem.B.dylib"],
},
);
assert.deepEqual(viaMcp, inspected);
});
const { app } = await buildMacosBundleFixture(root);
const trace = await artifactCli("trace-dylib-resolution", app, [
"--shared-cache",
cachePath,
]);
assert.equal(trace.shared_cache.uuid, inspected.uuid);
const main = "Contents/MacOS/MacFixture";
const cached = trace.edges.filter(
({ root: edgeRoot, resolution }) =>
edgeRoot === main && resolution.status === "shared-cache",
);
assert.ok(
cached.some(
({ install_name: name }) => name === "/usr/lib/libSystem.B.dylib",
),
);
assert.ok(
trace.edges.every(({ install_name: name, resolution }) =>
name.startsWith("/usr/lib/") || name.startsWith("/System/")
? resolution.status === "shared-cache"
: true,
),
"a system install path was not found in the host cache",
);
const { stderr } = await exec(join(app, main), [], {
env: { PATH: "/usr/bin:/bin", DYLD_PRINT_LIBRARIES: "1" },
});
const loaded = new Set(
stderr
.split("\n")
.map((line) => /^dyld\[\d+\]: <[0-9A-F-]+> (.+)$/u.exec(line)?.[1])
.filter((path) => path !== undefined),
);
for (const { resolution } of cached)
assert.ok(
loaded.has(resolution.image),
`${resolution.image} was not loaded by dyld`,
);
process.stdout.write(
`${JSON.stringify({
ok: true,
mocked: false,
cli: true,
stdio_mcp: true,
cache: {
architecture: inspected.architecture,
os_version: inspected.os_version,
images: inspected.images_total,
},
images_matching_dyld_info: compared,
trace_shared_cache_edges: cached.length,
dyld_loaded_every_cached_edge: true,
host: await realpath(cachePath),
})}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}