[GH-ISSUE #5589] [Bug] Resource inspection rejects app bundles without CFBundleExecutable #3394

Closed
opened 2026-10-31 00:39:53 +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/5589

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

inspect-plist, inspect-keyed-archive, inspect-asset-catalog, and decode-interface-builder reject an Apple app bundle whose Info.plist omits CFBundleExecutable, even when it has a readable Mach-O program and resources. Each exits 1 with target_unavailable before returning resource Evidence. Adding only the executable declaration to a disposable copy makes the same commands succeed.

This was independently reproduced with a synthetic bundle and Muse 6.0. CoreFoundation resolved the missing-key Muse copy's existing program, and Apple's assetutil read its catalog, without launching the app.

Reproduction

The following script was executed from the built source checkout on macOS. It creates an isolated synthetic bundle with a copied system true executable and a valid binary keyed archive; it never runs the copied executable. Dependencies and the built REA runtime must already be available. Each command runs once per variant, and the verifier also replayed this script unchanged.

python3 - <<'PY'
from pathlib import Path
import json, os, plistlib, shutil, subprocess, tempfile

entry = Path('scripts/rea.mjs').resolve()
work = Path(os.environ.get('REA_REPRO_WORK') or tempfile.mkdtemp()).resolve()
work.mkdir(parents=True, exist_ok=True)
for name in ['home', 'config', 'data', 'cache', 'state', 'database', 'output']:
    (work / name).mkdir(exist_ok=True)
app = work / 'Fixture.app'
(app / 'Contents/MacOS').mkdir(parents=True)
(app / 'Contents/Resources').mkdir()
subprocess.run(['cp', '-c', shutil.which('true'), str(app / 'Contents/MacOS/Fixture')], check=True)
info = {'CFBundleName': 'Fixture', 'CFBundlePackageType': 'APPL'}
plist = app / 'Contents/Info.plist'
plist.write_bytes(plistlib.dumps(info))
archive = {'$archiver': 'NSKeyedArchiver', '$version': 100000,
           '$objects': ['$null', 'hello'], '$top': {'root': plistlib.UID(1)}}
(app / 'Contents/Resources/Example.plist').write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_BINARY))
env = {'PATH': os.environ['PATH'], 'HOME': str(work / 'home'),
       'XDG_CONFIG_HOME': str(work / 'config'), 'XDG_DATA_HOME': str(work / 'data'),
       'XDG_CACHE_HOME': str(work / 'cache'), 'XDG_STATE_HOME': str(work / 'state'),
       'TMPDIR': str(work / 'database'),
       'REA_IDA_MCP_CONFIG': str(work / 'config/ida.json'), 'REA_LOG_LEVEL': 'silent'}
commands = ['inspect-plist', 'inspect-keyed-archive', 'inspect-asset-catalog', 'decode-interface-builder']
for variant in ['missing', 'declared']:
    if variant == 'declared':
        info['CFBundleExecutable'] = 'Fixture'
        plist.write_bytes(plistlib.dumps(info))
    for command in commands:
        args = ['node', str(entry), command, str(app)]
        if command == 'inspect-keyed-archive':
            args.append('Contents/Resources/Example.plist')
        result = subprocess.run(args + ['--format', 'json'], cwd=work, env=env, capture_output=True, text=True, timeout=60)
        (work / 'output' / f'{variant}-{command}.json').write_text(result.stdout)
        (work / 'output' / f'{variant}-{command}.stderr').write_text(result.stderr)
        value = json.loads(result.stdout)
        print(variant, command, result.returncode, value.get('code', 'success'))
        assert result.returncode == (1 if variant == 'missing' else 0)
        if variant == 'missing':
            assert value['code'] == 'target_unavailable'
            assert 'CFBundleExecutable is missing' in value['message']
        elif command == 'inspect-keyed-archive':
            assert value['normalized_result']['objects'][1]['value'] == 'hello'
print('Evidence directory:', work / 'output')
PY

The synthetic fixture deliberately has no asset catalog or Interface Builder resources. Its declared-key controls return explicit empty inventories for those two commands; the keyed-archive control returns hello through root UID 1.

Expected behavior

A readable app bundle whose program can be resolved without an explicit executable key should permit these resource queries: read its Info.plist and selected keyed archive, and inspect its catalog/interface resources or report their absence. The omission alone should not prevent resource inspection. Invalid plists and unsafe or unresolved program paths should retain appropriate errors.

