[GH-ISSUE #7620] [Bug] Interface Builder decode diagnostics silently stop after 16 failed archives #4550

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

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

Running node scripts/rea.mjs decode-interface-builder <app> --json on a bundle containing 17 malformed nib archives exits 0 and reports partial decoding, but its failed-archive limitation names only 16 paths. It gives neither the diagnostic-list ceiling nor a count of additional failures. The seventeenth path, Contents/Resources/bad-16.nib, is absent from the entire JSON Evidence.

A control containing 16 malformed archives names all 16. The normalized limitations arrays are identical for these two fixtures, even though archive_decode.examined rises from 16 to 17. Both results correctly report graph.truncated: true and partial archive coverage; the problem is the missing diagnostic-list disclosure.

Reproduction

  1. Use a built checkout at the revision below and Node.js 24.22.1.
  2. Run the following from the repository root. It creates its fixtures inline, isolates HOME and XDG state, invokes the public CLI, prints the relevant observations, and removes the fixtures afterward.
python3 - <<'PY'
import json, os, pathlib, plistlib, re, subprocess, tempfile

with tempfile.TemporaryDirectory(prefix="rea-ib-cap-", dir=os.environ.get("REA_VERIFY_SCRATCH")) as work:
    root = pathlib.Path(work)
    env = os.environ.copy()
    for key in list(env):
        if key.startswith(("REA_", "HOPPER_", "GHIDRA_")):
            env.pop(key)
    for key in ("HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "TMPDIR"):
        location = root / key.lower()
        location.mkdir()
        env[key] = str(location)
    for count in (16, 17):
        app = root / f"Invalid{count}.app"
        resources = app / "Contents" / "Resources"
        resources.mkdir(parents=True)
        (app / "Contents" / "Info.plist").write_bytes(plistlib.dumps({
            "CFBundleIdentifier": "example.verifier.invalid",
            "CFBundleName": "Invalid", "CFBundleExecutable": "Invalid",
            "CFBundlePackageType": "APPL"
        }))
        for index in range(count):
            (resources / f"bad-{index:02d}.nib").write_text("not a plist")
        run = subprocess.run(["node", "scripts/rea.mjs", "decode-interface-builder", str(app), "--json"], env=env, capture_output=True, text=True, check=True)
        evidence = json.loads(run.stdout)
        result = evidence["normalized_result"]
        limitation = next(value for value in result["limitations"] if value.startswith("Some Interface Builder archives could not be decoded:"))
        coverage = next(value for value in result["graph"]["coverage"] if value["facet"] == "archive_decode")
        print(json.dumps({"fixture_archives": count, "exit": run.returncode,
            "reported_failure_paths": len(re.findall(r"Contents/Resources/bad-\d{2}\.nib", limitation)),
            "bad_16_anywhere_in_evidence": "bad-16.nib" in run.stdout,
            "documents": len(result["documents"]), "archive_decode": coverage,
            "graph_truncated": result["graph"]["truncated"]}))
        print(limitation)
PY
  1. Compare fixture_archives and reported_failure_paths, and read each printed limitation. The 17-archive case still lists only 16 failures and never names bad-16.nib or says that another diagnostic was dropped.

Each fixture was run twice at this baseline with the same result. No mocks, reduced budgets, analysis provider, or GUI application were used.

Expected behavior

The documented tool-design contract requires truncation and partial failures to be reported, with the ceiling and recovery named. A bounded diagnostic sample should disclose that it is a sample, name its 16-entry ceiling, and state how many additional failure diagnostics were excluded, or provide the complete diagnostics through a supported recovery route.

The existing partial graph status should remain. It identifies incomplete analysis but does not explain which diagnostic information was discarded or the list's separate ceiling. The adjacent archive-omission reporting already discloses its own sample bound and additional unreported count; malformed archives need equivalent honesty about their diagnostic sample.

Impact and scope

For the tested 17-failure bundle, the returned Evidence omits one rejected archive's path and parser reason. A caller cannot recover that diagnostic from the result, despite all 17 decode attempts being counted. Source inspection indicates the same presentation rule applies whenever more than 16 entries reach this invalid-archive collection.

This is bounded diagnostic evidence loss. The source files remain unchanged and incomplete graph coverage remains explicit. Real-application frequency, other platforms, and a practical workaround preserving the original bundle identity were not verified. No known-good release or regression window is established.

REA version

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

6.8.0

Source revision: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. The built CLI ran without an analysis provider.

Target and artifact context

Two self-authored inert .app fixtures with valid Contents/Info.plist files and either 16 or 17 Contents/Resources/bad-NN.nib files. Each nib contains the text not a plist and fails actual plist/XML decoding.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The inline reproduction printed these summaries; each command exited 0:

{"fixture_archives": 16, "exit": 0, "reported_failure_paths": 16, "bad_16_anywhere_in_evidence": false, "documents": 0, "archive_decode": {"facet": "archive_decode", "status": "partial", "reason": "one_or_more_archives_invalid", "examined": 16, "omitted": 0}, "graph_truncated": true}
{"fixture_archives": 17, "exit": 0, "reported_failure_paths": 16, "bad_16_anywhere_in_evidence": false, "documents": 0, "archive_decode": {"facet": "archive_decode", "status": "partial", "reason": "one_or_more_archives_invalid", "examined": 17, "omitted": 0}, "graph_truncated": true}

In both runs the diagnostic limitation begins Some Interface Builder archives could not be decoded: and lists bad-00.nib through bad-15.nib. Each entry reports XML is not well-formed: 1:11: text data outside of root node. There is no additional-failure count or diagnostic ceiling notice.

At the stated revision:

  • docs/tool-design.md:80-84 requires explicit truncation, partial failure, ceiling and recovery reporting. The CLI help exposes a default 64-document decode limit but no 16-diagnostic limit.
  • src/artifacts/apple/InterfaceBuilderAnalysis.ts:283-285 records actual malformed-archive paths and reasons after the typed reader, cancellation, budget and unsupported-UID guards.
  • src/artifacts/apple/InterfaceBuilderAnalysis.ts:563-567 serializes only the first 16 invalid-archive diagnostics. The built runtime at dist/artifacts/apple/InterfaceBuilderAnalysis.js:384 contains the same rule.
  • src/artifacts/apple/InterfaceBuilderAnalysis.ts:582-597 separately reports omission counts and the additional unreported path count for its 64-path omission sample.
  • tests/boundary/filesystem/interfaceBuilderBundle.test.ts:196-264 asserts that separate omission-sample disclosure. Searches found no test establishing an intended 16-path invalid-diagnostic contract.
  • docs/native-investigation.md:1073-1090 promises counts for omitted archives specifically. That is supporting comparison, not a claim that failed decode attempts must increment archive_decode.omitted.

Typed outcome or recovery expected

Keep partial decoding coverage and add explicit accounting for the failure-diagnostic sample: retained count, additional failure count, and ceiling, with a supported recovery if complete diagnostics are available elsewhere. An unbounded inline list is not required.

Analysis or suspected cause

Source-derived: the analyzer retains one diagnostic per invalid archive in memory but projects only its first 16 entries into limitations. It does not serialize the remaining paths or a count describing that diagnostic loss. The graph's truncation flag reflects incomplete analysis and cannot distinguish this separate diagnostic truncation.

Additional context

Related closed reports #6066 (read/identity error classification), #5912 (rejected-plist budget retention), #6442 (ordinary plist accepted as a nib), #6582 (symlink preselection), and #6825 (filename traversal coverage) concern different defects. Open/closed issue searches, issue-comment searches, and open/closed/merged PR searches found no report of this diagnostic sample cap. No fix or post-fix verification is claimed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed CLI returns Evidence missing a rejected archive's path and decode reason once 17 files fail, without disclosing the separate diagnostic ceiling or dropped count. Coverage already reports partial analysis, limiting the consequence.
  • Workaround status: unverified
  • Workaround basis: Reanalyzing smaller disposable subsets might recover missing diagnostics, but that workflow was not run and changes bundle identity; no verified workaround is claimed.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7620 ### 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 Running `node scripts/rea.mjs decode-interface-builder <app> --json` on a bundle containing 17 malformed nib archives exits 0 and reports partial decoding, but its failed-archive limitation names only 16 paths. It gives neither the diagnostic-list ceiling nor a count of additional failures. The seventeenth path, `Contents/Resources/bad-16.nib`, is absent from the entire JSON Evidence. A control containing 16 malformed archives names all 16. The normalized limitations arrays are identical for these two fixtures, even though `archive_decode.examined` rises from 16 to 17. Both results correctly report `graph.truncated: true` and partial archive coverage; the problem is the missing diagnostic-list disclosure. ### Reproduction 1. Use a built checkout at the revision below and Node.js 24.22.1. 2. Run the following from the repository root. It creates its fixtures inline, isolates HOME and XDG state, invokes the public CLI, prints the relevant observations, and removes the fixtures afterward. ```sh python3 - <<'PY' import json, os, pathlib, plistlib, re, subprocess, tempfile with tempfile.TemporaryDirectory(prefix="rea-ib-cap-", dir=os.environ.get("REA_VERIFY_SCRATCH")) as work: root = pathlib.Path(work) env = os.environ.copy() for key in list(env): if key.startswith(("REA_", "HOPPER_", "GHIDRA_")): env.pop(key) for key in ("HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "TMPDIR"): location = root / key.lower() location.mkdir() env[key] = str(location) for count in (16, 17): app = root / f"Invalid{count}.app" resources = app / "Contents" / "Resources" resources.mkdir(parents=True) (app / "Contents" / "Info.plist").write_bytes(plistlib.dumps({ "CFBundleIdentifier": "example.verifier.invalid", "CFBundleName": "Invalid", "CFBundleExecutable": "Invalid", "CFBundlePackageType": "APPL" })) for index in range(count): (resources / f"bad-{index:02d}.nib").write_text("not a plist") run = subprocess.run(["node", "scripts/rea.mjs", "decode-interface-builder", str(app), "--json"], env=env, capture_output=True, text=True, check=True) evidence = json.loads(run.stdout) result = evidence["normalized_result"] limitation = next(value for value in result["limitations"] if value.startswith("Some Interface Builder archives could not be decoded:")) coverage = next(value for value in result["graph"]["coverage"] if value["facet"] == "archive_decode") print(json.dumps({"fixture_archives": count, "exit": run.returncode, "reported_failure_paths": len(re.findall(r"Contents/Resources/bad-\d{2}\.nib", limitation)), "bad_16_anywhere_in_evidence": "bad-16.nib" in run.stdout, "documents": len(result["documents"]), "archive_decode": coverage, "graph_truncated": result["graph"]["truncated"]})) print(limitation) PY ``` 3. Compare `fixture_archives` and `reported_failure_paths`, and read each printed limitation. The 17-archive case still lists only 16 failures and never names `bad-16.nib` or says that another diagnostic was dropped. Each fixture was run twice at this baseline with the same result. No mocks, reduced budgets, analysis provider, or GUI application were used. ### Expected behavior The documented tool-design contract requires truncation and partial failures to be reported, with the ceiling and recovery named. A bounded diagnostic sample should disclose that it is a sample, name its 16-entry ceiling, and state how many additional failure diagnostics were excluded, or provide the complete diagnostics through a supported recovery route. The existing partial graph status should remain. It identifies incomplete analysis but does not explain which diagnostic information was discarded or the list's separate ceiling. The adjacent archive-omission reporting already discloses its own sample bound and additional unreported count; malformed archives need equivalent honesty about their diagnostic sample. ### Impact and scope For the tested 17-failure bundle, the returned Evidence omits one rejected archive's path and parser reason. A caller cannot recover that diagnostic from the result, despite all 17 decode attempts being counted. Source inspection indicates the same presentation rule applies whenever more than 16 entries reach this invalid-archive collection. This is bounded diagnostic evidence loss. The source files remain unchanged and incomplete graph coverage remains explicit. Real-application frequency, other platforms, and a practical workaround preserving the original bundle identity were not verified. No known-good release or regression window is established. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Source revision: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. The built CLI ran without an analysis provider. ### Target and artifact context Two self-authored inert `.app` fixtures with valid `Contents/Info.plist` files and either 16 or 17 `Contents/Resources/bad-NN.nib` files. Each nib contains the text `not a plist` and fails actual plist/XML decoding. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The inline reproduction printed these summaries; each command exited 0: ```json {"fixture_archives": 16, "exit": 0, "reported_failure_paths": 16, "bad_16_anywhere_in_evidence": false, "documents": 0, "archive_decode": {"facet": "archive_decode", "status": "partial", "reason": "one_or_more_archives_invalid", "examined": 16, "omitted": 0}, "graph_truncated": true} {"fixture_archives": 17, "exit": 0, "reported_failure_paths": 16, "bad_16_anywhere_in_evidence": false, "documents": 0, "archive_decode": {"facet": "archive_decode", "status": "partial", "reason": "one_or_more_archives_invalid", "examined": 17, "omitted": 0}, "graph_truncated": true} ``` In both runs the diagnostic limitation begins `Some Interface Builder archives could not be decoded:` and lists `bad-00.nib` through `bad-15.nib`. Each entry reports `XML is not well-formed: 1:11: text data outside of root node.` There is no additional-failure count or diagnostic ceiling notice. At the stated revision: - `docs/tool-design.md:80-84` requires explicit truncation, partial failure, ceiling and recovery reporting. The CLI help exposes a default 64-document decode limit but no 16-diagnostic limit. - `src/artifacts/apple/InterfaceBuilderAnalysis.ts:283-285` records actual malformed-archive paths and reasons after the typed reader, cancellation, budget and unsupported-UID guards. - `src/artifacts/apple/InterfaceBuilderAnalysis.ts:563-567` serializes only the first 16 invalid-archive diagnostics. The built runtime at `dist/artifacts/apple/InterfaceBuilderAnalysis.js:384` contains the same rule. - `src/artifacts/apple/InterfaceBuilderAnalysis.ts:582-597` separately reports omission counts and the additional unreported path count for its 64-path omission sample. - `tests/boundary/filesystem/interfaceBuilderBundle.test.ts:196-264` asserts that separate omission-sample disclosure. Searches found no test establishing an intended 16-path invalid-diagnostic contract. - `docs/native-investigation.md:1073-1090` promises counts for omitted archives specifically. That is supporting comparison, not a claim that failed decode attempts must increment `archive_decode.omitted`. ### Typed outcome or recovery expected Keep partial decoding coverage and add explicit accounting for the failure-diagnostic sample: retained count, additional failure count, and ceiling, with a supported recovery if complete diagnostics are available elsewhere. An unbounded inline list is not required. ### Analysis or suspected cause Source-derived: the analyzer retains one diagnostic per invalid archive in memory but projects only its first 16 entries into limitations. It does not serialize the remaining paths or a count describing that diagnostic loss. The graph's truncation flag reflects incomplete analysis and cannot distinguish this separate diagnostic truncation. ### Additional context Related closed reports #6066 (read/identity error classification), #5912 (rejected-plist budget retention), #6442 (ordinary plist accepted as a nib), #6582 (symlink preselection), and #6825 (filename traversal coverage) concern different defects. Open/closed issue searches, issue-comment searches, and open/closed/merged PR searches found no report of this diagnostic sample cap. No fix or post-fix verification is claimed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed CLI returns Evidence missing a rejected archive's path and decode reason once 17 files fail, without disclosing the separate diagnostic ceiling or dropped count. Coverage already reports partial analysis, limiting the consequence. - Workaround status: unverified - Workaround basis: Reanalyzing smaller disposable subsets might recover missing diagnostics, but that workflow was not run and changes bundle identity; no verified workaround is claimed.
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#4550
No description provided.