[GH-ISSUE #6540] [Bug] Empty Zip64 archives are rejected before the archive reader #3933

Closed
opened 2026-10-31 00:45:21 +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/6540

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-artifact and extract-artifact reject an intact 98-byte empty Zip64 archive with target_unavailable / unsupported_target and Cannot open artifact: unsupported binary format. Both commands accept the classic empty ZIP control. REA's ZipArtifactReader itself accepts both inputs and enumerates zero members.

The production inventory boundary also classifies the Zip64 input as root_format: "file" with root-only coverage, rather than a ZIP container. These are executed source-boundary and source-CLI observations, not a source-only prediction.

Reproduction

The initial run used the checkout's locked @zip.js/zip.js writer to create these two inputs, then invoked each source CLI operation in a fresh Node process. The commands below retain the executed fixture construction and CLI runner, with neutral scratch paths. Start in the stated source checkout with locked dependencies installed and its pinned Node on PATH:

repo="$PWD"
work="$(mktemp -d)"
mkdir -p "$work/home" "$work/config" "$work/cache" "$work/data" "$work/state" "$work/runtime" "$work/temp"
node --input-type=module - "$repo" "$work" <<'JS'
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
const [repo, work] = process.argv.slice(2);
const { ZipWriter, Uint8ArrayWriter } = await import(
  pathToFileURL(join(repo, 'node_modules/@zip.js/zip.js/index.js')).href);
for (const zip64 of [false, true]) {
  const writer = new ZipWriter(new Uint8ArrayWriter(),
    { zip64, supportZip64SplitFile: false });
  await writeFile(join(work, zip64 ? 'empty-zip64.zip' : 'empty-classic.zip'),
    Buffer.from(await writer.close()));
}
JS
cat > "$work/cli-once.mts" <<'JS'
import { pathToFileURL } from 'node:url';
import { join } from 'node:path';
const [repo, ...args] = process.argv.slice(2);
const { createCli } = await import(pathToFileURL(join(repo, 'src/cli.ts')).href);
await createCli(process.env).serve(args);
JS
for label in empty-classic empty-zip64; do
  for operation in inspect-artifact extract-artifact; do
    env -i PATH="$PATH" HOME="$work/home" XDG_CONFIG_HOME="$work/config" \
      XDG_CACHE_HOME="$work/cache" XDG_DATA_HOME="$work/data" \
      XDG_STATE_HOME="$work/state" XDG_RUNTIME_DIR="$work/runtime" \
      TMPDIR="$work/temp" REA_LOG_LEVEL=silent NO_COLOR=1 \
      node --import "$repo/node_modules/tsx/dist/loader.mjs" \
      "$work/cli-once.mts" "$repo" "$operation" "$work/$label.zip" --format json
  done
done

Separately in the initial evidence collection, Python zipfile.ZipFile opened each exact input, returned namelist() == [] and testzip() is None. A direct ZipArtifactReader(path, "zip") traversal completed with zero entries for each. Each operation had one independently launched CLI process per fixture; no population-frequency claim is made.

Expected behavior

Admit the intact empty Zip64 archive as a ZIP container, retain its original root content identity, report zero child members, and complete empty extraction as the classic control does. The reader explicitly supports Zip64, and the existing empty ZIP extraction contract requires a successful empty extraction with no selected occurrences or artifacts. The initial direct-reader result establishes that these bytes are already readable by that owner.

Impact and scope

This is a bounded compatibility failure for empty archives using Zip64 end records. Public inspection and extraction produce no Evidence for the original input, and direct inventory loses its archive classification. There are no file payloads to lose, and the inputs were unchanged. Nonempty Zip64 archives, other operating systems, MCP transport, and a published-package runtime were not exercised.

Python provides an observed way to inspect the original archive's empty member set. Re-encoding it as the classic control permits REA operations but changes its root digest; a public REA workaround preserving the original archive identity was not established.

REA version

Source revision e073f64be0a5a30c32d8b3cec23f9bd411080903 on main; package version 6.7.0. The pinned source checkout was used without building or changing production code. These observations do not establish behavior of the published npm artifact.

Runtime and platform

Node.js v24.18.0; Linux x86_64; locked @zip.js/zip.js 2.15.0; Python 3.14.8. Source CLI loaded through the checkout's locked tsx. HOME, XDG and temporary locations for CLI subprocesses were isolated. No native analysis engine or target execution was involved.

Target and artifact context

Synthetic single-disk empty Zip64 ZIP: 56-byte Zip64 end record, 20-byte locator, and 22-byte classic end record with sentinel counts/sizes/offsets. It starts with 50 4b 06 06; its Zip64 counts, directory size and directory offset are zero. The classic control is 22 bytes and starts with 50 4b 05 06.

Interface

CLI

Earliest failure stage

Setup or provider admission

Relevant output or evidence

Observed fresh-process results; every stderr was empty:

empty-classic  inspect-artifact  exit=0  Evidence returned
empty-classic  extract-artifact  exit=0  Evidence returned
empty-zip64    inspect-artifact  exit=1  target_unavailable / unsupported_target
empty-zip64    extract-artifact  exit=1  target_unavailable / unsupported_target

Both failures include:

{
  "code": "target_unavailable",
  "category": "unsupported_target",
  "message": "Cannot open artifact: unsupported binary format.",
  "details": {
    "reason": "unsupported binary format",
    "constraint": "file_format_rejected"
  }
}

Exact fixture SHA-256 values:

empty-classic: 8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85
empty-zip64:   1386d0a55e7ede0dd147c146d17edfa81720aa3a50171ba3cc6b774839dc9593

The direct inventory of the Zip64 input returned root_format: "file", coverage: {scope: "root", status: "complete"}, and Artifact has no child container entries. Evidence provenance: observed. Local verification: reproduced through production boundaries and fresh source CLI processes. Reproduction completeness: complete for these synthetic fixtures. No repair was attempted.

Typed outcome or recovery expected

Successful ZIP inventory for the original empty Zip64 bytes and a successful empty extraction. A restoration check should retain the Zip64 root digest and zero-member result, keep the classic control working, and preserve refusal of malformed end records. This check is proposed; it has not been run against a fix.

Analysis or suspected cause

Source-derived: hasZipSignature recognizes the local-header, classic empty-archive and split signatures, but omits the Zip64 end-record signature used at byte zero here. The target resolver and content classifier depend on that predicate. The trailing ZIP fallback explicitly excludes empty/Zip64 layouts, so it does not recover this case.

Additional context

GitHub searches used repo:morluto/rea is:issue Zip64 empty, repo:morluto/rea is:issue "Zip64", and repo:morluto/rea is:issue "PK" "0606", each separately filtered to open and closed issues. The first two open searches returned only #6471; their closed searches and both signature searches returned no matches. That report concerns a corrupted Zip64 signature and wrong error classification inside APK-signature parsing. This input is intact and fails earlier in shared target admission. Closed #5732 concerns leading bytes before a nonempty classic ZIP; this archive has no prefix or local member header. Closed #3953 concerns malformed member offsets beyond EOF; this archive has no members.

No bug reproduction was rerun after writing this draft.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: An intact empty Zip64 archive is accepted by the existing reader but rejected by both public artifact operations, while direct inventory reports the wrong root format. The observed failure is limited to this zero-member layout.
  • Workaround status: unverified
  • Workaround basis: Python inspected the original archive successfully and the classic empty control works, but re-encoding changes the root digest; a public REA route preserving the original archive identity was not established.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6540 ## 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-artifact` and `extract-artifact` reject an intact 98-byte empty Zip64 archive with `target_unavailable` / `unsupported_target` and `Cannot open artifact: unsupported binary format.` Both commands accept the classic empty ZIP control. REA's `ZipArtifactReader` itself accepts both inputs and enumerates zero members. The production inventory boundary also classifies the Zip64 input as `root_format: "file"` with root-only coverage, rather than a ZIP container. These are executed source-boundary and source-CLI observations, not a source-only prediction. ## Reproduction The initial run used the checkout's locked `@zip.js/zip.js` writer to create these two inputs, then invoked each source CLI operation in a fresh Node process. The commands below retain the executed fixture construction and CLI runner, with neutral scratch paths. Start in the stated source checkout with locked dependencies installed and its pinned Node on `PATH`: ```sh repo="$PWD" work="$(mktemp -d)" mkdir -p "$work/home" "$work/config" "$work/cache" "$work/data" "$work/state" "$work/runtime" "$work/temp" node --input-type=module - "$repo" "$work" <<'JS' import { writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; const [repo, work] = process.argv.slice(2); const { ZipWriter, Uint8ArrayWriter } = await import( pathToFileURL(join(repo, 'node_modules/@zip.js/zip.js/index.js')).href); for (const zip64 of [false, true]) { const writer = new ZipWriter(new Uint8ArrayWriter(), { zip64, supportZip64SplitFile: false }); await writeFile(join(work, zip64 ? 'empty-zip64.zip' : 'empty-classic.zip'), Buffer.from(await writer.close())); } JS cat > "$work/cli-once.mts" <<'JS' import { pathToFileURL } from 'node:url'; import { join } from 'node:path'; const [repo, ...args] = process.argv.slice(2); const { createCli } = await import(pathToFileURL(join(repo, 'src/cli.ts')).href); await createCli(process.env).serve(args); JS for label in empty-classic empty-zip64; do for operation in inspect-artifact extract-artifact; do env -i PATH="$PATH" HOME="$work/home" XDG_CONFIG_HOME="$work/config" \ XDG_CACHE_HOME="$work/cache" XDG_DATA_HOME="$work/data" \ XDG_STATE_HOME="$work/state" XDG_RUNTIME_DIR="$work/runtime" \ TMPDIR="$work/temp" REA_LOG_LEVEL=silent NO_COLOR=1 \ node --import "$repo/node_modules/tsx/dist/loader.mjs" \ "$work/cli-once.mts" "$repo" "$operation" "$work/$label.zip" --format json done done ``` Separately in the initial evidence collection, Python `zipfile.ZipFile` opened each exact input, returned `namelist() == []` and `testzip() is None`. A direct `ZipArtifactReader(path, "zip")` traversal completed with zero entries for each. Each operation had one independently launched CLI process per fixture; no population-frequency claim is made. ## Expected behavior Admit the intact empty Zip64 archive as a ZIP container, retain its original root content identity, report zero child members, and complete empty extraction as the classic control does. The [reader explicitly supports Zip64](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/artifacts/ZipArtifactReader.ts#L175-L203), and the existing [empty ZIP extraction contract](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/tests/boundary/filesystem/artifactZipIntegrity.test.ts#L26-L46) requires a successful empty extraction with no selected occurrences or artifacts. The initial direct-reader result establishes that these bytes are already readable by that owner. ## Impact and scope This is a bounded compatibility failure for empty archives using Zip64 end records. Public inspection and extraction produce no Evidence for the original input, and direct inventory loses its archive classification. There are no file payloads to lose, and the inputs were unchanged. Nonempty Zip64 archives, other operating systems, MCP transport, and a published-package runtime were not exercised. Python provides an observed way to inspect the original archive's empty member set. Re-encoding it as the classic control permits REA operations but changes its root digest; a public REA workaround preserving the original archive identity was not established. ## REA version Source revision `e073f64be0a5a30c32d8b3cec23f9bd411080903` on `main`; package version `6.7.0`. The pinned source checkout was used without building or changing production code. These observations do not establish behavior of the published npm artifact. ## Runtime and platform Node.js `v24.18.0`; Linux x86_64; locked `@zip.js/zip.js 2.15.0`; Python `3.14.8`. Source CLI loaded through the checkout's locked `tsx`. HOME, XDG and temporary locations for CLI subprocesses were isolated. No native analysis engine or target execution was involved. ## Target and artifact context Synthetic single-disk empty Zip64 ZIP: 56-byte Zip64 end record, 20-byte locator, and 22-byte classic end record with sentinel counts/sizes/offsets. It starts with `50 4b 06 06`; its Zip64 counts, directory size and directory offset are zero. The classic control is 22 bytes and starts with `50 4b 05 06`. ## Interface CLI ## Earliest failure stage Setup or provider admission ## Relevant output or evidence Observed fresh-process results; every stderr was empty: ```text empty-classic inspect-artifact exit=0 Evidence returned empty-classic extract-artifact exit=0 Evidence returned empty-zip64 inspect-artifact exit=1 target_unavailable / unsupported_target empty-zip64 extract-artifact exit=1 target_unavailable / unsupported_target ``` Both failures include: ```json { "code": "target_unavailable", "category": "unsupported_target", "message": "Cannot open artifact: unsupported binary format.", "details": { "reason": "unsupported binary format", "constraint": "file_format_rejected" } } ``` Exact fixture SHA-256 values: ```text empty-classic: 8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85 empty-zip64: 1386d0a55e7ede0dd147c146d17edfa81720aa3a50171ba3cc6b774839dc9593 ``` The direct inventory of the Zip64 input returned `root_format: "file"`, `coverage: {scope: "root", status: "complete"}`, and `Artifact has no child container entries.` Evidence provenance: observed. Local verification: reproduced through production boundaries and fresh source CLI processes. Reproduction completeness: complete for these synthetic fixtures. No repair was attempted. ## Typed outcome or recovery expected Successful ZIP inventory for the original empty Zip64 bytes and a successful empty extraction. A restoration check should retain the Zip64 root digest and zero-member result, keep the classic control working, and preserve refusal of malformed end records. This check is proposed; it has not been run against a fix. ## Analysis or suspected cause Source-derived: [`hasZipSignature`](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/domain/zipPackageFormat.ts#L20-L27) recognizes the local-header, classic empty-archive and split signatures, but omits the Zip64 end-record signature used at byte zero here. The [target resolver](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/application/BinaryTargetResolver.ts#L382-L385) and [content classifier](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/artifacts/inventory/ArtifactGraphConstruction.ts#L562-L569) depend on that predicate. The [trailing ZIP fallback](https://github.com/morluto/rea/blob/e073f64be0a5a30c32d8b3cec23f9bd411080903/src/domain/zipPackageFormat.ts#L42-L83) explicitly excludes empty/Zip64 layouts, so it does not recover this case. ## Additional context GitHub searches used `repo:morluto/rea is:issue Zip64 empty`, `repo:morluto/rea is:issue "Zip64"`, and `repo:morluto/rea is:issue "PK" "0606"`, each separately filtered to open and closed issues. The first two open searches returned only [#6471](https://github.com/morluto/rea/issues/6471); their closed searches and both signature searches returned no matches. That report concerns a corrupted Zip64 signature and wrong error classification inside APK-signature parsing. This input is intact and fails earlier in shared target admission. Closed [#5732](https://github.com/morluto/rea/issues/5732) concerns leading bytes before a nonempty classic ZIP; this archive has no prefix or local member header. Closed [#3953](https://github.com/morluto/rea/issues/3953) concerns malformed member offsets beyond EOF; this archive has no members. No bug reproduction was rerun after writing this draft. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: An intact empty Zip64 archive is accepted by the existing reader but rejected by both public artifact operations, while direct inventory reports the wrong root format. The observed failure is limited to this zero-member layout. - Workaround status: unverified - Workaround basis: Python inspected the original archive successfully and the classic empty control works, but re-encoding changes the root digest; a public REA route preserving the original archive identity was not established.
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#3933
No description provided.