Repository: rustls/rustls (language: rust)

Issue:
Unbuffered API: `DecryptError` is reported by TLS client when server uses large-sized certificates
**Describe the bug**
This issue can be found under the situation:
 - Server uses large certificate that require numbers of TLS messages to be sent to client.
 - One or more TLS messages are divided into multiple data packets.

After some investigations, the issue can be found after the patch set https://github.com/rustls/rustls/pull/2049 was merged. My guess is that this might be a regression issue related to the deframer or TLS message processing logic.

When a previous TLS message has been decrypted but not discarded, and the current TLS message is incomplete, upon receiving the full message, the `process_more_input()` will start back from the *decrypted content* that is not expected.

**To Reproduce**
Unfortunately, I can't reproduce the issue with existing examples because this issue is found under specific network channel (the TLS messages are divided).

**Applicable Version(s)**
v/0.23.13
v/0.23.14

**Additional context**
`UnbufferedServerConnection` and `UnbufferedClientConnection` are used in my case.

Unbuffered API: `DecryptError` is reported by TLS client when server uses large-sized certificates
**Describe the bug**
This issue can be found under the situation:
 - Server uses large certificate that require numbers of TLS messages to be sent to client.
 - One or more TLS messages are divided into multiple data packets.

After some investigations, the issue can be found after the patch set https://github.com/rustls/rustls/pull/2049 was merged. My guess is that this might be a regression issue related to the deframer or TLS message processing logic.

When a previous TLS message has been decrypted but not discarded, and the current TLS message is incomplete, upon receiving the full message, the `process_more_input()` will start back from the *decrypted content* that is not expected.

**To Reproduce**
Unfortunately, I can't reproduce the issue with existing examples because this issue is found under specific network channel (the TLS messages are divided).

**Applicable Version(s)**
v/0.23.13
v/0.23.14

**Additional context**
`UnbufferedServerConnection` and `UnbufferedClientConnection` are used in my case.



Discussion / hints:
Thanks for the report -- could you provide any more info, eg a packet capture when this happens?
This is sufficient to reproduce:

```patch
diff --git a/rustls/tests/unbuffered.rs b/rustls/tests/unbuffered.rs
index 97a0f189..eccc04b6 100644
--- a/rustls/tests/unbuffered.rs
+++ b/rustls/tests/unbuffered.rs
@@ -100,7 +100,8 @@ fn tls13_handshake() {
 }
 
 fn handshake(version: &'static rustls::SupportedProtocolVersion) -> Outcome {
-    let server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
+    let mut server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
+    server_config.max_fragment_size = Some(1024);
     let client_config = make_client_config(KeyType::Rsa2048);
 
     run(

```

I think maybe we should be cautious about chasing individual bugs in the unbuffered API, and instead try and to work more widely to address the testing deficit we have there. (There are multiple tests for precisely this case, but they are only run for the buffered API.)
Thanks @ctz . I tried to set the `max_fragment_size` but I still cannot reproduce the issue. My understanding is decreasing the fragment size results in smaller TLS messages, thereby preventing a single TLS message from being fragmented across multiple network packets. Please advise if this assumption is incorrect.

I included the following `log` in the code:

```
diff --git a/rustls/src/msgs/deframer/mod.rs b/rustls/src/msgs/deframer/mod.rs
index 2f44962c..7dd9de53 100644
--- a/rustls/src/msgs/deframer/mod.rs
+++ b/rustls/src/msgs/deframer/mod.rs
@@ -46,6 +46,7 @@ impl<'a> Iterator for DeframerIter<'a> {
     type Item = Result<InboundOpaqueMessage<'a>, Error>;
 
     fn next(&mut self) -> Option<Self::Item> {
+        log::info!("0x{:x} bytes opaque message: {:x?}\n", self.buf.len(), self.buf);
         let mut reader = Reader::init(self.buf);
 
         let (typ, version, len) = match read_opaque_message_header(&mut reader) {
diff --git a/rustls/src/record_layer.rs b/rustls/src/record_layer.rs
index 8c2ea6b9..f4434e8c 100644
--- a/rustls/src/record_layer.rs
+++ b/rustls/src/record_layer.rs
@@ -83,6 +83,7 @@ impl RecordLayer {
             .decrypt(encr, self.read_seq)
         {
             Ok(plaintext) => {
+                log::info!("plain message: {:x?}\n", plaintext.payload.as_ref());
                 self.read_seq += 1;
                 if !self.has_decrypted {
                     self.has_decrypted = true;
```