The advertised app inputs are in src/cli/artifactCommands.ts:30,118,155; src/cli/utilityCommands.ts:192 advertises app/Mach-O plist inspection with the bundle Info.plist as its default. docs/native-investigation.md:356,360,383 documents these resource operations. These references are at the tested source revision below. CoreFoundation resolution and the passing declared-key controls are the observed applicability evidence; plist syntax acceptance alone is not the basis for this expectation.

Impact and scope

Four documented resource-inspection routes fail for a bounded class of bundles. This was observed in Muse 6.0 and the synthetic fixture; prevalence beyond them is unknown. No original app was modified or launched, and no data loss was observed.

A demonstrated workaround adds CFBundleExecutable to a disposable copy. Executable and catalog bytes remain unchanged, but Info.plist and bundle identity change, so this cannot inspect the untouched plist or preserve original bundle identity.

REA version

6.7.0; tested source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64. Public CLI commands ran from the built checkout with isolated HOME, XDG, configuration, temporary database space, and output directories. No Hopper, Ghidra, or IDA session was used.

Target and artifact context

A synthetic macOS-layout app bundle with an XML Info.plist, a readable Mach-O program, and a valid two-object binary NSKeyedArchiver archive; also disposable copies of Muse 6.0 with existing asset catalog and Interface Builder resources. The failing Info.plist omits CFBundleExecutable while retaining CFBundleName and CFBundlePackageType: APPL.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Independent CLI observations, for each of the four commands:

missing-key variant:  exit 1, code target_unavailable
reason: CFBundleExecutable is missing
app Info.plist is malformed, unsupported, or lacks CFBundleExecutable

declared-key control: exit 0

For the missing-key Muse copy, plutil -lint succeeded, CFBundleCreate / CFBundleCopyExecutableURL resolved the real program, and assetutil --info returned 681 records. All four REA commands nevertheless failed at bundle admission. On a second copy with the executable key added, asset inspection returned 681 records, Interface Builder decoding returned five documents, and plist inspection succeeded. The catalog SHA-256 matched across copies. A valid keyed-archive fixture added temporarily to both copies failed on the missing-key copy and decoded hello on the declared-key control. Assets.car was not treated as a keyed archive.

Typed outcome or recovery expected

With the synthetic key still omitted, plist inspection should preserve that omission, keyed-archive inspection should return hello, and the two resource inventories should report empty collections. Existing Muse resources should remain inspectable. These are proposed post-fix checks; the observed successes are declared-key input controls, not an implemented fix.

Analysis or suspected cause

Source-derived reasoning: all four operations open the target before reading the selected resource. src/application/DirectAnalysis.ts:159,340 routes through session opening; src/application/binary/BinarySessionOpen.ts:81 invokes target resolution; src/application/BinaryTargetResolver.ts:108,463 reaches bundle program resolution. src/application/AppBundleExecutable.ts:272,276 requires a string CFBundleExecutable, and :211 constructs the rejection.

A fresh upstream check at origin/main commit 080c0e2b2b82a7d9394b59d3d86630d9d85346a5 found the executable resolver, session-opening module, CLI contracts, and cited documentation unchanged from the tested revision. Later changes in DirectAnalysis concern Evidence handling, and later BinaryTargetResolver changes concern Mach-O header validation after bundle resolution. This is a source check of current main, not a claimed runtime reproduction on that revision.

Additional context

