[GH-ISSUE #6805] [Bug] Keyed archive inspection rejects valid three-byte offset and reference tables #4153

Closed
opened 2026-10-31 00:47:14 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6805

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

The production decodeKeyedArchiveBytes reader rejects binary NSKeyedArchiver archives whose offset-table integer width or object-reference width is three bytes: TypeError: binary plist trailer integer sizes are invalid.

An equivalent one-byte-table control resolves $top.root to object 1 ("value"). Python 3.14.8 plistlib.loads decoded all three inputs to equal archive values. These were direct production-reader calls, not CLI or MCP executions.

Reproduction

In the source checkout with locked dependencies, save the following as /tmp/rea-plist-widths.mts. Run it with the checkout's tsx runner and set REA_ROOT to the checkout directory:

node "$REA_ROOT/node_modules/tsx/dist/cli.mjs" /tmp/rea-plist-widths.mts
import {pathToFileURL} from 'node:url';
const {decodeKeyedArchiveBytes} = await import(pathToFileURL(
  `${process.env.REA_ROOT}/src/artifacts/apple/KeyedArchiveReader.ts`).href);
const fixtures = {
  control: 'YnBsaXN0MDDUAQIDBAUGBwpZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIICVUkbnVsbFV2YWx1ZdELDFRyb290gAEIERskLTJESUxSWFtgAAAAAAAAAQEAAAAAAAAADQAAAAAAAAAAAAAAAAAAAGI=',
  offset3: 'YnBsaXN0MDDUAQIDBAUGBwpZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIICVUkbnVsbFV2YWx1ZdELDFRyb290gAEAAAgAABEAABsAACQAAC0AADIAAEQAAEkAAEwAAFIAAFgAAFsAAGAAAAAAAAADAQAAAAAAAAANAAAAAAAAAAAAAAAAAAAAYg==',
  reference3: 'YnBsaXN0MDDUAAABAAACAAADAAAEAAAFAAAGAAAHAAAKWSRhcmNoaXZlclgkdmVyc2lvblgkb2JqZWN0c1QkdG9wXxAPTlNLZXllZEFyY2hpdmVyEgABhqCiAAAIAAAJVSRudWxsVXZhbHVl0QAACwAADFRyb290gAEIISs0PUJUWWBmbHN4AAAAAAAAAQMAAAAAAAAADQAAAAAAAAAAAAAAAAAAAHo=',
};
for (const [name, encoded] of Object.entries(fixtures)) {
  const bytes = Buffer.from(encoded, 'base64');
  try {
    const report = decodeKeyedArchiveBytes(bytes, {offset:0, limit:50});
    console.log(name, JSON.stringify({roots:report.roots, references:report.references}));
  } catch (error) {
    console.log(name, error.constructor.name, error.message);
  }
}

Each case was executed once after fixture construction. The fixtures encode $archiver: "NSKeyedArchiver", $version: 100000, $objects: ["$null", "value"], and a genuine binary UID 1 under $top.root. Only the table widths and their required serialized layout differ. The control is 143 bytes; the offset-width-three input is 169 bytes; the reference-width-three input is 167 bytes. UID payloads remain one byte wide in every case.

Expected behavior

Decode the same archive values and resolved root for these valid table encodings, preserving each input's own identity. A valid non-power-of-two table width should not be classified as malformed.

The released Swift Foundation 6.2.3 binary-plist reader explicitly supports non-power-of-two integer sizes, and trailer validation permits positive table widths. Independent Python decoding was observed; Foundation runtime acceptance was not executed.

Impact and scope

A bounded class of valid local binary archives cannot be inspected, and the error incorrectly attributes the refusal to invalid archive structure. Both small three-byte variants fail deterministically in the reader; the one-byte control succeeds. No data mutation, evidence loss, security effect, or prevalence among real exporters is asserted. Other non-power-of-two widths and other hosts were not tested.

REA version

Package metadata 6.7.0; executed source revision 6a34be416b355e197aa2c1efe4b2f110abe47de4. The latest published npm version was 6.7.0; the npm package artifact was not separately executed.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; locked plist 5.0.0. Independent reader: Python 3.14.8 plistlib. No Hopper, Ghidra, or target execution.

Target and artifact context

Three reduced, synthetic bplist00 NSKeyedArchiver archives with in-bounds object/reference/offset tables and a genuine one-byte UID. The reference-width-three and offset-width-three fixtures are separate cases of the same trailer-admission rule.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed production-reader results:

case         offset width  ref width  bytes  result
control      1             1          143    root UID 1 resolved; object 1 = "value"
offset3      3             1          169    TypeError: binary plist trailer integer sizes are invalid
reference3   1             3          167    TypeError: binary plist trailer integer sizes are invalid

Python plistlib.loads decoded each exact input and an equality assertion against the same expected archive passed for all three. The base64 strings above are the complete tested inputs.

Source-derived reachability: the keyed-archive reader runs the preflight before decoding. The preflight rejects widths outside 1, 2, 4 and 8, even though its subsequent integer-reading loop is width-generic. The dependency's sized-integer helper also accepts only those four widths; removing the preflight check alone would not complete support.

Typed outcome or recovery expected

Successful archive inspection with the same root/reference facts as the one-byte control. If a format limitation remains, identify it as unsupported encoding rather than invalid archive structure. Original bytes must remain unchanged.

Analysis or suspected cause

Source-derived: the binary-plist preflight treats the dependency's limited integer widths as format-validity rules. Offset/reference table widths are separate from scalar integer payload widths and UID payload widths.

Additional context

Related closed #6762 concerned three-byte UID payloads with one-byte offset/reference tables. This report uses one-byte UIDs and fails at the earlier table-width check, before UID normalization or the dependency decoder. Its existing UID normalization does not handle these table encodings.

NOT_ENOUGH_INFO: introducing revision, last known good REA release, real-exporter prevalence, and a verified general conversion workaround. No bug verification was repeated after this draft was written.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Direct production-reader calls reject two small valid binary archive encodings and label them invalid, preventing inspection of those inputs; the equivalent supported encoding succeeds.
  • Workaround status: unverified
  • Workaround basis: Equivalent one-byte-table values decode, but no general archive conversion preserving the selected artifact and its evidence was tested.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6805 ## 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 The production `decodeKeyedArchiveBytes` reader rejects binary NSKeyedArchiver archives whose offset-table integer width or object-reference width is three bytes: `TypeError: binary plist trailer integer sizes are invalid`. An equivalent one-byte-table control resolves `$top.root` to object 1 (`"value"`). Python 3.14.8 `plistlib.loads` decoded all three inputs to equal archive values. These were direct production-reader calls, not CLI or MCP executions. ## Reproduction In the source checkout with locked dependencies, save the following as `/tmp/rea-plist-widths.mts`. Run it with the checkout's `tsx` runner and set `REA_ROOT` to the checkout directory: ```sh node "$REA_ROOT/node_modules/tsx/dist/cli.mjs" /tmp/rea-plist-widths.mts ``` ```typescript import {pathToFileURL} from 'node:url'; const {decodeKeyedArchiveBytes} = await import(pathToFileURL( `${process.env.REA_ROOT}/src/artifacts/apple/KeyedArchiveReader.ts`).href); const fixtures = { control: 'YnBsaXN0MDDUAQIDBAUGBwpZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIICVUkbnVsbFV2YWx1ZdELDFRyb290gAEIERskLTJESUxSWFtgAAAAAAAAAQEAAAAAAAAADQAAAAAAAAAAAAAAAAAAAGI=', offset3: 'YnBsaXN0MDDUAQIDBAUGBwpZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIICVUkbnVsbFV2YWx1ZdELDFRyb290gAEAAAgAABEAABsAACQAAC0AADIAAEQAAEkAAEwAAFIAAFgAAFsAAGAAAAAAAAADAQAAAAAAAAANAAAAAAAAAAAAAAAAAAAAYg==', reference3: 'YnBsaXN0MDDUAAABAAACAAADAAAEAAAFAAAGAAAHAAAKWSRhcmNoaXZlclgkdmVyc2lvblgkb2JqZWN0c1QkdG9wXxAPTlNLZXllZEFyY2hpdmVyEgABhqCiAAAIAAAJVSRudWxsVXZhbHVl0QAACwAADFRyb290gAEIISs0PUJUWWBmbHN4AAAAAAAAAQMAAAAAAAAADQAAAAAAAAAAAAAAAAAAAHo=', }; for (const [name, encoded] of Object.entries(fixtures)) { const bytes = Buffer.from(encoded, 'base64'); try { const report = decodeKeyedArchiveBytes(bytes, {offset:0, limit:50}); console.log(name, JSON.stringify({roots:report.roots, references:report.references})); } catch (error) { console.log(name, error.constructor.name, error.message); } } ``` Each case was executed once after fixture construction. The fixtures encode `$archiver: "NSKeyedArchiver"`, `$version: 100000`, `$objects: ["$null", "value"]`, and a genuine binary UID 1 under `$top.root`. Only the table widths and their required serialized layout differ. The control is 143 bytes; the offset-width-three input is 169 bytes; the reference-width-three input is 167 bytes. UID payloads remain one byte wide in every case. ## Expected behavior Decode the same archive values and resolved root for these valid table encodings, preserving each input's own identity. A valid non-power-of-two table width should not be classified as malformed. The released [Swift Foundation 6.2.3 binary-plist reader explicitly supports non-power-of-two integer sizes](https://github.com/swiftlang/swift-corelibs-foundation/blob/swift-6.2.3-RELEASE/Sources/CoreFoundation/CFBinaryPList.c#L707-L730), and [trailer validation permits positive table widths](https://github.com/swiftlang/swift-corelibs-foundation/blob/swift-6.2.3-RELEASE/Sources/CoreFoundation/CFBinaryPList.c#L759-L784). Independent Python decoding was observed; Foundation runtime acceptance was not executed. ## Impact and scope A bounded class of valid local binary archives cannot be inspected, and the error incorrectly attributes the refusal to invalid archive structure. Both small three-byte variants fail deterministically in the reader; the one-byte control succeeds. No data mutation, evidence loss, security effect, or prevalence among real exporters is asserted. Other non-power-of-two widths and other hosts were not tested. ## REA version Package metadata `6.7.0`; executed source revision `6a34be416b355e197aa2c1efe4b2f110abe47de4`. The latest published npm version was 6.7.0; the npm package artifact was not separately executed. ## Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy x86_64; locked `plist` 5.0.0. Independent reader: Python 3.14.8 `plistlib`. No Hopper, Ghidra, or target execution. ## Target and artifact context Three reduced, synthetic `bplist00` NSKeyedArchiver archives with in-bounds object/reference/offset tables and a genuine one-byte UID. The reference-width-three and offset-width-three fixtures are separate cases of the same trailer-admission rule. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed production-reader results: ```text case offset width ref width bytes result control 1 1 143 root UID 1 resolved; object 1 = "value" offset3 3 1 169 TypeError: binary plist trailer integer sizes are invalid reference3 1 3 167 TypeError: binary plist trailer integer sizes are invalid ``` Python `plistlib.loads` decoded each exact input and an equality assertion against the same expected archive passed for all three. The base64 strings above are the complete tested inputs. Source-derived reachability: [the keyed-archive reader runs the preflight before decoding](https://github.com/morluto/rea/blob/6a34be416b355e197aa2c1efe4b2f110abe47de4/src/artifacts/apple/KeyedArchiveReader.ts#L65-L100). [The preflight rejects widths outside 1, 2, 4 and 8](https://github.com/morluto/rea/blob/6a34be416b355e197aa2c1efe4b2f110abe47de4/src/artifacts/apple/PropertyListDecodeBudget.ts#L234-L247), even though its subsequent integer-reading loop is width-generic. The dependency's sized-integer helper also accepts only those four widths; removing the preflight check alone would not complete support. ## Typed outcome or recovery expected Successful archive inspection with the same root/reference facts as the one-byte control. If a format limitation remains, identify it as unsupported encoding rather than invalid archive structure. Original bytes must remain unchanged. ## Analysis or suspected cause Source-derived: the binary-plist preflight treats the dependency's limited integer widths as format-validity rules. Offset/reference table widths are separate from scalar integer payload widths and UID payload widths. ## Additional context Related closed [#6762](https://github.com/morluto/rea/issues/6762) concerned three-byte **UID payloads** with one-byte offset/reference tables. This report uses one-byte UIDs and fails at the earlier table-width check, before UID normalization or the dependency decoder. Its existing UID normalization does not handle these table encodings. NOT_ENOUGH_INFO: introducing revision, last known good REA release, real-exporter prevalence, and a verified general conversion workaround. No bug verification was repeated after this draft was written. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Direct production-reader calls reject two small valid binary archive encodings and label them invalid, preventing inspection of those inputs; the equivalent supported encoding succeeds. - Workaround status: unverified - Workaround basis: Equivalent one-byte-table values decode, but no general archive conversion preserving the selected artifact and its evidence was tested.
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#4153
No description provided.