[GH-ISSUE #4008] [Bug] Mach-O load-command evidence drops earlier LC_LINKER_OPTION strings #1595

Closed
opened 2026-10-16 21:07:40 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Native analysis or decompilation

Observed behavior

inspect-macho drops the first string from a compiler-produced two-string LC_LINKER_OPTION. It returns count: 2 with only string: "#2 Foundation", losing -framework. Both executed CLI inspections succeeded; the repeat returned status 0. The load-command collection claimed exhaustive: true with no limitations.

Reproduction

Run from the built repository on macOS with Xcode command-line tools. The object is only inspected statically.

work=$(mktemp -d)
cat > "$work/options.c" <<'C'
__asm__(".linker_option \"-framework\", \"Foundation\"");
int sample(void) { return 7; }
C
clang -c "$work/options.c" -o "$work/options.o"
otool -l "$work/options.o"
node scripts/rea.mjs inspect-macho "$work/options.o" --json > "$work/result.json"
node --input-type=module - "$work/result.json" <<'JS'
import { readFileSync } from 'node:fs';
const evidence = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const result = evidence.normalized_result ?? evidence;
console.log(result.load_commands.items.find(item => item.kind === 'LC_LINKER_OPTION'));
JS

The recorded reproduction used a prepared compiled CLI without rebuilding and ran the inspection twice with the same missing first option.

Expected behavior

Retain both ordered option strings and the declared count in the corresponding load-command record. Static inspection documentation says raw load-command fields retain their original values. src/contracts/native/nativeToolContracts.ts defines load-command inspection in the inspect_macho contract.

Impact and scope

Compiler-recorded link requirements disappear from successful exhaustive evidence. This was observed twice for the two-string option; broader option families were not tested. Running otool -l independently is a verified manual workaround that exposes both strings.

REA version

rea 6.5.0 at source commit 4692bb3330; freshness checked by source at origin/main 6fcfc5b5e0

Runtime and platform

Node 24.18.0; macOS arm64; Apple clang 21.0.0 (clang-2100.3.34.2); llvm-otool/cctools-1040.

Target and artifact context

Compiler-produced Mach-O arm64 relocatable object containing .linker_option "-framework", "Foundation"; never executed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Native otool -l output retained both values:

     cmd LC_LINKER_OPTION
 cmdsize 40
   count 2
  string #1 -framework
  string #2 Foundation

The CLI returned this record in an exhaustive collection without limitations:

{"index":4,"kind":"LC_LINKER_OPTION","fields":{"cmd":"LC_LINKER_OPTION","cmdsize":40,"count":2,"string":"#2 Foundation"}}

The failing parser and projection are unchanged on the current origin/main revision above. Intervening edits to docs/native-investigation.md concern Objective-C properties and do not change the cited contract. This freshness check used source inspection, not a new CLI execution.

Typed outcome or recovery expected

The returned Evidence should retain every option in its original order and within its own load command. A proposed restoration check should cover one-string and multi-string options plus multiple distinct linker-option commands, preserving declared counts and truthful coverage.

Analysis or suspected cause

Source-derived: parseFields in src/native/parsers/otool.ts assigns every parsed row into fields[key]. Both option rows have the key string, so the later row overwrites the earlier one. NativeMachoInspection returns the command map in a collection marked exhaustive.

Additional context

Related open PR #3430 preserves whitespace in name/path load-command fields. It does not address repeated string keys or retain all LC_LINKER_OPTION arguments. No last known good version is established.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4008 ### 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 Native analysis or decompilation ### Observed behavior `inspect-macho` drops the first string from a compiler-produced two-string `LC_LINKER_OPTION`. It returns `count: 2` with only `string: "#2 Foundation"`, losing `-framework`. Both executed CLI inspections succeeded; the repeat returned status 0. The load-command collection claimed `exhaustive: true` with no limitations. ### Reproduction Run from the built repository on macOS with Xcode command-line tools. The object is only inspected statically. ```sh work=$(mktemp -d) cat > "$work/options.c" <<'C' __asm__(".linker_option \"-framework\", \"Foundation\""); int sample(void) { return 7; } C clang -c "$work/options.c" -o "$work/options.o" otool -l "$work/options.o" node scripts/rea.mjs inspect-macho "$work/options.o" --json > "$work/result.json" node --input-type=module - "$work/result.json" <<'JS' import { readFileSync } from 'node:fs'; const evidence = JSON.parse(readFileSync(process.argv[2], 'utf8')); const result = evidence.normalized_result ?? evidence; console.log(result.load_commands.items.find(item => item.kind === 'LC_LINKER_OPTION')); JS ``` The recorded reproduction used a prepared compiled CLI without rebuilding and ran the inspection twice with the same missing first option. ### Expected behavior Retain both ordered option strings and the declared count in the corresponding load-command record. [Static inspection documentation](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/native-investigation.md#L272) says raw load-command fields retain their original values. `src/contracts/native/nativeToolContracts.ts` defines load-command inspection in the `inspect_macho` contract. ### Impact and scope Compiler-recorded link requirements disappear from successful exhaustive evidence. This was observed twice for the two-string option; broader option families were not tested. Running `otool -l` independently is a verified manual workaround that exposes both strings. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860; freshness checked by source at origin/main 6fcfc5b5e0c063300cd5278dd8bad5b1ab1374f6 ### Runtime and platform Node 24.18.0; macOS arm64; Apple clang 21.0.0 (clang-2100.3.34.2); llvm-otool/cctools-1040. ### Target and artifact context Compiler-produced Mach-O arm64 relocatable object containing `.linker_option "-framework", "Foundation"`; never executed. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Native `otool -l` output retained both values: ```text cmd LC_LINKER_OPTION cmdsize 40 count 2 string #1 -framework string #2 Foundation ``` The CLI returned this record in an exhaustive collection without limitations: ```json {"index":4,"kind":"LC_LINKER_OPTION","fields":{"cmd":"LC_LINKER_OPTION","cmdsize":40,"count":2,"string":"#2 Foundation"}} ``` The failing parser and projection are unchanged on the current origin/main revision above. Intervening edits to `docs/native-investigation.md` concern Objective-C properties and do not change the cited contract. This freshness check used source inspection, not a new CLI execution. ### Typed outcome or recovery expected The returned Evidence should retain every option in its original order and within its own load command. A proposed restoration check should cover one-string and multi-string options plus multiple distinct linker-option commands, preserving declared counts and truthful coverage. ### Analysis or suspected cause Source-derived: [parseFields in `src/native/parsers/otool.ts`](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/native/parsers/otool.ts#L423) assigns every parsed row into `fields[key]`. Both option rows have the key `string`, so the later row overwrites the earlier one. [NativeMachoInspection](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/native/NativeMachoInspection.ts#L285) returns the command map in a collection marked exhaustive. ### Additional context Related open PR #3430 preserves whitespace in `name`/`path` load-command fields. It does not address repeated `string` keys or retain all `LC_LINKER_OPTION` arguments. No last known good version is established.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Confirmed — inspect-macho reports count: 2 but only keeps the second LC_LINKER_OPTION string, dropping the leading "-framework" from the compiler-recorded two-string option. That silently removes link requirements from supposedly exhaustive evidence. The parser in parseFields should append each option string rather than overwriting the previous one (or preserve them as an ordered array) so the declared count always matches the retained strings.

<!-- gh-comment-id:6187406597 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed — inspect-macho reports count: 2 but only keeps the second LC_LINKER_OPTION string, dropping the leading "-framework" from the compiler-recorded two-string option. That silently removes link requirements from supposedly exhaustive evidence. The parser in parseFields should append each option string rather than overwriting the previous one (or preserve them as an ordered array) so the declared count always matches the retained strings.
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#1595
No description provided.