mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(apple): derive dyld environment findings from each traced slice (#2733)
deriveFindings looked up each traced root's slice again by architecture name. A universal executable with arm64e and arm64e.x1 slices reports both as "arm64e", so both roots resolved to the first slice: a later slice's LC_DYLD_ENVIRONMENT produced no finding, or the first slice's command was reported twice. The per-slice dyld_environment arrays were already correct. traceDylibLoading already has the exact slice when it records a root, so pass it to deriveFindings instead of re-deriving it from the name. The public roots and findings shapes are unchanged. Fixes #2661 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e05d3399f1
commit
52d4ce6ced
3 files changed
+74
-15
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
DYLIB_RESOLUTION_LIMITATIONS,
|
||||
deriveFindings,
|
||||
type TracedRootSlice,
|
||||
} from "./dylibResolutionFindings.js";
|
||||
|
||||
/**
|
||||
@@ -568,6 +569,7 @@ export const traceDylibLoading = async (
|
||||
for (const path of request.unclassified ?? [])
|
||||
images.set(path, await view.image(path));
|
||||
const roots: DylibTrace["roots"][number][] = [];
|
||||
const rootSlices: TracedRootSlice[] = [];
|
||||
const edges: Edge[] = [];
|
||||
const withoutArchitecture: string[] = [];
|
||||
const environmentRoots = new Set<string>();
|
||||
@@ -597,6 +599,7 @@ export const traceDylibLoading = async (
|
||||
)
|
||||
environmentRoots.add(root);
|
||||
roots.push({ image: root, architecture: slice.architecture });
|
||||
rootSlices.push({ image: root, slice });
|
||||
edges.push(
|
||||
...(await traceProcess(
|
||||
{
|
||||
@@ -641,7 +644,7 @@ export const traceDylibLoading = async (
|
||||
facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [],
|
||||
})),
|
||||
edges,
|
||||
findings: deriveFindings(edges, roots, images),
|
||||
findings: deriveFindings(edges, rootSlices),
|
||||
coverage: {
|
||||
status:
|
||||
unparsed.length === 0 &&
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import type {
|
||||
DylibEdge as Edge,
|
||||
DylibFinding as Finding,
|
||||
DylibTrace,
|
||||
MachoImageFacts,
|
||||
MachoSlice,
|
||||
} from "./dylibResolution.js";
|
||||
|
||||
/** One traced process root with the exact slice it was traced from. */
|
||||
export interface TracedRootSlice {
|
||||
readonly image: string;
|
||||
readonly slice: MachoSlice;
|
||||
}
|
||||
|
||||
export const deriveFindings = (
|
||||
edges: readonly Edge[],
|
||||
roots: DylibTrace["roots"],
|
||||
images: ReadonlyMap<string, MachoImageFacts>,
|
||||
roots: readonly TracedRootSlice[],
|
||||
): Finding[] => {
|
||||
const findings: Finding[] = [];
|
||||
edges.forEach((edge, index) => {
|
||||
@@ -32,15 +36,11 @@ export const deriveFindings = (
|
||||
explanation: `The modeled dyld search checks ${earlier.join(", ")} before ${edge.resolution.image ?? edge.install_name}. A compatible Mach-O placed at an earlier modeled path could take precedence over that fallback, subject to unmodeled search inputs and code-signing library validation; library validation is not evaluated here (see inspect_signature).${edge.loader_conditional ? " This loader loads only conditionally; if it never loads, this search never occurs." : ""}${edge.resolution.status === "conditional" ? " Unmodeled environment overrides or earlier unknown candidates may take precedence; placing an image at this rpath does not establish which image dyld will load." : ""}`,
|
||||
});
|
||||
});
|
||||
for (const { image, architecture } of roots) {
|
||||
const facts = images.get(image);
|
||||
const slice =
|
||||
facts?.status === "parsed"
|
||||
? facts.slices.find(
|
||||
(candidate) => candidate.architecture === architecture,
|
||||
)
|
||||
: undefined;
|
||||
const environment = slice?.dyld_environment ?? [];
|
||||
// Slices can share an architecture name (arm64e and arm64e.x1), so each
|
||||
// finding uses the slice its root was traced from, not a name lookup.
|
||||
for (const { image, slice } of roots) {
|
||||
const { architecture } = slice;
|
||||
const environment = slice.dyld_environment;
|
||||
if (environment.length > 0)
|
||||
findings.push({
|
||||
kind: "dyld-environment-present",
|
||||
@@ -48,7 +48,7 @@ export const deriveFindings = (
|
||||
image,
|
||||
basis: "derived",
|
||||
explanation:
|
||||
slice?.file_type === "execute"
|
||||
slice.file_type === "execute"
|
||||
? `${image} (${architecture}) sets dyld environment variables through LC_DYLD_ENVIRONMENT (${environment.join(", ")}); any image-selection overrides they request are not modeled. Diagnostic settings alone do not alter resolution.`
|
||||
: `${image} (${architecture}) contains LC_DYLD_ENVIRONMENT (${environment.join(", ")}). Dyld reads these commands only from the main executable; entries in this non-executable root are observations and do not alter resolution.`,
|
||||
});
|
||||
|
||||
@@ -4,11 +4,13 @@ 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";
|
||||
@@ -471,6 +473,60 @@ cliTest(
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"reports each same-named slice's own embedded dyld environment",
|
||||
async ({ cli }) => {
|
||||
// arm64e and arm64e.x1 slices both report the architecture name arm64e.
|
||||
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)],
|
||||
}),
|
||||
};
|
||||
}),
|
||||
),
|
||||
});
|
||||
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).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 }) => {
|
||||
|
||||
Reference in new issue
Block a user