[GH-ISSUE #6762] [Bug] inspect-keyed-archive rejects valid three-byte UID payloads #4119

Closed
opened 2026-10-31 00:46:57 +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/6762

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 inspectBundleKeyedArchive reader rejects a 145-byte binary NSKeyedArchiver fixture containing a three-byte UID for object 1: ArtifactReaderFailure, reason format, message Cannot decode selected Foundation keyed archive: Unsupported int size: 3. The same serialized graph with a one-byte UID succeeds and returns the root reference to scalar object 1. Both inputs pass REA's binary expansion preflight. Python's standard-library plist reader decodes both to the same root UID and value. This is a local source-boundary observation, not a CLI/MCP or native Foundation execution.

Reproduction

Executed: generated the two inert archives, passed them to inspectBundleKeyedArchive, and decoded both with Python plistlib. The three-byte failing case was executed once. A preliminary control invocation used a null target digest without a resource-root identity and failed output validation; only the one-byte control was repeated with a synthetic digest, after which it succeeded. The three-byte error arises inside parseBinary, before result identity validation. No source was modified.

For a self-contained reproduction from the checkout with its locked dependencies available, save this as /tmp/uid-width-probe.mts, then run node --import ./node_modules/tsx/dist/loader.mjs /tmp/uid-width-probe.mts "$PWD". The snippet uses the corrected synthetic digest for both cases; that corrected three-byte invocation was not rerun during this investigation.

import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { pathToFileURL } from 'node:url';
const { inspectBundleKeyedArchive } = await import(pathToFileURL(join(process.argv[2]!, 'src/artifacts/apple/KeyedArchiveReader.ts')).href);
const root = await mkdtemp(join(tmpdir(), 'uid-width-'));
const fixtures = [
  [1, 'YnBsaXN0MDDUAQIDBAUGBwhZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIJCtELDFUkbnVsbFV2YWx1ZVRyb290gAEIERskLTJESUxPVVtgAAAAAAAAAQEAAAAAAAAADQAAAAAAAAAAAAAAAAAAAGI='],
  [3, 'YnBsaXN0MDDUAQIDBAUGBwhZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIJCtELDFUkbnVsbFV2YWx1ZVRyb290ggAAAQgRGyQtMkRJTE9VW2AAAAAAAAABAQAAAAAAAAANAAAAAAAAAAAAAAAAAAAAZA=='],
] as const;
try {
  await mkdir(join(root, 'Contents/Resources'), { recursive: true });
  for (const [uidWidth, base64] of fixtures) {
    const path = `Contents/Resources/uid-${uidWidth}.plist`;
    await writeFile(join(root, path), Buffer.from(base64, 'base64'));
    try {
      const result = await inspectBundleKeyedArchive({ bundlePath: root, targetSha256: 'a'.repeat(64), parameters: { path } });
      console.log({ uidWidth, roots: result.roots, objects: result.objects, references: result.references });
    } catch (error: any) {
      console.log({ uidWidth, name: error.name, reason: error.reason, message: error.message });
    }
  }
} finally {
  await rm(root, { recursive: true, force: true });
}

Expected behavior

Decode the valid UID payload and preserve its reference to original object-table entry 1, as for the one-byte control. The REA tool contract promises binary Foundation archive inspection with original object-table identities and UID edges. CoreFoundation's UID reader takes the marker's low nibble plus one as the payload width and accepts nonminimal encodings; its sized-integer helper handles non-power-of-two widths. The fixture's 82 00 00 01 is therefore a valid three-byte UID for object 1. No claim is made that Apple's default writer normally chooses this representation.

Impact and scope

A valid, small binary archive cannot be inspected because of its UID encoding width. This occurs below the configured input, object-count, nesting, and decode-budget limits. The demonstrated failure is bounded to the three-byte fixture; five-, six-, and seven-byte UID behavior, other readers, real-app frequency, and platform-specific output were not tested. A semantically equivalent one-byte control was decoded successfully, but a general safe conversion procedure for affected user archives was not verified.

REA version

rea-agents 6.7.0; source main at 2bcd7e6d13f0a8118d6619718254255e84f75d86. Locked plist dependency: 5.0.0.

Runtime and platform

Linux x64; Node.js v24.22.1; repository tsx loader. Dependencies used the repository lockfile. No build, full test suite, macOS, Apple SDK, native provider, or target execution was exercised.

Target and artifact context

Two generated bplist00 archives with the same NSKeyedArchiver envelope, $objects: ["$null", "value"], and $top.root referencing index 1. The control is 143 bytes and uses 80 01; the failing archive is 145 bytes and uses 82 00 00 01. Binary offset/reference tables remain one byte wide and contain only in-bounds references. The synthetic target digest is source-boundary scaffolding, not an observed executable identity.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed reader results:

UID width  bytes  preflight estimated bytes  reader result
1          143    7617                       root UID 1 resolved; object 1 = "value"
3          145    7623                       format: Unsupported int size: 3

Independent Python decode, executed once per fixture:

[
  {"uidWidth":1,"archiver":"NSKeyedArchiver","rootUID":1,"resolvedRoot":"value"},
  {"uidWidth":3,"archiver":"NSKeyedArchiver","rootUID":1,"resolvedRoot":"value"}
]

The REA reader invokes the pinned binary decoder before graph projection. In the installed dependency, node_modules/plist/dist/parse-binary.js:1-15,103-105 sends UID payload widths to a sized-integer helper that accepts only 1, 2, 4, and 8. The production wrapper classifies the exception as an archive format failure. These source observations explain the runtime failure; no repair was tested.

Typed outcome or recovery expected

The three-byte fixture should produce the same root UID, object values, and resolved reference status as the one-byte fixture, while retaining each input's own archive digest. The successful control must remain successful. This is the restoration criterion; no specific implementation is prescribed.

Additional context

The supplied snapshot of 1,064 open issues was searched for unsupported int size, three-byte, 3-byte, uid width, uid widths, and non-power-of-two; the broad text hits concerned other subsystems. Related #5638 concerns false references from authored UID dictionaries, whereas this fixture contains a genuine binary UID marker and is rejected before graph construction. Known nib, plist-budget, identity/read-failure, and UID-dictionary reports were excluded. Native Foundation acceptance is source-derived and supported by the independent Python decoder, not directly observed. The submitting agent reviewed the retained probe, output, and report without rerunning the failure. NOT_ENOUGH_INFO: regression introduction, last known good REA version, and real-exporter prevalence.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A local production-reader invocation rejects a valid 145-byte archive solely because of the UID payload width, preventing inspection of that bounded input; the equivalent supported encoding succeeds.
  • Workaround status: unverified
  • Workaround basis: The one-byte control establishes that equivalent serialized values can be decoded, but no safe general conversion of an affected archive was performed; native writer incidence is unknown.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6762 ### 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 `inspectBundleKeyedArchive` reader rejects a 145-byte binary NSKeyedArchiver fixture containing a three-byte UID for object 1: `ArtifactReaderFailure`, reason `format`, message `Cannot decode selected Foundation keyed archive: Unsupported int size: 3`. The same serialized graph with a one-byte UID succeeds and returns the root reference to scalar object 1. Both inputs pass REA's binary expansion preflight. Python's standard-library plist reader decodes both to the same root UID and value. This is a local source-boundary observation, not a CLI/MCP or native Foundation execution. ### Reproduction Executed: generated the two inert archives, passed them to `inspectBundleKeyedArchive`, and decoded both with Python `plistlib`. The three-byte failing case was executed once. A preliminary control invocation used a null target digest without a resource-root identity and failed output validation; only the one-byte control was repeated with a synthetic digest, after which it succeeded. The three-byte error arises inside `parseBinary`, before result identity validation. No source was modified. For a self-contained reproduction from the checkout with its locked dependencies available, save this as `/tmp/uid-width-probe.mts`, then run `node --import ./node_modules/tsx/dist/loader.mjs /tmp/uid-width-probe.mts "$PWD"`. The snippet uses the corrected synthetic digest for both cases; that corrected three-byte invocation was not rerun during this investigation. ```ts import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { pathToFileURL } from 'node:url'; const { inspectBundleKeyedArchive } = await import(pathToFileURL(join(process.argv[2]!, 'src/artifacts/apple/KeyedArchiveReader.ts')).href); const root = await mkdtemp(join(tmpdir(), 'uid-width-')); const fixtures = [ [1, 'YnBsaXN0MDDUAQIDBAUGBwhZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIJCtELDFUkbnVsbFV2YWx1ZVRyb290gAEIERskLTJESUxPVVtgAAAAAAAAAQEAAAAAAAAADQAAAAAAAAAAAAAAAAAAAGI='], [3, 'YnBsaXN0MDDUAQIDBAUGBwhZJGFyY2hpdmVyWCR2ZXJzaW9uWCRvYmplY3RzVCR0b3BfEA9OU0tleWVkQXJjaGl2ZXISAAGGoKIJCtELDFUkbnVsbFV2YWx1ZVRyb290ggAAAQgRGyQtMkRJTE9VW2AAAAAAAAABAQAAAAAAAAANAAAAAAAAAAAAAAAAAAAAZA=='], ] as const; try { await mkdir(join(root, 'Contents/Resources'), { recursive: true }); for (const [uidWidth, base64] of fixtures) { const path = `Contents/Resources/uid-${uidWidth}.plist`; await writeFile(join(root, path), Buffer.from(base64, 'base64')); try { const result = await inspectBundleKeyedArchive({ bundlePath: root, targetSha256: 'a'.repeat(64), parameters: { path } }); console.log({ uidWidth, roots: result.roots, objects: result.objects, references: result.references }); } catch (error: any) { console.log({ uidWidth, name: error.name, reason: error.reason, message: error.message }); } } } finally { await rm(root, { recursive: true, force: true }); } ``` ### Expected behavior Decode the valid UID payload and preserve its reference to original object-table entry 1, as for the one-byte control. The [REA tool contract](https://github.com/morluto/rea/blob/2bcd7e6d13f0a8118d6619718254255e84f75d86/src/contracts/artifactToolContracts.ts#L111-L114) promises binary Foundation archive inspection with original object-table identities and UID edges. [CoreFoundation's UID reader](https://github.com/swiftlang/swift-corelibs-foundation/blob/384ff680be7c7445fee6ce0c55e9ae11a46e07c5/Sources/CoreFoundation/CFBinaryPList.c#L1381-L1405) takes the marker's low nibble plus one as the payload width and accepts nonminimal encodings; its sized-integer helper handles non-power-of-two widths. The fixture's `82 00 00 01` is therefore a valid three-byte UID for object 1. No claim is made that Apple's default writer normally chooses this representation. ### Impact and scope A valid, small binary archive cannot be inspected because of its UID encoding width. This occurs below the configured input, object-count, nesting, and decode-budget limits. The demonstrated failure is bounded to the three-byte fixture; five-, six-, and seven-byte UID behavior, other readers, real-app frequency, and platform-specific output were not tested. A semantically equivalent one-byte control was decoded successfully, but a general safe conversion procedure for affected user archives was not verified. ### REA version rea-agents 6.7.0; source `main` at `2bcd7e6d13f0a8118d6619718254255e84f75d86`. Locked plist dependency: 5.0.0. ### Runtime and platform Linux x64; Node.js v24.22.1; repository tsx loader. Dependencies used the repository lockfile. No build, full test suite, macOS, Apple SDK, native provider, or target execution was exercised. ### Target and artifact context Two generated `bplist00` archives with the same NSKeyedArchiver envelope, `$objects: ["$null", "value"]`, and `$top.root` referencing index 1. The control is 143 bytes and uses `80 01`; the failing archive is 145 bytes and uses `82 00 00 01`. Binary offset/reference tables remain one byte wide and contain only in-bounds references. The synthetic target digest is source-boundary scaffolding, not an observed executable identity. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed reader results: ```text UID width bytes preflight estimated bytes reader result 1 143 7617 root UID 1 resolved; object 1 = "value" 3 145 7623 format: Unsupported int size: 3 ``` Independent Python decode, executed once per fixture: ```json [ {"uidWidth":1,"archiver":"NSKeyedArchiver","rootUID":1,"resolvedRoot":"value"}, {"uidWidth":3,"archiver":"NSKeyedArchiver","rootUID":1,"resolvedRoot":"value"} ] ``` [The REA reader invokes the pinned binary decoder before graph projection](https://github.com/morluto/rea/blob/2bcd7e6d13f0a8118d6619718254255e84f75d86/src/artifacts/apple/KeyedArchiveReader.ts#L76-L101). In the installed dependency, `node_modules/plist/dist/parse-binary.js:1-15,103-105` sends UID payload widths to a sized-integer helper that accepts only 1, 2, 4, and 8. [The production wrapper classifies the exception as an archive format failure](https://github.com/morluto/rea/blob/2bcd7e6d13f0a8118d6619718254255e84f75d86/src/artifacts/apple/KeyedArchiveReader.ts#L240-L272). These source observations explain the runtime failure; no repair was tested. ### Typed outcome or recovery expected The three-byte fixture should produce the same root UID, object values, and resolved reference status as the one-byte fixture, while retaining each input's own archive digest. The successful control must remain successful. This is the restoration criterion; no specific implementation is prescribed. ### Additional context The supplied snapshot of 1,064 open issues was searched for `unsupported int size`, `three-byte`, `3-byte`, `uid width`, `uid widths`, and `non-power-of-two`; the broad text hits concerned other subsystems. Related #5638 concerns false references from authored UID dictionaries, whereas this fixture contains a genuine binary UID marker and is rejected before graph construction. Known nib, plist-budget, identity/read-failure, and UID-dictionary reports were excluded. Native Foundation acceptance is source-derived and supported by the independent Python decoder, not directly observed. The submitting agent reviewed the retained probe, output, and report without rerunning the failure. NOT_ENOUGH_INFO: regression introduction, last known good REA version, and real-exporter prevalence. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A local production-reader invocation rejects a valid 145-byte archive solely because of the UID payload width, preventing inspection of that bounded input; the equivalent supported encoding succeeds. - Workaround status: unverified - Workaround basis: The one-byte control establishes that equivalent serialized values can be decoded, but no safe general conversion of an affected archive was performed; native writer incidence is unknown.
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#4119
No description provided.