From c13d03f6c36d19d3f1f6b81725381776c5fac15d Mon Sep 17 00:00:00 2001 From: Arni Dagur Date: Thu, 16 Jul 2026 04:07:19 +0100 Subject: [PATCH 1/2] fix: run integration tests again after workspace conversion The workspace conversion moved the crate into ktls/ but left tests/ at the workspace root. The root is a virtual manifest, which owns no test targets, so cargo has not discovered or run the integration suite since: 'cargo test --workspace' builds only the two unit-test binaries. Move the directory into the ktls package, whose dev-dependencies it already uses. --- {tests => ktls/tests}/integration_test.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests => ktls/tests}/integration_test.rs (100%) diff --git a/tests/integration_test.rs b/ktls/tests/integration_test.rs similarity index 100% rename from tests/integration_test.rs rename to ktls/tests/integration_test.rs From 1ff7c64ef39305a7a6b7ad7ce794766499afb509 Mon Sep 17 00:00:00 2001 From: Arni Dagur Date: Thu, 16 Jul 2026 04:11:26 +0100 Subject: [PATCH 2/2] fix: error instead of returning Ok(0) when writing after close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once write_closed is set — the peer sent close_notify or a fatal alert, or we shut the stream down — poll_write returned Ready(Ok(0)) forever. Ok(0) is a poor fit here: callers that retry short writes cannot make progress and spin at 100% CPU. We hit exactly that in production through h2, whose frame-write loop re-polls until its buffer drains. Return BrokenPipe instead, matching what writing to the raw socket after shutdown would produce (EPIPE). The existing write-after-shutdown assertions passed only because write_all's zero-write fallback produced the error; assert on a bare write and the error kind so the stream's own behavior is pinned, and cover the peer-initiated close path on both the client and server sides. --- ktls/src/ktls_stream.rs | 2 +- ktls/tests/integration_test.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ktls/src/ktls_stream.rs b/ktls/src/ktls_stream.rs index d5c0928..dd32302 100644 --- a/ktls/src/ktls_stream.rs +++ b/ktls/src/ktls_stream.rs @@ -269,7 +269,7 @@ where buf: &[u8], ) -> task::Poll> { if self.write_closed { - return task::Poll::Ready(Ok(0)); + return task::Poll::Ready(Err(io::ErrorKind::BrokenPipe.into())); } self.project().inner.poll_write(cx, buf) diff --git a/ktls/tests/integration_test.rs b/ktls/tests/integration_test.rs index c20c39d..ddde6be 100644 --- a/ktls/tests/integration_test.rs +++ b/ktls/tests/integration_test.rs @@ -193,13 +193,18 @@ async fn server_test_inner(cipher_suite: KtlsCipherSuite, flavor: ServerTestFlav stream.read_exact(&mut buf[..1]).await.is_err(), "Session still open?" ); + + debug!("Server trying to write after the peer closed"); + let err = stream.write(&PAYLOADS.server).await.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::BrokenPipe); } ServerTestFlavor::ServerCloses => { debug!("Server sending close notify (5/5)"); stream.shutdown().await.unwrap(); debug!("Server trying to write after closing"); - stream.write_all(&PAYLOADS.server).await.unwrap_err(); + let err = stream.write(&PAYLOADS.server).await.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::BrokenPipe); } } @@ -425,6 +430,10 @@ async fn client_test_inner(cipher_suite: KtlsCipherSuite, flavor: ClientTestFlav ); assert!(stream.read_exact(buf).await.is_err(), "Session still open?"); + debug!("Client trying to write after the peer closed"); + let err = stream.write(&PAYLOADS.client).await.unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::BrokenPipe); + jh.await.unwrap(); }