[GH-ISSUE #5705] [Bug] inspect-plist still loses negative-zero sign through XML fallback after #4839 #3456

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

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

On macOS, inspect-plist reports a binary plist's negative-zero real as observed numeric 0 when a sibling data value forces XML fallback. Removing only the data value returns the documented typed negative zero. The positive-zero control with the same data value returns the same normalized value as the negative fixture. The generic XML-fallback limitation does not disclose the lost sign. This still reproduces after merged #4839.

Reproduction

Use a source checkout at the baseline under Runtime or environment with dependencies installed and the CLI already built. Run from the repository root on macOS with the system plutil available.

work_dir="$(mktemp -d)"
mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" \
  "$work_dir/cache" "$work_dir/state" "$work_dir/tmp"
python3 -I - "$work_dir" <<'PY'
import math
import pathlib
import plistlib
import sys

root = pathlib.Path(sys.argv[1])
fixtures = {
    "negative-only": {"z": -0.0},
    "negative-data": {"z": -0.0, "d": b"\x00"},
    "positive-data": {"z": 0.0, "d": b"\x00"},
}
for name, value in fixtures.items():
    path = root / (name + ".plist")
    path.write_bytes(plistlib.dumps(value, fmt=plistlib.FMT_BINARY))
    decoded = plistlib.loads(path.read_bytes())
    print(name, repr(decoded), "sign:", math.copysign(1, decoded["z"]))
PY
for fixture in negative-only negative-data positive-data; do
  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-plist "$work_dir/$fixture.plist" --json \
    > "$work_dir/$fixture.rea.json"
  printf '%s CLI exit: %s\n' "$fixture" "$?"
  jq '{value: .normalized_result.value,
       limitations: .normalized_result.limitations,
       confidence: .confidence}' "$work_dir/$fixture.rea.json"
done

Compare the original binary values with the native conversion that REA uses:

/usr/bin/plutil -p "$work_dir/negative-data.plist"
/usr/bin/plutil -p "$work_dir/positive-data.plist"
/usr/bin/plutil -convert json -o - "$work_dir/negative-data.plist"
printf 'JSON conversion exit: %s\n' "$?"
/usr/bin/plutil -convert xml1 -o - "$work_dir/negative-data.plist" \
  > "$work_dir/negative-data.xml"
/usr/bin/plutil -convert xml1 -o - "$work_dir/positive-data.plist" \
  > "$work_dir/positive-data.xml"
rg '<real>' "$work_dir/negative-data.xml" "$work_dir/positive-data.xml"

Python reloads both negative fixtures with sign -1.0 and the positive control with sign 1.0. The native human-readable inspection prints -0 for the negative original and 0 for the positive original. JSON conversion rejects the data-bearing negative fixture with exit 1 and Invalid object in plist for JSON format; both XML conversions succeed and contain <real>0.0</real>.

The independent verifier executed these commands at the baseline below. I replayed both shell blocks against a fresh build of current main, changing only the temporary fixture directory location. Each of the three CLI calls exited 0 and reproduced the outputs below.

Expected behavior

Return negative-zero reals as { "$plist_type": "real", "value": "-0" } with a counted negative-zero limitation. Positive zero remains numeric 0. Adding a separate data value must not change the observed identity of the real.

The documented projection explicitly preserves negative zero; the native conversion documentation applies the shared projection to XML conversion. The fixture is a shallow valid binary plist within that contract.

Impact and scope

P2: a bounded wrong scalar result is presented as observed evidence. Python plistlib and plutil -p recover the original sign from these fixtures, providing a demonstrated external-inspection workaround. They do not produce REA's normalized Evidence result. Frequency in real application plists is unknown. Only the binary negative-zero plus data trigger and its two controls were executed; MCP, other platforms and date-triggered fallback were not tested. No security boundary crossing or source mutation was observed.

REA version

rea-agents 6.7.0, verified source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Rechecked after successful npm run build:cached on fetched main 52c84f6f58fa3cc7b39326654c6d3f261f957957, also 6.7.0: still reproduces. The lockfile is unchanged between these revisions.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64; Python 3 plistlib and system plutil. Hopper/Ghidra are not involved.

Target and artifact context

Self-authored binary property lists containing a negative-zero real, a negative-zero real plus one data byte, and a positive-zero real plus the same data byte. No app bundle or live service is required.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

On rebuilt main, the negative-only control returns {"z":{"$plist_type":"real","value":"-0"}} and a limitation counting one negative-zero real. Both data-bearing fixtures return this identical subset:

{
  "value": {
    "d": { "$plist_type": "data", "base64": "AA==" },
    "z": 0
  },
  "limitations": [
    "plutil cannot express this plist as JSON, so it was decoded from plutil's XML conversion; data and date values are typed objects such as { \"$plist_type\": \"data\", \"base64\": ... }."
  ],
  "confidence": "observed"
}