These are the relevant TLS message traces from TLS client side for your reference:

INFO - 0x1000 bytes opaque message: [**16, 3, 3, 0, bb**, 2, 0, 0, b7, 3, 3, b5, a7, 4e, b2, e7, 36, 58, 23, 49, b9, c9, 83, 59, c5, 39, cf, 9c, 71, db, e0, ce, c3, 23, dd, 9d, a1, 3a, 89, f5, 49, 53, 19, 20, 23, 64, 3, 8a, 15, 61, 7b, b2, 56, cc, fc, 6a, 77, 82, 3e, 26, 62, 42, 60, 37, a3, 5, 2a, be, 97, 32, 2c, 24, 9d, d3, eb, 1, 13, 2, 0, 0, 6f, 0, 33, 0, 65, 0, 18, 0, 61, 4, a1, c6, e1, 1f, 14, bd, 16, 9d, 42, e2, 5b, 70, d9, 65, 82, 3e, 95, 6c, 56, a9, c, 9c, 3, cf, 1d, be, 27, 31, 3b, 6a, 5a, 85, d7, fe, e9, f0, 59, a7, 19, c0, 47, 26, e0, 28, 93, 4e, 12, 29, 1e, dd, 1a, 36, fe, b5, ff, d2, 90, 93, d4, b6, 6f, fe, c3, 76, 3b, f7, 1c, e6, b7, 5e, 94, c3, 80, 91, af, b7, 20, fb, da, 8e, b6, e, 96, 24, 86, ee, 6e, 72, 6, 14, 9c, a0, 88, 98, 9d, 5, 0, 2b, 0, 2, 3, 4, 14, 3, 3, 0, 1, 1, 17, 3, 3, 0, 1b, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xf40 bytes opaque message: [**14, 3, 3, 0, 1**, 1, 17, 3, 3, 0, 1b, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xf3a bytes opaque message: [**17, 3, 3, 0, 1b**, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - plain message: [8, 0, 0, 6, 0, 4, 0, 0, 0, 0]

INFO - 0xf1a bytes opaque message: [**17, 3, 3, 0, 30**, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - plain message: [d, 0, 0, 1b, 0, 0, 18, 0, d, 0, 14, 0, 12, 5, 3, 4, 3, 8, 7, 8, 6, 8, 5, 8, 4, 6, 1, 5, 1, 4, 1]

INFO - 0xee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0x1ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , a5, fa, 86, b4, 5a, 12, 41, 60, 35, 7f]

INFO - 0x2ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , e0, ec, 92, 6b, 53, 8b, 31, 4c, b0, c1]

INFO - 0x3ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , c9, ca, ca, 95, 5d, 4b, 67, 4a, 5b, f9]

**The issue began at this point: the opaque message buffer hold by `DeframerIter` contains one complete message and one incomplete message:**

INFO - 0x4ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , 5f, 2, 6, 73, 7, 61, 3c, **17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ..., 51, 1f, b6, da, 3c, 63, fd, c7]

**The first complete message is decrypted into:**

INFO - plain message: [**b, 0, c8, 25, 0, 0, c8, 21, 0, c8, 1c, 30, 82, c8, 18, ... , 20, 20, 20, 20, 20, 20, 30, 2c, a, 20, 20, 20, 20, 20**]

INFO - 0xecf bytes opaque message: [**17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ... , dc, 11, 32, 51, 1f, b6, da, 3c, 63, fd, c7]

**The second message was fully received, but the iter still point to the first message:**

INFO - 0x5ee5 bytes opaque message: [17, 3, 3, 40, 11, **b, 0, c8, 25, 0, 0, c8, 21, 0, c8, 1c, 30, 82, c8, 18, ... , 20, 20, 20, 20, 20, 30, 2c, a, 20, 20, 20, 20, 20**, 16, a8, 3, 1c, e3, 8e, 59, 8f, 27, 9b, 5f, 2, 6, 73, 7, 61, 3c, **17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ... , c2, f8, b8, 54, aa, 10, 91, 90, 81]

I'm not sure if these traces are adequate considering the data size. I can offer more specific information if required :)
> Thanks @ctz . I tried to set the `max_fragment_size` but I still cannot reproduce the issue. My understanding is decreasing the fragment size results in smaller TLS messages, thereby preventing a single TLS message from being fragmented across multiple network packets. Please advise if this assumption is incorrect.

