Files
reverse-engineering-anything/docs/apple-application-analysis.md
T
Kaoru0822andClaude Opus 5.5 ade11fa02d feat(apple): inspect dyld shared caches and resolve system paths
macOS 11 and later ship system libraries only inside the dyld shared
cache. trace_dylib_resolution therefore left every /usr/lib and /System
load undetermined, and REA had no way to see what a cache contains.

inspect_dyld_shared_cache (CLI inspect-dyld-shared-cache) reads a cache
file set selected by path, without an open target. Its pure TypeScript
reader parses modern and legacy dyld_cache_header layouts and reports:
- UUID, architecture, platform, OS version, Mac Catalyst alternate,
  cache type, shared region, max slide;
- every VM mapping;
- each subcache by file suffix, checked by UUID. A missing or
  mismatched subcache makes coverage partial;
- the complete image list.
For each requested install path it reads that image's load commands
through the cache mappings with the existing Mach-O reader.

trace_dylib_resolution accepts an optional shared_cache. Absolute
candidates found in a cache that serves the process's CPU family resolve
with the new outcome and status `shared-cache`. That status also takes
precedence over later in-bundle fallbacks such as a back-deployed
libswiftCore. The result names the cache UUID and OS version it used.

verify:dyld-shared-cache checks every image in the host cache (3,649 on
macOS 26.6), with dependencies and their re-export, weak, upward and
delay-init attributes, against `dyld_info -all_dyld_cache
-linked_dylibs`. It also checks CLI/MCP parity, and that each
shared-cache edge in the fixture trace is a library dyld actually loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:43:15 +00:00

12 KiB

Apple Application Inventory Projection

REA can project one or more authenticated inventory_artifact Evidence records for the same IPA, macOS .app directory, ZIP, or DMG into an Apple application inventory. The projection is execution-free: it reports exact paths, content hashes, detected formats, bundle anatomy, runtime-family hints, and path-based bridge hypotheses. It does not parse plist or CMS semantics or claim observed runtime calls.

rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
{
  "name": "project_apple_application_graph",
  "arguments": {
    "inventory_evidence": ["<inventory_artifact Evidence>"]
  }
}

The result includes every component in each inventory category and every JavaScript-to-native bridge candidate pair. It has no caller-selected component budget, prefix truncation, or omitted-count fields. Duplicate inventory pages are merged by artifact identity and occurrence path; their Evidence IDs remain attached as source Evidence.

Coverage is complete-within-inventory when the supplied pages reconstruct the complete authenticated inventory, and partial otherwise. A partial result states that absence is unknown. Component and bridge-candidate arrays are still the complete projection of the inventory that was supplied.

Application roots

Inventory root Application root
IPA Each Payload/<Name>.app
.app directory ., the inventoried bundle itself
Directory, ZIP, or DMG containing apps Each outermost <Name>.app followed by Contents/

ZIP archives made with ditto -c -k --keepParent report <Name>.app. A DMG reports <image>.dmg/<Volume>/<Name>.app; DMG child inventory needs the native macOS mounting adapter. When a DMG has only its root identity, the projection is partial and says that bundle absence is unknown. Other inventory formats, such as APK or ASAR, are rejected as input errors.

An inventoried directory whose name ends in .app is its own root even when its inventory is partial or lacks Contents/. For directory, ZIP, and DMG inventories, components outside every application root are not attributed to the application. This covers installers, disk-image extras, and AppleDouble sidecars, which therefore stay out of runtime families and bridge candidates. A limitation reports how many inventoried entries were left out. IPA projection still lists every archive component, including SwiftSupport/.

platforms lists macos for application roots with Contents/. It lists ios for shallow application roots that have an Info.plist. An application root with neither, such as an empty inventory, has no platform.

macOS bundle anatomy

bundles lists each application root and every nested bundle under it. Each bundle reports its parent_path, layout, info_plist_path, executable_candidates, and signing_paths.

Layout Meaning Content directory
macos-deep Has Contents/ Contents/, executables in Contents/MacOS/
versioned-framework Has Versions/ Each real Versions/<version>/
shallow Neither The bundle directory itself

Roles are path conventions (role_basis: "path-convention"):

Role Path
app-extension *.appex
xpc-service *.xpc, including XPC services inside frameworks
framework *.framework
system-extension *.systemextension
driver-extension *.dext
plug-in PlugIns/*.bundle, *.plugin, *.qlgenerator, *.mdimporter
resource-bundle Resources/*.bundle, such as SwiftPM resource bundles
bundle Any other *.bundle
login-item Contents/Library/LoginItems/*.app
helper-application Contents/Helpers/*.app
nested-application Any other nested *.app

The components also list:

  • privileged_helpers: files directly in Contents/Library/LaunchServices/, where SMJobBless helpers live;
  • launchd_plists: .plist files directly in Contents/Library/LaunchAgents/ (domain: "agent") or Contents/Library/LaunchDaemons/ (domain: "daemon"), as used by SMAppService;
  • helpers: files directly in Contents/Helpers/.

Symlinks are reported by path in symlinks. Their targets are not inventoried, so a versioned framework is described through its real Versions/<version>/ directories rather than Versions/Current. When a framework has zero or several Versions/*/Resources/Info.plist files, its info_plist_path is null and a limitation explains why.