Python reloads both original negative fixtures with sign -1.0 and the positive control with sign 1.0. plutil -p distinguishes -0 from 0 in the original binaries. JSON conversion of negative-data exits 1 with Invalid object in plist for JSON format. XML conversion succeeds for both data-bearing inputs and emits <real>0.0</real> for both. The source sign has disappeared before REA projects the converted XML.

Evidence provenance: observed public CLI and independent binary/native conversion readback. Local verification: independently reproduced and rechecked on main.

Typed outcome or recovery expected

The negative-data fixture must preserve its data bytes and return the typed negative-zero real with a limitation counting one such value. The negative-only control must retain its typed value; positive-data must keep numeric 0 without a negative-zero limitation. Merely warning while asserting the same wrong observed scalar would leave the failure unresolved.

Analysis or suspected cause

Source-derived reasoning: NativeMacOSProvider.ts chooses XML after JSON conversion fails and supplies the converted XML to parsePlistXml. parsers/plist.ts projects values after that conversion has erased the sign. The existing negative-zero parser test supplies signed XML directly, so it does not exercise the native conversion loss. Current main retains the fallback. The observed loss occurs in system plutil conversion, not in a bundled decoder dependency.

Additional context

Still reproduces after #4839, the merged fix for closed #4834. That fix commit is already in the tested baseline. Its validation record states that the macOS plutil path was not run. This is a surviving fallback gap; no last-known-good/first-known-bad interval was established.

Sibling reports #5701, #5702 and #5703 concern XML-fallback rejection, Unicode string rewriting and fractional-date loss. This fixture contains no strings or dates, and preserving those values can leave its zero sign lost. Open #4047 and #4054 concern XML real character data and CDATA, which this native conversion does not contain. A broader source-preserving decoder could address multiple reports, but their restoration checks differ.