To reproduce `max_fragment_size` needs to be reduced on the server, so that the server will fragment its certificate into several TLS messages.
Thanks for the report -- could you provide any more info, eg a packet capture when this happens?
This is sufficient to reproduce:

```patch
diff --git a/rustls/tests/unbuffered.rs b/rustls/tests/unbuffered.rs
index 97a0f189..eccc04b6 100644
--- a/rustls/tests/unbuffered.rs
+++ b/rustls/tests/unbuffered.rs
@@ -100,7 +100,8 @@ fn tls13_handshake() {
 }
 
 fn handshake(version: &'static rustls::SupportedProtocolVersion) -> Outcome {
-    let server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
+    let mut server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
+    server_config.max_fragment_size = Some(1024);
     let client_config = make_client_config(KeyType::Rsa2048);
 
     run(

```

I think maybe we should be cautious about chasing individual bugs in the unbuffered API, and instead try and to work more widely to address the testing deficit we have there. (There are multiple tests for precisely this case, but they are only run for the buffered API.)
Thanks @ctz . I tried to set the `max_fragment_size` but I still cannot reproduce the issue. My understanding is decreasing the fragment size results in smaller TLS messages, thereby preventing a single TLS message from being fragmented across multiple network packets. Please advise if this assumption is incorrect.

I included the following `log` in the code:

```
diff --git a/rustls/src/msgs/deframer/mod.rs b/rustls/src/msgs/deframer/mod.rs
index 2f44962c..7dd9de53 100644
--- a/rustls/src/msgs/deframer/mod.rs
+++ b/rustls/src/msgs/deframer/mod.rs
@@ -46,6 +46,7 @@ impl<'a> Iterator for DeframerIter<'a> {
     type Item = Result<InboundOpaqueMessage<'a>, Error>;
 
     fn next(&mut self) -> Option<Self::Item> {
+        log::info!("0x{:x} bytes opaque message: {:x?}\n", self.buf.len(), self.buf);
         let mut reader = Reader::init(self.buf);
 
         let (typ, version, len) = match read_opaque_message_header(&mut reader) {
diff --git a/rustls/src/record_layer.rs b/rustls/src/record_layer.rs
index 8c2ea6b9..f4434e8c 100644
--- a/rustls/src/record_layer.rs
+++ b/rustls/src/record_layer.rs
@@ -83,6 +83,7 @@ impl RecordLayer {
             .decrypt(encr, self.read_seq)
         {
             Ok(plaintext) => {
+                log::info!("plain message: {:x?}\n", plaintext.payload.as_ref());
                 self.read_seq += 1;
                 if !self.has_decrypted {
                     self.has_decrypted = true;
```

These are the relevant TLS message traces from TLS client side for your reference:

