[GH-ISSUE #7391] [Bug] inspect-macho aborts on a linker-generated section name containing a line break #4396

Closed
opened 2026-10-31 00:49:29 +00:00 by bsakaguchi · 0 comments
Owner

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

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

The public inspect-macho command exits 1 with unreadable_output when a linked arm64 executable contains a section named x\naddr 0x1, where \n represents an actual LF byte. The error reason is Malformed otool load-command output, although the captured producer exits 0. REA returns no normalized inspection result.

Apple's linker creates the section directly, without a binary patch, and the executable passes codesign --verify --strict. An otherwise equivalent executable with the ordinary custom section name __probe returns a successful inspection. list-architectures and otool -h -l succeed for both inputs. Neither executable is run.

Reproduction

Run this from the root of source commit f8f7ac96de8a66a4ab9a23d87e04779bfcd11045 on macOS arm64, with the locked dependencies and built dist/ already prepared. It uses Node 24 from the shown Homebrew path and Apple's command-line development tools. All CLI home, XDG and temporary directories are isolated below $WORK; it creates synthetic fixtures outside the checkout.

set -eu
export PATH="/opt/homebrew/opt/node@24/bin:$PATH"
export WORK="$(mktemp -d)"
python3 -I - <<'PY'
import json, os, pathlib, struct, subprocess
root = pathlib.Path.cwd()
w = pathlib.Path(os.environ['WORK'])
env = {'PATH': os.environ['PATH']}
for key, folder in [('HOME', 'home'), ('XDG_CONFIG_HOME', 'config'),
                    ('XDG_DATA_HOME', 'data'), ('XDG_CACHE_HOME', 'cache'),
                    ('XDG_STATE_HOME', 'state'), ('XDG_RUNTIME_DIR', 'runtime'),
                    ('TMPDIR', 'temp')]:
    (w / folder).mkdir()
    env[key] = str(w / folder)
(w / 'main.c').write_text('int main(void) { return 0; }\n')
(w / 'payload').write_bytes(b'hello')
def call(args):
    return subprocess.run(args, cwd=root, env=env, capture_output=True, text=True)
print('REA:', call(['node', 'scripts/rea.mjs', '--version']).stdout.strip())
print('Node:', call(['node', '--version']).stdout.strip())
for label, name in [('control', '__probe'), ('linebreak', 'x\naddr 0x1')]:
    target = w / label
    linked = call(['clang', '-arch', 'arm64', '-o', str(target),
                   str(w / 'main.c'),
                   '-Wl,-sectcreate,__DATA,' + name + ',' + str(w / 'payload')])
    assert linked.returncode == 0, linked.stderr
    before = target.read_bytes()
    found = []
    pos = 32
    for _ in range(struct.unpack_from('<I', before, 16)[0]):
        cmd, size = struct.unpack_from('<II', before, pos)
        if cmd == 0x19:
            for i in range(struct.unpack_from('<I', before, pos + 64)[0]):
                at = pos + 72 + i * 80
                found.append(before[at:at+16].split(b'\0')[0])
        pos += size
    assert name.encode() in found
    signed = call(['codesign', '--verify', '--strict', str(target)])
    raw = call(['otool', '-h', '-l', str(target)])
    arch = call(['node', 'scripts/rea.mjs', 'list-architectures', str(target), '--json'])
    result = call(['node', 'scripts/rea.mjs', 'inspect-macho', str(target), '--json'])
    (w / (label + '.json')).write_text(result.stdout)
    (w / (label + '.otool.txt')).write_text(raw.stdout)
    assert target.read_bytes() == before
    j = json.loads(result.stdout)
    print(label, 'name=' + repr(name), 'codesign=' + str(signed.returncode),
          'otool=' + str(raw.returncode), 'architectures=' + str(arch.returncode),
          'inspect=' + str(result.returncode))
    if result.returncode:
        print(json.dumps({'code': j['code'], 'reason': j['details']['reason']}))
    else:
        print('returned sections:', [s['name'] for g in
              j['normalized_result']['segments']['items'] for s in g['sections']['items']])
    assert signed.returncode == raw.returncode == arch.returncode == 0
    assert result.returncode == (0 if label == 'control' else 1)
PY

The complete script was run successfully twice with the output below. The binary-table assertions confirm the section-name bytes, and the final byte comparison confirms that inspection did not modify either input.

Expected behavior

Inspect the linker-generated executable and preserve its actual section name and the other available Mach-O facts. The native inspection contract includes sections and load commands; the guide promises original raw load-command values, and the name parser's contract says names are returned exactly as observed.

A name payload containing an LF must not be interpreted as an additional numeric addr field. If the producer's textual representation cannot preserve a name unambiguously, return an explicit limitation alongside the facts that can be retained rather than treating successful tool output as malformed and discarding the whole observation.

Impact and scope

P2: this is a bounded but consequential static-inspection failure for unusual, linker-accepted section names. The affected input loses the complete normalized result, including unrelated sections, load commands and symbol inventories. The ordinary custom-section control succeeds, and both files remain unchanged. Broader frequency, other architectures and segment-name variants were not measured.

A manual workaround is available: direct otool -h -l retains the section-name text and other load-command facts, and list-architectures still succeeds. These do not supply an equivalent successful inspect-macho Evidence record. No workaround within that operation was verified.

REA version

Exact node scripts/rea.mjs --version output: 6.8.0.
Source commit: f8f7ac96de8a66a4ab9a23d87e04779bfcd11045.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64. Apple clang/linker, otool and codesign; no deep-analysis provider or GUI was used.

Target and artifact context

Two synthetic thin arm64 Mach-O executables linked from the same C source with the same five-byte custom-section content. One uses __DATA,__probe; the other uses __DATA,x\naddr 0x1 with an actual embedded LF. Both are fresh linker outputs that pass strict signature verification.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed output from the reproduction:

REA: 6.8.0
Node: v24.22.1
control name='__probe' codesign=0 otool=0 architectures=0 inspect=0
returned sections: ['__text', '__unwind_info', '__probe']
linebreak name='x\naddr 0x1' codesign=0 otool=0 architectures=0 inspect=1
{"code": "unreadable_output", "reason": "Malformed otool load-command output"}

The affected genuine otool capture contains this section header, with the name's LF printed verbatim:

Section
  sectname x
addr 0x1
   segname __DATA

Its normal address field follows separately. Calling the built production parser on that same real output throws TypeError: otool field addr is missing or repeated. The CLI's captured producer result has exit code 0, no signal and truncated: false.

Expected source: src/contracts/native/nativeToolContracts.ts:102, docs/native-investigation.md:541, and src/native/parsers/otool.ts:634.

Failure source: src/native/parsers/otool.ts:470, src/native/parsers/otool.ts:535, src/native/parsers/otool.ts:559, and src/native/NativeMachoInspection.ts:175.

Analysis or suspected cause

Source-derived explanation: field parsing splits the capture into lines before interpreting the fixed-width section name. The name continuation becomes another addr field, so the required-field uniqueness check rejects it. The inspection workflow translates that parser exception into the observed error and returns no partial observation. The direct production-parser replay corroborates the exception; no fix was implemented or tested.

Additional context

Related closed reports cover different inputs and failures: #6818 concerns surrounding whitespace trimmed during successful section-name parsing, #5680 concerns the literal name version, and #760 concerns LF in a filename affecting lipo architecture parsing. Their existing fixes do not handle an LF inside a section-name payload. The surrounding-whitespace fix is already present in the tested baseline. Open and closed issue searches found no report of this specific failure.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7391 ## 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 The public `inspect-macho` command exits 1 with `unreadable_output` when a linked arm64 executable contains a section named `x\naddr 0x1`, where `\n` represents an actual LF byte. The error reason is `Malformed otool load-command output`, although the captured producer exits 0. REA returns no normalized inspection result. Apple's linker creates the section directly, without a binary patch, and the executable passes `codesign --verify --strict`. An otherwise equivalent executable with the ordinary custom section name `__probe` returns a successful inspection. `list-architectures` and `otool -h -l` succeed for both inputs. Neither executable is run. ## Reproduction Run this from the root of source commit `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045` on macOS arm64, with the locked dependencies and built `dist/` already prepared. It uses Node 24 from the shown Homebrew path and Apple's command-line development tools. All CLI home, XDG and temporary directories are isolated below `$WORK`; it creates synthetic fixtures outside the checkout. ```sh set -eu export PATH="/opt/homebrew/opt/node@24/bin:$PATH" export WORK="$(mktemp -d)" python3 -I - <<'PY' import json, os, pathlib, struct, subprocess root = pathlib.Path.cwd() w = pathlib.Path(os.environ['WORK']) env = {'PATH': os.environ['PATH']} for key, folder in [('HOME', 'home'), ('XDG_CONFIG_HOME', 'config'), ('XDG_DATA_HOME', 'data'), ('XDG_CACHE_HOME', 'cache'), ('XDG_STATE_HOME', 'state'), ('XDG_RUNTIME_DIR', 'runtime'), ('TMPDIR', 'temp')]: (w / folder).mkdir() env[key] = str(w / folder) (w / 'main.c').write_text('int main(void) { return 0; }\n') (w / 'payload').write_bytes(b'hello') def call(args): return subprocess.run(args, cwd=root, env=env, capture_output=True, text=True) print('REA:', call(['node', 'scripts/rea.mjs', '--version']).stdout.strip()) print('Node:', call(['node', '--version']).stdout.strip()) for label, name in [('control', '__probe'), ('linebreak', 'x\naddr 0x1')]: target = w / label linked = call(['clang', '-arch', 'arm64', '-o', str(target), str(w / 'main.c'), '-Wl,-sectcreate,__DATA,' + name + ',' + str(w / 'payload')]) assert linked.returncode == 0, linked.stderr before = target.read_bytes() found = [] pos = 32 for _ in range(struct.unpack_from('<I', before, 16)[0]): cmd, size = struct.unpack_from('<II', before, pos) if cmd == 0x19: for i in range(struct.unpack_from('<I', before, pos + 64)[0]): at = pos + 72 + i * 80 found.append(before[at:at+16].split(b'\0')[0]) pos += size assert name.encode() in found signed = call(['codesign', '--verify', '--strict', str(target)]) raw = call(['otool', '-h', '-l', str(target)]) arch = call(['node', 'scripts/rea.mjs', 'list-architectures', str(target), '--json']) result = call(['node', 'scripts/rea.mjs', 'inspect-macho', str(target), '--json']) (w / (label + '.json')).write_text(result.stdout) (w / (label + '.otool.txt')).write_text(raw.stdout) assert target.read_bytes() == before j = json.loads(result.stdout) print(label, 'name=' + repr(name), 'codesign=' + str(signed.returncode), 'otool=' + str(raw.returncode), 'architectures=' + str(arch.returncode), 'inspect=' + str(result.returncode)) if result.returncode: print(json.dumps({'code': j['code'], 'reason': j['details']['reason']})) else: print('returned sections:', [s['name'] for g in j['normalized_result']['segments']['items'] for s in g['sections']['items']]) assert signed.returncode == raw.returncode == arch.returncode == 0 assert result.returncode == (0 if label == 'control' else 1) PY ``` The complete script was run successfully twice with the output below. The binary-table assertions confirm the section-name bytes, and the final byte comparison confirms that inspection did not modify either input. ## Expected behavior Inspect the linker-generated executable and preserve its actual section name and the other available Mach-O facts. The native inspection contract includes sections and load commands; the guide promises original raw load-command values, and the name parser's contract says names are returned exactly as observed. A name payload containing an LF must not be interpreted as an additional numeric `addr` field. If the producer's textual representation cannot preserve a name unambiguously, return an explicit limitation alongside the facts that can be retained rather than treating successful tool output as malformed and discarding the whole observation. ## Impact and scope P2: this is a bounded but consequential static-inspection failure for unusual, linker-accepted section names. The affected input loses the complete normalized result, including unrelated sections, load commands and symbol inventories. The ordinary custom-section control succeeds, and both files remain unchanged. Broader frequency, other architectures and segment-name variants were not measured. A manual workaround is available: direct `otool -h -l` retains the section-name text and other load-command facts, and `list-architectures` still succeeds. These do not supply an equivalent successful `inspect-macho` Evidence record. No workaround within that operation was verified. ## REA version Exact `node scripts/rea.mjs --version` output: `6.8.0`. Source commit: `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64. Apple clang/linker, otool and codesign; no deep-analysis provider or GUI was used. ## Target and artifact context Two synthetic thin arm64 Mach-O executables linked from the same C source with the same five-byte custom-section content. One uses `__DATA,__probe`; the other uses `__DATA,x\naddr 0x1` with an actual embedded LF. Both are fresh linker outputs that pass strict signature verification. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed output from the reproduction: ```text REA: 6.8.0 Node: v24.22.1 control name='__probe' codesign=0 otool=0 architectures=0 inspect=0 returned sections: ['__text', '__unwind_info', '__probe'] linebreak name='x\naddr 0x1' codesign=0 otool=0 architectures=0 inspect=1 {"code": "unreadable_output", "reason": "Malformed otool load-command output"} ``` The affected genuine otool capture contains this section header, with the name's LF printed verbatim: ```text Section sectname x addr 0x1 segname __DATA ``` Its normal address field follows separately. Calling the built production parser on that same real output throws `TypeError: otool field addr is missing or repeated`. The CLI's captured producer result has exit code 0, no signal and `truncated: false`. Expected source: `src/contracts/native/nativeToolContracts.ts:102`, `docs/native-investigation.md:541`, and `src/native/parsers/otool.ts:634`. Failure source: `src/native/parsers/otool.ts:470`, `src/native/parsers/otool.ts:535`, `src/native/parsers/otool.ts:559`, and `src/native/NativeMachoInspection.ts:175`. ## Analysis or suspected cause Source-derived explanation: field parsing splits the capture into lines before interpreting the fixed-width section name. The name continuation becomes another `addr` field, so the required-field uniqueness check rejects it. The inspection workflow translates that parser exception into the observed error and returns no partial observation. The direct production-parser replay corroborates the exception; no fix was implemented or tested. ## Additional context Related closed reports cover different inputs and failures: #6818 concerns surrounding whitespace trimmed during successful section-name parsing, #5680 concerns the literal name `version`, and #760 concerns LF in a filename affecting lipo architecture parsing. Their existing fixes do not handle an LF inside a section-name payload. The surrounding-whitespace fix is already present in the tested baseline. Open and closed issue searches found no report of this specific failure.
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#4396
No description provided.