[GH-ISSUE #3174] [Bug] IPA projection omits extensionless Mach-O components outside application roots #1111

Closed
opened 2026-10-15 18:31:10 +00:00 by bsakaguchi · 1 comment
Owner

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

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 project-apple-application-graph CLI accepts a complete authenticated synthetic IPA inventory containing SwiftSupport/HelperTool with format mach-o, but the result omits that helper from all nine component arrays. It also omits the helper's path-based hypothesis with SwiftSupport/outside.js. The neighboring SwiftSupport/iphoneos/libswiftCore.dylib remains present and forms the only bridge candidate with that JavaScript component.

The command exits successfully. Candidate coverage reports complete, with one total and emitted candidate and zero omitted candidates. Overall coverage reports complete-within-inventory and inventory_complete: true.

Moving the extensionless helper into Payload/Fixture.app/HelperTool retains it in executables. Changing only the original inventory's root format to ZIP excludes unrooted native and JavaScript entries and emits zero candidates, as that separate scoping contract requires. The ordinary text control stays out of native arrays.

Reproduction

  1. Start from a clean checkout at 58300885207133422af235e0f2b10658cf958b03 with the repository's pinned Node.js/npm toolchain. Prepare the runtime:
npm ci
npm run build:cached
  1. Create the fixture generator. It computes the content-addressed graph and Evidence identifiers rather than submitting placeholder records:
cat > repro-fixture.mjs <<'JS'
import { createHash } from 'node:crypto';
import { writeFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';
const load = (p) => import(pathToFileURL(`${process.cwd()}/dist/domain/${p}.js`));
const { createEvidence } = await load('evidence');
const { digestCanonicalValue: digest } = await load('canonicalDigest');
const sha = (s) => createHash('sha256').update(s).digest('hex');
const id = (s) => `art_${digest({ sha256: s })}`;
const rootSha = sha('fixture-ipa');
const rootId = id(rootSha);
const entries = [
  ['.', 'ipa'],
  ['Payload/Fixture.app/Fixture', 'mach-o'],
  ['Payload/Fixture.app/Info.plist', 'plist'],
  ['SwiftSupport/HelperTool', 'mach-o'],
  ['SwiftSupport/iphoneos/libswiftCore.dylib', 'mach-o'],
  ['SwiftSupport/outside.js', 'javascript-bundle'],
  ['SwiftSupport/notes.txt', 'file'],
];
if (process.argv[3] === 'rooted') entries[3][0] = 'Payload/Fixture.app/HelperTool';
if (process.argv[3] === 'zip') entries[0][1] = 'zip';
const nodes = entries.map(([path, format]) => {
  const hash = path === '.' ? rootSha : sha(path);
  return { artifact_id: id(hash), format: ['mach-o', 'plist'].includes(format) ? format : 'file',
    sha256: hash, size: 1, media_type: null, architecture: null,
    content_state: 'materialized', limitations: [] };
}).sort((a, b) => a.artifact_id < b.artifact_id ? -1 : 1);
const occurrences = entries.map(([path, format]) => ({
  occurrence_id: `occ_${digest(path === '.' ? { root: rootId } :
    { root_artifact_id: rootId, logical_path: path, entry_kind: 'file' })}`,
  artifact_id: id(path === '.' ? rootSha : sha(path)), parent_occurrence_id: null,
  logical_path: path, entry_kind: 'file', artifact_kind: 'resource', artifact_format: format,
  declared_size: null, compressed_size: null, executable: false, encrypted: false,
  hash_status: 'verified', source_location: null, limitations: []
})).sort((a, b) => a.logical_path < b.logical_path ? -1 : 1);
const graph = { nodes, occurrences, edges: [], integrity_contradictions: [] };
const graphSha = digest(graph);
const rootFormat = entries[0][1];
const inventory = createEvidence({ path: `Fixture.${rootFormat}`, sha256: rootSha, format: rootFormat },
  { id: 'rea-artifact-graph', name: 'reproduction fixture', version: '1' },
  { operation: 'inventory_artifact', parameters: {}, confidence: 'observed', authority: 'shipped-artifact',
    result: { manifest: { manifest_id: `agm_${digest({ root_artifact_id: rootId, graph_sha256: graphSha })}`,
      root_artifact_id: rootId, root_sha256: rootSha, root_format: rootFormat,
      graph_sha256: graphSha, node_count: nodes.length, occurrence_count: occurrences.length, edge_count: 0 },
      nodes, occurrences, edges: [], provenance: [], coverage_status: 'complete', limitations: [] } });
writeFileSync(process.argv[2], JSON.stringify({ inventory_evidence: [inventory] }));
JS
node repro-fixture.mjs repro-input.json
node scripts/rea.mjs project-apple-application-graph repro-input.json --format json > repro-output.json
  1. Read the result and check whether the helper appears in any component category:
node --input-type=module <<'JS'
import { readFileSync } from 'node:fs';
const r = JSON.parse(readFileSync('repro-output.json', 'utf8')).normalized_result;
console.log(JSON.stringify({
  helper_present: Object.values(r.components).some(items =>
    items.some(item => item.path === 'SwiftSupport/HelperTool')),
  executables: r.components.executables.map(item => item.path),
  native_libraries: r.components.native_libraries.map(item => item.path),
  bridge_candidates: r.bridge_candidates,
  bridge_candidate_coverage: r.bridge_candidate_coverage,
  coverage: r.coverage
}, null, 2));
JS
  1. Run the controls using the same generator and CLI. The rooted variant moves the extensionless helper into the application. The ZIP variant preserves the original paths and changes the root format:
node repro-fixture.mjs repro-rooted-input.json rooted
node scripts/rea.mjs project-apple-application-graph repro-rooted-input.json --format json > repro-rooted-output.json
node repro-fixture.mjs repro-zip-input.json zip
node scripts/rea.mjs project-apple-application-graph repro-zip-input.json --format json > repro-zip-output.json

Fixture generation and all three public CLI projections were independently executed with a prepared baseline runtime and isolated HOME/XDG directories. npm ci and npm run build:cached are clean-checkout prerequisites; the verifier did not rerun dependency installation or building. The fixture exercises projection of authenticated inventory Evidence, not archive reading or Mach-O byte detection.

Expected behavior

docs/apple-application-analysis.md:52-55 promises that IPA components outside every application root remain in component lists and that their JavaScript/native candidates pair with other unrooted components. Lines 57-61 describe coverage and complete component arrays. Lines 77-83 distinguish ZIP/directory/DMG exclusion from IPA retention, explicitly including SwiftSupport/.

The projection should retain SwiftSupport/HelperTool in a native component category with its exact path/hash identity and emit both unrooted JavaScript/native hypotheses. The rooted executable and Swift dylib should remain present. The helper should stay unrooted rather than acquiring an application-bundle attribution.

Impact and scope

P2. The projection silently loses a valid native inventory component and its associated hypothesis for a bounded IPA input class while reporting complete coverage. The original inventory Evidence retains the omitted record, and the rooted control continues to project. No severe harm, security boundary violation, or failure of the whole analysis workflow was established.

Observed reproduction covers the public CLI with synthetic authenticated inventory. No physical IPA producer, native analysis engine, or MCP request was executed. Source inspection shows the MCP contract shares the workflow, but runtime MCP behavior was not verified. Reading the original inventory preserves knowledge of the helper's presence but does not restore the documented projection output; a caller-side workaround that preserves paths and restores both results remains unverified.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node 26.10.0; macOS 27.0 arm64

Target and artifact context

Complete authenticated synthetic inventory Evidence with seven occurrences: an IPA root, rooted Mach-O and plist, unrooted extensionless Mach-O, unrooted dylib, unrooted JavaScript, and ordinary text. Content-addressed artifact, occurrence, graph, manifest, and Evidence identifiers are computed inline. No physical IPA or real Mach-O bytes are required for this projection-boundary reproduction.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

helper_present: false
executables: ["Payload/Fixture.app/Fixture"]
native_libraries: ["SwiftSupport/iphoneos/libswiftCore.dylib"]
bridge_candidates: [{"source_path":"SwiftSupport/outside.js","native_path":"SwiftSupport/iphoneos/libswiftCore.dylib","basis":"javascript-and-native-content"}]
bridge_candidate_coverage: {"status":"complete","total_candidates":1,"emitted_candidates":1,"omitted_candidates":0}
coverage: {"status":"complete-within-inventory","inventory_complete":true}

Typed outcome or recovery expected

The successful result should retain the helper in a native component array and emit its hypothesis with SwiftSupport/outside.js. This input should have two total and emitted candidates and zero budget omissions. Keep the rooted relocation control working, preserve the ZIP control's exclusion of unrooted entries, and exclude the ordinary text control from native categories.

Analysis or suspected cause

Source-derived at the reported baseline: src/domain/apple/appleApplication.ts:237-242 retains non-sidecar IPA components outside application roots. However, lines 276-280 require application-root membership for Mach-O executables and use filename suffixes for native libraries. An extensionless unrooted Mach-O fits neither array. Lines 162-169 construct native bridge inputs only from executables and native_libraries, so the missing component cannot form its hypothesis.

Source-derived reachability: src/cli/applicationCommands.ts:112-128 registers the public command and dispatches to the shared projection service. src/domain/artifactInventoryEvidence.ts:55-84 authenticates and validates inventory pages. The successful CLI reproduction establishes that this concrete fixture passes those guards.

The complete candidate coverage counts the already-classified candidate set. This report establishes the missing native component and consequent missing pair; it does not establish a defect in byte-budget omission accounting.

Additional context

P2 follows from consequential incorrect projection data for a bounded IPA input class. Rooted components continue to project, and the original inventory retains the missing component. The verified evidence does not support a higher impact rating.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3174 ### 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 `project-apple-application-graph` CLI accepts a complete authenticated synthetic IPA inventory containing `SwiftSupport/HelperTool` with format `mach-o`, but the result omits that helper from all nine component arrays. It also omits the helper's path-based hypothesis with `SwiftSupport/outside.js`. The neighboring `SwiftSupport/iphoneos/libswiftCore.dylib` remains present and forms the only bridge candidate with that JavaScript component. The command exits successfully. Candidate coverage reports `complete`, with one total and emitted candidate and zero omitted candidates. Overall coverage reports `complete-within-inventory` and `inventory_complete: true`. Moving the extensionless helper into `Payload/Fixture.app/HelperTool` retains it in `executables`. Changing only the original inventory's root format to ZIP excludes unrooted native and JavaScript entries and emits zero candidates, as that separate scoping contract requires. The ordinary text control stays out of native arrays. ### Reproduction 1. Start from a clean checkout at `58300885207133422af235e0f2b10658cf958b03` with the repository's pinned Node.js/npm toolchain. Prepare the runtime: ```sh npm ci npm run build:cached ``` 2. Create the fixture generator. It computes the content-addressed graph and Evidence identifiers rather than submitting placeholder records: ```sh cat > repro-fixture.mjs <<'JS' import { createHash } from 'node:crypto'; import { writeFileSync } from 'node:fs'; import { pathToFileURL } from 'node:url'; const load = (p) => import(pathToFileURL(`${process.cwd()}/dist/domain/${p}.js`)); const { createEvidence } = await load('evidence'); const { digestCanonicalValue: digest } = await load('canonicalDigest'); const sha = (s) => createHash('sha256').update(s).digest('hex'); const id = (s) => `art_${digest({ sha256: s })}`; const rootSha = sha('fixture-ipa'); const rootId = id(rootSha); const entries = [ ['.', 'ipa'], ['Payload/Fixture.app/Fixture', 'mach-o'], ['Payload/Fixture.app/Info.plist', 'plist'], ['SwiftSupport/HelperTool', 'mach-o'], ['SwiftSupport/iphoneos/libswiftCore.dylib', 'mach-o'], ['SwiftSupport/outside.js', 'javascript-bundle'], ['SwiftSupport/notes.txt', 'file'], ]; if (process.argv[3] === 'rooted') entries[3][0] = 'Payload/Fixture.app/HelperTool'; if (process.argv[3] === 'zip') entries[0][1] = 'zip'; const nodes = entries.map(([path, format]) => { const hash = path === '.' ? rootSha : sha(path); return { artifact_id: id(hash), format: ['mach-o', 'plist'].includes(format) ? format : 'file', sha256: hash, size: 1, media_type: null, architecture: null, content_state: 'materialized', limitations: [] }; }).sort((a, b) => a.artifact_id < b.artifact_id ? -1 : 1); const occurrences = entries.map(([path, format]) => ({ occurrence_id: `occ_${digest(path === '.' ? { root: rootId } : { root_artifact_id: rootId, logical_path: path, entry_kind: 'file' })}`, artifact_id: id(path === '.' ? rootSha : sha(path)), parent_occurrence_id: null, logical_path: path, entry_kind: 'file', artifact_kind: 'resource', artifact_format: format, declared_size: null, compressed_size: null, executable: false, encrypted: false, hash_status: 'verified', source_location: null, limitations: [] })).sort((a, b) => a.logical_path < b.logical_path ? -1 : 1); const graph = { nodes, occurrences, edges: [], integrity_contradictions: [] }; const graphSha = digest(graph); const rootFormat = entries[0][1]; const inventory = createEvidence({ path: `Fixture.${rootFormat}`, sha256: rootSha, format: rootFormat }, { id: 'rea-artifact-graph', name: 'reproduction fixture', version: '1' }, { operation: 'inventory_artifact', parameters: {}, confidence: 'observed', authority: 'shipped-artifact', result: { manifest: { manifest_id: `agm_${digest({ root_artifact_id: rootId, graph_sha256: graphSha })}`, root_artifact_id: rootId, root_sha256: rootSha, root_format: rootFormat, graph_sha256: graphSha, node_count: nodes.length, occurrence_count: occurrences.length, edge_count: 0 }, nodes, occurrences, edges: [], provenance: [], coverage_status: 'complete', limitations: [] } }); writeFileSync(process.argv[2], JSON.stringify({ inventory_evidence: [inventory] })); JS node repro-fixture.mjs repro-input.json node scripts/rea.mjs project-apple-application-graph repro-input.json --format json > repro-output.json ``` 3. Read the result and check whether the helper appears in any component category: ```sh node --input-type=module <<'JS' import { readFileSync } from 'node:fs'; const r = JSON.parse(readFileSync('repro-output.json', 'utf8')).normalized_result; console.log(JSON.stringify({ helper_present: Object.values(r.components).some(items => items.some(item => item.path === 'SwiftSupport/HelperTool')), executables: r.components.executables.map(item => item.path), native_libraries: r.components.native_libraries.map(item => item.path), bridge_candidates: r.bridge_candidates, bridge_candidate_coverage: r.bridge_candidate_coverage, coverage: r.coverage }, null, 2)); JS ``` 4. Run the controls using the same generator and CLI. The rooted variant moves the extensionless helper into the application. The ZIP variant preserves the original paths and changes the root format: ```sh node repro-fixture.mjs repro-rooted-input.json rooted node scripts/rea.mjs project-apple-application-graph repro-rooted-input.json --format json > repro-rooted-output.json node repro-fixture.mjs repro-zip-input.json zip node scripts/rea.mjs project-apple-application-graph repro-zip-input.json --format json > repro-zip-output.json ``` Fixture generation and all three public CLI projections were independently executed with a prepared baseline runtime and isolated HOME/XDG directories. `npm ci` and `npm run build:cached` are clean-checkout prerequisites; the verifier did not rerun dependency installation or building. The fixture exercises projection of authenticated inventory Evidence, not archive reading or Mach-O byte detection. ### Expected behavior `docs/apple-application-analysis.md:52-55` promises that IPA components outside every application root remain in component lists and that their JavaScript/native candidates pair with other unrooted components. Lines 57-61 describe coverage and complete component arrays. Lines 77-83 distinguish ZIP/directory/DMG exclusion from IPA retention, explicitly including `SwiftSupport/`. The projection should retain `SwiftSupport/HelperTool` in a native component category with its exact path/hash identity and emit both unrooted JavaScript/native hypotheses. The rooted executable and Swift dylib should remain present. The helper should stay unrooted rather than acquiring an application-bundle attribution. ### Impact and scope P2. The projection silently loses a valid native inventory component and its associated hypothesis for a bounded IPA input class while reporting complete coverage. The original inventory Evidence retains the omitted record, and the rooted control continues to project. No severe harm, security boundary violation, or failure of the whole analysis workflow was established. Observed reproduction covers the public CLI with synthetic authenticated inventory. No physical IPA producer, native analysis engine, or MCP request was executed. Source inspection shows the MCP contract shares the workflow, but runtime MCP behavior was not verified. Reading the original inventory preserves knowledge of the helper's presence but does not restore the documented projection output; a caller-side workaround that preserves paths and restores both results remains unverified. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node 26.10.0; macOS 27.0 arm64 ### Target and artifact context Complete authenticated synthetic inventory Evidence with seven occurrences: an IPA root, rooted Mach-O and plist, unrooted extensionless Mach-O, unrooted dylib, unrooted JavaScript, and ordinary text. Content-addressed artifact, occurrence, graph, manifest, and Evidence identifiers are computed inline. No physical IPA or real Mach-O bytes are required for this projection-boundary reproduction. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence ```text helper_present: false executables: ["Payload/Fixture.app/Fixture"] native_libraries: ["SwiftSupport/iphoneos/libswiftCore.dylib"] bridge_candidates: [{"source_path":"SwiftSupport/outside.js","native_path":"SwiftSupport/iphoneos/libswiftCore.dylib","basis":"javascript-and-native-content"}] bridge_candidate_coverage: {"status":"complete","total_candidates":1,"emitted_candidates":1,"omitted_candidates":0} coverage: {"status":"complete-within-inventory","inventory_complete":true} ``` ### Typed outcome or recovery expected The successful result should retain the helper in a native component array and emit its hypothesis with `SwiftSupport/outside.js`. This input should have two total and emitted candidates and zero budget omissions. Keep the rooted relocation control working, preserve the ZIP control's exclusion of unrooted entries, and exclude the ordinary text control from native categories. ### Analysis or suspected cause Source-derived at the reported baseline: `src/domain/apple/appleApplication.ts:237-242` retains non-sidecar IPA components outside application roots. However, lines 276-280 require application-root membership for Mach-O executables and use filename suffixes for native libraries. An extensionless unrooted Mach-O fits neither array. Lines 162-169 construct native bridge inputs only from `executables` and `native_libraries`, so the missing component cannot form its hypothesis. Source-derived reachability: `src/cli/applicationCommands.ts:112-128` registers the public command and dispatches to the shared projection service. `src/domain/artifactInventoryEvidence.ts:55-84` authenticates and validates inventory pages. The successful CLI reproduction establishes that this concrete fixture passes those guards. The `complete` candidate coverage counts the already-classified candidate set. This report establishes the missing native component and consequent missing pair; it does not establish a defect in byte-budget omission accounting. ### Additional context P2 follows from consequential incorrect projection data for a bounded IPA input class. Rooted components continue to project, and the original inventory retains the missing component. The verified evidence does not support a higher impact rating.
bsakaguchi 2026-10-15 18:31:10 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@Sriram-PR commented on GitHub (Oct 14, 2026):

I'll take this one, PR shortly.

<!-- gh-comment-id:6164472057 --> @Sriram-PR commented on GitHub (Oct 14, 2026): I'll take this one, PR shortly.
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#1111
No description provided.