[GH-ISSUE #3950] [Bug] Interface Builder hierarchy entries beyond 20,000 disappear without partial coverage #1556

Closed
opened 2026-10-16 21:07:06 +00:00 by bsakaguchi · 0 comments
Owner

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

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 CLI accepts an ibtool hierarchy dictionary with 20,001 entries, silently drops an identified hierarchy node at the final position, and reports the document's hierarchy as complete with zero omissions. A control with the same identified node at position 20,000 retains its node and containment edge. This misleads callers about the completeness of the decoded graph even though the resulting graph is far below the configured object limit.

Reproduction

Run from the baseline repository root with dependencies installed and its runtime already built. The following creates an isolated home and two app fixtures. Each hierarchy contains empty structural records followed by one identified record, so the only graph object derived from the hierarchy is tail.

export WORK="$(mktemp -d)"
mkdir -p "$WORK/home"
node --input-type=module <<'NODE'
import { build } from "plist";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
for (const count of [20000, 20001]) {
  const contents = join(process.env.WORK, `Rows${count}.app`, "Contents");
  mkdirSync(join(contents, "Resources"), { recursive: true });
  mkdirSync(join(contents, "MacOS"), { recursive: true });
  const mach = Buffer.alloc(32);
  mach.writeUInt32BE(0xfeedfacf, 0);
  mach.writeUInt32BE(0x0100000c, 4);
  writeFileSync(join(contents, "MacOS", "Fixture"), mach);
  writeFileSync(join(contents, "Info.plist"), build({ CFBundleExecutable: "Fixture" }));
  const hierarchy = Array.from({ length: count }, (_, i) => ({ children: [], label: `row-${i + 1}` }));
  hierarchy[count - 1] = { objectID: "tail", children: [] };
  writeFileSync(join(contents, "Resources", "Main.nib"), build({
    "com.apple.ibtool.document.objects": {},
    "com.apple.ibtool.document.connections": {},
    "com.apple.ibtool.document.classes": {},
    "com.apple.ibtool.document.hierarchy": hierarchy,
  }));
}
NODE
for count in 20000 20001; do
  HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/.config" XDG_CACHE_HOME="$WORK/home/.cache" REA_LOG_LEVEL=silent REA_ANALYSIS_PROVIDER=auto node scripts/rea.mjs decode-interface-builder "$WORK/Rows$count.app" --json > "$WORK/report-$count.json"
done
node --input-type=module <<'NODE'
import { readFileSync } from "node:fs";
import { join } from "node:path";
for (const count of [20000, 20001]) {
  const result = JSON.parse(readFileSync(join(process.env.WORK, `report-${count}.json`), "utf8")).normalized_result;
  console.log(JSON.stringify({
    input_entries: count,
    hierarchy_complete: result.documents[0].hierarchy_complete,
    truncated: result.graph.truncated,
    tail_nodes: result.graph.nodes.filter(n => n.attributes.interface_builder_object_id === "tail").length,
    edges: result.graph.edges.length,
    hierarchy: result.graph.coverage.find(f => f.facet === "hierarchy:Contents/Resources/Main.nib"),
  }));
}
NODE

Expected behavior

When a limit prevents retaining a hierarchy node, edge, or traversal, the affected document must report partial hierarchy coverage. The decoder may retain the final node and its edge if they fit its resource policy, or explicitly report their omission. It must not omit them while reporting complete hierarchy coverage and an untruncated graph.

Impact and scope

A bounded input class loses a recognized hierarchy node and its containment edge while returning a complete-coverage claim. The synthetic fixture demonstrates misleading evidence completeness, but real-world frequency and broader downstream effects were not measured.

Workaround: The CLI options and shared decoder impose a fixed parser cut before graph construction. No caller option recovers the omitted hierarchy records while preserving the selected archive and the decoder's graph result.

Assessment: P2.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Synthetic Mach-O app bundles containing shallow XML plist nibs with ibtool-shaped hierarchy arrays. No native ibtool or running app was used.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Both CLI invocations exit 0. At 20,000 input entries, the result retains one tail node and one containment edge, reports hierarchy_complete: true and graph.truncated: false, and gives the hierarchy facet status: "complete", reason: null, examined: 1, omitted: 0. At 20,001 entries, the result contains no tail node and zero edges, but still reports hierarchy_complete: true and graph.truncated: false; the same facet reads status: "complete", reason: null, examined: 0, omitted: 0. Archive decoding remains complete. Generic decoder limitations do not disclose this hierarchy cut.

The source-derived cause is a fixed slice of the hierarchy array whose discarded entries do not contribute to omission accounting. The executed control with the identified record first in a 20,001-entry array retains its node and edge, which confirms that position at the parser boundary determines the loss. The original all-structural 20,001-entry variant also reproduces the complete coverage claim, but the identified final record demonstrates the consequential graph omission directly. MCP parity and occurrence in real app bundles were not tested.

Typed outcome or recovery expected

Rerun the two fixtures. For the 20,001-entry input, either retain the tail node and containment edge or report partial hierarchy coverage with an explicit omission reason or count and a truncated graph. The 20,000-entry control must retain its node and edge and continue reporting complete hierarchy coverage. Also retain the identified node and edge when it is first in the 20,001-entry array; this prevents a repair that discards additional recognized data. No particular parser algorithm or counter name is required.

Additional context

Freshness checked against origin/main at 22ec8bdb2f. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision. The cited native-investigation guide changed only in an unrelated Objective-C property section.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3950 ### 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 CLI accepts an ibtool hierarchy dictionary with 20,001 entries, silently drops an identified hierarchy node at the final position, and reports the document's hierarchy as complete with zero omissions. A control with the same identified node at position 20,000 retains its node and containment edge. This misleads callers about the completeness of the decoded graph even though the resulting graph is far below the configured object limit. ### Reproduction Run from the baseline repository root with dependencies installed and its runtime already built. The following creates an isolated home and two app fixtures. Each hierarchy contains empty structural records followed by one identified record, so the only graph object derived from the hierarchy is `tail`. ```sh export WORK="$(mktemp -d)" mkdir -p "$WORK/home" node --input-type=module <<'NODE' import { build } from "plist"; import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; for (const count of [20000, 20001]) { const contents = join(process.env.WORK, `Rows${count}.app`, "Contents"); mkdirSync(join(contents, "Resources"), { recursive: true }); mkdirSync(join(contents, "MacOS"), { recursive: true }); const mach = Buffer.alloc(32); mach.writeUInt32BE(0xfeedfacf, 0); mach.writeUInt32BE(0x0100000c, 4); writeFileSync(join(contents, "MacOS", "Fixture"), mach); writeFileSync(join(contents, "Info.plist"), build({ CFBundleExecutable: "Fixture" })); const hierarchy = Array.from({ length: count }, (_, i) => ({ children: [], label: `row-${i + 1}` })); hierarchy[count - 1] = { objectID: "tail", children: [] }; writeFileSync(join(contents, "Resources", "Main.nib"), build({ "com.apple.ibtool.document.objects": {}, "com.apple.ibtool.document.connections": {}, "com.apple.ibtool.document.classes": {}, "com.apple.ibtool.document.hierarchy": hierarchy, })); } NODE for count in 20000 20001; do HOME="$WORK/home" XDG_CONFIG_HOME="$WORK/home/.config" XDG_CACHE_HOME="$WORK/home/.cache" REA_LOG_LEVEL=silent REA_ANALYSIS_PROVIDER=auto node scripts/rea.mjs decode-interface-builder "$WORK/Rows$count.app" --json > "$WORK/report-$count.json" done node --input-type=module <<'NODE' import { readFileSync } from "node:fs"; import { join } from "node:path"; for (const count of [20000, 20001]) { const result = JSON.parse(readFileSync(join(process.env.WORK, `report-${count}.json`), "utf8")).normalized_result; console.log(JSON.stringify({ input_entries: count, hierarchy_complete: result.documents[0].hierarchy_complete, truncated: result.graph.truncated, tail_nodes: result.graph.nodes.filter(n => n.attributes.interface_builder_object_id === "tail").length, edges: result.graph.edges.length, hierarchy: result.graph.coverage.find(f => f.facet === "hierarchy:Contents/Resources/Main.nib"), })); } NODE ``` ### Expected behavior When a limit prevents retaining a hierarchy node, edge, or traversal, the affected document must report partial hierarchy coverage. The decoder may retain the final node and its edge if they fit its resource policy, or explicitly report their omission. It must not omit them while reporting complete hierarchy coverage and an untruncated graph. ### Impact and scope A bounded input class loses a recognized hierarchy node and its containment edge while returning a complete-coverage claim. The synthetic fixture demonstrates misleading evidence completeness, but real-world frequency and broader downstream effects were not measured. Workaround: The CLI options and shared decoder impose a fixed parser cut before graph construction. No caller option recovers the omitted hierarchy records while preserving the selected archive and the decoder's graph result. Assessment: P2. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Synthetic Mach-O app bundles containing shallow XML plist nibs with ibtool-shaped hierarchy arrays. No native ibtool or running app was used. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Both CLI invocations exit 0. At 20,000 input entries, the result retains one `tail` node and one containment edge, reports `hierarchy_complete: true` and `graph.truncated: false`, and gives the hierarchy facet `status: "complete", reason: null, examined: 1, omitted: 0`. At 20,001 entries, the result contains no `tail` node and zero edges, but still reports `hierarchy_complete: true` and `graph.truncated: false`; the same facet reads `status: "complete", reason: null, examined: 0, omitted: 0`. Archive decoding remains complete. Generic decoder limitations do not disclose this hierarchy cut. The source-derived cause is a fixed slice of the hierarchy array whose discarded entries do not contribute to omission accounting. The executed control with the identified record first in a 20,001-entry array retains its node and edge, which confirms that position at the parser boundary determines the loss. The original all-structural 20,001-entry variant also reproduces the complete coverage claim, but the identified final record demonstrates the consequential graph omission directly. MCP parity and occurrence in real app bundles were not tested. - Expected source: [docs/native-investigation.md:538-544](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/native-investigation.md#L538) requires partial per-document hierarchy coverage when a limit omits hierarchy traversal or edges; [docs/tool-design.md:67-69,77-82](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/tool-design.md#L67) requires reporting truncation and deriving completeness from examined records and exhaustiveness. - Failure source: observed production CLI results above. Source inspection at [src/domain/apple/interfaceBuilderKeyedArchive.ts:131-140](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/apple/interfaceBuilderKeyedArchive.ts#L131) shows the fixed array cut and zero hierarchy omissions; [src/domain/apple/interfaceBuilderGraph.ts:435-443,492-509](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/apple/interfaceBuilderGraph.ts#L435) then derives complete hierarchy coverage. The production guard and bundle-analysis invocation are at [src/artifacts/ArtifactProvider.ts:104-129](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/artifacts/ArtifactProvider.ts#L104). ### Typed outcome or recovery expected Rerun the two fixtures. For the 20,001-entry input, either retain the `tail` node and containment edge or report partial hierarchy coverage with an explicit omission reason or count and a truncated graph. The 20,000-entry control must retain its node and edge and continue reporting complete hierarchy coverage. Also retain the identified node and edge when it is first in the 20,001-entry array; this prevents a repair that discards additional recognized data. No particular parser algorithm or counter name is required. ### Additional context Freshness checked against origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision. The cited native-investigation guide changed only in an unrelated Objective-C property section.
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#1556
No description provided.