fix(sdk): zip64/APPNOTE conformance in the TDF3 zip reader and writer (DSPX-4591) - #1017
Open
dmihalcik-virtru wants to merge 1 commit into
Open
fix(sdk): zip64/APPNOTE conformance in the TDF3 zip reader and writer (DSPX-4591)#1017dmihalcik-virtru wants to merge 1 commit into
dmihalcik-virtru wants to merge 1 commit into
Conversation
|
Warning Review limit reachedNext included review available in 52 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This was referenced Sep 4, 2026
dmihalcik-virtru
added a commit
to opentdf/tests
that referenced
this pull request
Sep 10, 2026
Closes [DSPX-4592](https://virtru.atlassian.net/browse/DSPX-4592). ## Why ZIP central-directory offsets and sizes are 32-bit **unsigned** on the wire. A reader that widens one with a signed read sees anything `>= 2**31` as negative; at or above `2**32` the format mandates the ZIP64 sentinel, so the 32-bit field never holds a real value. That leaves exactly one broken window, `[2**31, 2**32)`, and nothing in this suite reached it — `--large` is 5 GiB, which steps straight over. ## What lands - `xtest/sizes.py` — adds `medium` (2 254 857 830 B, ~102 MiB inside the low edge of the window — shrinking it doesn't make the test cheaper, it makes it vacuous) and the window predicates (`in_zip64_window`, `exercises_zip64_window`). - `xtest/zipinspect.py` — a raw ZIP central-directory reader that keeps the 32-bit fields alongside the resolved values. `zipfile` normalises ZIP64 away, which is exactly the encoding under test. Lets a failure name the SDK at fault instead of reporting "decrypt failed" after an hour and 6 GiB of IO. - `xtest/test_zip64.py` — the roundtrip cell, marked `zip64` and **deselected** (not skipped) unless the session's sizes reach `2**31`. Asserts an offset actually landed in the window, so a mis-sized payload fails rather than passes vacuously. Writer conformance is checked *before* the reader xfail is applied, so a writer regression can't hide behind a known reader bug. Reuses `tdfs.skip_chunky_skew` (from [#590](#590)) to keep the independent segment-defaulting defect out of the ZIP64 result. - `tdfs.zip64_reader_xfail` — `xfail(strict=True)` keyed on semver for java decryptors predating java-sdk#393. Strict, so the cell must flip to a hard failure when the fix ships and somebody deletes the predicate. - A nightly-only `zip64` job in `xtest.yml`: own 90 m timeout, matrixed over the encrypting SDK, no `--skip-released-pairs` (a released java decryptor is the point). Parses its own junit XML and fails if no cell executed. Also pins the `bench` job's platform ref through the same resolved main SHA the zip64 job uses, so both share one commit instead of resolving "main" independently. - `xtest/test_zip64_units.py` (20 tests) on the offline PR gate, since the nightly's verdict is only as good as this parser. - `spec/DSPX-4592.md` — spec and live-run findings. ## Sibling PRs | Repo | PR | Covers | |---|---|---| | java-sdk | opentdf/java-sdk#396 | DSPX-4589 — `readUnsignedInt`, `needsZip64`, segment-size defaulting | | platform (go) | opentdf/platform#3979 | DSPX-4590 — `resolveSegmentSizes`, `LoadTDF` payload size | | web-sdk | opentdf/web-sdk#1017 | DSPX-4591 — ZIP64 writer conformance | Stacked on [#590](#590) (chunky segment-defaulting), which stacks on [#589](#589) (configurable payload sizes), which stacks on [#588](#588) (XT_FORCE_SUPPORTS). This PR is scoped to ZIP64 conformance only — chunky segment-defaulting coverage split out to #590 since it's an orthogonal, independently-mergeable concern found along the way. ## Follow-ups (not in this PR) - Once the go and java fixes release, replace the `exit 1` in the `chunky)` case of `xtest/sdk/{go,java}/cli.sh` with real version gates. - Consider widening `zip64_reader_xfail` once the first nightly reports which cells actually fail. ## Verification `ruff check` / `ruff format` / `pyright` clean from `xtest/`. Full offline harness suite (177 tests) passes. `actionlint` on `xtest.yml`/`check.yml` reports the same 15 pre-existing shellcheck info findings as `main`, no new ones. Draft: the `zip64` job has not had a live `workflow_dispatch` run yet. Doing that against this branch is the last gate before marking ready. [DSPX-4592]: https://virtru.atlassian.net/browse/DSPX-4592?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added configurable payload-size testing for small, medium, chunky, and large scenarios. - Added optional controls for forcing feature support and running ZIP64 validation workflows. - Added cross-SDK ZIP64 boundary coverage for large files and multi-segment containers. - **Bug Fixes** - Improved detection and reporting of malformed ZIP64 structures and unexpected test-support errors. - **Documentation** - Documented test-size options, environment settings, and ZIP64 validation coverage. - Documented the deprecated `--large` option alias. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
dmihalcik-virtru
added this pull request to stack #1033
September 10, 2026 20:52
dmihalcik-virtru
marked this pull request as ready for review
September 10, 2026 20:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



https://virtru.atlassian.net/browse/DSPX-4591
Audit of java-sdk #393 against the Go
and Web zip implementations turned up four ZIP64/APPNOTE conformance divergences in
web-sdk. All four are addressed here.
Note on the writer's ZIP64 threshold: there isn't one, and that's deliberate.
ZipWritersetsthis.zip64 = truein its constructor and never clears it, soweb-sdk unconditionally emits ZIP64 structures. That is already the safest posture
and is unchanged by this PR. The siblings (DSPX-4589 java-sdk, DSPX-4590 go-sdk) are
the ones moving thresholds.
Finding 1 — the reader never parsed the end of central directory record
getCDBuffers()scanned the last 1000 bytes of the file backwards for anythingmatching the central directory signature
0x02014b50and treated every hit as anentry, inferring record boundaries from the position of the next hit.
Replaced with real end-of-central-directory parsing:
ZipReader.getEndOfCentralDirectory()locates the EOCD record by scanning the tailfor
0x06054b50and accepting a candidate only when its declared comment lengthputs the end of the comment exactly at the end of the archive. That rules out
payload bytes that happen to spell the signature, and it handles trailing archive
comments. The tail read starts at 1 KiB (enough for the comment-less containers we
write) and widens once to
22 + 0xffff + 20bytes if the record isn't in there.0xffffentry count,0xffffffffCD size,0xffffffffCD offset) the reader follows the ZIP64 end ofcentral directory locator that sits immediately before the EOCD and reads the ZIP64
EOCD record it points at, taking the entry count, CD size and CD offset from there.
getCDBuffers()now takes the central directory chunk plus the declared entrycount, and walks exactly that many records from the declared offset, advancing by
46 + fileNameLength + extraFieldLength + fileCommentLengthand validating thesignature and the remaining length at each step.
must be at least
46 * entryCount, and it is bounded at 16 MiB so a hostile orcorrupt EOCD can't ask us to buffer the sentinel 4 GiB.
getCDBufferschanged signature. It's a public method onZipReaderbutZipReaderis not re-exported from any package entry point (
lib/tdf3/src/utils/index.tsonly),so this isn't a published API break.
Finding 2 — ZIP64 extra field ignored unless
versionNeededToExtract >= 45Dropped the version gate in
parseCDBuffer. APPNOTE 4.5.3 does not condition thevalidity of a ZIP64 extended information extra field on the version-needed-to-extract
byte; the sentinel values in the fixed-size fields are what select it. The gate meant
a conformant producer writing a ZIP64 extra with a lower declared version had it
silently dropped, leaving
0xffffffffto flow intobyteStart/byteEndarithmeticas if it were a real number.
sliceExtraFieldsalready did the right thing (iterates the whole extra-field area,rejects conflicting duplicate header IDs, bounds-checks
dataSize, reads the ZIP64fields in APPNOTE order) and is unchanged. Also added a length guard to
parseCDBufferso a record shorter than the 46-byte fixed prefix is rejected rather than parsed out
of a short buffer.
Finding 3 — non-ZIP64 data descriptor wrote
uncompressedSizetwiceKept the branch and fixed it, rather than deleting it. Justification:
ZipWriter.zip64is a public mutable field and the existing unit tests exercise
zip64 = falsefor allfour writer methods (
getLocalFileHeader,writeDataDescriptor,writeCentralDirectoryRecord,writeEndOfCentralDirectoryRecord). Deleting one ofthose four branches would leave the writer unable to emit a coherent non-ZIP64 archive
while the other three remain, which is a worse state than the bug.
writeDataDescriptornow takes an explicit optionalcompressedSize(defaulting touncompressedSize, since we only ever STORE) and writes it into the compressed sizeslot in both the ZIP64 and non-ZIP64 branches. Output for every existing call site is
byte-identical; the existing characteristic tests are unchanged. Two new tests pass
distinct compressed and uncompressed sizes and assert each lands in its own slot.
Finding 4 — 32-bit shift on a potentially large size in an error path
(cdObj.uncompressedSize >> 10)becameMath.floor(cdObj.uncompressedSize / 1024).>>coerces to signed 32 bits, so a 2 GiB manifest used to be reported as-2,097,152 KiB. Error-message only.Tests
lib/tests/mocha/unit/zip.spec.tsgains abuildZiphelper that assembles completein-memory archives with
ZipWriter, so the reader is exercised end to end rather thanagainst hand-rolled record fragments. New coverage:
0x02014b50— the false-positive casecentral directory record and inside a full archive)
versionNeededToExtractforced to 20The
0x02014b50test was verified against the pre-change reader: with the old backwardscanner restored,
getCentralDirectory()returns four entries(
['', '', '0.manifest.json', '0.payload']) instead of two, the two empty-named onesbeing payload bytes misread as central directory records. It passes with the new
reader.
Deliberately not changed: segment-size emission
lib/tdf3/src/tdf.tsomitssegmentSize/encryptedSegmentSizefrom a segmentobject when they equal the manifest-level defaults, and that stays as it is. The
emission is legal:
manifest.schema.jsongivessegments/itemsnorequiredlist,and web-sdk's own reader defaults them back. go-sdk and java-sdk fail to apply that
fallback, which is why web-sdk TDFs over 1 MiB are currently unreadable by them —
those are the bugs, tracked as DSPX-4590 finding 7 and DSPX-4589 finding 4. Making
web-sdk write the redundant fields would paper over two real reader bugs and silently
un-cover them.
Cross-SDK interop
The shared interop harness is DSPX-4592 and is already built in
opentdf/testsonbranch
DSPX-4592-java-underflow. It needs nothing from this PR.