[GH-ISSUE #5551] [Bug] trace-dylib-resolution omits lazy dylib dependencies while reporting complete coverage #3379

Closed
opened 2026-10-31 00:39:44 +00:00 by bsakaguchi · 1 comment
Owner

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

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

node scripts/rea.mjs trace-dylib-resolution "$WORK/lazy" --json silently omits a real lazy dependency encoded by Apple clang as LC_LAZY_LOAD_DYLIB_INFO (0x3a). With the dylib present or absent, REA returns only the libSystem edge, no findings, and coverage.status: "complete". The normally linked control resolves the same dylib when present and reports required-load-unresolved when absent.

An independent verifier compiled both fixtures and ran all four CLI comparisons, then replayed this reproduction successfully. All four REA calls exited 0 with empty stderr. This report was prepared with AI assistance and independently reproduced.

Reproduction

From the built source checkout on macOS with Apple clang and otool, run the following verified commands. Do not enable automatic exit-on-error: the two fixture programs intentionally return 3 when their library is present. The trace commands use an isolated environment and need no database or external API.

WORK=$(mktemp -d "${TMPDIR:?}/rea-lazy.XXXXXX")
mkdir -p "$WORK/src" "$WORK/lib" "$WORK/home" "$WORK/config" \
  "$WORK/data" "$WORK/cache" "$WORK/state" "$WORK/out"
printf 'int lf(void){return 3;}\n' > "$WORK/src/l.c"
printf 'extern int lf(void);\nint main(void){return lf();}\n' > "$WORK/src/m.c"
clang -dynamiclib -install_name @rpath/libl.dylib \
  -o "$WORK/lib/libl.dylib" "$WORK/src/l.c"
clang -o "$WORK/normal" "$WORK/src/m.c" -L"$WORK/lib" -ll \
  -Wl,-rpath,@executable_path/lib
clang -o "$WORK/lazy" "$WORK/src/m.c" \
  -Wl,-lazy_library,"$WORK/lib/libl.dylib" \
  -Wl,-rpath,@executable_path/lib

otool -L "$WORK/normal"
otool -L "$WORK/lazy"
otool -l "$WORK/lazy" > "$WORK/out/lazy-load-commands.txt"

trace_fixture() {
  env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \
    XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" \
    XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/cache" \
    REA_LOG_LEVEL=silent \
    node scripts/rea.mjs trace-dylib-resolution "$WORK/$1" --json
}
trace_fixture normal > "$WORK/out/normal-present.json"
trace_fixture lazy > "$WORK/out/lazy-present.json"

# Both executables return 3 with the library present.
"$WORK/normal"
"$WORK/lazy"

mv "$WORK/lib/libl.dylib" "$WORK/lib/libl.dylib.moved"
trace_fixture normal > "$WORK/out/normal-absent.json"
trace_fixture lazy > "$WORK/out/lazy-absent.json"

# Observe the first-call failure without writing a core dump.
python3 - "$WORK/lazy" <<'PY'
import os, resource, subprocess, sys
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
child_env = {k: v for k, v in os.environ.items() if not k.startswith('DYLD_')}
result = subprocess.run([sys.argv[1]], env=child_env, capture_output=True, text=True)
print('returncode=' + str(result.returncode))
print(result.stderr)
PY
mv "$WORK/lib/libl.dylib.moved" "$WORK/lib/libl.dylib"

Expected behavior

docs/apple-application-analysis.md:163 and src/contracts/artifactToolContracts.ts:117 promise to trace each Mach-O dylib load. The findings contract at docs/apple-application-analysis.md:224-226 explicitly resolves legacy LC_LAZY_LOAD_DYLIB dependencies without traversing them and distinguishes first-use failure from launch failure. It does not name the newer lazy-info encoding or document its exclusion.

The lazy dependency should appear with its candidate and resolution, including a lazy unresolved finding when absent. If this encoding is unsupported, REA should expose the missing coverage rather than silently claiming a complete trace. docs/tool-design.md:55 requires missing coverage to remain unknown rather than become absence.

Impact and scope

A caller can miss a real dependency and its first-use failure while trusting a complete dependency trace. The observed defect is bounded to the tested lazy-info encoding on macOS arm64; other linker versions, architectures, and production binaries were not tested. No security-boundary crossing or data loss was established.

A manual workaround was checked for this fixture: identify the lazy-info command with otool, read its payload for the install name, and expand the recorded rpath to check the candidate file. Complete tracing coverage does not itself mean every dependency resolves; the normal control correctly retains complete coverage while reporting the absent required load.

REA version

rea-agents 6.7.0, source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node v26.12.0; macOS 27.0.1, build 26A434; arm64; Apple clang 21.0.0, clang-2100.3.34.2. No Hopper or Ghidra provider is involved. Node 26 is within the declared engines; the pinned developer Node version was not used.

Target and artifact context

Locally compiled Mach-O executable calling lf() from @rpath/libl.dylib, with rpath @executable_path/lib. A normal-link control uses the same source, install name, and rpath. No binary patching was used.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

For both lazy-client traces, the relevant projection of returned edges, findings, coverage, and images is:

{
  "edge_install_names": ["/usr/lib/libSystem.B.dylib"],
  "findings": [],
  "coverage": {
    "status": "complete",
    "unparsed_images": [],
    "roots_without_architecture": []
  },
  "image_paths": ["lazy"]
}

edge_install_names and image_paths are projections, not literal result fields. The output mentions legacy lazy resolution without traversal but gives no warning about omitting this payload.

otool -l shows LC_LAZY_LOAD_DYLIB_INFO, cmdsize 16, dataoff 49304, and datasize 56; the separately inspected payload contains @rpath/libl.dylib and _lf. otool -L lists only libSystem for the lazy executable, while it lists the test library for the normal control.

Both fixture executables returned 3 with the dylib present. With it absent, the lazy executable returned -6 through Python subprocess, indicating SIGABRT, and stderr began:

lazy load of '@rpath/libl.dylib' failed: dlopen(@rpath/libl.dylib, 0x0100)

This establishes a first-call failure, not a launch-time failure for lazy dependencies.

Typed outcome or recovery expected

Represent the dependency and its resolution, or explicitly identify unsupported coverage. An absent lazy dependency should remain distinct from a required dependency that prevents launch.

Analysis or suspected cause

Source-derived explanation: src/artifacts/apple/MachoLoadCommandReader.ts:91-99 recognizes legacy 0x20, but not the observed 0x3a. The dispatch at lines 428-430 ignores unrecognized commands. src/domain/apple/dylibResolution.ts:528-532 resolves only emitted dependencies, and lines 652-660 derive complete coverage without tracking this omission. The CLI observations establish the defect independently of that diagnosis.

Additional context

Fresh upstream comparison against main at 55cce6fc11a4edc32b4a6d4c2d8fd4f2ed2a4f18 found only the unrelated alignment refactor 108c0e7e495b1c9dbecd3fd6e6ef068d41ee9483 in the cited files after the tested baseline. It leaves the dependency dispatch unchanged; no runtime reproduction on that newer revision is claimed.

Searched open and closed issues and PRs by command, lazy-info name, symptom, and source module. The older #532 retains legacy LC_LAZY_LOAD_DYLIB in native inspection; it does not cover this 0x3a encoding. #927 introduces tracing and #1100 fixes candidate/environment handling, without reporting this omission.

Last known good version: NOT_ENOUGH_INFO; no earlier-version comparison was run.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5551 ### 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 `node scripts/rea.mjs trace-dylib-resolution "$WORK/lazy" --json` silently omits a real lazy dependency encoded by Apple clang as `LC_LAZY_LOAD_DYLIB_INFO` (`0x3a`). With the dylib present or absent, REA returns only the libSystem edge, no findings, and `coverage.status: "complete"`. The normally linked control resolves the same dylib when present and reports `required-load-unresolved` when absent. An independent verifier compiled both fixtures and ran all four CLI comparisons, then replayed this reproduction successfully. All four REA calls exited 0 with empty stderr. This report was prepared with AI assistance and independently reproduced. ### Reproduction From the built source checkout on macOS with Apple clang and `otool`, run the following verified commands. Do not enable automatic exit-on-error: the two fixture programs intentionally return 3 when their library is present. The trace commands use an isolated environment and need no database or external API. ```sh WORK=$(mktemp -d "${TMPDIR:?}/rea-lazy.XXXXXX") mkdir -p "$WORK/src" "$WORK/lib" "$WORK/home" "$WORK/config" \ "$WORK/data" "$WORK/cache" "$WORK/state" "$WORK/out" printf 'int lf(void){return 3;}\n' > "$WORK/src/l.c" printf 'extern int lf(void);\nint main(void){return lf();}\n' > "$WORK/src/m.c" clang -dynamiclib -install_name @rpath/libl.dylib \ -o "$WORK/lib/libl.dylib" "$WORK/src/l.c" clang -o "$WORK/normal" "$WORK/src/m.c" -L"$WORK/lib" -ll \ -Wl,-rpath,@executable_path/lib clang -o "$WORK/lazy" "$WORK/src/m.c" \ -Wl,-lazy_library,"$WORK/lib/libl.dylib" \ -Wl,-rpath,@executable_path/lib otool -L "$WORK/normal" otool -L "$WORK/lazy" otool -l "$WORK/lazy" > "$WORK/out/lazy-load-commands.txt" trace_fixture() { env HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/config" \ XDG_DATA_HOME="$WORK/data" XDG_CACHE_HOME="$WORK/cache" \ XDG_STATE_HOME="$WORK/state" TMPDIR="$WORK/cache" \ REA_LOG_LEVEL=silent \ node scripts/rea.mjs trace-dylib-resolution "$WORK/$1" --json } trace_fixture normal > "$WORK/out/normal-present.json" trace_fixture lazy > "$WORK/out/lazy-present.json" # Both executables return 3 with the library present. "$WORK/normal" "$WORK/lazy" mv "$WORK/lib/libl.dylib" "$WORK/lib/libl.dylib.moved" trace_fixture normal > "$WORK/out/normal-absent.json" trace_fixture lazy > "$WORK/out/lazy-absent.json" # Observe the first-call failure without writing a core dump. python3 - "$WORK/lazy" <<'PY' import os, resource, subprocess, sys resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) child_env = {k: v for k, v in os.environ.items() if not k.startswith('DYLD_')} result = subprocess.run([sys.argv[1]], env=child_env, capture_output=True, text=True) print('returncode=' + str(result.returncode)) print(result.stderr) PY mv "$WORK/lib/libl.dylib.moved" "$WORK/lib/libl.dylib" ``` ### Expected behavior `docs/apple-application-analysis.md:163` and `src/contracts/artifactToolContracts.ts:117` promise to trace each Mach-O dylib load. The findings contract at `docs/apple-application-analysis.md:224-226` explicitly resolves legacy `LC_LAZY_LOAD_DYLIB` dependencies without traversing them and distinguishes first-use failure from launch failure. It does not name the newer lazy-info encoding or document its exclusion. The lazy dependency should appear with its candidate and resolution, including a lazy unresolved finding when absent. If this encoding is unsupported, REA should expose the missing coverage rather than silently claiming a complete trace. `docs/tool-design.md:55` requires missing coverage to remain unknown rather than become absence. ### Impact and scope A caller can miss a real dependency and its first-use failure while trusting a complete dependency trace. The observed defect is bounded to the tested lazy-info encoding on macOS arm64; other linker versions, architectures, and production binaries were not tested. No security-boundary crossing or data loss was established. A manual workaround was checked for this fixture: identify the lazy-info command with `otool`, read its payload for the install name, and expand the recorded rpath to check the candidate file. Complete tracing coverage does not itself mean every dependency resolves; the normal control correctly retains complete coverage while reporting the absent required load. ### REA version rea-agents 6.7.0, source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node v26.12.0; macOS 27.0.1, build 26A434; arm64; Apple clang 21.0.0, clang-2100.3.34.2. No Hopper or Ghidra provider is involved. Node 26 is within the declared engines; the pinned developer Node version was not used. ### Target and artifact context Locally compiled Mach-O executable calling `lf()` from `@rpath/libl.dylib`, with rpath `@executable_path/lib`. A normal-link control uses the same source, install name, and rpath. No binary patching was used. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence For both lazy-client traces, the relevant projection of returned `edges`, `findings`, `coverage`, and `images` is: ```json { "edge_install_names": ["/usr/lib/libSystem.B.dylib"], "findings": [], "coverage": { "status": "complete", "unparsed_images": [], "roots_without_architecture": [] }, "image_paths": ["lazy"] } ``` `edge_install_names` and `image_paths` are projections, not literal result fields. The output mentions legacy lazy resolution without traversal but gives no warning about omitting this payload. `otool -l` shows `LC_LAZY_LOAD_DYLIB_INFO`, `cmdsize 16`, `dataoff 49304`, and `datasize 56`; the separately inspected payload contains `@rpath/libl.dylib` and `_lf`. `otool -L` lists only libSystem for the lazy executable, while it lists the test library for the normal control. Both fixture executables returned 3 with the dylib present. With it absent, the lazy executable returned `-6` through Python subprocess, indicating SIGABRT, and stderr began: ```text lazy load of '@rpath/libl.dylib' failed: dlopen(@rpath/libl.dylib, 0x0100) ``` This establishes a first-call failure, not a launch-time failure for lazy dependencies. ### Typed outcome or recovery expected Represent the dependency and its resolution, or explicitly identify unsupported coverage. An absent lazy dependency should remain distinct from a required dependency that prevents launch. ### Analysis or suspected cause Source-derived explanation: `src/artifacts/apple/MachoLoadCommandReader.ts:91-99` recognizes legacy `0x20`, but not the observed `0x3a`. The dispatch at lines 428-430 ignores unrecognized commands. `src/domain/apple/dylibResolution.ts:528-532` resolves only emitted dependencies, and lines 652-660 derive complete coverage without tracking this omission. The CLI observations establish the defect independently of that diagnosis. ### Additional context Fresh upstream comparison against `main` at `55cce6fc11a4edc32b4a6d4c2d8fd4f2ed2a4f18` found only the unrelated alignment refactor `108c0e7e495b1c9dbecd3fd6e6ef068d41ee9483` in the cited files after the tested baseline. It leaves the dependency dispatch unchanged; no runtime reproduction on that newer revision is claimed. Searched open and closed issues and PRs by command, lazy-info name, symptom, and source module. The older #532 retains legacy `LC_LAZY_LOAD_DYLIB` in native inspection; it does not cover this `0x3a` encoding. #927 introduces tracing and #1100 fixes candidate/environment handling, without reporting this omission. Last known good version: NOT_ENOUGH_INFO; no earlier-version comparison was run.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #5362 ([Bug] Dylib path expansion rejects line terminators in dependency names and rpaths). Both address Mach-O dynamic library dependency tracing (LC_LAZY_LOAD_DYLIB) and path expansion in trace-dylib-resolution. Cataloged under Cluster BC in consolidation tracker #4330.

<!-- gh-comment-id:6295546568 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #5362 (`[Bug] Dylib path expansion rejects line terminators in dependency names and rpaths`). Both address Mach-O dynamic library dependency tracing (`LC_LAZY_LOAD_DYLIB`) and path expansion in `trace-dylib-resolution`. Cataloged under Cluster BC in consolidation tracker #4330.
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#3379
No description provided.