INFO - 0x1000 bytes opaque message: [**16, 3, 3, 0, bb**, 2, 0, 0, b7, 3, 3, b5, a7, 4e, b2, e7, 36, 58, 23, 49, b9, c9, 83, 59, c5, 39, cf, 9c, 71, db, e0, ce, c3, 23, dd, 9d, a1, 3a, 89, f5, 49, 53, 19, 20, 23, 64, 3, 8a, 15, 61, 7b, b2, 56, cc, fc, 6a, 77, 82, 3e, 26, 62, 42, 60, 37, a3, 5, 2a, be, 97, 32, 2c, 24, 9d, d3, eb, 1, 13, 2, 0, 0, 6f, 0, 33, 0, 65, 0, 18, 0, 61, 4, a1, c6, e1, 1f, 14, bd, 16, 9d, 42, e2, 5b, 70, d9, 65, 82, 3e, 95, 6c, 56, a9, c, 9c, 3, cf, 1d, be, 27, 31, 3b, 6a, 5a, 85, d7, fe, e9, f0, 59, a7, 19, c0, 47, 26, e0, 28, 93, 4e, 12, 29, 1e, dd, 1a, 36, fe, b5, ff, d2, 90, 93, d4, b6, 6f, fe, c3, 76, 3b, f7, 1c, e6, b7, 5e, 94, c3, 80, 91, af, b7, 20, fb, da, 8e, b6, e, 96, 24, 86, ee, 6e, 72, 6, 14, 9c, a0, 88, 98, 9d, 5, 0, 2b, 0, 2, 3, 4, 14, 3, 3, 0, 1, 1, 17, 3, 3, 0, 1b, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xf40 bytes opaque message: [**14, 3, 3, 0, 1**, 1, 17, 3, 3, 0, 1b, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xf3a bytes opaque message: [**17, 3, 3, 0, 1b**, 9, cc, 9e, 7d, e8, 4e, 81, b3, 99, bd, 6b, a7, f4, c8, 5b, 4c, c4, 13, 72, 8b, 94, 7e, 34, 46, 70, cf, 1f, 17, 3, 3, 0, 30, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - plain message: [8, 0, 0, 6, 0, 4, 0, 0, 0, 0]

INFO - 0xf1a bytes opaque message: [**17, 3, 3, 0, 30**, cc, 60, ae, bc, 86, 4a, 71, 6a, 9b, 36, 38, 3c, 4, 7a, 9f, b, 0, 1e, 85, d2, b2, d1, b1, ee, 45, e0, 8b, a6, e8, b9, fb, eb, 70, 4, 8d, 28, 43, 12, f7, b5, ee, a2, 99, a8, 34, f9, a3, 64, 17, 3, 3, 40, 11, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - plain message: [d, 0, 0, 1b, 0, 0, 18, 0, d, 0, 14, 0, 12, 5, 3, 4, 3, 8, 7, 8, 6, 8, 5, 8, 4, 6, 1, 5, 1, 4, 1]

INFO - 0xee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0xee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , f2, 0, 59, 76, d7, 1c, f4, 44, e9, fa]

INFO - 0x1ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , a5, fa, 86, b4, 5a, 12, 41, 60, 35, 7f]

INFO - 0x2ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , e0, ec, 92, 6b, 53, 8b, 31, 4c, b0, c1]

INFO - 0x3ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , c9, ca, ca, 95, 5d, 4b, 67, 4a, 5b, f9]

**The issue began at this point: the opaque message buffer hold by `DeframerIter` contains one complete message and one incomplete message:**

INFO - 0x4ee5 bytes opaque message: [**17, 3, 3, 40, 11**, 87, 7f, 11, bc, 53, ... , 5f, 2, 6, 73, 7, 61, 3c, **17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ..., 51, 1f, b6, da, 3c, 63, fd, c7]

**The first complete message is decrypted into:**

INFO - plain message: [**b, 0, c8, 25, 0, 0, c8, 21, 0, c8, 1c, 30, 82, c8, 18, ... , 20, 20, 20, 20, 20, 20, 30, 2c, a, 20, 20, 20, 20, 20**]

INFO - 0xecf bytes opaque message: [**17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ... , dc, 11, 32, 51, 1f, b6, da, 3c, 63, fd, c7]

**The second message was fully received, but the iter still point to the first message:**

INFO - 0x5ee5 bytes opaque message: [17, 3, 3, 40, 11, **b, 0, c8, 25, 0, 0, c8, 21, 0, c8, 1c, 30, 82, c8, 18, ... , 20, 20, 20, 20, 20, 30, 2c, a, 20, 20, 20, 20, 20**, 16, a8, 3, 1c, e3, 8e, 59, 8f, 27, 9b, 5f, 2, 6, 73, 7, 61, 3c, **17, 3, 3, 40, 11**, ff, d, 8b, 47, e9, ee, 94, 6, 27, ... , c2, f8, b8, 54, aa, 10, 91, 90, 81]

I'm not sure if these traces are adequate considering the data size. I can offer more specific information if required :)
> Thanks @ctz . I tried to set the `max_fragment_size` but I still cannot reproduce the issue. My understanding is decreasing the fragment size results in smaller TLS messages, thereby preventing a single TLS message from being fragmented across multiple network packets. Please advise if this assumption is incorrect.

To reproduce `max_fragment_size` needs to be reduced on the server, so that the server will fragment its certificate into several TLS messages.

Fix patch:
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index eb519f9154e..cf1b5205d25 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -43,3 +43,7 @@ path = "fuzzers/server.rs"
 [[bin]]
 name = "server_name"
 path = "fuzzers/server_name.rs"