AppleDouble sidecar files (._* and __MACOSX/) are inventory facts, but they describe neighbouring files. They are excluded from roots, bundle roles, and the anatomy components.

The projection does not read plists. Use inspect_plist on a bundle's info_plist_path or a launchd plist to read CFBundleExecutable, identifiers, SMPrivilegedExecutables, BundleProgram, URL schemes, and usage descriptions. Use inspect_signature on an executable candidate for code-signing claims.

Dylib load resolution

trace_dylib_resolution answers which file each Mach-O dylib load reaches. It runs on the active Mach-O, or on every executable in the active .app. Each executable in a bundle (the main app, XPC services, app extensions, login items, privileged helpers, and helper tools) is a separate process root with its own @executable_path.

rea trace-dylib-resolution MyApp.app --json
rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json
{
  "name": "trace_dylib_resolution",
  "arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" }
}

Load commands are parsed in TypeScript from Mach-O and FAT headers, including the dylib_use_command encoding newer linkers emit for flags such as delayed initialization. Nothing is executed and no Apple tool is required, so the tool also works off macOS.

Resolution follows dyld:

  • @executable_path is the directory of the process root's executable. It is undetermined when the root is a library.
  • @loader_path is the directory of the image that holds the load command, or of the image that owns the LC_RPATH entry being expanded.
  • @rpath tries each LC_RPATH of the loading image first, then those of the image that loaded it, up to the process root.
  • Dependencies load dependents-first in load-command order, and each image loads once per process. A later request whose install name matches an already loaded image reuses it.
  • Symlinks such as Versions/Current are followed inside the analyzed root. An escape through .. or an absolute link is escapes-target.

Each edge lists every candidate path tried, with its outcome: resolved, absent, not-mach-o, malformed, unsupported (recognized Mach-O outside parser coverage, such as big-endian images), architecture-missing, outside-target, shared-cache, 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.
shared-cache An absolute install path, or an @rpath candidate outside the root, was found in the dyld shared cache passed as shared_cache (CLI --shared-cache). dyld loads the cached image, so later in-root candidates are unused.

Absolute install names such as /usr/lib/libSystem.B.dylib are outside-target. On macOS 11 and later most of them live in the dyld shared cache rather than on disk, so REA does not check them against the host.

findings are derived, not observed:

  • required-load-unresolved and weak-load-unresolved.
  • earlier-rpath-candidate-absent: a Mach-O placed at an earlier search path would load first. Whether code-signing library validation would reject it is not evaluated; use inspect_signature.
  • dyld-environment-present: LC_DYLD_ENVIRONMENT search paths are not modeled.

verify:macos-bundle checks the parser against otool -l for every traced image. It also compares the predicted load order of two process roots with the images dyld actually loads (DYLD_PRINT_LIBRARIES).

With shared_cache, absolute candidates are looked up in that cache's image list. This only happens when the cache serves the process's CPU family: an arm64e cache serves arm64 and arm64e processes. The result's shared_cache names the cache path, UUID, architecture and OS version. Cached images are not traversed. Without a cache, system paths stay undetermined.

dyld shared cache

inspect_dyld_shared_cache reads a dyld shared cache file set selected by path. On current macOS this is /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e; a cache extracted from an IPSW also works. It runs without an open target.

rea inspect-dyld-shared-cache /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e --image /usr/lib/libSystem.B.dylib --json
{
  "name": "inspect_dyld_shared_cache",
  "arguments": {
    "cache_path": "/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e",
    "images": ["/usr/lib/libSystem.B.dylib"]
  }
}

The result reports:

  • the magic, architecture, UUID, platform and OS version, any alternate platform (Mac Catalyst), cache type, shared region and maximum slide;
  • every VM mapping, with the file that holds it;
  • every subcache by file suffix, with its UUID checked against the main cache. A missing or mismatched subcache makes coverage partial, and images mapped there are unmapped;
  • the complete image list with addresses.

For each requested install path, the tool reads that image's load commands (install name, dependencies with their re-export, weak, upward and delayed-init flags, and rpaths) through the cache mappings. A path the cache does not contain is absent. Older single-file caches are read from their legacy header fields.

Nothing is executed, slid or extracted. Slide info, local symbols and the cache's code signature are not parsed. The Evidence subject is the main cache file, identified by its digest.