[GH-ISSUE #7348] [Bug] Keyed-archive table normalization changes a post-table string to observed null #4368

Closed
opened 2026-10-31 00:49:15 +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/7348

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

Source-only finding: inspecting the three-byte-offset fixture below reaches a decoder-copy rewrite that changes $objects[1] from the serialized string hello to null. Graph projection labels that value kind: "scalar", status: "observed". The reader retains a generic incomplete-number-metadata limitation, which does not identify this substituted string. REA CLI/runtime reproduction was not run by this verifier; the result follows from the concrete source trace and byte offsets below. An independent Python read confirms that the original fixture contains hello.

Reproduction

Executed locally: decoded the exact fixture bytes with Python plistlib, checked their SHA-256 digests, and compared one-, three-, and four-byte offset tables with the string before and after the table. All six original archives decoded to the same values. The inline Python fixture setup below was independently executed. The REA invocations are proposed replay steps, unexecuted by this verifier. Run from the stated baseline checkout root with its pinned runtime and built CLI.

work_dir="$(mktemp -d)"
python3 - "$work_dir" <<'PY'
import base64, hashlib, pathlib, plistlib, sys
root = pathlib.Path(sys.argv[1])
fixtures = {
    'after-w3.bplist': 'YnBsaXN0MDBVJG51bGzUAwUGBwQICQtZJGFyY2hpdmVyXxAPTlNLZXllZEFyY2hpdmVyWCRvYmplY3RzVCR0b3BYJHZlcnNpb26iAAHRCgxUcm9vdBIAAYaggAEAAAgAAIMAAA4AABcAACEAADMAADwAAEEAAEoAAE0AAFAAAFUAAFpVaGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAQAAAAAAAAANAAAAAAAAAAIAAAAAAAAAXA==',
    'after-w1.bplist': 'YnBsaXN0MDBVJG51bGzUAwUGBwQICQtZJGFyY2hpdmVyXxAPTlNLZXllZEFyY2hpdmVyWCRvYmplY3RzVCR0b3BYJHZlcnNpb26iAAHRCgxUcm9vdBIAAYaggAEIaQ4XITM8QUpNUFVaVWhlbGxvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAADQAAAAAAAAACAAAAAAAAAFw=',
}
for name, encoded in fixtures.items():
    data = base64.b64decode(encoded)
    (root / name).write_bytes(data)
    value = plistlib.loads(data)
    assert value['$objects'] == ['$null', 'hello']
    assert value['$top']['root'] == plistlib.UID(1)
    print(name, len(data), hashlib.sha256(data).hexdigest(), value['$objects'])
PY
node scripts/rea.mjs inspect-keyed-archive "$work_dir/after-w3.bplist" --format json --filter-output normalized_result
node scripts/rea.mjs inspect-keyed-archive "$work_dir/after-w1.bplist" --format json --filter-output normalized_result

The locally checked digests are 4d03b4c4cc1da87ec569293f04ed77a281ec21c0848746d1c9e00a67b726d7d8 for the 217-byte three-byte fixture and 2259e0aed793ef8f3c3e5ee5ca2cccfa333eac5f84b6978db0604c53dd17a093 for the 191-byte one-byte control. Source-derived replay signal: the three-byte case reports object 1 as observed null; the one-byte control retains hello.

Expected behavior

Preserve the selected archive's serialized string through the documented private-copy normalization. This report concerns REA's explicit decoder-bounds admission policy, which admits these string records after the offset table; it does not claim that native Foundation accepts this layout. An unsupported layout must not silently become a different observed value.

Impact and scope

A bounded inert archive can produce incorrect value Evidence through the public keyed-archive inspection path. The demonstrated source case combines a three-byte offset table with a post-table string; the original file is not changed. Real-exporter prevalence, other scalar kinds, other clone triggers, MCP output, and production-user effects are NOT_ENOUGH_INFO because they were not independently tested. This is passive plist interpretation; no security boundary or execution capability is involved.

REA version

Source revision ace4dbf36e8c8f36a0e192aff9a11484ed428358; source-checkout verification only. Current main's clone and preflight source blobs matched the checked baseline during review; no installed or published package was executed.

Runtime and platform

Verifier: macOS/Darwin arm64, Python 3.15.1 standard-library plistlib. REA/Node runtime: NOT_ENOUGH_INFO for this verifier because REA was not executed. The locked plist decoder source was inspected; no native provider or Foundation runtime was invoked.

Target and artifact context

Synthetic bplist00 NSKeyedArchiver envelope with 13 binary-plist objects, $objects: ["$null", "hello"], $top.root: UID(1), and $version: 100000. The failing string starts immediately after the three-byte offset table and ends before the trailer. The artifact is inert and contains no classes to instantiate.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/native-investigation.md:608-635 promises original archive facts and private normalization of nonstandard table widths; src/artifacts/apple/PropertyListDecodeBudget.ts:170-176,292-293,413-414,479-489 explicitly selects decoder-style object bounds and admits the complete string extent. src/artifacts/apple/KeyedArchiveReader.ts:80-92 enables that policy for this operation.
  • Failure source: src/artifacts/apple/PlistScalars.ts:417-437,454-524 selects width 1 and rebuilds the table while copying the post-table region to a new address without remapping its object offset. For this input: table start 92, old table end/string start 131, new table end/string start 105, retained object-1 entry 131, and byte at retained entry 0x00. src/artifacts/apple/BinaryPlistMaterializer.ts:14-58,78-139 uses the compatible copy's unchanged entry for scalar decoding. The locked decoder's parse-binary.js:55-57 interprets 0x00 as null; src/domain/apple/keyedArchive.ts:214-249 projects that null as scalar and observed.
  • Evidence provenance: static. The violation is established by the source trace. Original bytes, digests, and independent Python decoding were observed separately; REA output was not independently executed.
  • Local verification: static-proof. Runtime reproduction was not run. The public CLI registration in src/cli/artifactCommands.ts:28-66 dispatches to artifact analysis; src/application/DirectAnalysis.ts:153-244 chooses the artifact session; src/application/BinaryTargetResolver.ts:379-404,646-670 recognizes the binary signature; src/domain/apple/artifactTargetApplicability.ts:25-30 admits standalone plists; src/artifacts/ArtifactProvider.ts:263-306 dispatches to the standalone reader. Its KeyedArchiveReader.ts:230-309 selection, stable-read, size, and digest checks pass for an unchanged regular fixture. The 217-byte, acyclic graph uses one-byte references/UIDs, 13 entries, safe offsets, shallow nesting, and pre-table numeric payloads, so the format and resource guards in PropertyListDecodeBudget.ts:269-543 do not refuse it. The graph envelope and result-schema checks admit null and do not recover the original string.
  • Reproduction completeness: complete for the concrete static trigger and public entry path. CLI execution and native Foundation acceptance are not claimed.

Additional context

Restoration check: with the exact three-byte fixture, object 1 must retain hello and the one-byte control must continue to retain hello; also compare the four-byte post-table control and three-byte pre-table control so a passing result cannot hide a general rejection of width normalization or string decoding. If the product intentionally refuses this layout instead, verify a truthful typed failure rather than a successful observed null. These REA checks are proposed, not executed by this verifier.

Duplicate review: closed #6805 concerns admitting three-byte tables, while closed #5792 concerns numeric payloads crossing into table bytes. Closed #5783 and #5805 concern numeric overlap/alias interpretation. Their restoration checks do not restore this string. Open PR #7166 and its review discussion already mention post-table relocation, but its body explicitly excludes it from that date/trailer fix. Its diff does not remap these offsets. No standalone matching issue was found in the supplied open/closed issue inventory and full bodies.

NOT_ENOUGH_INFO: introducing revision, last known good release, native Foundation layout acceptance, real-producer frequency, and an identity-preserving REA workaround. Python can read the supplied originals, but that does not establish recovery of REA's requested Evidence.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The concrete admitted archive produces a source-proven wrong observed scalar; the demonstrated effect is bounded to this passive inspection input and does not mutate user data. Production prevalence and broader reach are unknown.
  • Workaround status: unverified
  • Workaround basis: Independent Python decoding retrieves the source string, but no supported identity-preserving REA recovery route was executed or established; the semantically equivalent control changes artifact identity.
Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7348 ## 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 Source-only finding: inspecting the three-byte-offset fixture below reaches a decoder-copy rewrite that changes `$objects[1]` from the serialized string `hello` to `null`. Graph projection labels that value `kind: "scalar"`, `status: "observed"`. The reader retains a generic incomplete-number-metadata limitation, which does not identify this substituted string. REA CLI/runtime reproduction was not run by this verifier; the result follows from the concrete source trace and byte offsets below. An independent Python read confirms that the original fixture contains `hello`. ## Reproduction Executed locally: decoded the exact fixture bytes with Python `plistlib`, checked their SHA-256 digests, and compared one-, three-, and four-byte offset tables with the string before and after the table. All six original archives decoded to the same values. The inline Python fixture setup below was independently executed. The REA invocations are proposed replay steps, unexecuted by this verifier. Run from the stated baseline checkout root with its pinned runtime and built CLI. ```sh work_dir="$(mktemp -d)" python3 - "$work_dir" <<'PY' import base64, hashlib, pathlib, plistlib, sys root = pathlib.Path(sys.argv[1]) fixtures = { 'after-w3.bplist': 'YnBsaXN0MDBVJG51bGzUAwUGBwQICQtZJGFyY2hpdmVyXxAPTlNLZXllZEFyY2hpdmVyWCRvYmplY3RzVCR0b3BYJHZlcnNpb26iAAHRCgxUcm9vdBIAAYaggAEAAAgAAIMAAA4AABcAACEAADMAADwAAEEAAEoAAE0AAFAAAFUAAFpVaGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAQAAAAAAAAANAAAAAAAAAAIAAAAAAAAAXA==', 'after-w1.bplist': 'YnBsaXN0MDBVJG51bGzUAwUGBwQICQtZJGFyY2hpdmVyXxAPTlNLZXllZEFyY2hpdmVyWCRvYmplY3RzVCR0b3BYJHZlcnNpb26iAAHRCgxUcm9vdBIAAYaggAEIaQ4XITM8QUpNUFVaVWhlbGxvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAADQAAAAAAAAACAAAAAAAAAFw=', } for name, encoded in fixtures.items(): data = base64.b64decode(encoded) (root / name).write_bytes(data) value = plistlib.loads(data) assert value['$objects'] == ['$null', 'hello'] assert value['$top']['root'] == plistlib.UID(1) print(name, len(data), hashlib.sha256(data).hexdigest(), value['$objects']) PY node scripts/rea.mjs inspect-keyed-archive "$work_dir/after-w3.bplist" --format json --filter-output normalized_result node scripts/rea.mjs inspect-keyed-archive "$work_dir/after-w1.bplist" --format json --filter-output normalized_result ``` The locally checked digests are `4d03b4c4cc1da87ec569293f04ed77a281ec21c0848746d1c9e00a67b726d7d8` for the 217-byte three-byte fixture and `2259e0aed793ef8f3c3e5ee5ca2cccfa333eac5f84b6978db0604c53dd17a093` for the 191-byte one-byte control. Source-derived replay signal: the three-byte case reports object 1 as observed `null`; the one-byte control retains `hello`. ## Expected behavior Preserve the selected archive's serialized string through the documented private-copy normalization. This report concerns REA's explicit decoder-bounds admission policy, which admits these string records after the offset table; it does not claim that native Foundation accepts this layout. An unsupported layout must not silently become a different observed value. ## Impact and scope A bounded inert archive can produce incorrect value Evidence through the public keyed-archive inspection path. The demonstrated source case combines a three-byte offset table with a post-table string; the original file is not changed. Real-exporter prevalence, other scalar kinds, other clone triggers, MCP output, and production-user effects are `NOT_ENOUGH_INFO` because they were not independently tested. This is passive plist interpretation; no security boundary or execution capability is involved. ## REA version Source revision `ace4dbf36e8c8f36a0e192aff9a11484ed428358`; source-checkout verification only. Current main's clone and preflight source blobs matched the checked baseline during review; no installed or published package was executed. ## Runtime and platform Verifier: macOS/Darwin arm64, Python 3.15.1 standard-library `plistlib`. REA/Node runtime: `NOT_ENOUGH_INFO` for this verifier because REA was not executed. The locked plist decoder source was inspected; no native provider or Foundation runtime was invoked. ## Target and artifact context Synthetic `bplist00` NSKeyedArchiver envelope with 13 binary-plist objects, `$objects: ["$null", "hello"]`, `$top.root: UID(1)`, and `$version: 100000`. The failing string starts immediately after the three-byte offset table and ends before the trailer. The artifact is inert and contains no classes to instantiate. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: `docs/native-investigation.md:608-635` promises original archive facts and private normalization of nonstandard table widths; `src/artifacts/apple/PropertyListDecodeBudget.ts:170-176,292-293,413-414,479-489` explicitly selects decoder-style object bounds and admits the complete string extent. `src/artifacts/apple/KeyedArchiveReader.ts:80-92` enables that policy for this operation. - Failure source: `src/artifacts/apple/PlistScalars.ts:417-437,454-524` selects width 1 and rebuilds the table while copying the post-table region to a new address without remapping its object offset. For this input: table start 92, old table end/string start 131, new table end/string start 105, retained object-1 entry 131, and byte at retained entry `0x00`. `src/artifacts/apple/BinaryPlistMaterializer.ts:14-58,78-139` uses the compatible copy's unchanged entry for scalar decoding. The locked decoder's `parse-binary.js:55-57` interprets `0x00` as null; `src/domain/apple/keyedArchive.ts:214-249` projects that null as scalar and observed. - Evidence provenance: static. The violation is established by the source trace. Original bytes, digests, and independent Python decoding were observed separately; REA output was not independently executed. - Local verification: static-proof. Runtime reproduction was not run. The public CLI registration in `src/cli/artifactCommands.ts:28-66` dispatches to artifact analysis; `src/application/DirectAnalysis.ts:153-244` chooses the artifact session; `src/application/BinaryTargetResolver.ts:379-404,646-670` recognizes the binary signature; `src/domain/apple/artifactTargetApplicability.ts:25-30` admits standalone plists; `src/artifacts/ArtifactProvider.ts:263-306` dispatches to the standalone reader. Its `KeyedArchiveReader.ts:230-309` selection, stable-read, size, and digest checks pass for an unchanged regular fixture. The 217-byte, acyclic graph uses one-byte references/UIDs, 13 entries, safe offsets, shallow nesting, and pre-table numeric payloads, so the format and resource guards in `PropertyListDecodeBudget.ts:269-543` do not refuse it. The graph envelope and result-schema checks admit null and do not recover the original string. - Reproduction completeness: complete for the concrete static trigger and public entry path. CLI execution and native Foundation acceptance are not claimed. ## Additional context Restoration check: with the exact three-byte fixture, object 1 must retain `hello` and the one-byte control must continue to retain `hello`; also compare the four-byte post-table control and three-byte pre-table control so a passing result cannot hide a general rejection of width normalization or string decoding. If the product intentionally refuses this layout instead, verify a truthful typed failure rather than a successful observed null. These REA checks are proposed, not executed by this verifier. Duplicate review: closed [#6805](https://github.com/morluto/rea/issues/6805) concerns admitting three-byte tables, while closed [#5792](https://github.com/morluto/rea/issues/5792) concerns numeric payloads crossing into table bytes. Closed [#5783](https://github.com/morluto/rea/issues/5783) and [#5805](https://github.com/morluto/rea/issues/5805) concern numeric overlap/alias interpretation. Their restoration checks do not restore this string. Open [PR #7166](https://github.com/morluto/rea/pull/7166) and its review discussion already mention post-table relocation, but its body explicitly excludes it from that date/trailer fix. Its diff does not remap these offsets. No standalone matching issue was found in the supplied open/closed issue inventory and full bodies. NOT_ENOUGH_INFO: introducing revision, last known good release, native Foundation layout acceptance, real-producer frequency, and an identity-preserving REA workaround. Python can read the supplied originals, but that does not establish recovery of REA's requested Evidence. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The concrete admitted archive produces a source-proven wrong observed scalar; the demonstrated effect is bounded to this passive inspection input and does not mutate user data. Production prevalence and broader reach are unknown. - Workaround status: unverified - Workaround basis: Independent Python decoding retrieves the source string, but no supported identity-preserving REA recovery route was executed or established; the semantically equivalent control changes artifact identity.
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#4368
No description provided.