Prepared with AI assistance and independently reproduced through the public CLI. No REA implementation changes were applied. Open and closed issues and PRs were searched using the command name, symptom, and source module, including merged PRs. Related reports about binary plist decoding (#1476), other bundle types (#4310), and default plist selection (#3774) concern different inputs and behavior.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5589 ### 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 `inspect-plist`, `inspect-keyed-archive`, `inspect-asset-catalog`, and `decode-interface-builder` reject an Apple app bundle whose Info.plist omits `CFBundleExecutable`, even when it has a readable Mach-O program and resources. Each exits 1 with `target_unavailable` before returning resource Evidence. Adding only the executable declaration to a disposable copy makes the same commands succeed. This was independently reproduced with a synthetic bundle and Muse 6.0. CoreFoundation resolved the missing-key Muse copy's existing program, and Apple's assetutil read its catalog, without launching the app. ### Reproduction The following script was executed from the built source checkout on macOS. It creates an isolated synthetic bundle with a copied system true executable and a valid binary keyed archive; it never runs the copied executable. Dependencies and the built REA runtime must already be available. Each command runs once per variant, and the verifier also replayed this script unchanged. ```sh python3 - <<'PY' from pathlib import Path import json, os, plistlib, shutil, subprocess, tempfile entry = Path('scripts/rea.mjs').resolve() work = Path(os.environ.get('REA_REPRO_WORK') or tempfile.mkdtemp()).resolve() work.mkdir(parents=True, exist_ok=True) for name in ['home', 'config', 'data', 'cache', 'state', 'database', 'output']: (work / name).mkdir(exist_ok=True) app = work / 'Fixture.app' (app / 'Contents/MacOS').mkdir(parents=True) (app / 'Contents/Resources').mkdir() subprocess.run(['cp', '-c', shutil.which('true'), str(app / 'Contents/MacOS/Fixture')], check=True) info = {'CFBundleName': 'Fixture', 'CFBundlePackageType': 'APPL'} plist = app / 'Contents/Info.plist' plist.write_bytes(plistlib.dumps(info)) archive = {'$archiver': 'NSKeyedArchiver', '$version': 100000, '$objects': ['$null', 'hello'], '$top': {'root': plistlib.UID(1)}} (app / 'Contents/Resources/Example.plist').write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_BINARY)) env = {'PATH': os.environ['PATH'], 'HOME': str(work / 'home'), 'XDG_CONFIG_HOME': str(work / 'config'), 'XDG_DATA_HOME': str(work / 'data'), 'XDG_CACHE_HOME': str(work / 'cache'), 'XDG_STATE_HOME': str(work / 'state'), 'TMPDIR': str(work / 'database'), 'REA_IDA_MCP_CONFIG': str(work / 'config/ida.json'), 'REA_LOG_LEVEL': 'silent'} commands = ['inspect-plist', 'inspect-keyed-archive', 'inspect-asset-catalog', 'decode-interface-builder'] for variant in ['missing', 'declared']: if variant == 'declared': info['CFBundleExecutable'] = 'Fixture' plist.write_bytes(plistlib.dumps(info)) for command in commands: args = ['node', str(entry), command, str(app)] if command == 'inspect-keyed-archive': args.append('Contents/Resources/Example.plist') result = subprocess.run(args + ['--format', 'json'], cwd=work, env=env, capture_output=True, text=True, timeout=60) (work / 'output' / f'{variant}-{command}.json').write_text(result.stdout) (work / 'output' / f'{variant}-{command}.stderr').write_text(result.stderr) value = json.loads(result.stdout) print(variant, command, result.returncode, value.get('code', 'success')) assert result.returncode == (1 if variant == 'missing' else 0) if variant == 'missing': assert value['code'] == 'target_unavailable' assert 'CFBundleExecutable is missing' in value['message'] elif command == 'inspect-keyed-archive': assert value['normalized_result']['objects'][1]['value'] == 'hello' print('Evidence directory:', work / 'output') PY ``` The synthetic fixture deliberately has no asset catalog or Interface Builder resources. Its declared-key controls return explicit empty inventories for those two commands; the keyed-archive control returns `hello` through root UID 1. ### Expected behavior A readable app bundle whose program can be resolved without an explicit executable key should permit these resource queries: read its Info.plist and selected keyed archive, and inspect its catalog/interface resources or report their absence. The omission alone should not prevent resource inspection. Invalid plists and unsafe or unresolved program paths should retain appropriate errors. The advertised app inputs are in `src/cli/artifactCommands.ts:30,118,155`; `src/cli/utilityCommands.ts:192` advertises app/Mach-O plist inspection with the bundle Info.plist as its default. `docs/native-investigation.md:356,360,383` documents these resource operations. These references are at the tested source revision below. CoreFoundation resolution and the passing declared-key controls are the observed applicability evidence; plist syntax acceptance alone is not the basis for this expectation. ### Impact and scope Four documented resource-inspection routes fail for a bounded class of bundles. This was observed in Muse 6.0 and the synthetic fixture; prevalence beyond them is unknown. No original app was modified or launched, and no data loss was observed. A demonstrated workaround adds `CFBundleExecutable` to a disposable copy. Executable and catalog bytes remain unchanged, but Info.plist and bundle identity change, so this cannot inspect the untouched plist or preserve original bundle identity. ### REA version 6.7.0; tested source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8`. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64. Public CLI commands ran from the built checkout with isolated HOME, XDG, configuration, temporary database space, and output directories. No Hopper, Ghidra, or IDA session was used. ### Target and artifact context A synthetic macOS-layout app bundle with an XML Info.plist, a readable Mach-O program, and a valid two-object binary NSKeyedArchiver archive; also disposable copies of Muse 6.0 with existing asset catalog and Interface Builder resources. The failing Info.plist omits `CFBundleExecutable` while retaining `CFBundleName` and `CFBundlePackageType: APPL`. ### Interface CLI ### Earliest failure stage Setup or provider admission ### Relevant output or evidence Independent CLI observations, for each of the four commands: ```text missing-key variant: exit 1, code target_unavailable reason: CFBundleExecutable is missing app Info.plist is malformed, unsupported, or lacks CFBundleExecutable declared-key control: exit 0 ``` For the missing-key Muse copy, `plutil -lint` succeeded, `CFBundleCreate` / `CFBundleCopyExecutableURL` resolved the real program, and `assetutil --info` returned 681 records. All four REA commands nevertheless failed at bundle admission. On a second copy with the executable key added, asset inspection returned 681 records, Interface Builder decoding returned five documents, and plist inspection succeeded. The catalog SHA-256 matched across copies. A valid keyed-archive fixture added temporarily to both copies failed on the missing-key copy and decoded `hello` on the declared-key control. Assets.car was not treated as a keyed archive. ### Typed outcome or recovery expected With the synthetic key still omitted, plist inspection should preserve that omission, keyed-archive inspection should return `hello`, and the two resource inventories should report empty collections. Existing Muse resources should remain inspectable. These are proposed post-fix checks; the observed successes are declared-key input controls, not an implemented fix. ### Analysis or suspected cause Source-derived reasoning: all four operations open the target before reading the selected resource. `src/application/DirectAnalysis.ts:159,340` routes through session opening; `src/application/binary/BinarySessionOpen.ts:81` invokes target resolution; `src/application/BinaryTargetResolver.ts:108,463` reaches bundle program resolution. `src/application/AppBundleExecutable.ts:272,276` requires a string `CFBundleExecutable`, and `:211` constructs the rejection. A fresh upstream check at `origin/main` commit `080c0e2b2b82a7d9394b59d3d86630d9d85346a5` found the executable resolver, session-opening module, CLI contracts, and cited documentation unchanged from the tested revision. Later changes in DirectAnalysis concern Evidence handling, and later BinaryTargetResolver changes concern Mach-O header validation after bundle resolution. This is a source check of current main, not a claimed runtime reproduction on that revision. ### Additional context Prepared with AI assistance and independently reproduced through the public CLI. No REA implementation changes were applied. Open and closed issues and PRs were searched using the command name, symptom, and source module, including merged PRs. Related reports about binary plist decoding (#1476), other bundle types (#4310), and default plist selection (#3774) concern different inputs and behavior.
Author
Owner

@morluto commented on GitHub (Oct 21, 2026):

Closing as fixed on main by github.com/morluto/rea@6a6adf703f, retained at 67a6477f01.

Resource-only bundle sessions retain the selected resource root without requiring an executable identity. The bundle-resource CLI/MCP acceptance matrix covers all four reported operations with the executable key omitted; binary/snapshot operations remain explicitly inapplicable. No new native app/provider run was performed.

This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.

<!-- gh-comment-id:6324267050 --> @morluto commented on GitHub (Oct 21, 2026): Closing as fixed on main by https://github.com/morluto/rea/commit/6a6adf703fa44100643947d20fbc670f86260b1b, retained at 67a6477f011ae358fc333e5a6e26c86d932654ed. Resource-only bundle sessions retain the selected resource root without requiring an executable identity. The bundle-resource CLI/MCP acceptance matrix covers all four reported operations with the executable key omitted; binary/snapshot operations remain explicitly inapplicable. No new native app/provider run was performed. This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.
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#3394
No description provided.