Streaming skip can loop forever when the input stops advancing
perfloop/pbj · NON PROGRESS LOOP
https://perfloop.ai/t/oss/case_8ksqe054k3
Verdict
VERIFIED · settled 2026-10-01
What happened: The assertion is violated on the comparison and satisfied with this change.
Hypothesis
A positive skip on a stream of unknown length can keep looping after it reaches EOF. A stream may also return zero from `InputStream.skip` while bytes remain, so a zero result alone does not prove truncation. How often consumers encounter either condition is unmeasured.
The `ReadableStreamingData(InputStream)` constructor sets `limit` to `Long.MAX_VALUE`. For a positive count within that limit, `ReadableStreamingData.skip` subtracts `in.skip(toSkip)` from `toSkip`; when that call returns zero, the loop's remaining count and position stay unchanged, and there is no other loop exit. `position` is advanced only after the entire skip succeeds. `ReadableStreamingData` already signals confirmed end-of-input during reads with `EOFException`, a `BufferUnderflowException`. The guarantee concerns underlying `skip` and fallback `read` calls that return or throw, not an input stream that itself blocks indefinitely.
A focused `ReadableStreamingDataTest` can use an `InputStream`-backed short byte array and an instrumented stream whose `skip` returns zero before `read` supplies a byte or EOF. Bound the instrumented skip-call count to show whether a zero is retried without advancing; after the repair, assert completion when enough bytes exist, defined EOF failure when they do not, `position` equal to bytes consumed even on partial failure, unchanged `limit`, and no remaining data after confirmed EOF. Known-length input, explicit-limit rejection for ordinary counts, and zero/negative skips control the existing semantics. A repeated zero skip without an intervening byte read or failure, or an incorrect position after partial consumption, would refute the repair. The skip body requests that the underlying stream advance; it contains no large data copy to remove under the assigned copy strategy.
Change to test: Make a positive skip consume the requested bytes or fail on confirmed EOF rather than retry a zero-progress skip. On a zero result, read and discard one byte or report EOF; update `position` for bytes actually consumed, mark confirmed EOF, and leave `limit` unchanged.
Where it lives
Evidence
The assertion is violated on the comparison and satisfied with this change: `For a positive in-limit skip on an unknown-length ReadableStreamingData backed by a nonblocking InputStream, a zero skip result is followed by a read to distinguish remaining data from confirmed EOF; returned skip and read bytes advance position. A short input that confirms EOF causes EOFException with position at the bytes consumed, unchanged limit, and no remaining data. A subsequent positive in-limit skip after a read has already confirmed EOF throws EOFException without further underlying skip or read calls. Returned skip progress remains reflected in position if a later skip throws IOException; a fallback read IOException propagates without retry. ProtoParserTools.skipField for a length-delimited payload longer than the available input terminates with EOFException, consumed position, unchanged limit, and no remaining data.`
Checks: 2 of 2 passed. Verification: no defect found.