Prepared with AI assistance and an independent verifier. No fix or successful restoration has been tested.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5705 ### 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 On macOS, `inspect-plist` reports a binary plist's negative-zero real as observed numeric `0` when a sibling data value forces XML fallback. Removing only the data value returns the documented typed negative zero. The positive-zero control with the same data value returns the same normalized value as the negative fixture. The generic XML-fallback limitation does not disclose the lost sign. This still reproduces after merged #4839. ### Reproduction Use a source checkout at the baseline under Runtime or environment with dependencies installed and the CLI already built. Run from the repository root on macOS with the system `plutil` available. ```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" python3 -I - "$work_dir" <<'PY' import math import pathlib import plistlib import sys root = pathlib.Path(sys.argv[1]) fixtures = { "negative-only": {"z": -0.0}, "negative-data": {"z": -0.0, "d": b"\x00"}, "positive-data": {"z": 0.0, "d": b"\x00"}, } for name, value in fixtures.items(): path = root / (name + ".plist") path.write_bytes(plistlib.dumps(value, fmt=plistlib.FMT_BINARY)) decoded = plistlib.loads(path.read_bytes()) print(name, repr(decoded), "sign:", math.copysign(1, decoded["z"])) PY for fixture in negative-only negative-data positive-data; do 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-plist "$work_dir/$fixture.plist" --json \ > "$work_dir/$fixture.rea.json" printf '%s CLI exit: %s\n' "$fixture" "$?" jq '{value: .normalized_result.value, limitations: .normalized_result.limitations, confidence: .confidence}' "$work_dir/$fixture.rea.json" done ``` Compare the original binary values with the native conversion that REA uses: ```sh /usr/bin/plutil -p "$work_dir/negative-data.plist" /usr/bin/plutil -p "$work_dir/positive-data.plist" /usr/bin/plutil -convert json -o - "$work_dir/negative-data.plist" printf 'JSON conversion exit: %s\n' "$?" /usr/bin/plutil -convert xml1 -o - "$work_dir/negative-data.plist" \ > "$work_dir/negative-data.xml" /usr/bin/plutil -convert xml1 -o - "$work_dir/positive-data.plist" \ > "$work_dir/positive-data.xml" rg '<real>' "$work_dir/negative-data.xml" "$work_dir/positive-data.xml" ``` Python reloads both negative fixtures with sign `-1.0` and the positive control with sign `1.0`. The native human-readable inspection prints `-0` for the negative original and `0` for the positive original. JSON conversion rejects the data-bearing negative fixture with exit 1 and `Invalid object in plist for JSON format`; both XML conversions succeed and contain `<real>0.0</real>`. The independent verifier executed these commands at the baseline below. I replayed both shell blocks against a fresh build of current main, changing only the temporary fixture directory location. Each of the three CLI calls exited 0 and reproduced the outputs below. ### Expected behavior Return negative-zero reals as `{ "$plist_type": "real", "value": "-0" }` with a counted negative-zero limitation. Positive zero remains numeric `0`. Adding a separate data value must not change the observed identity of the real. The [documented projection](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/native-investigation.md#L689-L693) explicitly preserves negative zero; [the native conversion documentation](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/docs/native-investigation.md#L717) applies the shared projection to XML conversion. The fixture is a shallow valid binary plist within that contract. ### Impact and scope P2: a bounded wrong scalar result is presented as observed evidence. Python plistlib and `plutil -p` recover the original sign from these fixtures, providing a demonstrated external-inspection workaround. They do not produce REA's normalized Evidence result. Frequency in real application plists is unknown. Only the binary negative-zero plus data trigger and its two controls were executed; MCP, other platforms and date-triggered fallback were not tested. No security boundary crossing or source mutation was observed. ### REA version `rea-agents` 6.7.0, verified source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Rechecked after successful `npm run build:cached` on fetched main `52c84f6f58fa3cc7b39326654c6d3f261f957957`, also 6.7.0: still reproduces. The lockfile is unchanged between these revisions. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64; Python 3 plistlib and system `plutil`. Hopper/Ghidra are not involved. ### Target and artifact context Self-authored binary property lists containing a negative-zero real, a negative-zero real plus one data byte, and a positive-zero real plus the same data byte. No app bundle or live service is required. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence On rebuilt main, the negative-only control returns `{"z":{"$plist_type":"real","value":"-0"}}` and a limitation counting one negative-zero real. Both data-bearing fixtures return this identical subset: ```json { "value": { "d": { "$plist_type": "data", "base64": "AA==" }, "z": 0 }, "limitations": [ "plutil cannot express this plist as JSON, so it was decoded from plutil's XML conversion; data and date values are typed objects such as { \"$plist_type\": \"data\", \"base64\": ... }." ], "confidence": "observed" } ``` Python reloads both original negative fixtures with sign `-1.0` and the positive control with sign `1.0`. `plutil -p` distinguishes `-0` from `0` in the original binaries. JSON conversion of negative-data exits 1 with `Invalid object in plist for JSON format`. XML conversion succeeds for both data-bearing inputs and emits `<real>0.0</real>` for both. The source sign has disappeared before REA projects the converted XML. Evidence provenance: observed public CLI and independent binary/native conversion readback. Local verification: independently reproduced and rechecked on main. ### Typed outcome or recovery expected The negative-data fixture must preserve its data bytes and return the typed negative-zero real with a limitation counting one such value. The negative-only control must retain its typed value; positive-data must keep numeric `0` without a negative-zero limitation. Merely warning while asserting the same wrong observed scalar would leave the failure unresolved. ### Analysis or suspected cause Source-derived reasoning: [NativeMacOSProvider.ts](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/NativeMacOSProvider.ts#L828-L875) chooses XML after JSON conversion fails and supplies the converted XML to `parsePlistXml`. [parsers/plist.ts](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/parsers/plist.ts#L162-L172) projects values after that conversion has erased the sign. The existing [negative-zero parser test](https://github.com/morluto/rea/blob/9d8e7803326a382cbbb3ffdb900f5e93e5442d68/src/native/parsers/plist.test.ts#L146-L163) supplies signed XML directly, so it does not exercise the native conversion loss. Current main retains the fallback. The observed loss occurs in system plutil conversion, not in a bundled decoder dependency. ### Additional context Still reproduces after #4839, the merged fix for closed #4834. That fix commit is already in the tested baseline. Its validation record states that the macOS plutil path was not run. This is a surviving fallback gap; no last-known-good/first-known-bad interval was established. Sibling reports #5701, #5702 and #5703 concern XML-fallback rejection, Unicode string rewriting and fractional-date loss. This fixture contains no strings or dates, and preserving those values can leave its zero sign lost. Open #4047 and #4054 concern XML real character data and CDATA, which this native conversion does not contain. A broader source-preserving decoder could address multiple reports, but their restoration checks differ. Prepared with AI assistance and an independent verifier. No fix or successful restoration has been tested.
Author
Owner

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

Cross-referencing with canonical issue #3371 (CDATA in XML keyed-archive keys truncates root and field names) and sibling plist decoding reports #3373, #4042, #4714, #4849, #4852, #5701, #5702, #5703.

Consolidated under Cluster K (XML Keyed-Archive Deserialization & CDATA Handling / Plist Decoder) in tracker #4330.

<!-- gh-comment-id:6304470792 --> @matbrgz commented on GitHub (Oct 20, 2026): Cross-referencing with canonical issue #3371 (`CDATA in XML keyed-archive keys truncates root and field names`) and sibling plist decoding reports #3373, #4042, #4714, #4849, #4852, #5701, #5702, #5703. Consolidated under **Cluster K (XML Keyed-Archive Deserialization & CDATA Handling / Plist Decoder)** 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#3456
No description provided.