[GH-ISSUE #5747] [Bug] trace-dylib-resolution says an existing incompatible candidate does not exist #3493

Closed
opened 2026-10-31 00:41:08 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5747

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

For an arm64 app whose only modeled dependency candidate is an existing x86_64 dylib, trace-dylib-resolution correctly returns an unresolved edge with outcome: "architecture-missing" and a populated resolved_path. Its derived required-load-unresolved finding nevertheless says that no candidate exists in the analyzed root. The explanation contradicts the file-presence evidence in the same response.

An independent verifier reproduced the three cases below at the baseline; the submitter replayed them against freshly built current main. All REA calls exited 0. The fixtures were inspected statically during the submitter replay.

Reproduction

Run from a source checkout with locked dependencies installed and npm run build:cached completed, on macOS arm64 with Apple clang and jq. Link against a compatible dylib first, then substitute the wrong-architecture dylib. This block compiles disposable fixtures without launching them; REA uses an isolated HOME/XDG/TMPDIR profile.

work_dir="$(mktemp -d)"
S="$work_dir"
mkdir -p "$S/home" "$S/cfg" "$S/data" "$S/cache" "$S/state" "$S/tmp" \
  "$S/Probe.app/Contents/MacOS" "$S/Probe.app/Contents/Frameworks"
cat > "$S/value.c" <<'C'
int value(void) { return 42; }
C
cat > "$S/main.c" <<'C'
#include <stdio.h>
extern int value(void);
int main(void) { printf("%d\n", value()); return 0; }
C
cat > "$S/Probe.app/Contents/Info.plist" <<'PLIST'
<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>Probe</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>
PLIST
clang -arch arm64 -dynamiclib -install_name @rpath/libvalue.dylib \
  -o "$S/arm64.dylib" "$S/value.c"
clang -arch x86_64 -dynamiclib -install_name @rpath/libvalue.dylib \
  -o "$S/x86_64.dylib" "$S/value.c"
cp "$S/arm64.dylib" "$S/Probe.app/Contents/Frameworks/libvalue.dylib"
clang -arch arm64 -o "$S/Probe.app/Contents/MacOS/Probe" "$S/main.c" \
  "$S/Probe.app/Contents/Frameworks/libvalue.dylib" \
  -Wl,-rpath,@executable_path/../Frameworks
trace() {
  env -i PATH="$PATH" HOME="$S/home" XDG_CONFIG_HOME="$S/cfg" \
    XDG_DATA_HOME="$S/data" XDG_CACHE_HOME="$S/cache" \
    XDG_STATE_HOME="$S/state" TMPDIR="$S/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs trace-dylib-resolution "$S/Probe.app" \
    --root Contents/MacOS/Probe --architecture arm64 --json
}
trace > "$S/compatible.json"
cp "$S/x86_64.dylib" "$S/Probe.app/Contents/Frameworks/libvalue.dylib"
lipo -info "$S/Probe.app/Contents/MacOS/Probe" \
  "$S/Probe.app/Contents/Frameworks/libvalue.dylib"
trace > "$S/wrong-architecture.json"
rm "$S/Probe.app/Contents/Frameworks/libvalue.dylib"
trace > "$S/absent.json"
for scenario in compatible wrong-architecture absent; do
  printf '%s\n' "$scenario"
  jq '.normalized_result | {
    edge: (.edges[] | select(.install_name == "@rpath/libvalue.dylib") |
      {resolution, candidates}),
    findings: [.findings[] | select(.kind == "required-load-unresolved")]
  }' "$S/$scenario.json"
done

Expected behavior

Describe a present but unusable candidate without asserting physical absence. Preserve the correct unresolved status, architecture-missing outcome, and actual resolved_path. An absent candidate may still be described as missing, and the compatible control must continue to resolve without an unresolved-load finding.

The Apple guide distinguishes architecture-missing from absent, defines unresolved candidates as absent or unusable, and describes findings as derived.

Impact and scope

P3 wording defect: the finding falsely asserts physical absence, but the resolution decision and detailed candidate evidence remain correct. The verified workaround is to read candidates[].outcome and resolved_path in the same JSON response. No consequential change to load decisions, security impact, or data loss was demonstrated. Observed for this self-built arm64 app with one x86_64 candidate; other unusable-candidate classes and MCP were not replayed.

REA version

rea-agents 6.7.0, baseline source commit 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rechecked by building and replaying all three cases on main at d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f (also 6.7.0): still reproduces. These are source-checkout observations.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64; Apple clang 21.0.0. No Hopper/Ghidra or external analysis provider is involved.

Target and artifact context

Self-built thin arm64 Mach-O app importing @rpath/libvalue.dylib through @executable_path/../Frameworks. The dependency at the same bundle path is compatible arm64, incompatible x86_64, or absent. lipo -info confirmed the executable and incompatible dependency architectures.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

