Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

StreamingAeadSeekableDecryptingChannel.read() never returns -1 — infinite CPU spin / hang on empty or truncated ciphertexts

オープン
#79 コメント 0 件 リアクション 0 件 担当者 1 名 GitHub で見る

@willinois がすでに取り組んでいます。

2026年9月21日 から。

  • #81 @arimu1 による — マージされずにクローズ

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
74/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java
領域
cryptography

調査の方向性

Start in StreamingAeadSeekableDecryptingChannel.java, focusing on read(), reachedEnd(), and the fill loop; review StreamingTestUtil to understand why EOF is not covered. Add regression coverage for empty, boundary, and truncated ciphertexts across AES-GCM-HKDF and AES-CTR-HMAC, and verify reads return -1 rather than repeatedly returning 0.

索引モデルが issue の本文から書いたものです。

説明

tink-java: StreamingAeadSeekableDecryptingChannel.read() never returns -1 — infinite CPU spin / hang on empty or truncated ciphertexts

Repo: tink-crypto/tink-java (OT0)

Summary

StreamingAeadSeekableDecryptingChannel (returned by the public
StreamingAead.newSeekableDecryptingChannel API) violates the
SeekableByteChannel contract: read(ByteBuffer) never returns -1 at
end-of-stream. Callers using the universal idiom spin forever burning CPU, and
Channels.newInputStream(ch).readAllBytes() / transferTo never return.

Verified against HEAD (810d752), PoC output:

valid empty-plaintext ciphertext: 56 bytes
HANG: valid-empty (seg 64): read() returned 0 one-million times (11 ms of CPU spin), never -1
HANG: valid-empty (seg 64): Channels.newInputStream(...).readAllBytes() did not return within 3 s
HANG: truncated-56B (seg 64): read() returned 0 one-million times, never -1

Affects both AES-GCM-HKDF and legacy AES-CTR-HMAC (shared plumbing).

Root cause

read() returns 0 whenever read == 0 && !reachedEnd()
(StreamingAeadSeekableDecryptingChannel.java). reachedEnd() (line 282) requires
isCurrentSegmentDecrypted && currentSegmentNr == numberOfSegments-1 && plaintextSegment.remaining() == 0. When plaintextSize == 0 — which happens for a
valid empty-plaintext ciphertext, and for attacker-truncated ciphertexts whose
computed size fills complete segments — the fill loop
while (dst.remaining() > 0 && plaintextPosition < plaintextSize) never runs, no
segment is ever decrypted, so reachedEnd() is permanently false and read() loops
on 0 forever.

Note the impact split:

  • No attacker needed: encrypt an empty stream with the library itself, then read
    it through the seekable channel — the service thread hangs. A valid
    round-trip that hangs is a correctness bug on its own.
  • Attacker-controlled: decrypting an untrusted/truncated file via
    newSeekableDecryptingChannel hangs the thread (truncation scan of a 70-byte
    plaintext hangs at offsets 56/80/144 for segment size 64).

tink-go's reader handles the identical 56-byte ciphertext correctly (io.ReadAll
terminates — verified), so this is Java-specific. The existing test suite misses it
because StreamingTestUtil allocates plaintext.length-sized buffers and never
reads to EOF.

Fix direction

When the fill loop makes no progress and plaintextPosition >= plaintextSize (or
ciphertext is exhausted), decrypt the final (possibly empty) segment so
reachedEnd() becomes true and read() returns -1. Reproducer and a table-driven
regression test (empty, 0/1/±tag-boundary, truncated lengths) are ready to send.

Verified against HEAD (810d752). Reproducer harness output above was produced with the AES-GCM-HKDF keyset at segment size 64; AES-CTR-HMAC shares the plumbing and behaves identically.

主要言語
Java
スター
310
フォーク
40
PR マージ指標
30日以内にマージされた PR はありません

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

tink-crypto/tink-java のほかの issue

tink-crypto/tink-java の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。