[GH-ISSUE #1485] fix(artifacts): keep ZIP-tail bytes out of unchanged and complete claims #502

Closed
opened 2026-10-15 18:25:39 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @cursor[bot] on GitHub (Oct 10, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1485

TL;DR: Two APKs with the same ZIP entries and different APK Signing Blocks compare as unchanged. Inspection can report complete-within-substeps without saying the signing block was never read.

Inventory completeness is being reused as artifact completeness. The signing-block limitation added for the Android graph stops at that projection.

Area

Evidence, artifacts, or reconstruction

Observed behavior

ZIP inventory walks the central directory. Bytes after the end of the central directory, including the APK Signing Block (v2/v3/v4) and a ZIP comment, are not entries. inventoryLimitations is empty for a normal ZIP reader. The Android projection now records: "ZIP inventory cannot see the APK Signing Block, so an empty signing array does not mean the APK is unsigned." inspect-artifact and compare-artifacts do not.

compareArtifacts marks a side covered when inventory.complete is true and the only limitations are integrity contradictions. Comparison then classifies path presence as added / removed and, when every logical path matches, sets status unchanged. It records both manifest ids and does not consult root_sha256. Two packages can share every entry hash, differ in the signing block, and still come back unchanged. Root hashes differ, and that difference is not a change.

inspectionCoverage returns complete-within-substeps whenever the retained inspection lists are not truncated. The inspection limitations copy the inventory limitations and add a generic static-hypothesis sentence. They do not name the signing block or any other non-entry region.

absenceClaimable treats coverage status complete with no omissions as authority to say something is absent. Callers in JavaScript version comparison use the graph coverage that is built from this inventory.

Expected behavior

A comparison of two inventories should stay partial for any region the inventory cannot see, or it should surface a root-hash difference as its own change. Inspection of a ZIP package should carry the signing-block limitation on the inventory evidence itself, so every consumer sees it. An empty v1 META-INF signing list remains a statement about those entries only.

Where

  • src/domain/artifactComparison.ts — compareArtifacts, hasCoverageLimitation
  • src/domain/artifactInspection.ts — createArtifactInspection, inspectionCoverage
  • src/artifacts/inventory/reader.ts — inventoryLimitations
  • src/domain/android/androidApplication.ts — signing-block limitation, currently only on this projection
  • src/domain/comparisonSemantics.ts — absenceClaimable
Originally created by @cursor[bot] on GitHub (Oct 10, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1485 **TL;DR:** Two APKs with the same ZIP entries and different APK Signing Blocks compare as `unchanged`. Inspection can report `complete-within-substeps` without saying the signing block was never read. Inventory completeness is being reused as artifact completeness. The signing-block limitation added for the Android graph stops at that projection. ### Area Evidence, artifacts, or reconstruction ### Observed behavior ZIP inventory walks the central directory. Bytes after the end of the central directory, including the APK Signing Block (v2/v3/v4) and a ZIP comment, are not entries. `inventoryLimitations` is empty for a normal ZIP reader. The Android projection now records: "ZIP inventory cannot see the APK Signing Block, so an empty signing array does not mean the APK is unsigned." `inspect-artifact` and `compare-artifacts` do not. `compareArtifacts` marks a side covered when `inventory.complete` is true and the only limitations are integrity contradictions. Comparison then classifies path presence as `added` / `removed` and, when every logical path matches, sets status `unchanged`. It records both manifest ids and does not consult `root_sha256`. Two packages can share every entry hash, differ in the signing block, and still come back `unchanged`. Root hashes differ, and that difference is not a change. `inspectionCoverage` returns `complete-within-substeps` whenever the retained inspection lists are not truncated. The inspection limitations copy the inventory limitations and add a generic static-hypothesis sentence. They do not name the signing block or any other non-entry region. `absenceClaimable` treats coverage status `complete` with no omissions as authority to say something is absent. Callers in JavaScript version comparison use the graph coverage that is built from this inventory. ### Expected behavior A comparison of two inventories should stay partial for any region the inventory cannot see, or it should surface a root-hash difference as its own change. Inspection of a ZIP package should carry the signing-block limitation on the inventory evidence itself, so every consumer sees it. An empty v1 `META-INF` signing list remains a statement about those entries only. ### Where - `src/domain/artifactComparison.ts` — `compareArtifacts`, `hasCoverageLimitation` - `src/domain/artifactInspection.ts` — `createArtifactInspection`, `inspectionCoverage` - `src/artifacts/inventory/reader.ts` — `inventoryLimitations` - `src/domain/android/androidApplication.ts` — signing-block limitation, currently only on this projection - `src/domain/comparisonSemantics.ts` — `absenceClaimable`
bsakaguchi 2026-10-15 18:25:39 +00:00
Author
Owner

@CassieFox23 commented on GitHub (Oct 10, 2026):

TL;DR: Half of this issue reproduces on 983b070d4. Inspection says complete-within-substeps and never names the signing block. The comparison half did not reproduce: a signing-block change comes back changed, not unchanged.

I used WordleSolver (GPL-3.0-only) from F-Droid, 48 entries. I made three copies with the same entries and CRCs:

  • sigflip: one byte flipped in the signing block's padding value.
  • comment: a real 57-byte ZIP comment after the end record.
  • tail: 26 raw bytes after the end record.

Inspection: reproduced

file coverage.status inventory limitations "Signing Block" in output
original complete-within-substeps [] 0
sigflip complete-within-substeps [] 0
comment complete-within-substeps [] 0
tail complete-within-substeps [] 0

The signing-block note shows up only in project-android-application-graph.

Comparison: not reproduced

I ran compare_artifacts over MCP stdio. Each side was the inventory Evidence from the CLI inspect-artifact run.

pair status summary
original vs sigflip changed 48 unchanged, 1 changed
original vs comment changed 48 unchanged, 1 changed
original vs tail changed 48 unchanged, 1 changed
original vs original unchanged 49 unchanged
{"classification":"changed","logical_path":".","dimensions":["content"]}

The root . is an occurrence too. Its artifact id follows the root bytes, so the root change is caught. The same APK renamed to .zip gives . with ["format"] instead. I got the same sigflip and comment results on 9defed972. That was main when this issue was filed, judging by commit times.

One gap is real, though. The change only says the root content differs. Nothing says the bytes sit outside the ZIP entries, and limitations is [].

Not run

  • absenceClaimable and its callers.
  • Inventories that come in pages or stop short.
  • Roots that are not ZIP-based.

Edit: The sources for WordleSolver do not agree on its licence. F-Droid says GPL-3.0-only, the repo's LICENSE file says Apache-2.0, and the source headers say GPL-3.0-or-later.

Commands
rea inspect-artifact solver-sigflip.apk --format json
# MCP stdio (rea mcp): tools/call compare_artifacts
#   {left: <original substeps[0].evidence>, right: <sigflip substeps[0].evidence>}
Test setup
  • Box: Nobara 44 (Fedora 44 base), Linux 7.2.9
  • CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads
  • Node: 24.18.0
  • Head: 983b070d4 (also 9defed972 for the comparison)
  • App: WordleSolver 1.13 from F-Droid, which is GPL-3.0-only
  • APK hash: same as the F-Droid index

🤖 Generated with Claude Code (Opus 5.5) 🦊

<!-- gh-comment-id:6095831673 --> @CassieFox23 commented on GitHub (Oct 10, 2026): **TL;DR:** Half of this issue reproduces on `983b070d4`. Inspection says `complete-within-substeps` and never names the signing block. The comparison half did not reproduce: a signing-block change comes back `changed`, not `unchanged`. I used [WordleSolver](https://f-droid.org/packages/org.billthefarmer.solver/) ([GPL-3.0-only](https://gitlab.com/fdroid/fdroiddata/-/blob/93455203833a2fe27d499d6f7870319a23712004/metadata/org.billthefarmer.solver.yml#L4)) from F-Droid, 48 entries. I made three copies with the same entries and CRCs: - **sigflip:** one byte flipped in the signing block's padding value. - **comment:** a real 57-byte ZIP comment after the end record. - **tail:** 26 raw bytes after the end record. ### Inspection: reproduced | file | `coverage.status` | inventory `limitations` | "Signing Block" in output | |---|---|---|---| | original | `complete-within-substeps` | `[]` | 0 | | sigflip | `complete-within-substeps` | `[]` | 0 | | comment | `complete-within-substeps` | `[]` | 0 | | tail | `complete-within-substeps` | `[]` | 0 | The signing-block note shows up only in `project-android-application-graph`. ### Comparison: not reproduced I ran `compare_artifacts` over MCP stdio. Each side was the inventory Evidence from the CLI `inspect-artifact` run. | pair | `status` | summary | |---|---|---| | original vs sigflip | `changed` | 48 unchanged, 1 changed | | original vs comment | `changed` | 48 unchanged, 1 changed | | original vs tail | `changed` | 48 unchanged, 1 changed | | original vs original | `unchanged` | 49 unchanged | ```json {"classification":"changed","logical_path":".","dimensions":["content"]} ``` The root `.` is an occurrence too. Its artifact id follows the root bytes, so the root change is caught. The same APK renamed to `.zip` gives `.` with `["format"]` instead. I got the same sigflip and comment results on `9defed972`. That was main when this issue was filed, judging by commit times. One gap is real, though. The change only says the root content differs. Nothing says the bytes sit outside the ZIP entries, and `limitations` is `[]`. ### Not run - `absenceClaimable` and its callers. - Inventories that come in pages or stop short. - Roots that are not ZIP-based. Edit: The sources for WordleSolver do not agree on its licence. F-Droid says `GPL-3.0-only`, the repo's [LICENSE](https://github.com/billthefarmer/wordlesolver/blob/59180002e66c3befaebdbbf0a41073baf9f0cef4/LICENSE) file says `Apache-2.0`, and the [source headers](https://github.com/billthefarmer/wordlesolver/blob/59180002e66c3befaebdbbf0a41073baf9f0cef4/src/main/java/org/billthefarmer/solver/Main.java#L8-L10) say `GPL-3.0-or-later`. <details> <summary>Commands</summary> ```sh rea inspect-artifact solver-sigflip.apk --format json # MCP stdio (rea mcp): tools/call compare_artifacts # {left: <original substeps[0].evidence>, right: <sigflip substeps[0].evidence>} ``` </details> <details> <summary>Test setup</summary> - Box: Nobara 44 (Fedora 44 base), Linux 7.2.9 - CPU: AMD Ryzen 9 5900X, 12 cores / 24 threads - Node: 24.18.0 - Head: `983b070d4` (also `9defed972` for the comparison) - App: [WordleSolver 1.13](https://f-droid.org/packages/org.billthefarmer.solver/) from F-Droid, which is [GPL-3.0-only](https://gitlab.com/fdroid/fdroiddata/-/blob/93455203833a2fe27d499d6f7870319a23712004/metadata/org.billthefarmer.solver.yml#L4) - APK hash: same as the F-Droid index </details> 🤖 Generated with Claude Code (Opus 5.5) 🦊
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#502
No description provided.