Add a plain x86_64 slice to the dyld fixture while retaining both same-named arm64e slices and their independent environment assertions. The original fixture was rejected by real x64 CLI admission before tracing. The contributing PR's complete Linux CI run passes all 30 jobs, including the portable suite. Codex and Greptile reviews completed without findings; no unresolved threads remain. This restores the main test baseline needed for #2635. The change is limited to test data and preserves the original author's contribution. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
633 lines
21 KiB
TypeScript
633 lines
21 KiB
TypeScript
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
|
|
import { describe, expect } from "vitest";
|
|
|
|
import {
|
|
CPU,
|
|
FILE_TYPE,
|
|
LC,
|
|
buildVersionCommand,
|
|
dylibCommand,
|
|
dyldEnvironmentCommand,
|
|
fatImage,
|
|
machoImage,
|
|
rpathCommand,
|
|
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
|
|
import { dylibResolutionResultSchema } from "../../../src/domain/apple/dylibResolution.js";
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
|
|
|
|
const ENVIRONMENT = { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" };
|
|
|
|
const writeFiles = async (
|
|
root: string,
|
|
files: Readonly<Record<string, string | Uint8Array>>,
|
|
): Promise<void> => {
|
|
for (const [path, content] of Object.entries(files)) {
|
|
await mkdir(dirname(join(root, path)), { recursive: true });
|
|
await writeFile(join(root, path), content);
|
|
}
|
|
};
|
|
|
|
/** A caller-selected root that cannot be traced is an input issue on that root. */
|
|
const expectRootInputError = async (
|
|
cli: TestCli,
|
|
{
|
|
app,
|
|
root,
|
|
reason,
|
|
message,
|
|
}: {
|
|
readonly app: string;
|
|
readonly root: string;
|
|
readonly reason: string;
|
|
readonly message: string;
|
|
},
|
|
): Promise<void> => {
|
|
const rejected = await cli.run({
|
|
arguments: ["trace-dylib-resolution", app, "--root", root, "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(rejected.exitCode).toBe(1);
|
|
expect(rejected.json).toMatchObject({
|
|
error: "Analysis failed",
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["roots", 0],
|
|
reason,
|
|
message: expect.stringContaining(message),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
};
|
|
|
|
const fixtureApp = async (): Promise<string> => {
|
|
const app = join(
|
|
await createTestTempDirectory("rea-dylib-cli-"),
|
|
"Fixture.app",
|
|
);
|
|
await writeFiles(app, {
|
|
"Contents/Info.plist":
|
|
"<plist><dict><key>CFBundleExecutable</key><string>App</string></dict></plist>",
|
|
"Contents/MacOS/App": machoImage({
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
rpathCommand("@executable_path/../Frameworks"),
|
|
dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"),
|
|
dylibCommand(LC.LOAD_WEAK_DYLIB, "@rpath/libgone.dylib"),
|
|
],
|
|
}),
|
|
"Contents/Frameworks/libcore.dylib": machoImage({
|
|
fileType: FILE_TYPE.dylib,
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
dylibCommand(LC.ID_DYLIB, "@rpath/libcore.dylib"),
|
|
],
|
|
}),
|
|
"Contents/Helpers/tool": machoImage({
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/libnone.dylib"),
|
|
],
|
|
}),
|
|
});
|
|
return app;
|
|
};
|
|
|
|
const withBrokenHelper = async (app: string): Promise<void> => {
|
|
const broken = machoImage({ commands: [rpathCommand("x")] });
|
|
// cmdsize 0 makes the command table unreadable.
|
|
new DataView(broken.buffer).setUint32(32 + 4, 0, true);
|
|
await writeFiles(app, { "Contents/Helpers/broken": broken });
|
|
};
|
|
|
|
describe("trace-dylib-resolution CLI", () => {
|
|
cliTest("traces every executable root of an app bundle", async ({ cli }) => {
|
|
const app = await fixtureApp();
|
|
const result = await cli.run({
|
|
arguments: ["trace-dylib-resolution", app, "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
(result.json as { normalized_result: unknown }).normalized_result,
|
|
);
|
|
expect(trace.root_path).toBe(app);
|
|
expect(trace.roots.map(({ image }) => image)).toEqual([
|
|
"Contents/Helpers/tool",
|
|
"Contents/MacOS/App",
|
|
]);
|
|
// The normalized result is the provider's only representation; Evidence
|
|
// must not carry it a second time as raw_result.
|
|
expect(result.json).toMatchObject({ raw_result: null });
|
|
expect(
|
|
trace.edges.map(({ loader, install_name: name, resolution }) => [
|
|
loader,
|
|
name,
|
|
resolution.status,
|
|
resolution.image,
|
|
]),
|
|
).toEqual([
|
|
[
|
|
"Contents/Helpers/tool",
|
|
"@loader_path/libnone.dylib",
|
|
"unresolved",
|
|
null,
|
|
],
|
|
[
|
|
"Contents/MacOS/App",
|
|
"@rpath/libcore.dylib",
|
|
"resolved",
|
|
"Contents/Frameworks/libcore.dylib",
|
|
],
|
|
["Contents/MacOS/App", "@rpath/libgone.dylib", "unresolved", null],
|
|
]);
|
|
expect(trace.findings.map(({ kind }) => kind)).toEqual([
|
|
"required-load-unresolved",
|
|
"weak-load-unresolved",
|
|
]);
|
|
expect(
|
|
trace.images.every(({ sha256 }) => /^[0-9a-f]{64}$/u.test(sha256)),
|
|
).toBe(true);
|
|
});
|
|
|
|
cliTest(
|
|
"narrows roots and architecture, and rejects escaping roots",
|
|
async ({ cli }) => {
|
|
const app = await fixtureApp();
|
|
const narrowed = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
app,
|
|
"--root",
|
|
"Contents/MacOS/App",
|
|
"--architecture",
|
|
"arm64",
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(narrowed.exitCode).toBe(0);
|
|
expect(
|
|
(narrowed.json as { normalized_result: { roots: unknown } })
|
|
.normalized_result.roots,
|
|
).toEqual([{ image: "Contents/MacOS/App", architecture: "arm64" }]);
|
|
// A root below a symlinked directory that points outside the bundle.
|
|
const outside = await createTestTempDirectory("rea-dylib-outside-");
|
|
await writeFiles(outside, { Tool: machoImage({}) });
|
|
await symlink(outside, join(app, "Contents/Linked"));
|
|
for (const [root, reason, message] of [
|
|
["../outside", "invalid_format", "Use a normalized path"],
|
|
["Contents/Info.plist", "invalid_value", "is not a Mach-O image"],
|
|
[
|
|
"Contents/Linked/Tool",
|
|
"invalid_value",
|
|
"resolves outside the analyzed root",
|
|
],
|
|
] as const) {
|
|
await expectRootInputError(cli, { app, root, reason, message });
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"resolves a standalone Mach-O against its own directory",
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory("rea-dylib-standalone-");
|
|
await writeFiles(directory, {
|
|
tool: machoImage({
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/lib/libhelper.dylib"),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/../escape.dylib"),
|
|
],
|
|
}),
|
|
"lib/libhelper.dylib": machoImage({
|
|
fileType: FILE_TYPE.dylib,
|
|
commands: [buildVersionCommand(1)],
|
|
}),
|
|
"unrelated.txt": "not traversed",
|
|
});
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
join(directory, "tool"),
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
(result.json as { normalized_result: unknown }).normalized_result,
|
|
);
|
|
expect(trace.roots).toEqual([{ image: "tool", architecture: "arm64" }]);
|
|
expect(trace.edges.map(({ resolution }) => resolution)).toEqual([
|
|
{ status: "resolved", image: "lib/libhelper.dylib" },
|
|
{ status: "undetermined", image: null },
|
|
]);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reports unparsed Mach-O files found while enumerating roots",
|
|
async ({ cli }) => {
|
|
const app = await fixtureApp();
|
|
await withBrokenHelper(app);
|
|
const result = await cli.run({
|
|
arguments: ["trace-dylib-resolution", app, "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
(result.json as { normalized_result: unknown }).normalized_result,
|
|
);
|
|
expect(trace.coverage).toMatchObject({
|
|
status: "partial",
|
|
unparsed_images: ["Contents/Helpers/broken"],
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
cliTest(
|
|
"rejects build-version tool records outside their load command",
|
|
async ({ cli }) => {
|
|
const app = await fixtureApp();
|
|
const withTool = new Uint8Array(32);
|
|
withTool.set(buildVersionCommand(0xffffffff));
|
|
const toolView = new DataView(withTool.buffer);
|
|
toolView.setUint32(4, 32, true);
|
|
toolView.setUint32(20, 1, true);
|
|
toolView.setUint32(24, 3, true);
|
|
toolView.setUint32(28, 0x10000, true);
|
|
|
|
const missingTool = buildVersionCommand(1);
|
|
new DataView(missingTool.buffer).setUint32(20, 1, true);
|
|
const excessiveTools = buildVersionCommand(1);
|
|
new DataView(excessiveTools.buffer).setUint32(20, 0xffffffff, true);
|
|
const missingSecondTool = withTool.slice();
|
|
new DataView(missingSecondTool.buffer).setUint32(20, 2, true);
|
|
const shortHeader = buildVersionCommand(1).slice(0, 16);
|
|
new DataView(shortHeader.buffer).setUint32(4, 16, true);
|
|
const cases = [
|
|
["no-tools", [buildVersionCommand(1)], "parsed"],
|
|
["one-tool", [withTool], "parsed"],
|
|
// The following command and the image's trailing bytes cannot satisfy ntools.
|
|
["missing-tool", [missingTool, rpathCommand("x")], "malformed"],
|
|
["excessive-tools", [excessiveTools], "malformed"],
|
|
["missing-second-tool", [missingSecondTool], "malformed"],
|
|
["short-header", [shortHeader], "malformed"],
|
|
] as const;
|
|
for (const [name, commands] of cases) {
|
|
await writeFiles(app, {
|
|
[`Contents/Helpers/${name}`]: machoImage({ commands }),
|
|
});
|
|
}
|
|
|
|
const result = await cli.run({
|
|
arguments: ["trace-dylib-resolution", app, "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
(result.json as { normalized_result: unknown }).normalized_result,
|
|
);
|
|
for (const [name, , status] of cases) {
|
|
const path = `Contents/Helpers/${name}`;
|
|
const image = trace.images.find((item) => item.path === path);
|
|
expect(image, name).toMatchObject({ parse_status: status });
|
|
if (status === "malformed") {
|
|
expect(image).toMatchObject({
|
|
reason: expect.stringContaining("LC_BUILD_VERSION"),
|
|
slices: [],
|
|
});
|
|
expect(trace.roots.some((root) => root.image === path)).toBe(false);
|
|
expect(trace.coverage.unparsed_images).toContain(path);
|
|
} else {
|
|
expect(image).toMatchObject({
|
|
reason: null,
|
|
slices: [{ platforms: [name === "one-tool" ? 0xffffffff : 1] }],
|
|
});
|
|
}
|
|
}
|
|
expect(trace.coverage.status).toBe("partial");
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"retains directory requirements when resolving dylib install names",
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory("rea-dylib-path-cli-");
|
|
const names = [
|
|
"@loader_path/lib/libhelper.dylib/",
|
|
"@loader_path/lib/libhelper.dylib/.",
|
|
"@rpath/libhelper.dylib/",
|
|
"@rpath/libhelper.dylib/.",
|
|
"@rpath/",
|
|
"@loader_path/lib/./libhelper.dylib",
|
|
"@loader_path/lib//libhelper.dylib",
|
|
];
|
|
await writeFiles(directory, {
|
|
tool: machoImage({
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
rpathCommand("@loader_path/lib/libhelper.dylib"),
|
|
rpathCommand("@loader_path/lib"),
|
|
...names.map((name) => dylibCommand(LC.LOAD_DYLIB, name)),
|
|
],
|
|
}),
|
|
"lib/libhelper.dylib": machoImage({
|
|
fileType: FILE_TYPE.dylib,
|
|
commands: [buildVersionCommand(1)],
|
|
}),
|
|
});
|
|
const result = await cli.run({
|
|
arguments: ["trace-dylib-resolution", join(directory, "tool"), "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
parseEvidence(result.json).normalized_result,
|
|
);
|
|
expect(
|
|
trace.edges.map(({ install_name, candidates, resolution }) => ({
|
|
install_name,
|
|
outcome: candidates[0]?.outcome,
|
|
resolution,
|
|
})),
|
|
).toEqual(
|
|
names.map((name, index) => ({
|
|
install_name: name,
|
|
outcome: index < 5 ? "absent" : "resolved",
|
|
resolution:
|
|
index < 5
|
|
? { status: "unresolved", image: null }
|
|
: { status: "resolved", image: "lib/libhelper.dylib" },
|
|
})),
|
|
);
|
|
expect(
|
|
trace.findings.map(({ kind, edge_index }) => [kind, edge_index]),
|
|
).toEqual(
|
|
[0, 1, 2, 3, 4].map((index) => ["required-load-unresolved", index]),
|
|
);
|
|
},
|
|
);
|
|
|
|
describe.skipIf(process.getuid?.() === 0)(
|
|
"trace-dylib-resolution permissions",
|
|
() => {
|
|
cliTest(
|
|
"reports an unreadable dependency as a permission failure",
|
|
async ({ cli }) => {
|
|
const directory = await createTestTempDirectory("rea-dylib-denied-");
|
|
await writeFiles(directory, {
|
|
tool: machoImage({
|
|
commands: [
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/libsecret.dylib"),
|
|
],
|
|
}),
|
|
"libsecret.dylib": machoImage({ fileType: FILE_TYPE.dylib }),
|
|
});
|
|
await chmod(join(directory, "libsecret.dylib"), 0o000);
|
|
try {
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
join(directory, "tool"),
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject({
|
|
details: {
|
|
reason: "unavailable",
|
|
detail: expect.stringContaining(
|
|
"Permission denied (EACCES) reading libsecret.dylib",
|
|
),
|
|
},
|
|
});
|
|
} finally {
|
|
await chmod(join(directory, "libsecret.dylib"), 0o644);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reports embedded path overrides as conditional without changing diagnostic-only resolution",
|
|
async ({ cli }) => {
|
|
for (const [setting, expected] of [
|
|
["DYLD_LIBRARY_PATH=/external", "conditional"],
|
|
["DYLD_PRINT_LIBRARIES=1", "resolved"],
|
|
["DYLD_FALLBACK_LIBRARY_PATH=/external", "resolved"],
|
|
["DYLD_FRAMEWORK_PATH=/external", "resolved"],
|
|
["DYLD_ROOT_PATH=/external", "resolved"],
|
|
["DYLD_OVERLAY_PATH=/external", "resolved"],
|
|
] as const) {
|
|
const app = await fixtureApp();
|
|
await writeFiles(app, {
|
|
"Contents/MacOS/App": machoImage({
|
|
commands: [
|
|
dyldEnvironmentCommand(setting),
|
|
buildVersionCommand(1),
|
|
rpathCommand("@executable_path/../Absent"),
|
|
rpathCommand("@executable_path/../Frameworks"),
|
|
dylibCommand(LC.LOAD_DYLIB, "@rpath/libcore.dylib"),
|
|
],
|
|
}),
|
|
});
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
app,
|
|
"--root",
|
|
"Contents/MacOS/App",
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
parseEvidence(result.json).normalized_result,
|
|
);
|
|
expect(trace.edges[0]?.resolution.status).toBe(expected);
|
|
expect(trace.coverage.status).toBe(
|
|
expected === "conditional" ? "partial" : "complete",
|
|
);
|
|
expect(
|
|
trace.images.find(({ path }) => path === "Contents/MacOS/App")
|
|
?.slices[0]?.dyld_environment,
|
|
).toEqual([setting]);
|
|
if (expected === "conditional")
|
|
expect(trace.findings[0]?.explanation).toContain(
|
|
"does not establish which image dyld will load",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reports each same-named slice's own embedded dyld environment",
|
|
async ({ cli }) => {
|
|
// arm64e and arm64e.x1 slices both report the architecture name arm64e.
|
|
// A plain x86_64 slice keeps the bundle openable on x64 hosts.
|
|
for (const [settings, expected] of [
|
|
[[null, "DYLD_PRINT_RPATHS=1"], ["DYLD_PRINT_RPATHS=1"]],
|
|
[
|
|
["DYLD_PRINT_LIBRARIES=1", "DYLD_PRINT_RPATHS=1"],
|
|
["DYLD_PRINT_LIBRARIES=1", "DYLD_PRINT_RPATHS=1"],
|
|
],
|
|
] as const) {
|
|
const app = await fixtureApp();
|
|
await writeFiles(app, {
|
|
"Contents/MacOS/App": fatImage([
|
|
...[CPU.arm64e, CPU.arm64eX1].map((cpu, index) => {
|
|
const setting = settings[index];
|
|
return {
|
|
cpu,
|
|
bytes: machoImage({
|
|
cpu,
|
|
commands:
|
|
setting === null || setting === undefined
|
|
? []
|
|
: [dyldEnvironmentCommand(setting)],
|
|
}),
|
|
};
|
|
}),
|
|
{ cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
|
|
]),
|
|
});
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
app,
|
|
"--root",
|
|
"Contents/MacOS/App",
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
parseEvidence(result.json).normalized_result,
|
|
);
|
|
expect(
|
|
trace.roots.filter(({ architecture }) => architecture === "arm64e"),
|
|
).toHaveLength(2);
|
|
const environments = trace.findings
|
|
.filter(({ kind }) => kind === "dyld-environment-present")
|
|
.map(({ explanation }) =>
|
|
expected.filter((setting) => explanation.includes(setting)),
|
|
);
|
|
expect(environments).toEqual(expected.map((setting) => [setting]));
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"retains definitive load findings for an explicitly selected app library root",
|
|
async ({ cli }) => {
|
|
const app = await fixtureApp();
|
|
const settings = [
|
|
"DYLD_LIBRARY_PATH=/external",
|
|
"DYLD_INSERT_LIBRARIES=/external/injected.dylib",
|
|
];
|
|
await writeFiles(app, {
|
|
"Contents/Frameworks/libcore.dylib": machoImage({
|
|
fileType: FILE_TYPE.dylib,
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
...settings.map(dyldEnvironmentCommand),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/child.dylib"),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/missing.dylib"),
|
|
],
|
|
}),
|
|
"Contents/Frameworks/child.dylib": machoImage({
|
|
fileType: FILE_TYPE.dylib,
|
|
commands: [buildVersionCommand(1)],
|
|
}),
|
|
});
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"trace-dylib-resolution",
|
|
app,
|
|
"--root",
|
|
"Contents/Frameworks/libcore.dylib",
|
|
"--json",
|
|
],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
parseEvidence(result.json).normalized_result,
|
|
);
|
|
expect(trace.edges.map(({ resolution }) => resolution.status)).toEqual([
|
|
"resolved",
|
|
"unresolved",
|
|
]);
|
|
expect(trace.coverage.status).toBe("complete");
|
|
expect(trace.findings.map(({ kind }) => kind)).toContain(
|
|
"required-load-unresolved",
|
|
);
|
|
expect(
|
|
trace.images.find(
|
|
({ path }) => path === "Contents/Frameworks/libcore.dylib",
|
|
)?.slices[0]?.dyld_environment,
|
|
).toEqual(settings);
|
|
expect(
|
|
trace.findings.find(({ kind }) => kind === "dyld-environment-present")
|
|
?.explanation,
|
|
).toContain("entries in this non-executable root are observations");
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"distinguishes empty search directories from empty versioned or suffix lists",
|
|
async ({ cli }) => {
|
|
for (const [setting, status] of [
|
|
["DYLD_FALLBACK_LIBRARY_PATH=", "undetermined"],
|
|
["DYLD_VERSIONED_LIBRARY_PATH=", "unresolved"],
|
|
["DYLD_IMAGE_SUFFIX=:", "unresolved"],
|
|
] as const) {
|
|
const root = await createTestTempDirectory("rea-dyld-empty-cli-");
|
|
const path = join(root, "program");
|
|
await writeFile(
|
|
path,
|
|
machoImage({
|
|
commands: [
|
|
buildVersionCommand(1),
|
|
dyldEnvironmentCommand(setting),
|
|
dylibCommand(LC.LOAD_DYLIB, "@loader_path/missing.dylib"),
|
|
],
|
|
}),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: ["trace-dylib-resolution", path, "--json"],
|
|
environment: ENVIRONMENT,
|
|
});
|
|
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
|
|
const trace = dylibResolutionResultSchema.parse(
|
|
parseEvidence(result.json).normalized_result,
|
|
);
|
|
expect(trace.edges[0]?.resolution.status).toBe(status);
|
|
expect(trace.coverage.status).toBe(
|
|
status === "undetermined" ? "partial" : "complete",
|
|
);
|
|
expect(
|
|
trace.images.find(({ path }) => path === "program")?.slices[0]
|
|
?.dyld_environment,
|
|
).toEqual([setting]);
|
|
expect(
|
|
trace.findings.some(({ kind }) => kind === "required-load-unresolved"),
|
|
).toBe(status === "unresolved");
|
|
}
|
|
},
|
|
);
|