Skip to content

fix(streaming): read the padded final AES block; stop rotating readers that make no progress - #940

Merged
javi11 merged 1 commit into
mainfrom
fix/aes-tail-read-spin
Sep 8, 2026
Merged

javi11 merged 1 commit into
mainfrom
fix/aes-tail-read-spin

Conversation

@javi11

@javi11 javi11 commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Regression from fix(streaming): clamp a Range end past EOF instead of marking the file corrupted #937. createUsenetReader clamped the range end to FileSize-1. For AES-CBC encrypted RAR5/7z files whose plaintext size is not a multiple of 16, the stored segments cover the padded ciphertext (up to 15 bytes past FileSize, verified on a real .meta: usableTotal = FileSize + 8) and the decryptor needs that final block. With the clamp the AES reader hit EOF a few bytes short of the range end.
  • Read/ReadAtContext treated EOF && hasMoreDataToRead() as "rotate to the next range" and rebuilt the reader at the same offset in a tight loop — ~1.85 cores, 187k segment-cache hits in 2 s, holding mvf.mu — until the 2-minute stream read timeout fired, which cannot break that loop either. Surfaced in the nzb-streaming-benchmarks run as a 180 s hang on the last-2 MB integrity read of every encrypted archive with size % 16 != 0, plus the CPU it stole from neighbouring measurements.
  • Fix: clamp to the segment-covered extent (segmentOffsetIndex.totalBytes()) instead of FileSize — this still fixes fix(streaming): clamp a Range end past EOF instead of marking the file corrupted #937's over-EOF case — and add a no-progress guard in both rotate-on-EOF loops that returns io.ErrUnexpectedEOF instead of spinning.

Test plan

  • New aes_tail_read_test.go: tail read of an AES file with size % 16 == 8 returns the exact plaintext (hung before the fix); truncated-metadata variant returns ErrUnexpectedEOF promptly instead of looping.
  • range_past_eof_test.go (fix(streaming): clamp a Range end past EOF instead of marking the file corrupted #937) still passes.
  • go test -race ./internal/nzbfilesystem/ ./internal/usenet/ ./internal/webdav/ ./internal/encryption/...
  • Standalone repro against the real 7z-split-medium post: last-2 MB range now returns in ~5 s with the same sha256 every other app serves (3a59d975…); was a 180 s timeout.
  • Harness rerun (raw,altmount, smoke/core/stress): zero stream read timeouts; the two affected items' integrity wall 180 s → ~0.5 s, CPU 345 s → 15 s per item.
  • makegolangci-lint currently fails locally on a Go 1.27 export-data version mismatch in untouched files; CI lint should confirm.

…s that make no progress

createUsenetReader clamped a range end to FileSize-1 (#937). For AES-CBC
encrypted RAR5/7z files whose size is not a multiple of 16 the stored segments
cover the padded ciphertext (up to 15 bytes past FileSize) and the decryptor
needs that final block, so the last plaintext bytes could never be produced:
the AES reader hit EOF short of the range end and Read/ReadAtContext rebuilt
the reader at the same offset in a tight loop, holding mvf.mu at ~2 cores
until the stream read timeout, which cannot break the loop either.

Clamp to the segment-covered extent instead of FileSize, and return
io.ErrUnexpectedEOF when a rebuilt reader ends at the same offset again.
@javi11
javi11 merged commit d08914c into main Sep 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant