pjfanning opened a new issue, #3732: URL: https://github.com/apache/pekko/issues/3732
### Summary With `pekko.stream.materializer.tls.engine = graph-stage`, `TlsGraphStage` fails the stream with `IllegalStateException: Stuck in unwrap loop` when ciphertext from the peer arrives while the `SSLEngine` needs to wrap (`NEED_WRAP`) and the ciphertext output has no downstream demand. ### How it was hit I found this while writing the real-`SSLEngine` tests for #3731. A test drove a plain client `SSLEngine` against a server `TlsGraphStage` (TLS 1.3, `TestSource`/`TestSink` on the ciphertext ports), and requested server ciphertext one element at a time, only when the client needed to unwrap. 1. The client sends ClientHello. The server has to send its whole flight (ServerHello … Finished), but the test only requested one element. 2. The client unwraps ServerHello and then sends its 6-byte middlebox-compatibility ChangeCipherSpec. 3. That CCS reaches the server while the server engine is still `NEED_WRAP` and the ciphertext output has no demand. ``` java.lang.IllegalStateException: Stuck in unwrap loop, bailing out, last handshake status [NEED_WRAP], remaining=6, out=0, (https://github.com/apache/pekko/issues/442) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1.doUnwrap(TlsGraphStage.scala:487) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1.doInbound(TlsGraphStage.scala:361) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1.runPhase(TlsGraphStage.scala:225) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1.pump(TlsGraphStage.scala:188) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1.org$apache$pekko$stream$impl$io$TlsGraphStage$$anon$$runPump(TlsGraphStage.scala:178) at org.apache.pekko.stream.impl.io.TlsGraphStage$$anon$1$TransportInputSlot.onPush(TlsGraphStage.scala:1021) ``` (Line numbers are from 8f3d71b90e.) The test in #3731 avoids this by requesting some demand up front, so it is not part of that PR. ### Analysis In `doUnwrap`, `unwrap` returns `OK` with `NEED_WRAP` without consuming the record, and the stage puts the bytes back (`putBackUnreadBuffer`). Nothing can be wrapped without transport demand, but the inbound side still reports ready (transport data present and plaintext demand available). So the pump keeps re-running `doInbound` → `doUnwrap` until `inlineHandshakeReentries` exceeds `MaxInlineEngineProgressAttempts` and the stage fails. Back-pressure on the ciphertext output is legitimate, so this should wait for demand rather than fail. Over real TCP the ciphertext output usually has demand, so this needs the TCP write side to be back-pressured at the moment the peer sends mid-flight. It should be rare, but it is plausible with slow networks or large handshake flights. ### Suggested direction When the last handshake status is `NEED_WRAP` and the transport output has no demand, treat the inbound side as not ready (or stop the inbound loop) until `onPull` on the ciphertext output re-runs the pump, instead of re-unwrapping the same bytes. ### Legacy engine `TLSActor` has the same guard with a comment asking for a reproducer (#442). I have not checked whether the legacy engine fails with the same sequence; if it does, this may be a reproducer for #442. ### References Refs #442, #3731 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
