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
This commit is contained in:
morluto committed 2026-10-21 12:04:29 +08:00
1 parent ccfaf520b7
commit 90062c0922
4 files changed
+358 -90

No files matched your search

+19 -10
View File
@@ -216,17 +216,26 @@ parser coverage, such as big-endian images), `architecture-missing`,
`outside-target`, `escapes-target`, or `undetermined`. Slices follow dyld's
graded architectures, so an `x86_64h` process also loads `x86_64`. The edge's `resolution` is one of:
| Status | Meaning |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
| `conditional` | An image inside the root resolves, but an earlier candidate outside the root (for example `/usr/lib/swift`) would win if it exists on the running system. |
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
| Status | Meaning |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
| `conditional` | An in-root image may load, but an earlier outside candidate, unmodeled environment input, or unknown platform compatibility can change which image dyld loads. |
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
A `conditional` image is still traced, because it is the image the analyzed
root supplies. Every edge below it has `loader_conditional: true`, and a
request that reuses it by install name is `conditional` too. Findings on those
edges say that they apply only if the conditional image loads.
A `conditional` image is still traced, because it is an in-root candidate
that may load. Every edge below it has `loader_conditional: true`, and a
request that reuses it by install name is `conditional` too. An in-root dylib
with no platform load command remains unknown unless the root is an executable
with observed macOS platform and the candidate’s CPU type is x86_64, i386, or arm64. In that macOS context,
[Apple's dyld](https://github.com/apple-oss-distributions/dyld/blob/fd8d0c4d52320ebf64db34f3cb280310d905c5ae/mach_o/UnsafeHeader.cpp#L1137-L1145) supplies an implicit macOS platform to legacy dylibs on those CPU
types. The candidate records `platform_basis: "macos-dyld-implicit"`; the
reported slice fields `platform: null` and `platforms: []` remain the parsed
facts. For other process contexts the platform remains unknown, coverage is
partial, and the candidate is not treated as incompatible. If a later known
candidate resolves, it remains the traced image with conditional resolution;
the trace does not claim the unknown candidate's dependency graph. Findings on
conditional edges say that they apply only if the conditional image loads.
Absolute install names that name a path beneath the analyzed root are checked
within that root, with symlinks and `..` resolved by the same bounded tree
@@ -142,6 +142,112 @@ it("uses legacy simulator and device platform facts when resolving dylibs", asyn
);
});
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",
+98 -20
View File
@@ -441,26 +441,6 @@ describe("dyld resolution outcomes: checking candidate format and compatibility"
});
});
it("preserves unknown platform compatibility before a later known candidate", async () => {
const trace = await traceDylibLoading(
memoryView({
[MAIN]: executable({
rpaths: ["@executable_path/a", "@executable_path/b"],
dependencies: [dependency("@rpath/lib.dylib")],
}),
"Contents/MacOS/a/lib.dylib": parsed(slice({ platform: null })),
"Contents/MacOS/b/lib.dylib": parsed(slice()),
}),
{ roots: [MAIN] },
);
const edge = edgeFor(trace, MAIN, "@rpath/lib.dylib");
expect(edge.candidates.map(({ outcome }) => outcome)).toEqual([
"undetermined",
"resolved",
]);
expect(edge.resolution.status).toBe("conditional");
});
it("applies dyld Catalyst and zippered platform compatibility", async () => {
const catalystProcess = await traceDylibLoading(
memoryView({
@@ -506,6 +486,104 @@ describe("dyld resolution outcomes: checking candidate format and compatibility"
});
});
describe("platform-less dylib compatibility", () => {
it("keeps an unknown candidate unresolved and traces the later known fallback", async () => {
const trace = await traceDylibLoading(
memoryView({
[MAIN]: executable({
platform: 6,
platforms: [6],
rpaths: ["@executable_path/a", "@executable_path/b"],
dependencies: [dependency("@rpath/lib.dylib")],
}),
"Contents/MacOS/a/lib.dylib": parsed(slice({ platform: null })),
"Contents/MacOS/b/lib.dylib": parsed(
slice({
platform: 6,
platforms: [6],
dependencies: [dependency("@loader_path/libChild.dylib")],
}),
),
"Contents/MacOS/b/libChild.dylib": parsed(
slice({ platform: 6, platforms: [6] }),
),
}),
{ roots: [MAIN] },
);
const edge = edgeFor(trace, MAIN, "@rpath/lib.dylib");
expect(edge.candidates.map(({ outcome }) => outcome)).toEqual([
"undetermined",
"resolved",
]);
expect(edge.resolution).toEqual({
status: "conditional",
image: "Contents/MacOS/b/lib.dylib",
});
expect(edge.candidates[0]).not.toHaveProperty("platform_basis");
expect(trace.edges.map(({ loader }) => loader)).toContain(
"Contents/MacOS/b/lib.dylib",
);
expect(trace.images.map(({ path }) => path)).toContain(
"Contents/MacOS/b/libChild.dylib",
);
expect(trace.coverage.status).toBe("partial");
expect(trace.limitations).toContainEqual(
expect.stringContaining("Contents/MacOS/a/lib.dylib"),
);
});
it("does not infer a platform-less image's platform when process platform is unknown", async () => {
const trace = await traceDylibLoading(
memoryView({
[MAIN]: executable({
platform: null,
platforms: [],
rpaths: ["@executable_path/a"],
dependencies: [dependency("@rpath/lib.dylib")],
}),
"Contents/MacOS/a/lib.dylib": parsed(slice({ platform: null })),
}),
{ roots: [MAIN] },
);
const edge = edgeFor(trace, MAIN, "@rpath/lib.dylib");
expect(edge.candidates[0]).toMatchObject({
outcome: "undetermined",
resolved_path: "Contents/MacOS/a/lib.dylib",
});
expect(edge.candidates[0]).not.toHaveProperty("platform_basis");
expect(edge.resolution).toEqual({ status: "undetermined", image: null });
expect(trace.coverage.status).toBe("partial");
});
it("does not treat a selected dylib root as an executable process context", async () => {
const library = "Contents/Frameworks/libRoot.dylib";
const child = "Contents/Frameworks/libChild.dylib";
const trace = await traceDylibLoading(
memoryView({
[library]: parsed(
slice({
file_type: "dylib",
file_type_code: 6,
platform: 1,
platforms: [1],
dependencies: [dependency("@loader_path/libChild.dylib")],
}),
),
[child]: parsed(slice({ platform: null })),
}),
{ roots: [library] },
);
const edge = edgeFor(trace, library, "@loader_path/libChild.dylib");
expect(edge.candidates[0]).toMatchObject({
outcome: "undetermined",
resolved_path: child,
});
expect(edge.candidates[0]).not.toHaveProperty("platform_basis");
expect(edge.resolution).toEqual({ status: "undetermined", image: null });
expect(trace.coverage.status).toBe("partial");
});
});
describe("dyld load order", () => {
it("loads each image once per process and reuses matching install names", async () => {
const a = "Contents/Frameworks/libA.dylib";
+135 -60
View File
@@ -51,6 +51,13 @@ const dependencyCommandSchema = z.enum([
"LC_LOAD_UPWARD_DYLIB",
]);
const IMPLICIT_MACOS_PLATFORM = 1;
const MACOS_DYLD_IMPLICIT_PLATFORM_CPU_TYPES = new Set([
7, // CPU_TYPE_I386
0x01000007, // CPU_TYPE_X86_64
0x0100000c, // CPU_TYPE_ARM64, including arm64 subtypes
]);
const dependencySchema = z.strictObject({
command: dependencyCommandSchema,
encoding: z.enum([
@@ -145,6 +152,7 @@ const candidateSchema = z.strictObject({
"undetermined",
]),
resolved_path: z.string().nullable(),
platform_basis: z.enum(["macos-dyld-implicit"]).optional(),
});
const edgeSchema = z.strictObject({
@@ -227,7 +235,7 @@ interface LoadedImage {
readonly slice: MachoSlice;
/** Images from the process root to this image; the rpath stack, outermost first. */
readonly chain: readonly string[];
/** Some edge on the chain was conditional: an unknown earlier candidate may load instead. */
/** Some edge on the chain has unresolved candidate or platform facts. */
readonly conditional: boolean;
}
@@ -240,6 +248,8 @@ interface ProcessContext {
>;
readonly searchOverrides: readonly DyldSearchOverride[];
readonly environmentRoots: Set<string>;
readonly unknownPlatformCandidates: Set<string>;
readonly implicitMacOSPlatformCandidates: Set<string>;
readonly platforms: readonly number[];
readonly executable: string | null;
readonly loaded: Map<string, LoadedImage>;
@@ -346,47 +356,75 @@ const evaluateCandidate = async (
// also admits main executables and bundles for different operations.
else if (slice.file_type_code !== 6) outcome = "not-loadable";
else {
const platformResult = platformLoadability(
context.platforms,
slice.platforms,
slice.file_type_code,
context.processIdentity.architecture,
);
const platformResult = platformLoadability(context, slice);
outcome =
platformResult === "compatible"
platformResult.outcome === "compatible"
? "resolved"
: platformResult === "incompatible"
: platformResult.outcome === "incompatible"
? "platform-mismatch"
: "undetermined";
if (outcome === "undetermined")
context.unknownPlatformCandidates.add(lookup.path);
if (platformResult.basis !== undefined) {
context.implicitMacOSPlatformCandidates.add(lookup.path);
return {
...base,
outcome,
resolved_path: lookup.path,
platform_basis: platformResult.basis,
};
}
}
}
return { ...base, outcome, resolved_path: lookup.path };
};
type PlatformLoadability = "compatible" | "incompatible" | "unknown";
type PlatformLoadabilityResult = {
readonly outcome: PlatformLoadability;
readonly basis?: Candidate["platform_basis"];
};
/** Apply dyld's documented cross-platform load cases and retain uncertain host cases. */
const platformLoadability = (
processPlatforms: readonly number[],
imagePlatforms: readonly number[],
fileType: number,
architecture: string,
): PlatformLoadability => {
if (processPlatforms.length === 0 || imagePlatforms.length === 0)
return "unknown";
context: Pick<ProcessContext, "platforms" | "executable">,
image: MachoSlice,
): PlatformLoadabilityResult => {
const { platforms: processPlatforms } = context;
const { file_type_code: fileType, platforms: imagePlatforms } = image;
let effectiveImagePlatforms = imagePlatforms;
let basis: Candidate["platform_basis"];
if (imagePlatforms.length === 0) {
// macOS dyld supplies an implicit macOS platform to old dylibs on these
// CPU types. Keep the parsed platform facts untouched; this is only the
// compatibility interpretation used for an explicitly macOS process.
if (
context.executable !== null &&
processPlatforms.length === 1 &&
processPlatforms[0] === IMPLICIT_MACOS_PLATFORM &&
MACOS_DYLD_IMPLICIT_PLATFORM_CPU_TYPES.has(image.cpu_type)
) {
effectiveImagePlatforms = [IMPLICIT_MACOS_PLATFORM];
basis = "macos-dyld-implicit";
} else return { outcome: "unknown" };
}
if (processPlatforms.length === 0) return { outcome: "unknown" };
const outcomes = processPlatforms.map((processPlatform) =>
imageLoadabilityForPlatform(
processPlatform,
imagePlatforms,
effectiveImagePlatforms,
fileType,
architecture,
image.architecture,
),
);
if (outcomes.every((outcome) => outcome === "compatible"))
return "compatible";
return { outcome: "compatible", ...(basis === undefined ? {} : { basis }) };
if (outcomes.every((outcome) => outcome === "incompatible"))
return "incompatible";
return "unknown";
return {
outcome: "incompatible",
...(basis === undefined ? {} : { basis }),
};
return { outcome: "unknown", ...(basis === undefined ? {} : { basis }) };
};
const imageLoadabilityForPlatform = (
@@ -572,7 +610,7 @@ const traceProcess = async (
context.loaded.set(image, {
slice,
chain: [...current.chain, image],
// dyld may load an unknown earlier candidate instead of this fallback.
// A modeled earlier search uncertainty can make this image conditional.
conditional:
current.conditional || edge.resolution.status === "conditional",
});
@@ -589,6 +627,69 @@ const traceProcess = async (
return edges;
};
const projectTraceResult = (input: {
readonly images: ReadonlyMap<string, MachoImageFacts>;
readonly roots: DylibTrace["roots"];
readonly rootSlices: readonly TracedRootSlice[];
readonly edges: readonly Edge[];
readonly withoutArchitecture: readonly string[];
readonly environmentRoots: ReadonlySet<string>;
readonly unknownPlatformCandidates: ReadonlySet<string>;
readonly implicitMacOSPlatformCandidates: ReadonlySet<string>;
}): DylibTrace => {
const reported = [...input.images.entries()]
.filter(([, facts]) => facts.status !== "not-mach-o")
.sort(([left], [right]) => compare(left, right));
const unparsed = reported
.filter(([, facts]) => facts.status !== "parsed")
.map(([path]) => path);
return {
roots: input.roots,
images: reported.map(([path, facts]) => ({
path,
parse_status:
facts.status === "not-mach-o" ? "unsupported" : facts.status,
reason:
facts.status === "parsed" || facts.status === "not-mach-o"
? null
: facts.reason,
slices:
facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [],
})),
edges: [...input.edges],
findings: deriveFindings(input.edges, input.rootSlices),
coverage: {
status:
unparsed.length === 0 &&
input.withoutArchitecture.length === 0 &&
input.environmentRoots.size === 0 &&
input.unknownPlatformCandidates.size === 0
? "complete"
: "partial",
unparsed_images: unparsed,
roots_without_architecture: [...input.withoutArchitecture],
},
limitations: [
...DYLIB_RESOLUTION_LIMITATIONS,
...(input.environmentRoots.size === 0
? []
: [
`Roots ${[...input.environmentRoots].join(", ")} request applicable image-selection inputs through LC_DYLD_ENVIRONMENT that are not modeled; affected dependencies or inserted images remain uncertain.`,
]),
...(input.unknownPlatformCandidates.size === 0
? []
: [
`In-root dylib candidates ${[...input.unknownPlatformCandidates].sort(compare).join(", ")} have unknown platform compatibility and are not treated as incompatible. Their dependent graphs may be incomplete.`,
]),
...(input.implicitMacOSPlatformCandidates.size === 0
? []
: [
`In-root dylib candidates ${[...input.implicitMacOSPlatformCandidates].sort(compare).join(", ")} have no observed platform load command; macOS compatibility was derived from dyld's implicit-platform rule for an executable root with explicit macOS platform and a supported CPU type. Reported platform facts remain unchanged.`,
]),
],
};
};
/** Trace dyld load paths from each process root through the analyzed root. */
export const traceDylibLoading = async (
view: DylibTreeView,
@@ -608,6 +709,8 @@ export const traceDylibLoading = async (
const edges: Edge[] = [];
const withoutArchitecture: string[] = [];
const environmentRoots = new Set<string>();
const unknownPlatformCandidates = new Set<string>();
const implicitMacOSPlatformCandidates = new Set<string>();
for (const root of request.roots) {
const facts = await view.image(root);
images.set(root, facts);
@@ -647,6 +750,8 @@ export const traceDylibLoading = async (
},
searchOverrides,
environmentRoots,
unknownPlatformCandidates,
implicitMacOSPlatformCandidates,
platforms: slice.platforms,
executable,
loaded: new Map(),
@@ -659,46 +764,16 @@ export const traceDylibLoading = async (
);
}
}
const reported = [...images.entries()]
.filter(([, facts]) => facts.status !== "not-mach-o")
.sort(([left], [right]) => compare(left, right));
const unparsed = reported
.filter(([, facts]) => facts.status !== "parsed")
.map(([path]) => path);
return {
return projectTraceResult({
images,
roots,
images: reported.map(([path, facts]) => ({
path,
parse_status:
facts.status === "not-mach-o" ? "unsupported" : facts.status,
reason:
facts.status === "parsed" || facts.status === "not-mach-o"
? null
: facts.reason,
slices:
facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [],
})),
rootSlices,
edges,
findings: deriveFindings(edges, rootSlices),
coverage: {
status:
unparsed.length === 0 &&
withoutArchitecture.length === 0 &&
environmentRoots.size === 0
? "complete"
: "partial",
unparsed_images: unparsed,
roots_without_architecture: withoutArchitecture,
},
limitations: [
...DYLIB_RESOLUTION_LIMITATIONS,
...(environmentRoots.size === 0
? []
: [
`Roots ${[...environmentRoots].join(", ")} request applicable image-selection inputs through LC_DYLD_ENVIRONMENT that are not modeled; affected dependencies or inserted images remain uncertain.`,
]),
],
};
withoutArchitecture,
environmentRoots,
unknownPlatformCandidates,
implicitMacOSPlatformCandidates,
});
};
const withoutDependencies = (