[GH-ISSUE #5698] [Bug] inspect-asset-catalog reports a malformed catalog as an I/O failure and drops its diagnostic #3449

Closed
opened 2026-10-31 00:40:36 +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/5698

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

On macOS, inspect-asset-catalog reports a readable, truncated compiled catalog as an I/O failure. It advises checking file access and drops both the failing catalog's path and assetutil's invalid-catalog diagnosis. A valid main catalog succeeds alone; adding a damaged nested catalog makes the invocation fail without identifying which catalog needs attention.

Reproduction

From a built source checkout with locked dependencies, macOS assetutil, and Xcode providing clang/actool, run the following. It creates and inspects an inert app without launching it, with isolated HOME/XDG/temporary directories.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" \
  "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" \
  "$work_dir/Demo.app/Contents/MacOS" \
  "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources" \
  "$work_dir/Assets.xcassets/DemoColor.colorset"
printf 'int main(void){return 0;}\n' > "$work_dir/demo.c"
xcrun clang "$work_dir/demo.c" -o "$work_dir/Demo.app/Contents/MacOS/Demo"
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
  '<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Demo</string><key>CFBundleIdentifier</key><string>dev.example.demo</string></dict></plist>' \
  > "$work_dir/Demo.app/Contents/Info.plist"
printf '%s\n' '{"info":{"author":"xcode","version":1}}' \
  > "$work_dir/Assets.xcassets/Contents.json"
printf '%s\n' '{"colors":[{"color":{"color-space":"srgb","components":{"alpha":"1.000","blue":"0.000","green":"0.000","red":"1.000"}},"idiom":"universal"}],"info":{"author":"xcode","version":1}}' \
  > "$work_dir/Assets.xcassets/DemoColor.colorset/Contents.json"
xcrun actool "$work_dir/Assets.xcassets" \
  --compile "$work_dir/Demo.app/Contents/Resources" \
  --platform macosx --minimum-deployment-target 12.0 \
  --output-partial-info-plist "$work_dir/partial-info.plist" \
  > "$work_dir/actool.stdout" 2> "$work_dir/actool.stderr"
run_rea() {
  env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" \
    XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" \
    XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs inspect-asset-catalog "$work_dir/Demo.app" --json
}
/usr/bin/assetutil --info "$work_dir/Demo.app/Contents/Resources/Assets.car" \
  > "$work_dir/valid-assetutil.json"
run_rea > "$work_dir/control-rea.json"
printf 'valid-only REA exit: %s\n' "$?"
head -c 400 "$work_dir/Demo.app/Contents/Resources/Assets.car" \
  > "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car"
python3 -c 'import pathlib,sys; print("readable catalog bytes:",len(pathlib.Path(sys.argv[1]).read_bytes()))' \
  "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car"
/usr/bin/assetutil --info \
  "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car" \
  > "$work_dir/invalid-assetutil.stdout" 2> "$work_dir/invalid-assetutil.stderr"
printf 'invalid assetutil exit: %s\n' "$?"
cat "$work_dir/invalid-assetutil.stderr"
run_rea > "$work_dir/mixed-rea.json"
printf 'mixed REA exit: %s\n' "$?"
cat "$work_dir/mixed-rea.json"
cp "$work_dir/Demo.app/Contents/Resources/Assets.car" \
  "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car"
run_rea > "$work_dir/repaired-rea.json"
printf 'repaired fixture REA exit: %s\n' "$?"
python3 -c 'import json,sys; r=json.load(open(sys.argv[1]))["normalized_result"]; print("catalogs:",len(r["catalogs"]),"records:",r["total_records"])' \
  "$work_dir/repaired-rea.json"

An independent verifier executed this sequence at the baseline below. The submitter reran the same sequence after a fresh build of current main, placing the disposable fixture in a dedicated scratch directory. Both revisions reproduced the failure and passing controls.

Expected behavior

Distinguish malformed catalog bytes from filesystem-access failures, and retain the failing catalog's identity and meaningful tool diagnosis. A complete, attributed whole-operation error is sufficient; this report does not require partial-success results.

The baseline's evidence preservation contract requires preserving paths and meaningful failure reasons. The catalog workflow already classifies malformed JSON and unsupported records as format errors with catalog paths, and error presentation distinguishes invalid-artifact recovery from I/O advice. These are source-derived expectations, not a documented catalog-specific error code.

Impact and scope

P2: the damaged catalog produces misleading recovery advice and loses actionable failure attribution. The observed valid controls succeed; no data corruption or security boundary crossing was observed. Real-app prevalence is unknown. Direct read-only assetutil --info inspection of individual catalogs is a demonstrated workaround for identifying the invalid file and its diagnosis while preserving original evidence. MCP and other platforms were not tested.

REA version

rea-agents 6.7.0; verified source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rechecked after npm run build:cached on fetched origin/main at 949966712e68d8857a50a165ecddf04c2d7e6d3b (also 6.7.0): still reproduces. These are source-checkout CLI runs.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64; Xcode 27.1 (27A9275); system assetutil metadata reports DumpToolVersion 1010. No Hopper/Ghidra/IDA or deep-analysis provider is involved.

Target and artifact context

Locally generated macOS-layout app with a compiled Mach-O program, Info.plist, and actool-compiled color catalog. The damaged nested Widget.bundle/Contents/Resources/Assets.car is the first 400 bytes of the valid main catalog and can be read normally.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The main-recheck run returned:

valid-only REA exit: 0
readable catalog bytes: 400
invalid assetutil exit: 1
mixed REA exit: 1
repaired fixture REA exit: 0
catalogs: 2 records: 4

Direct assetutil stderr, with the disposable fixture prefix replaced by $work_dir:

BOMStreamWithFileAndSys: read: Operation not permitted
assetutil Error: Unable to read file at '$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car'
carutil: error:
    $work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car is not a valid AssetCatalog

REA's failing JSON is identical on the baseline and tested main:

{"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact could not be read or written. Check file access and try again.","retryable":false,"remediation":{"action":"Artifact could not be read or written. Check file access and try again."},"details":{"operation":"inspect_asset_catalog","reason":"io","detail":"assetutil failed (1)"}}

The valid-only control returns one catalog/two metadata records. Replacing only the nested file's bytes with a full copy of the valid catalog returns two catalogs/four records. This is an input-repair control, not a software fix. The tool's Operation not permitted message does not establish a filesystem permission failure: normal reading succeeds and valid bytes work at the same nested path. Evidence provenance: observed; local verification: independently reproduced.

Typed outcome or recovery expected

For the readable malformed catalog, retain its hierarchy path and meaningful invalid-catalog diagnosis, with recovery appropriate to invalid bytes. Genuine filesystem failures and unavailable assetutil should remain distinguishable. Those two negative controls are proposed post-fix checks, not runtime claims here. Preserve the successful valid-only and replacement controls.

Analysis or suspected cause

Source-derived reasoning: the assetutil runner maps nonzero child exit, outside selected availability/limit cases, to ArtifactReaderFailure("io", "assetutil failed (1)"); the original error is retained only as its cause. This occurs before the JSON format checks and agrees with the observed loss of the path and diagnostic. The tested main retains that mapping. No bundled-dependency root cause was identified.

Additional context

Prepared with AI assistance and independently reproduced using real Apple tools. No earlier known-good version was established and no REA fix was tested.

Related reports have different restoration signals: #5635 and #2176 concern Interface Builder resource-key joins; #5111 concerns empty-inventory pagination; #3914 and merged #4094 concern selecting an unrelated NotAssets.car directory. #4311 concerns host-tool readiness, whereas assetutil is available and rejects specific catalog bytes here. #5589 concerns bundle admission without CFBundleExecutable; this fixture includes that key and reaches assetutil.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5698 ### 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 On macOS, `inspect-asset-catalog` reports a readable, truncated compiled catalog as an I/O failure. It advises checking file access and drops both the failing catalog's path and assetutil's invalid-catalog diagnosis. A valid main catalog succeeds alone; adding a damaged nested catalog makes the invocation fail without identifying which catalog needs attention. ### Reproduction From a built source checkout with locked dependencies, macOS assetutil, and Xcode providing clang/actool, run the following. It creates and inspects an inert app without launching it, with isolated HOME/XDG/temporary directories. ```sh work_dir="$(mktemp -d)" mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" \ "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" \ "$work_dir/Demo.app/Contents/MacOS" \ "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources" \ "$work_dir/Assets.xcassets/DemoColor.colorset" printf 'int main(void){return 0;}\n' > "$work_dir/demo.c" xcrun clang "$work_dir/demo.c" -o "$work_dir/Demo.app/Contents/MacOS/Demo" printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \ '<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Demo</string><key>CFBundleIdentifier</key><string>dev.example.demo</string></dict></plist>' \ > "$work_dir/Demo.app/Contents/Info.plist" printf '%s\n' '{"info":{"author":"xcode","version":1}}' \ > "$work_dir/Assets.xcassets/Contents.json" printf '%s\n' '{"colors":[{"color":{"color-space":"srgb","components":{"alpha":"1.000","blue":"0.000","green":"0.000","red":"1.000"}},"idiom":"universal"}],"info":{"author":"xcode","version":1}}' \ > "$work_dir/Assets.xcassets/DemoColor.colorset/Contents.json" xcrun actool "$work_dir/Assets.xcassets" \ --compile "$work_dir/Demo.app/Contents/Resources" \ --platform macosx --minimum-deployment-target 12.0 \ --output-partial-info-plist "$work_dir/partial-info.plist" \ > "$work_dir/actool.stdout" 2> "$work_dir/actool.stderr" run_rea() { env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" \ XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" \ XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs inspect-asset-catalog "$work_dir/Demo.app" --json } /usr/bin/assetutil --info "$work_dir/Demo.app/Contents/Resources/Assets.car" \ > "$work_dir/valid-assetutil.json" run_rea > "$work_dir/control-rea.json" printf 'valid-only REA exit: %s\n' "$?" head -c 400 "$work_dir/Demo.app/Contents/Resources/Assets.car" \ > "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car" python3 -c 'import pathlib,sys; print("readable catalog bytes:",len(pathlib.Path(sys.argv[1]).read_bytes()))' \ "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car" /usr/bin/assetutil --info \ "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car" \ > "$work_dir/invalid-assetutil.stdout" 2> "$work_dir/invalid-assetutil.stderr" printf 'invalid assetutil exit: %s\n' "$?" cat "$work_dir/invalid-assetutil.stderr" run_rea > "$work_dir/mixed-rea.json" printf 'mixed REA exit: %s\n' "$?" cat "$work_dir/mixed-rea.json" cp "$work_dir/Demo.app/Contents/Resources/Assets.car" \ "$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car" run_rea > "$work_dir/repaired-rea.json" printf 'repaired fixture REA exit: %s\n' "$?" python3 -c 'import json,sys; r=json.load(open(sys.argv[1]))["normalized_result"]; print("catalogs:",len(r["catalogs"]),"records:",r["total_records"])' \ "$work_dir/repaired-rea.json" ``` An independent verifier executed this sequence at the baseline below. The submitter reran the same sequence after a fresh build of current main, placing the disposable fixture in a dedicated scratch directory. Both revisions reproduced the failure and passing controls. ### Expected behavior Distinguish malformed catalog bytes from filesystem-access failures, and retain the failing catalog's identity and meaningful tool diagnosis. A complete, attributed whole-operation error is sufficient; this report does not require partial-success results. The baseline's [evidence preservation contract](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/AGENTS.md#L13-L31) requires preserving paths and meaningful failure reasons. The catalog workflow already [classifies malformed JSON and unsupported records as format errors with catalog paths](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/artifacts/apple/AppleAssetCatalogAnalysis.ts#L188-L216), and [error presentation distinguishes invalid-artifact recovery from I/O advice](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/domain/analysisErrorPresentation.ts#L457-L460). These are source-derived expectations, not a documented catalog-specific error code. ### Impact and scope P2: the damaged catalog produces misleading recovery advice and loses actionable failure attribution. The observed valid controls succeed; no data corruption or security boundary crossing was observed. Real-app prevalence is unknown. Direct read-only `assetutil --info` inspection of individual catalogs is a demonstrated workaround for identifying the invalid file and its diagnosis while preserving original evidence. MCP and other platforms were not tested. ### REA version `rea-agents` 6.7.0; verified source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rechecked after `npm run build:cached` on fetched `origin/main` at `949966712e68d8857a50a165ecddf04c2d7e6d3b` (also 6.7.0): still reproduces. These are source-checkout CLI runs. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64; Xcode 27.1 (27A9275); system assetutil metadata reports DumpToolVersion 1010. No Hopper/Ghidra/IDA or deep-analysis provider is involved. ### Target and artifact context Locally generated macOS-layout app with a compiled Mach-O program, Info.plist, and actool-compiled color catalog. The damaged nested `Widget.bundle/Contents/Resources/Assets.car` is the first 400 bytes of the valid main catalog and can be read normally. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The main-recheck run returned: ```text valid-only REA exit: 0 readable catalog bytes: 400 invalid assetutil exit: 1 mixed REA exit: 1 repaired fixture REA exit: 0 catalogs: 2 records: 4 ``` Direct assetutil stderr, with the disposable fixture prefix replaced by `$work_dir`: ```text BOMStreamWithFileAndSys: read: Operation not permitted assetutil Error: Unable to read file at '$work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car' carutil: error: $work_dir/Demo.app/Contents/Resources/Widget.bundle/Contents/Resources/Assets.car is not a valid AssetCatalog ``` REA's failing JSON is identical on the baseline and tested main: ```json {"error":"Analysis failed","code":"artifact_operation_failed","category":"execution_failure","message":"Artifact could not be read or written. Check file access and try again.","retryable":false,"remediation":{"action":"Artifact could not be read or written. Check file access and try again."},"details":{"operation":"inspect_asset_catalog","reason":"io","detail":"assetutil failed (1)"}} ``` The valid-only control returns one catalog/two metadata records. Replacing only the nested file's bytes with a full copy of the valid catalog returns two catalogs/four records. This is an input-repair control, not a software fix. The tool's `Operation not permitted` message does not establish a filesystem permission failure: normal reading succeeds and valid bytes work at the same nested path. Evidence provenance: observed; local verification: independently reproduced. ### Typed outcome or recovery expected For the readable malformed catalog, retain its hierarchy path and meaningful invalid-catalog diagnosis, with recovery appropriate to invalid bytes. Genuine filesystem failures and unavailable assetutil should remain distinguishable. Those two negative controls are proposed post-fix checks, not runtime claims here. Preserve the successful valid-only and replacement controls. ### Analysis or suspected cause Source-derived reasoning: [the assetutil runner](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/artifacts/apple/AppleAssetCatalogAnalysis.ts#L252-L284) maps nonzero child exit, outside selected availability/limit cases, to `ArtifactReaderFailure("io", "assetutil failed (1)")`; the original error is retained only as its cause. This occurs before the JSON format checks and agrees with the observed loss of the path and diagnostic. The tested main retains that mapping. No bundled-dependency root cause was identified. ### Additional context Prepared with AI assistance and independently reproduced using real Apple tools. No earlier known-good version was established and no REA fix was tested. Related reports have different restoration signals: #5635 and #2176 concern Interface Builder resource-key joins; #5111 concerns empty-inventory pagination; #3914 and merged #4094 concern selecting an unrelated `NotAssets.car` directory. #4311 concerns host-tool readiness, whereas assetutil is available and rejects specific catalog bytes here. #5589 concerns bundle admission without `CFBundleExecutable`; this fixture includes that key and reaches assetutil.
Author
Owner

@matbrgz commented on GitHub (Oct 20, 2026):

Cross-referencing with canonical issue #5635 (inspect-asset-catalog omits AppKit NSCustomResource image references) and sibling asset catalog/keyed-archive report #5638.

Consolidated under Cluster BN (macOS Keyed Archive & Asset Catalog Reference Extraction) in tracker #4330.

<!-- gh-comment-id:6304472896 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #5635 (`inspect-asset-catalog omits AppKit NSCustomResource image references`) and sibling asset catalog/keyed-archive report #5638. Consolidated under **Cluster BN (macOS Keyed Archive & Asset Catalog Reference Extraction)** in tracker #4330.
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#3449
No description provided.