[GH-ISSUE #3371] [Bug] CDATA in XML keyed-archive keys truncates root and field names #1237

Closed
opened 2026-10-16 21:03:11 +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/3371

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

inspect-keyed-archive decodes the valid XML key ma<![CDATA[in]]> as ma instead of main. The unfiltered result silently changes the named root, and --root main rejects that existing root as invalid input. Apple's property-list parser and a binary conversion retain the complete name. The same behavior was observed on an object's ordinary field name, where user<![CDATA[ID]]> became user instead of userID.

The reduced unfiltered reproduction exits with status 0, reports truncated: false, and returns normalized_result.roots as {"ma":{"CF$UID":1}}. Its reference edge is also renamed to path: ["ma"]. Selecting the actual root exits with status 1 and returns code: "invalid_request" and category: "invalid_input".

plutil -p exits with status 0 and reports $top.main referencing object index 1. In the field-name variant, Apple's parser reports userID: 42, while REA reports object index 1 with value: {"user": 42}. Neither changed key receives an omission or incomplete-decode limitation. Plain-text XML and binary controls retain main and userID.

Reproduction

  1. Start from a clean source checkout at 8dde6370d0809ad04cda3342d4e6f290f48aa063, install its locked dependencies, and build the CLI.
npm ci
npm run build:cached
  1. Create a reduced XML keyed archive with one scalar and one named root. No UI archive, custom class, or analysis engine is required.
cat > sample.plist <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>$archiver</key><string>NSKeyedArchiver</string>
<key>$version</key><integer>100000</integer>
<key>$objects</key><array><string>$null</string><integer>42</integer></array>
<key>$top</key><dict><key>ma<![CDATA[in]]></key><dict><key>CF$UID</key><integer>1</integer></dict></dict>
</dict></plist>
XML
/usr/bin/plutil -p sample.plist
node scripts/rea.mjs inspect-keyed-archive sample.plist --json
node scripts/rea.mjs inspect-keyed-archive sample.plist --root main --json
  1. Reproduce the ordinary field-name variant by replacing the scalar in $objects with a dictionary. This variant was also executed and loses the field-name suffix.
python3 - <<'PYTHON'
from pathlib import Path
path = Path("sample.plist")
text = path.read_text()
text = text.replace('<string>$null</string><integer>42</integer>', '<string>$null</string><dict><key>user<![CDATA[ID]]></key><integer>42</integer></dict>')
path.write_text(text)
PYTHON
/usr/bin/plutil -p sample.plist
node scripts/rea.mjs inspect-keyed-archive sample.plist --json
  1. Check the plain-text XML control using main and userID. The executed plain-text control retained both names.
python3 - <<'PYTHON'
from pathlib import Path
text = Path("sample.plist").read_text()
text = text.replace('ma<![CDATA[in]]>', 'main').replace('user<![CDATA[ID]]>', 'userID')
Path("sample-plain.plist").write_text(text)
PYTHON
node scripts/rea.mjs inspect-keyed-archive sample-plain.plist --json
  1. Convert the field-name variant to a separate binary plist and inspect it. This conversion was executed successfully; REA retained main and userID and accepted --root main.
plutil -convert binary1 -o sample-binary.plist sample.plist
node scripts/rea.mjs inspect-keyed-archive sample-binary.plist --json
node scripts/rea.mjs inspect-keyed-archive sample-binary.plist --root main --json

Expected behavior

The named root is main, as established by Apple's parser. The unfiltered result should retain that exact name and its reference to object index 1; --root main should select it successfully. An object's userID field must also retain its complete key. These are ordinary string identities encoded with XML character data, so inspection must preserve the complete key instead of substituting its first text segment.

The inspect_keyed_archive description in src/contracts/artifactToolContracts.ts:107 promises named roots and root selection that preserve original identities. The executed plutil -p sample.plist establishes the complete name main; the field-name variant and binary control establish userID.

Impact and scope

Impact level: P2. Assessment status: supported. Supported XML keys lose part of their names, changing reported object fields and preventing selection of an existing named root. The observed cases are the reduced root fixture and the ordinary field-name variant; occurrence in real application archives was not measured.

Workaround status: available. A separate binary conversion of the field-name variant with plutil -convert binary1 -o sample-binary.plist sample.plist was executed successfully. REA retained main and userID and accepted --root main. The conversion preserves the original file and is available on the tested macOS host.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0 on macOS 27.0.1, using the system plutil. Architecture was not recorded. No Hopper, Ghidra, or application runtime was involved.

Target and artifact context

A reduced XML Foundation NSKeyedArchiver property list with one scalar at object index 1 and a named $top root. The field-name variant replaces that scalar with a dictionary containing user<![CDATA[ID]]>. The binary control contains the same root and field identities after conversion.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Unfiltered XML inspection: exit 0; truncated: false
normalized_result root excerpt:
{"roots":{"ma":{"CF$UID":1}}}
Reference edge path: ["ma"]

XML inspection with --root main: exit 1
code: "invalid_request"
category: "invalid_input"
{"path":["root"],"reason":"invalid_value","message":"Archive root does not exist: main. Select one of the archive's $top roots or omit root.","expected":["ma"]}

Apple plutil -p: exit 0; $top.main references object index 1
Field-name variant: Apple reports userID: 42; REA object index 1 has value: {"user": 42}
Plain-text XML and binary controls retain main and userID.

Typed outcome or recovery expected

Inspect the reduced XML fixture unchanged and require roots.main and its UID edge to retain the complete name and object index 1. Require --root main to succeed with the same root identity. Run the ordinary field-name variant and require value.userID: 42, without a substituted user key. The plain-text XML and binary controls must continue to retain those same identities.

Analysis or suspected cause

Source-derived reasoning: the XML path in src/artifacts/apple/KeyedArchiveReader.ts:77 reaches src/domain/propertyListKeys.ts:114. The installed plist decoder's key branch at node_modules/plist/dist/parse.js:150 returns only the first XML child value. This explains the observed loss when the key contains both text and CDATA segments. The observed CLI results establish the renamed root and field; this source analysis identifies the suspected decoding cause.

Additional context

  • #1594 concerns missing omission accounting for binary __proto__ dictionary keys; this report concerns ordinary XML key identities split across text and CDATA.
  • #2105 concerns XML nesting preflight before recursive decoding; this shallow fixture decodes successfully but returns changed keys.
  • #2751 concerns XML archives whose filenames lack .plist; this fixture already opens and fails while decoding its keys.
  • #765 concerns whitespace in a CFBundleExecutable value during file resolution; this report concerns dictionary-key character data.
  • #2177 concerns UIKit outlet interpretation and connection coverage; this report concerns identities in the raw archive result.
Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3371 ### 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 `inspect-keyed-archive` decodes the valid XML key `ma<![CDATA[in]]>` as `ma` instead of `main`. The unfiltered result silently changes the named root, and `--root main` rejects that existing root as invalid input. Apple's property-list parser and a binary conversion retain the complete name. The same behavior was observed on an object's ordinary field name, where `user<![CDATA[ID]]>` became `user` instead of `userID`. The reduced unfiltered reproduction exits with status `0`, reports `truncated: false`, and returns `normalized_result.roots` as `{"ma":{"CF$UID":1}}`. Its reference edge is also renamed to `path: ["ma"]`. Selecting the actual root exits with status `1` and returns `code: "invalid_request"` and `category: "invalid_input"`. `plutil -p` exits with status `0` and reports `$top.main` referencing object index `1`. In the field-name variant, Apple's parser reports `userID: 42`, while REA reports object index `1` with `value: {"user": 42}`. Neither changed key receives an omission or incomplete-decode limitation. Plain-text XML and binary controls retain `main` and `userID`. ### Reproduction 1. Start from a clean source checkout at `8dde6370d0809ad04cda3342d4e6f290f48aa063`, install its locked dependencies, and build the CLI. ```sh npm ci npm run build:cached ``` 2. Create a reduced XML keyed archive with one scalar and one named root. No UI archive, custom class, or analysis engine is required. ```sh cat > sample.plist <<'XML' <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"><dict> <key>$archiver</key><string>NSKeyedArchiver</string> <key>$version</key><integer>100000</integer> <key>$objects</key><array><string>$null</string><integer>42</integer></array> <key>$top</key><dict><key>ma<![CDATA[in]]></key><dict><key>CF$UID</key><integer>1</integer></dict></dict> </dict></plist> XML /usr/bin/plutil -p sample.plist node scripts/rea.mjs inspect-keyed-archive sample.plist --json node scripts/rea.mjs inspect-keyed-archive sample.plist --root main --json ``` 3. Reproduce the ordinary field-name variant by replacing the scalar in `$objects` with a dictionary. This variant was also executed and loses the field-name suffix. ```sh python3 - <<'PYTHON' from pathlib import Path path = Path("sample.plist") text = path.read_text() text = text.replace('<string>$null</string><integer>42</integer>', '<string>$null</string><dict><key>user<![CDATA[ID]]></key><integer>42</integer></dict>') path.write_text(text) PYTHON /usr/bin/plutil -p sample.plist node scripts/rea.mjs inspect-keyed-archive sample.plist --json ``` 4. Check the plain-text XML control using `main` and `userID`. The executed plain-text control retained both names. ```sh python3 - <<'PYTHON' from pathlib import Path text = Path("sample.plist").read_text() text = text.replace('ma<![CDATA[in]]>', 'main').replace('user<![CDATA[ID]]>', 'userID') Path("sample-plain.plist").write_text(text) PYTHON node scripts/rea.mjs inspect-keyed-archive sample-plain.plist --json ``` 5. Convert the field-name variant to a separate binary plist and inspect it. This conversion was executed successfully; REA retained `main` and `userID` and accepted `--root main`. ```sh plutil -convert binary1 -o sample-binary.plist sample.plist node scripts/rea.mjs inspect-keyed-archive sample-binary.plist --json node scripts/rea.mjs inspect-keyed-archive sample-binary.plist --root main --json ``` ### Expected behavior The named root is `main`, as established by Apple's parser. The unfiltered result should retain that exact name and its reference to object index `1`; `--root main` should select it successfully. An object's `userID` field must also retain its complete key. These are ordinary string identities encoded with XML character data, so inspection must preserve the complete key instead of substituting its first text segment. The `inspect_keyed_archive` description in `src/contracts/artifactToolContracts.ts:107` promises named roots and root selection that preserve original identities. The executed `plutil -p sample.plist` establishes the complete name `main`; the field-name variant and binary control establish `userID`. ### Impact and scope Impact level: P2. Assessment status: supported. Supported XML keys lose part of their names, changing reported object fields and preventing selection of an existing named root. The observed cases are the reduced root fixture and the ordinary field-name variant; occurrence in real application archives was not measured. Workaround status: available. A separate binary conversion of the field-name variant with `plutil -convert binary1 -o sample-binary.plist sample.plist` was executed successfully. REA retained `main` and `userID` and accepted `--root main`. The conversion preserves the original file and is available on the tested macOS host. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js `v26.10.0` on macOS `27.0.1`, using the system `plutil`. Architecture was not recorded. No Hopper, Ghidra, or application runtime was involved. ### Target and artifact context A reduced XML Foundation `NSKeyedArchiver` property list with one scalar at object index `1` and a named `$top` root. The field-name variant replaces that scalar with a dictionary containing `user<![CDATA[ID]]>`. The binary control contains the same root and field identities after conversion. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence ```text Unfiltered XML inspection: exit 0; truncated: false normalized_result root excerpt: {"roots":{"ma":{"CF$UID":1}}} Reference edge path: ["ma"] XML inspection with --root main: exit 1 code: "invalid_request" category: "invalid_input" {"path":["root"],"reason":"invalid_value","message":"Archive root does not exist: main. Select one of the archive's $top roots or omit root.","expected":["ma"]} Apple plutil -p: exit 0; $top.main references object index 1 Field-name variant: Apple reports userID: 42; REA object index 1 has value: {"user": 42} Plain-text XML and binary controls retain main and userID. ``` ### Typed outcome or recovery expected Inspect the reduced XML fixture unchanged and require `roots.main` and its UID edge to retain the complete name and object index `1`. Require `--root main` to succeed with the same root identity. Run the ordinary field-name variant and require `value.userID: 42`, without a substituted `user` key. The plain-text XML and binary controls must continue to retain those same identities. ### Analysis or suspected cause Source-derived reasoning: the XML path in `src/artifacts/apple/KeyedArchiveReader.ts:77` reaches `src/domain/propertyListKeys.ts:114`. The installed plist decoder's key branch at `node_modules/plist/dist/parse.js:150` returns only the first XML child value. This explains the observed loss when the key contains both text and CDATA segments. The observed CLI results establish the renamed root and field; this source analysis identifies the suspected decoding cause. ### Additional context - [#1594](https://github.com/morluto/rea/issues/1594) concerns missing omission accounting for binary `__proto__` dictionary keys; this report concerns ordinary XML key identities split across text and CDATA. - [#2105](https://github.com/morluto/rea/issues/2105) concerns XML nesting preflight before recursive decoding; this shallow fixture decodes successfully but returns changed keys. - [#2751](https://github.com/morluto/rea/issues/2751) concerns XML archives whose filenames lack `.plist`; this fixture already opens and fails while decoding its keys. - [#765](https://github.com/morluto/rea/issues/765) concerns whitespace in a `CFBundleExecutable` value during file resolution; this report concerns dictionary-key character data. - [#2177](https://github.com/morluto/rea/issues/2177) concerns UIKit outlet interpretation and connection coverage; this report concerns identities in the raw archive result.
Author
Owner

@morluto commented on GitHub (Oct 17, 2026):

Fixed on main in 78cf6100f.

Complete key identities now come from the validated DOM at the shared plist boundary. The same facts drive decoded names and prototype-key omission. Canonical aliases bypass the pinned decoder's first-child behavior and preserve mixed-spelling duplicate assignment order; original source values and bytes are retained. Empty keys, alias collisions and scalar <key> elements have dedicated controls.

Adapted @thangldw's proposal and tests in #3426. The implementation avoids the proposed closing-content scanner and key re-escaping by keeping the parsed key facts through the decoder boundary. Additional native fallback and public Interface Builder controls cover the shared consumers, including split CFBundleExecutable, $class and child-reference keys.

Validation: 180 focused checks in 20 files, build, check:fast, docs:check and commit/push hooks passed. Built CLI and actual stdio MCP agree for segmented/plain XML and independently authored binary controls, including root selection, references and original digests. The new public control fails on the prior commit (main becomes ma). Two read-only source reviews completed; their alias-leak finding was fixed and regression covered before publication.

24 tiny completed CLI cost comparisons recorded full-reply token estimates, elapsed/CPU time and OS/sampled process-family memory. Ordinary XML/binary replies and Evidence IDs stayed byte for byte equal. First and repeated processes were separated; this is not a statistical speed claim. No heavy profiling, investigated-target execution or real deep-provider verification; whole-agent/catalog and large-input costs remain unmeasured.

<!-- gh-comment-id:6233878136 --> @morluto commented on GitHub (Oct 17, 2026): Fixed on main in 78cf6100f. Complete key identities now come from the validated DOM at the shared plist boundary. The same facts drive decoded names and prototype-key omission. Canonical aliases bypass the pinned decoder's first-child behavior and preserve mixed-spelling duplicate assignment order; original source values and bytes are retained. Empty keys, alias collisions and scalar `<key>` elements have dedicated controls. Adapted @thangldw's proposal and tests in #3426. The implementation avoids the proposed closing-content scanner and key re-escaping by keeping the parsed key facts through the decoder boundary. Additional native fallback and public Interface Builder controls cover the shared consumers, including split `CFBundleExecutable`, `$class` and child-reference keys. Validation: 180 focused checks in 20 files, build, check:fast, docs:check and commit/push hooks passed. Built CLI and actual stdio MCP agree for segmented/plain XML and independently authored binary controls, including root selection, references and original digests. The new public control fails on the prior commit (`main` becomes `ma`). Two read-only source reviews completed; their alias-leak finding was fixed and regression covered before publication. 24 tiny completed CLI cost comparisons recorded full-reply token estimates, elapsed/CPU time and OS/sampled process-family memory. Ordinary XML/binary replies and Evidence IDs stayed byte for byte equal. First and repeated processes were separated; this is not a statistical speed claim. No heavy profiling, investigated-target execution or real deep-provider verification; whole-agent/catalog and large-input costs remain unmeasured.
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#1237
No description provided.