+
+[[bin]]
+name = "unbuffered"
+path = "fuzzers/unbuffered.rs"
diff --git a/fuzz/corpus/server/tls12-client.bin b/fuzz/corpus/server/accepter-tls12-client.bin
similarity index 76%
rename from fuzz/corpus/server/tls12-client.bin
rename to fuzz/corpus/server/accepter-tls12-client.bin
index 23717fba0e0..c502a5bffee 100644
Binary files a/fuzz/corpus/server/tls12-client.bin and b/fuzz/corpus/server/accepter-tls12-client.bin differ
diff --git a/fuzz/corpus/server/tls13-client.bin b/fuzz/corpus/server/accepter-tls13-client.bin
similarity index 70%
rename from fuzz/corpus/server/tls13-client.bin
rename to fuzz/corpus/server/accepter-tls13-client.bin
index e276402734d..6b8526bd298 100644
Binary files a/fuzz/corpus/server/tls13-client.bin and b/fuzz/corpus/server/accepter-tls13-client.bin differ
diff --git a/fuzz/corpus/server/buffered-tls12-client.bin b/fuzz/corpus/server/buffered-tls12-client.bin
new file mode 100644
index 00000000000..836c9649669
Binary files /dev/null and b/fuzz/corpus/server/buffered-tls12-client.bin differ
diff --git a/fuzz/corpus/server/buffered-tls13-client.bin b/fuzz/corpus/server/buffered-tls13-client.bin
new file mode 100644
index 00000000000..789d5e54093
Binary files /dev/null and b/fuzz/corpus/server/buffered-tls13-client.bin differ
diff --git a/fuzz/corpus/unbuffered/server-tls12.bin b/fuzz/corpus/unbuffered/server-tls12.bin
new file mode 100644
index 00000000000..c502a5bffee
Binary files /dev/null and b/fuzz/corpus/unbuffered/server-tls12.bin differ
diff --git a/fuzz/corpus/unbuffered/server-tls13.bin b/fuzz/corpus/unbuffered/server-tls13.bin
new file mode 100644
index 00000000000..6b8526bd298
Binary files /dev/null and b/fuzz/corpus/unbuffered/server-tls13.bin differ
diff --git a/fuzz/fuzzers/client.rs b/fuzz/fuzzers/client.rs
index c0c197db81a..b2e7e51eed5 100644
--- a/fuzz/fuzzers/client.rs
+++ b/fuzz/fuzzers/client.rs
@@ -17,6 +17,15 @@ fuzz_target!(|data: &[u8]| {
     );
     let example_com = "example.com".try_into().unwrap();
     let mut client = ClientConnection::new(config, example_com).unwrap();
-    let _ = client.read_tls(&mut io::Cursor::new(data));
-    let _ = client.process_new_packets();
+    let mut stream = io::Cursor::new(data);
+
+    loop {
+        let rd = client.read_tls(&mut stream).unwrap();
+        if client.process_new_packets().is_err() {
+            break;
+        }
+        if rd == 0 {
+            break;
+        }
+    }
 });
diff --git a/fuzz/fuzzers/server.rs b/fuzz/fuzzers/server.rs
index 0f3b82ea8c7..085dd9e9948 100644
--- a/fuzz/fuzzers/server.rs
+++ b/fuzz/fuzzers/server.rs
@@ -6,9 +6,47 @@ extern crate rustls;
 use std::io;
 use std::sync::Arc;
 
-use rustls::server::ResolvesServerCert;
+use rustls::server::{Acceptor, ResolvesServerCert};
 use rustls::{ServerConfig, ServerConnection};
 
+fuzz_target!(|data: &[u8]| {
+    match data.split_first() {
+        Some((0x00, rest)) => fuzz_buffered_api(rest),
+        Some((0x01, rest)) => fuzz_acceptor_api(rest),
+        Some((_, _)) | None => {}
+    }
+});
+
+fn fuzz_buffered_api(data: &[u8]) {
+    let config = Arc::new(
+        ServerConfig::builder()
+            .with_no_client_auth()
+            .with_cert_resolver(Arc::new(Fail)),
+    );
+    let mut stream = io::Cursor::new(data);
+    let mut server = ServerConnection::new(config).unwrap();
+    let _ = server.read_tls(&mut stream);
+    let _ = server.process_new_packets();
+}
+
+fn fuzz_acceptor_api(data: &[u8]) {
+    let mut server = Acceptor::default();
+    let mut stream = io::Cursor::new(data);
+
+    loop {
+        let rd = server.read_tls(&mut stream).unwrap();
+        match server.accept() {
+            Ok(Some(_)) | Err(_) => {
+                break;
+            }
+            Ok(None) => {}
+        }
+        if rd == 0 {
+            break;
+        }
+    }
+}
+
 #[derive(Debug)]
 struct Fail;
 