With the x86_64-only dylib present, this observed subset of normalized_result is identical at baseline and rechecked main:

{
  "resolution": {"status": "unresolved", "image": null},
  "candidates": [{
    "path": "Contents/MacOS/../Frameworks/libvalue.dylib",
    "source": "rpath",
    "rpath": "@executable_path/../Frameworks",
    "rpath_owner": "Contents/MacOS/Probe",
    "outcome": "architecture-missing",
    "resolved_path": "Contents/Frameworks/libvalue.dylib"
  }]
}

The derived finding has kind: "required-load-unresolved", basis: "derived", and this explanation:

No candidate for @rpath/libvalue.dylib exists in the analyzed root; dyld would fail to load this required dependency for Contents/MacOS/Probe (arm64) unless the image is supplied elsewhere.

Removing the file changes the candidate to outcome: "absent" and resolved_path: null, but produces the identical explanation. With the arm64 dylib present, the edge resolves and there are no findings. These controls distinguish the incorrect absence wording from the correct unresolved result. Evidence provenance: observed CLI output; local verification: independently reproduced and replayed on main.

Typed outcome or recovery expected

Keep existing candidate and resolution semantics. Derive an explanation that distinguishes an incompatible file from an absent candidate, or otherwise avoids asserting absence when a candidate is present. Proposed restoration check: repeat all three cases and preserve the compatible and absent controls while correcting the incompatible finding's wording.

Analysis or suspected cause

Source-derived reasoning: candidate evaluation retains the actual in-root path for an architecture-missing candidate. unresolvedFinding constructs the same absence assertion for every unresolved edge, without examining candidate outcomes. Rechecked main retains that logic. No bundled dependency was identified as the cause.

Additional context

Prepared with AI assistance. Related #5694 concerns omitted outcome documentation; #5696 concerns skipped traversal of platform-less images; closed #5637 concerns absolute-path containment. This fixture uses an ordinary rpath, has a definitive unresolved edge, and concerns its derived explanation. Merged PRs #1100, #1772, #2292, and #1623 cover candidate validation, slice matching, loaded-image reuse, and directory requirements respectively; none restores this explanation contract.

