[GH-ISSUE #7615] [Bug] IPA graph treats a macOS bundle outside Payload as an application root #4543

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

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

The public node scripts/rea.mjs inspect-artifact <fixture.ipa> --format json → node scripts/rea.mjs project-apple-application-graph <request.json> --format json workflow accepts an IPA containing Payload/Main.app and additional files beneath Extras/Helper.app/Contents/. The graph identifies both directories as application roots and reports platforms: ["ios", "macos"].

That extra root changes bridge grouping: Shared/other.js no longer pairs with the helper, even though both remain in the component arrays. An IPA control with the same scripts and helper bytes, but with the helper outside a Contents/ hierarchy, retains that unrooted pair. Every inspector and projection call exits successfully.

Reproduction

  1. Use the prepared source build at the revision below, with Node.js v24.22.1 selected on PATH. Run the following from the repository root. It creates both fixtures, uses isolated HOME/XDG paths for every REA process, and deletes its temporary inputs afterward. It requires Python 3 and no analysis provider.
import json, os, pathlib, subprocess, tempfile, zipfile

with tempfile.TemporaryDirectory() as directory:
    work = pathlib.Path(directory)
    env = {'PATH': os.environ['PATH'], 'HOME': str(work / 'home')}
    for variable in ('XDG_CONFIG_HOME', 'XDG_CACHE_HOME',
                     'XDG_DATA_HOME', 'XDG_STATE_HOME'):
        env[variable] = str(work / variable.lower())
    (work / 'home').mkdir()
    def cli(*args):
        run = subprocess.run(['node', 'scripts/rea.mjs', *args,
                              '--format', 'json'], env=env,
                             capture_output=True, text=True, check=True)
        return json.loads(run.stdout)
    macho = bytes.fromhex('cffaedfe070000010300008002000000') + bytes(40)
    for label, location in (('data', 'MacOS/Helper'),
                            ('deep', 'Contents/MacOS/Helper')):
        archive = work / (label + '.ipa')
        with zipfile.ZipFile(archive, 'w') as out:
            out.writestr('Payload/Main.app/Info.plist', '<plist><dict/></plist>')
            out.writestr('Payload/Main.app/Main', macho)
            out.writestr('Extras/Helper.app/Resources/helper.js', 'console.log(1)')
            out.writestr('Shared/other.js', 'console.log(2)')
            out.writestr('Extras/Helper.app/' + location, macho)
            if label == 'deep':
                out.writestr('Extras/Helper.app/Contents/Info.plist',
                             '<plist><dict/></plist>')
        inspected = cli('inspect-artifact', str(archive))
        request = work / 'request.json'
        request.write_text(json.dumps({'inventory_evidence': [inspected]}))
        graph = cli('project-apple-application-graph', str(request))['normalized_result']
        print(label, json.dumps({
            'root_format': graph['root_format'],
            'application_roots': graph['application_roots'],
            'platforms': graph['platforms'],
            'bridge_candidates': [[c['source_path'], c['native_path']]
                                  for c in graph['bridge_candidates']],
            'coverage': graph['coverage']['status']
        }, separators=(',', ':')))
  1. Compare application_roots, platforms, and the presence of the Shared/other.js bridge candidate in the two printed lines. The deep case adds an application outside Payload/ and loses that pair.

This exact shorter reproduction ran once. An independently authored expanded harness also ran both cases twice through the same public commands, with the same distinguishing results in all three executions. Its ordinary IPA and regular-leaf Contents controls retained only the Payload application.

Expected behavior

The Apple application guide assigns IPA application roots to Payload/<Name>.app. Its outermost app-with-Contents rule applies to directory, ZIP and DMG inventories. It also requires IPA components outside every application root to remain in the component lists and their script/native hypotheses to pair with other unrooted components.

Both IPA variants should therefore have application_roots: ["Payload/Main.app"] and platforms: ["ios"]. In the deep case, the helper remains an unrooted native component and must pair with both Extras/Helper.app/Resources/helper.js and Shared/other.js. Its additional metadata path does not make it an IPA application root.

Impact and scope

The observed defect affects application attribution, the platform list and path-based hypotheses for an accepted IPA containing a macOS-shaped extra bundle. It does not remove the scripts or native component from the component arrays. All tested component facts remain available for manual analysis. The deep fixture reports complete inventory coverage, just as the control does; this report does not assert that the corrected projection should become partial.

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The bounded input produces a false application/platform claim and drops a documented unrooted pair. Ordinary IPA controls still work; no persistent data loss was observed.
  • Workaround status: available
  • Workaround basis: For the tested fixture, restricting application roots to the documented Payload hierarchy and manually pairing retained unrooted JavaScript/native components recovers both pairs. This manual reconstruction was checked against the actual deep projection. Filtering the displayed root list alone does not repair its bridge candidates.

Only synthetic IPAs were exercised. Real distributed packages, unread members, signing attribution, actual runtime loading, the released npm package and runtime MCP behavior were not tested.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a. The prepared build was used without rebuilding or updating dependencies.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. Python 3 generated the archives. No native analysis provider, device or GUI application was involved.

Target and artifact context

Two authored ZIP-format IPA files. Each contains a shallow Payload application, two scripts and a helper. The deep variant moves the helper from MacOS/Helper to Contents/MacOS/Helper and adds a readable Contents/Info.plist outside Payload. Both script paths stay unchanged. The native members contain a 56-byte thin Mach-O-magic prefix fixture, detected as mach-o by inventory; they are not asserted to be complete runnable binaries. No member is executed or decrypted.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The exact reproduction printed:

data {"root_format":"ipa","application_roots":["Payload/Main.app"],"platforms":["ios"],"bridge_candidates":[["Extras/Helper.app/Resources/helper.js","Extras/Helper.app/MacOS/Helper"],["Shared/other.js","Extras/Helper.app/MacOS/Helper"]],"coverage":"complete-within-inventory"}
deep {"root_format":"ipa","application_roots":["Extras/Helper.app","Payload/Main.app"],"platforms":["ios","macos"],"bridge_candidates":[["Extras/Helper.app/Resources/helper.js","Extras/Helper.app/Contents/MacOS/Helper"]],"coverage":"complete-within-inventory"}

The expanded inspection confirmed that both inventories have root format ipa. In the deep result, the extra bundle has role application, layout macos-deep, its deep plist path, and its helper as an executable candidate. Inventory retains both scripts with format javascript-bundle and the helper with format mach-o; all have verified file identity. The projection retains those paths in its component arrays, so the absent Shared/other.js pair is a grouping effect.

Baseline contract and implementation locators:

  • docs/apple-application-analysis.md:52-55 documents unrooted IPA pairing; :74-78 assigns roots per container type; :95-105 describes attribution and platform inference.
  • src/domain/apple/appleBundleAnatomy.ts:114-147 discovers roots and consults the container format only for an explicitly selected .app directory.
  • src/domain/apple/appleBundleAnatomy.ts:364-376 derives the platform list from application-role bundles.
  • src/domain/apple/appleApplication.ts:110-144 passes the inventory format into anatomy and its roots into classification/pairing; :338-363 pairs by root ownership.

Typed outcome or recovery expected

Keep Extras/Helper.app out of IPA application_roots and application bundle attribution. Preserve all its component paths, derive platforms from the Payload application, and include the two unrooted helper/script candidates. Preserve the separate root rules for other container formats.

Analysis or suspected cause

Source-derived: root discovery checks both the Payload pattern and the app/Contents pattern for every supported inventory format. It therefore admits a real macOS-shaped directory inside an IPA as though the archive were a collection of macOS applications. Root-dependent platform inference and bridge ownership then propagate that admission.

Additional context

Searched issues in both states, PRs in all states and issue comments for Apple/IPA projection, Payload, application roots, Contents, bundle anatomy and outside-root components.

Issue #4779 is related but has a different closure condition: leaf files and symlinks must not establish directories. Its fix, 149a0fc06c3c67ee45624e6da14dfb0754473758, is present in this baseline and correctly excludes the regular-leaf control. Inspection of that fix shows it preserves the shared root-pattern rules; it does not add IPA format scoping. This report is not a recurrence of its leaf-identity defect. Merged PR #922 already described IPA roots as Payload apps, unchanged, and introduced the separate macOS-container rule.

Issue #3359 concerns layout within an existing application, #3174 concerns retaining already-unrooted extensionless Mach-O, and #4688 concerns unread file identity. Issue #5229 concerns unreadable JavaScript directory analysis and omitted inventory coverage. Their fixes or requested outcomes do not resolve this root admission. No direct duplicate was found.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7615 ### 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 The public `node scripts/rea.mjs inspect-artifact <fixture.ipa> --format json` → `node scripts/rea.mjs project-apple-application-graph <request.json> --format json` workflow accepts an IPA containing `Payload/Main.app` and additional files beneath `Extras/Helper.app/Contents/`. The graph identifies both directories as application roots and reports `platforms: ["ios", "macos"]`. That extra root changes bridge grouping: `Shared/other.js` no longer pairs with the helper, even though both remain in the component arrays. An IPA control with the same scripts and helper bytes, but with the helper outside a `Contents/` hierarchy, retains that unrooted pair. Every inspector and projection call exits successfully. ### Reproduction 1. Use the prepared source build at the revision below, with Node.js v24.22.1 selected on PATH. Run the following from the repository root. It creates both fixtures, uses isolated HOME/XDG paths for every REA process, and deletes its temporary inputs afterward. It requires Python 3 and no analysis provider. ```python import json, os, pathlib, subprocess, tempfile, zipfile with tempfile.TemporaryDirectory() as directory: work = pathlib.Path(directory) env = {'PATH': os.environ['PATH'], 'HOME': str(work / 'home')} for variable in ('XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME'): env[variable] = str(work / variable.lower()) (work / 'home').mkdir() def cli(*args): run = subprocess.run(['node', 'scripts/rea.mjs', *args, '--format', 'json'], env=env, capture_output=True, text=True, check=True) return json.loads(run.stdout) macho = bytes.fromhex('cffaedfe070000010300008002000000') + bytes(40) for label, location in (('data', 'MacOS/Helper'), ('deep', 'Contents/MacOS/Helper')): archive = work / (label + '.ipa') with zipfile.ZipFile(archive, 'w') as out: out.writestr('Payload/Main.app/Info.plist', '<plist><dict/></plist>') out.writestr('Payload/Main.app/Main', macho) out.writestr('Extras/Helper.app/Resources/helper.js', 'console.log(1)') out.writestr('Shared/other.js', 'console.log(2)') out.writestr('Extras/Helper.app/' + location, macho) if label == 'deep': out.writestr('Extras/Helper.app/Contents/Info.plist', '<plist><dict/></plist>') inspected = cli('inspect-artifact', str(archive)) request = work / 'request.json' request.write_text(json.dumps({'inventory_evidence': [inspected]})) graph = cli('project-apple-application-graph', str(request))['normalized_result'] print(label, json.dumps({ 'root_format': graph['root_format'], 'application_roots': graph['application_roots'], 'platforms': graph['platforms'], 'bridge_candidates': [[c['source_path'], c['native_path']] for c in graph['bridge_candidates']], 'coverage': graph['coverage']['status'] }, separators=(',', ':'))) ``` 2. Compare `application_roots`, `platforms`, and the presence of the `Shared/other.js` bridge candidate in the two printed lines. The deep case adds an application outside `Payload/` and loses that pair. This exact shorter reproduction ran once. An independently authored expanded harness also ran both cases twice through the same public commands, with the same distinguishing results in all three executions. Its ordinary IPA and regular-leaf `Contents` controls retained only the Payload application. ### Expected behavior The Apple application guide assigns IPA application roots to `Payload/<Name>.app`. Its outermost app-with-Contents rule applies to directory, ZIP and DMG inventories. It also requires IPA components outside every application root to remain in the component lists and their script/native hypotheses to pair with other unrooted components. Both IPA variants should therefore have `application_roots: ["Payload/Main.app"]` and `platforms: ["ios"]`. In the deep case, the helper remains an unrooted native component and must pair with both `Extras/Helper.app/Resources/helper.js` and `Shared/other.js`. Its additional metadata path does not make it an IPA application root. ### Impact and scope The observed defect affects application attribution, the platform list and path-based hypotheses for an accepted IPA containing a macOS-shaped extra bundle. It does not remove the scripts or native component from the component arrays. All tested component facts remain available for manual analysis. The deep fixture reports complete inventory coverage, just as the control does; this report does not assert that the corrected projection should become partial. - Impact level: P2 - Assessment status: supported - Impact basis: The bounded input produces a false application/platform claim and drops a documented unrooted pair. Ordinary IPA controls still work; no persistent data loss was observed. - Workaround status: available - Workaround basis: For the tested fixture, restricting application roots to the documented Payload hierarchy and manually pairing retained unrooted JavaScript/native components recovers both pairs. This manual reconstruction was checked against the actual deep projection. Filtering the displayed root list alone does not repair its bridge candidates. Only synthetic IPAs were exercised. Real distributed packages, unread members, signing attribution, actual runtime loading, the released npm package and runtime MCP behavior were not tested. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. The prepared build was used without rebuilding or updating dependencies. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. Python 3 generated the archives. No native analysis provider, device or GUI application was involved. ### Target and artifact context Two authored ZIP-format IPA files. Each contains a shallow Payload application, two scripts and a helper. The deep variant moves the helper from `MacOS/Helper` to `Contents/MacOS/Helper` and adds a readable `Contents/Info.plist` outside Payload. Both script paths stay unchanged. The native members contain a 56-byte thin Mach-O-magic prefix fixture, detected as `mach-o` by inventory; they are not asserted to be complete runnable binaries. No member is executed or decrypted. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The exact reproduction printed: ```text data {"root_format":"ipa","application_roots":["Payload/Main.app"],"platforms":["ios"],"bridge_candidates":[["Extras/Helper.app/Resources/helper.js","Extras/Helper.app/MacOS/Helper"],["Shared/other.js","Extras/Helper.app/MacOS/Helper"]],"coverage":"complete-within-inventory"} deep {"root_format":"ipa","application_roots":["Extras/Helper.app","Payload/Main.app"],"platforms":["ios","macos"],"bridge_candidates":[["Extras/Helper.app/Resources/helper.js","Extras/Helper.app/Contents/MacOS/Helper"]],"coverage":"complete-within-inventory"} ``` The expanded inspection confirmed that both inventories have root format `ipa`. In the deep result, the extra bundle has role `application`, layout `macos-deep`, its deep plist path, and its helper as an executable candidate. Inventory retains both scripts with format `javascript-bundle` and the helper with format `mach-o`; all have verified file identity. The projection retains those paths in its component arrays, so the absent `Shared/other.js` pair is a grouping effect. Baseline contract and implementation locators: - `docs/apple-application-analysis.md:52-55` documents unrooted IPA pairing; `:74-78` assigns roots per container type; `:95-105` describes attribution and platform inference. - `src/domain/apple/appleBundleAnatomy.ts:114-147` discovers roots and consults the container format only for an explicitly selected `.app` directory. - `src/domain/apple/appleBundleAnatomy.ts:364-376` derives the platform list from application-role bundles. - `src/domain/apple/appleApplication.ts:110-144` passes the inventory format into anatomy and its roots into classification/pairing; `:338-363` pairs by root ownership. ### Typed outcome or recovery expected Keep `Extras/Helper.app` out of IPA `application_roots` and application bundle attribution. Preserve all its component paths, derive platforms from the Payload application, and include the two unrooted helper/script candidates. Preserve the separate root rules for other container formats. ### Analysis or suspected cause Source-derived: root discovery checks both the Payload pattern and the app/Contents pattern for every supported inventory format. It therefore admits a real macOS-shaped directory inside an IPA as though the archive were a collection of macOS applications. Root-dependent platform inference and bridge ownership then propagate that admission. ### Additional context Searched issues in both states, PRs in all states and issue comments for Apple/IPA projection, Payload, application roots, Contents, bundle anatomy and outside-root components. [Issue #4779](https://github.com/morluto/rea/issues/4779) is related but has a different closure condition: leaf files and symlinks must not establish directories. Its fix, `149a0fc06c3c67ee45624e6da14dfb0754473758`, is present in this baseline and correctly excludes the regular-leaf control. Inspection of that fix shows it preserves the shared root-pattern rules; it does not add IPA format scoping. This report is not a recurrence of its leaf-identity defect. [Merged PR #922](https://github.com/morluto/rea/pull/922) already described IPA roots as Payload apps, unchanged, and introduced the separate macOS-container rule. [Issue #3359](https://github.com/morluto/rea/issues/3359) concerns layout within an existing application, [#3174](https://github.com/morluto/rea/issues/3174) concerns retaining already-unrooted extensionless Mach-O, and [#4688](https://github.com/morluto/rea/issues/4688) concerns unread file identity. [Issue #5229](https://github.com/morluto/rea/issues/5229) concerns unreadable JavaScript directory analysis and omitted inventory coverage. Their fixes or requested outcomes do not resolve this root admission. No direct duplicate was found.
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#4543
No description provided.