@@ -20,14 +58,3 @@ impl ResolvesServerCert for Fail {
         None
     }
 }
-
-fuzz_target!(|data: &[u8]| {
-    let config = Arc::new(
-        ServerConfig::builder()
-            .with_no_client_auth()
-            .with_cert_resolver(Arc::new(Fail)),
-    );
-    let mut server = ServerConnection::new(config).unwrap();
-    let _ = server.read_tls(&mut io::Cursor::new(data));
-    let _ = server.process_new_packets();
-});
diff --git a/fuzz/fuzzers/unbuffered.rs b/fuzz/fuzzers/unbuffered.rs
new file mode 100644
index 00000000000..bca77d958ea
--- /dev/null
+++ b/fuzz/fuzzers/unbuffered.rs
@@ -0,0 +1,88 @@
+#![no_main]
+#[macro_use]
+extern crate libfuzzer_sys;
+extern crate rustls;
+
+use std::sync::Arc;
+
+use rustls::client::UnbufferedClientConnection;
+use rustls::server::{ResolvesServerCert, UnbufferedServerConnection};
+use rustls::unbuffered::{ConnectionState, UnbufferedStatus};
+use rustls::{ClientConfig, RootCertStore, ServerConfig, SideData};
+
+fuzz_target!(|data: &[u8]| {
+    let mut data = data.to_vec();
+    match data.split_first_mut() {
+        Some((0x00, rest)) => client(rest),
+        Some((0x01, rest)) => server(rest),
+        Some((_, _)) | None => {}
+    }
+});
+
+fn client(data: &mut [u8]) {
+    let config = ClientConfig::builder()
+        .with_root_certificates(RootCertStore::empty())
+        .with_no_client_auth();
+    let conn =
+        UnbufferedClientConnection::new(config.into(), "localhost".try_into().unwrap()).unwrap();
+    fuzz_unbuffered(data, ClientServer::Client(conn));
+}
+
+fn server(data: &mut [u8]) {
+    let config = ServerConfig::builder()
+        .with_no_client_auth()
+        .with_cert_resolver(Arc::new(NoServerCert));
+    let conn = UnbufferedServerConnection::new(config.into()).unwrap();
+    fuzz_unbuffered(data, ClientServer::Server(conn));
+}
+
+enum ClientServer {
+    Client(UnbufferedClientConnection),
+    Server(UnbufferedServerConnection),
+}
+
+fn fuzz_unbuffered(mut data: &mut [u8], mut conn: ClientServer) {
+    while !data.is_empty() {
+        let consumed = match &mut conn {
+            ClientServer::Server(server) => process(server.process_tls_records(data)),
+            ClientServer::Client(client) => process(client.process_tls_records(data)),
+        };
+
+        match consumed {
+            Some(consumed) => {
+                data = &mut data[consumed..];
+            }
+            None => break,
+        };
+    }
+}
+
+fn process<S: SideData>(status: UnbufferedStatus<'_, '_, S>) -> Option<usize> {
+    let UnbufferedStatus { discard, state } = status;
+
+    match state {
+        Ok(ConnectionState::EncodeTlsData(mut enc)) => {
+            let mut buffer = [0u8; 16_384 + 5]; // big enough for largest TLS packet
+            enc.encode(&mut buffer).unwrap();
+        }
+        Ok(ConnectionState::TransmitTlsData(xmit)) => xmit.done(),
+        Ok(ConnectionState::WriteTraffic(_)) => return None,
+        Ok(ConnectionState::BlockedHandshake) => return None,
+        Ok(st) => panic!("unhandled state {st:?}"),
+        Err(_) => return None,
+    };
+
+    Some(discard)
+}
+
+#[derive(Debug)]
+struct NoServerCert;
+
+impl ResolvesServerCert for NoServerCert {
+    fn resolve(
+        &self,
+        _client_hello: rustls::server::ClientHello,
+    ) -> Option<Arc<rustls::sign::CertifiedKey>> {
+        None
+    }
+}
diff --git a/rustls/src/conn.rs b/rustls/src/conn.rs
index 331903dd0a3..f69f6408e96 100644
--- a/rustls/src/conn.rs
+++ b/rustls/src/conn.rs
@@ -654,8 +654,7 @@ impl<Data> ConnectionCommon<Data> {
     /// This is a shortcut to the `process_new_packets()` -> `process_msg()` ->
     /// `process_handshake_messages()` path, specialized for the first handshake message.
     pub(crate) fn first_handshake_message(&mut self) -> Result<Option<Message<'static>>, Error> {
-        let mut buffer_progress = BufferProgress::default();
-        buffer_progress.add_processed(self.deframer_buffer.processed);
+        let mut buffer_progress = self.core.hs_deframer.progress();
 
         let res = self
             .core
@@ -666,7 +665,6 @@ impl<Data> ConnectionCommon<Data> {
             )
             .map(|opt| opt.map(|pm| Message::try_from(pm).map(|m| m.into_owned())));
 
-        self.deframer_buffer.processed = buffer_progress.processed();
         match res? {
             Some(Ok(msg)) => {
                 self.deframer_buffer
@@ -829,8 +827,7 @@ impl<Data> ConnectionCore<Data> {
             }
         };
 
-        let mut buffer_progress = BufferProgress::default();
-        buffer_progress.add_processed(deframer_buffer.processed);
+        let mut buffer_progress = self.hs_deframer.progress();
 
         loop {
             let res = self.deframe(
@@ -875,7 +872,6 @@ impl<Data> ConnectionCore<Data> {
             deframer_buffer.discard(buffer_progress.take_discard());
         }
 
-        deframer_buffer.processed = buffer_progress.processed();
         deframer_buffer.discard(buffer_progress.take_discard());
         self.state = Ok(state);
         Ok(self.common_state.current_io_state())
diff --git a/rustls/src/conn/unbuffered.rs b/rustls/src/conn/unbuffered.rs
index 57e46c92654..ef8146e098e 100644
--- a/rustls/src/conn/unbuffered.rs
+++ b/rustls/src/conn/unbuffered.rs
@@ -8,7 +8,7 @@ use std::error::Error as StdError;
 
 use super::UnbufferedConnectionCommon;
 use crate::client::ClientConnectionData;
-use crate::msgs::deframer::buffers::{BufferProgress, DeframerSliceBuffer};
+use crate::msgs::deframer::buffers::DeframerSliceBuffer;
 use crate::server::ServerConnectionData;
 use crate::Error;
 
@@ -46,7 +46,7 @@ impl<Data> UnbufferedConnectionCommon<Data> {
         execute: impl FnOnce(&'c mut Self, &'i mut [u8], T) -> ConnectionState<'c, 'i, Data>,
     ) -> UnbufferedStatus<'c, 'i, Data> {
         let mut buffer = DeframerSliceBuffer::new(incoming_tls);
-        let mut buffer_progress = BufferProgress::default();
+        let mut buffer_progress = self.core.hs_deframer.progress();
 
         let (discard, state) = loop {
             if let Some(value) = check(self) {
diff --git a/rustls/src/msgs/deframer/buffers.rs b/rustls/src/msgs/deframer/buffers.rs
index c9162b3e0c3..bf098740700 100644
--- a/rustls/src/msgs/deframer/buffers.rs
+++ b/rustls/src/msgs/deframer/buffers.rs
@@ -91,7 +91,7 @@ impl<'b> Coalescer<'b> {
 }
 
 /// Accounting structure tracking progress in parsing a single buffer.
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug)]
 pub(crate) struct BufferProgress {
     /// Prefix of the buffer that has been processed so far.
     ///
@@ -113,6 +113,13 @@ pub(crate) struct BufferProgress {
 }
 
 impl BufferProgress {
+    pub(super) fn new(processed: usize) -> Self {
+        Self {
+            processed,
+            discard: 0,
+        }
+    }
+
     #[inline]
     pub(crate) fn add_discard(&mut self, discard: usize) {
         self.discard += discard;
@@ -149,8 +156,6 @@ pub(crate) struct DeframerVecBuffer {
 
     /// What size prefix of `buf` is used.
     used: usize,
-
-    pub(crate) processed: usize,
 }
 
 impl DeframerVecBuffer {
@@ -174,10 +179,8 @@ impl DeframerVecBuffer {
             self.buf
                 .copy_within(taken..self.used, 0);
             self.used -= taken;
-            self.processed = self.processed.saturating_sub(taken);
-        } else if taken == self.used {
+        } else if taken >= self.used {
             self.used = 0;
-            self.processed = 0;
         }
     }
 
diff --git a/rustls/src/msgs/deframer/handshake.rs b/rustls/src/msgs/deframer/handshake.rs
index 7619e3fd524..c781e9309cd 100644
--- a/rustls/src/msgs/deframer/handshake.rs
+++ b/rustls/src/msgs/deframer/handshake.rs
@@ -2,7 +2,7 @@ use alloc::vec::Vec;
 use core::mem;
 use core::ops::Range;
 
-use super::buffers::{Coalescer, Delocator, Locator};
+use super::buffers::{BufferProgress, Coalescer, Delocator, Locator};
 use crate::error::InvalidMessage;
 use crate::msgs::codec::{u24, Codec};
 use crate::msgs::message::InboundPlainMessage;
@@ -73,6 +73,11 @@ impl HandshakeDeframer {
         }
     }
 
+    /// Returns a `BufferProgress` that skips over unprocessed handshake data.
+    pub(crate) fn progress(&self) -> BufferProgress {
+        BufferProgress::new(self.outer_discard)
+    }
+
     /// Do we have a message ready? ie, would `iter().next()` return `Some`?
     pub(crate) fn has_message_ready(&self) -> bool {
         match self.spans.first() {
diff --git a/rustls/src/quic.rs b/rustls/src/quic.rs
index 225fda878ad..adfe168669f 100644
--- a/rustls/src/quic.rs
+++ b/rustls/src/quic.rs
@@ -384,10 +384,6 @@ mod connection {
                 range.end,
             );
 
-            // `core.process_new_packets` should not process any data in `deframer_buffer`;
-            // it is already ready in `hs_deframer`.
-            self.deframer_buffer.processed = range.end;
-
             self.core
                 .hs_deframer
                 .coalesce(self.deframer_buffer.filled_mut())?;


Test patch:
diff --git a/rustls/tests/unbuffered.rs b/rustls/tests/unbuffered.rs
index 5a36f42a36a..81311b2540c 100644
--- a/rustls/tests/unbuffered.rs
+++ b/rustls/tests/unbuffered.rs
@@ -58,6 +58,57 @@ fn tls12_handshake() {
     );
 }
 
+#[test]
+fn tls12_handshake_fragmented() {
+    let outcome = handshake_config(&rustls::version::TLS12, |client, server| {
+        client.max_fragment_size = Some(512);
+        client.cert_decompressors = vec![];
+        server.max_fragment_size = Some(512);
+    });
+    assert_eq!(
+        outcome.client_transcript,
+        vec![
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(WriteTraffic)"
+        ],
+        "client transcript mismatch"
+    );
+    assert_eq!(
+        outcome.server_transcript,
+        vec![
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(WriteTraffic)"
+        ],
+        "server transcript mismatch"
+    );
+}
+
 #[test]
 fn tls13_handshake() {
     let outcome = handshake(&rustls::version::TLS13);
@@ -93,9 +144,66 @@ fn tls13_handshake() {
     );
 }
 
+#[test]
+fn tls13_handshake_fragmented() {
+    let outcome = handshake_config(&rustls::version::TLS13, |client, server| {
+        client.max_fragment_size = Some(512);
+        client.cert_decompressors = vec![];
+        server.max_fragment_size = Some(512);
+    });
+    assert_eq!(
+        outcome.client_transcript,
+        vec![
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(WriteTraffic)",
+            "Ok(WriteTraffic)"
+        ],
+        "client transcript mismatch"
+    );
+    assert_eq!(
+        outcome.server_transcript,
+        vec![
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(BlockedHandshake)",
+            "Ok(EncodeTlsData)",
+            "Ok(TransmitTlsData)",
+            "Ok(WriteTraffic)"
+        ],
+        "server transcript mismatch"
+    );
+}
+
 fn handshake(version: &'static rustls::SupportedProtocolVersion) -> Outcome {
-    let server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
-    let client_config = make_client_config(KeyType::Rsa2048);
+    handshake_config(version, |_, _| ())
+}
+
+fn handshake_config(
+    version: &'static rustls::SupportedProtocolVersion,
+    editor: impl Fn(&mut ClientConfig, &mut ServerConfig),
+) -> Outcome {
+    let mut server_config = make_server_config_with_versions(KeyType::Rsa2048, &[version]);
+    let mut client_config = make_client_config(KeyType::Rsa2048);
+    editor(&mut client_config, &mut server_config);
 
     run(
         Arc::new(client_config),