The verifier separately compared actual dyld behavior at baseline: the compatible program printed 42 and exited 0; the incompatible case identified an existing file with incompatible architecture, while the absent case reported no such file. Those executions are verifier evidence, not part of the submitter's static replay. No fix or successful restoration has been tested; the last known good version is NOT_ENOUGH_INFO.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5747 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior For an arm64 app whose only modeled dependency candidate is an existing x86_64 dylib, `trace-dylib-resolution` correctly returns an unresolved edge with `outcome: "architecture-missing"` and a populated `resolved_path`. Its derived `required-load-unresolved` finding nevertheless says that no candidate exists in the analyzed root. The explanation contradicts the file-presence evidence in the same response. An independent verifier reproduced the three cases below at the baseline; the submitter replayed them against freshly built current main. All REA calls exited 0. The fixtures were inspected statically during the submitter replay. ### Reproduction Run from a source checkout with locked dependencies installed and `npm run build:cached` completed, on macOS arm64 with Apple clang and jq. Link against a compatible dylib first, then substitute the wrong-architecture dylib. This block compiles disposable fixtures without launching them; REA uses an isolated HOME/XDG/TMPDIR profile. ```sh work_dir="$(mktemp -d)" S="$work_dir" mkdir -p "$S/home" "$S/cfg" "$S/data" "$S/cache" "$S/state" "$S/tmp" \ "$S/Probe.app/Contents/MacOS" "$S/Probe.app/Contents/Frameworks" cat > "$S/value.c" <<'C' int value(void) { return 42; } C cat > "$S/main.c" <<'C' #include <stdio.h> extern int value(void); int main(void) { printf("%d\n", value()); return 0; } C cat > "$S/Probe.app/Contents/Info.plist" <<'PLIST' <?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>Probe</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist> PLIST clang -arch arm64 -dynamiclib -install_name @rpath/libvalue.dylib \ -o "$S/arm64.dylib" "$S/value.c" clang -arch x86_64 -dynamiclib -install_name @rpath/libvalue.dylib \ -o "$S/x86_64.dylib" "$S/value.c" cp "$S/arm64.dylib" "$S/Probe.app/Contents/Frameworks/libvalue.dylib" clang -arch arm64 -o "$S/Probe.app/Contents/MacOS/Probe" "$S/main.c" \ "$S/Probe.app/Contents/Frameworks/libvalue.dylib" \ -Wl,-rpath,@executable_path/../Frameworks trace() { env -i PATH="$PATH" HOME="$S/home" XDG_CONFIG_HOME="$S/cfg" \ XDG_DATA_HOME="$S/data" XDG_CACHE_HOME="$S/cache" \ XDG_STATE_HOME="$S/state" TMPDIR="$S/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs trace-dylib-resolution "$S/Probe.app" \ --root Contents/MacOS/Probe --architecture arm64 --json } trace > "$S/compatible.json" cp "$S/x86_64.dylib" "$S/Probe.app/Contents/Frameworks/libvalue.dylib" lipo -info "$S/Probe.app/Contents/MacOS/Probe" \ "$S/Probe.app/Contents/Frameworks/libvalue.dylib" trace > "$S/wrong-architecture.json" rm "$S/Probe.app/Contents/Frameworks/libvalue.dylib" trace > "$S/absent.json" for scenario in compatible wrong-architecture absent; do printf '%s\n' "$scenario" jq '.normalized_result | { edge: (.edges[] | select(.install_name == "@rpath/libvalue.dylib") | {resolution, candidates}), findings: [.findings[] | select(.kind == "required-load-unresolved")] }' "$S/$scenario.json" done ``` ### Expected behavior Describe a present but unusable candidate without asserting physical absence. Preserve the correct unresolved status, `architecture-missing` outcome, and actual `resolved_path`. An absent candidate may still be described as missing, and the compatible control must continue to resolve without an unresolved-load finding. The [Apple guide](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/apple-application-analysis.md#L213-L249) distinguishes `architecture-missing` from `absent`, defines unresolved candidates as absent or unusable, and describes findings as derived. ### Impact and scope P3 wording defect: the finding falsely asserts physical absence, but the resolution decision and detailed candidate evidence remain correct. The verified workaround is to read `candidates[].outcome` and `resolved_path` in the same JSON response. No consequential change to load decisions, security impact, or data loss was demonstrated. Observed for this self-built arm64 app with one x86_64 candidate; other unusable-candidate classes and MCP were not replayed. ### REA version `rea-agents` 6.7.0, baseline source commit `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rechecked by building and replaying all three cases on main at `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f` (also 6.7.0): still reproduces. These are source-checkout observations. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64; Apple clang 21.0.0. No Hopper/Ghidra or external analysis provider is involved. ### Target and artifact context Self-built thin arm64 Mach-O app importing `@rpath/libvalue.dylib` through `@executable_path/../Frameworks`. The dependency at the same bundle path is compatible arm64, incompatible x86_64, or absent. `lipo -info` confirmed the executable and incompatible dependency architectures. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence With the x86_64-only dylib present, this observed subset of `normalized_result` is identical at baseline and rechecked main: ```json { "resolution": {"status": "unresolved", "image": null}, "candidates": [{ "path": "Contents/MacOS/../Frameworks/libvalue.dylib", "source": "rpath", "rpath": "@executable_path/../Frameworks", "rpath_owner": "Contents/MacOS/Probe", "outcome": "architecture-missing", "resolved_path": "Contents/Frameworks/libvalue.dylib" }] } ``` The derived finding has `kind: "required-load-unresolved"`, `basis: "derived"`, and this explanation: ```text No candidate for @rpath/libvalue.dylib exists in the analyzed root; dyld would fail to load this required dependency for Contents/MacOS/Probe (arm64) unless the image is supplied elsewhere. ``` Removing the file changes the candidate to `outcome: "absent"` and `resolved_path: null`, but produces the identical explanation. With the arm64 dylib present, the edge resolves and there are no findings. These controls distinguish the incorrect absence wording from the correct unresolved result. Evidence provenance: observed CLI output; local verification: independently reproduced and replayed on main. ### Typed outcome or recovery expected Keep existing candidate and resolution semantics. Derive an explanation that distinguishes an incompatible file from an absent candidate, or otherwise avoids asserting absence when a candidate is present. Proposed restoration check: repeat all three cases and preserve the compatible and absent controls while correcting the incompatible finding's wording. ### Analysis or suspected cause Source-derived reasoning: [candidate evaluation](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/domain/apple/dylibResolution.ts#L298-L341) retains the actual in-root path for an `architecture-missing` candidate. [unresolvedFinding](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/domain/apple/dylibResolutionFindings.ts#L65-L96) constructs the same absence assertion for every unresolved edge, without examining candidate outcomes. Rechecked main retains that logic. No bundled dependency was identified as the cause. ### Additional context Prepared with AI assistance. Related #5694 concerns omitted outcome documentation; #5696 concerns skipped traversal of platform-less images; closed #5637 concerns absolute-path containment. This fixture uses an ordinary rpath, has a definitive unresolved edge, and concerns its derived explanation. Merged PRs #1100, #1772, #2292, and #1623 cover candidate validation, slice matching, loaded-image reuse, and directory requirements respectively; none restores this explanation contract. The verifier separately compared actual dyld behavior at baseline: the compatible program printed `42` and exited 0; the incompatible case identified an existing file with incompatible architecture, while the absent case reported no such file. Those executions are verifier evidence, not part of the submitter's static replay. No fix or successful restoration has been tested; the last known good version is NOT_ENOUGH_INFO.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3493
No description provided.