Fix up book-keeping between the write buffer and pending writes.

Writing application data goes through three steps:

1. Encrypt the data into the write buffer.
2. Flush the write buffer to the network.
3. Report to SSL_write's caller that the write succeeded.

In principle, steps 2 and 3 are done together, but it is possible that
BoringSSL needs to write something, but we are not in the middle of
servicing an SSL_write call. Then we must perform (2) but cannot perform
(3).

TLS 1.3 0-RTT on a client introduces a case like this. Suppose we write
some 0-RTT data, but it is blocked on the network. Meanwhile, the
application tries to read from the socket (protocols like HTTP/2 read
and write concurrently). We discover ServerHello..Finished and must then
respond with EndOfEarlyData..Finished. But to write, we must flush the
current write buffer.

To fix this, https://boringssl-review.googlesource.com/14164 split (2)
and (3) more explicitly. The write buffer may be flushed to the network
at any point, but the wpend_* book-keeping is separate. It represents
whether (3) is done. As part of that, we introduced a wpend_pending
boolean to track whether there was pending data.

This introduces an interesting corner case. We now keep NewSessionTicket
messages buffered until the next SSL_write. (KeyUpdate ACKs are
implemented similarly.) Suppose the caller calls SSL_write(nullptr, 0)
to flush the NewSessionTicket and this hits EWOULDBLOCK. We'll track a
zero-length pending write in wpend_*! A future attempt to write non-zero
data would then violate the moving buffer check. This is strange because
we don't build records for zero-length application writes in the first
place.

Instead, wpend_pending should have been wpend_tot > 0. Remove that and
rearrange the code to check that properly. Also remove wpend_ret as it
has the same data as wpend_tot.

Change-Id: I58c23842cd55e8a8dfbb1854b61278b108b5c7ea
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/53546
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
diff --git a/ssl/internal.h b/ssl/internal.h
index 2400f90..41630f5 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -2645,7 +2645,6 @@
   unsigned int wnum = 0;  // number of bytes sent so far
   int wpend_tot = 0;      // number bytes written
   int wpend_type = 0;
-  int wpend_ret = 0;  // number of bytes submitted
   const uint8_t *wpend_buf = nullptr;
 
   // read_shutdown is the shutdown state for the read half of the connection.
@@ -2726,9 +2725,6 @@
   // outstanding.
   bool key_update_pending : 1;
 
-  // wpend_pending is true if we have a pending write outstanding.
-  bool wpend_pending : 1;
-
   // early_data_accepted is true if early data was accepted by the server.
   bool early_data_accepted : 1;
 
diff --git a/ssl/s3_lib.cc b/ssl/s3_lib.cc
index fa73d34..2adf386 100644
--- a/ssl/s3_lib.cc
+++ b/ssl/s3_lib.cc
@@ -175,7 +175,6 @@
       send_connection_binding(false),
       channel_id_valid(false),
       key_update_pending(false),
-      wpend_pending(false),
       early_data_accepted(false),
       alert_dispatch(false),
       renegotiate_pending(false),
diff --git a/ssl/s3_pkt.cc b/ssl/s3_pkt.cc
index bc3e99a..efe5905 100644
--- a/ssl/s3_pkt.cc
+++ b/ssl/s3_pkt.cc
@@ -147,10 +147,10 @@
   // Ensure that if we end up with a smaller value of data to write out than
   // the the original len from a write which didn't complete for non-blocking
   // I/O and also somehow ended up avoiding the check for this in
-  // tls_write_pending/SSL_R_BAD_WRITE_RETRY as it must never be possible to
-  // end up with (len-tot) as a large number that will then promptly send
-  // beyond the end of the users buffer ... so we trap and report the error in
-  // a way the user will notice.
+  // do_tls_write/SSL_R_BAD_WRITE_RETRY as it must never be possible to end up
+  // with (len-tot) as a large number that will then promptly send beyond the
+  // end of the users buffer ... so we trap and report the error in a way the
+  // user will notice.
   if (len < 0 || (size_t)len < tot) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_LENGTH);
     return -1;
@@ -196,29 +196,31 @@
   }
 }
 
-static int tls_write_pending(SSL *ssl, int type, const uint8_t *in,
-                             unsigned int len) {
-  if (ssl->s3->wpend_tot > (int)len ||
-      (!(ssl->mode & SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) &&
-       ssl->s3->wpend_buf != in) ||
-      ssl->s3->wpend_type != type) {
+// do_tls_write writes an SSL record of the given type.
+static int do_tls_write(SSL *ssl, int type, const uint8_t *in, unsigned len) {
+  // If there is a pending write, the retry must be consistent.
+  if (ssl->s3->wpend_tot > 0 &&
+      (ssl->s3->wpend_tot > (int)len ||
+       (!(ssl->mode & SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) &&
+        ssl->s3->wpend_buf != in) ||
+       ssl->s3->wpend_type != type)) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_BAD_WRITE_RETRY);
     return -1;
   }
 
+  // Flush any unwritten data to the transport. There may be data to flush even
+  // if |wpend_tot| is zero.
   int ret = ssl_write_buffer_flush(ssl);
   if (ret <= 0) {
     return ret;
   }
-  ssl->s3->wpend_pending = false;
-  return ssl->s3->wpend_ret;
-}
 
-// do_tls_write writes an SSL record of the given type.
-static int do_tls_write(SSL *ssl, int type, const uint8_t *in, unsigned len) {
-  // If there is still data from the previous record, flush it.
-  if (ssl->s3->wpend_pending) {
-    return tls_write_pending(ssl, type, in, len);
+  // If there is a pending write, we just completed it. Report it to the caller.
+  if (ssl->s3->wpend_tot > 0) {
+    ret = ssl->s3->wpend_tot;
+    ssl->s3->wpend_buf = nullptr;
+    ssl->s3->wpend_tot = 0;
+    return ret;
   }
 
   SSLBuffer *buf = &ssl->s3->write_buffer;
@@ -282,16 +284,19 @@
   // acknowledgments.
   ssl->s3->key_update_pending = false;
 
-  // Memorize arguments so that tls_write_pending can detect bad write retries
-  // later.
-  ssl->s3->wpend_tot = len;
-  ssl->s3->wpend_buf = in;
-  ssl->s3->wpend_type = type;
-  ssl->s3->wpend_ret = len;
-  ssl->s3->wpend_pending = true;
+  // Flush the write buffer.
+  ret = ssl_write_buffer_flush(ssl);
+  if (ret <= 0) {
+    // Track the unfinished write.
+    if (len > 0) {
+      ssl->s3->wpend_tot = len;
+      ssl->s3->wpend_buf = in;
+      ssl->s3->wpend_type = type;
+    }
+    return ret;
+  }
 
-  // We now just need to write the buffer.
-  return tls_write_pending(ssl, type, in, len);
+  return len;
 }
 
 ssl_open_record_t tls_open_app_data(SSL *ssl, Span<uint8_t> *out,
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index bbcc3b1..7035748 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -1239,7 +1239,8 @@
   // Discard any unfinished writes from the perspective of |SSL_write|'s
   // retry. The handshake will transparently flush out the pending record
   // (discarded by the server) to keep the framing correct.
-  ssl->s3->wpend_pending = false;
+  ssl->s3->wpend_buf = nullptr;
+  ssl->s3->wpend_tot = 0;
 }
 
 enum ssl_early_data_reason_t SSL_get_early_data_reason(const SSL *ssl) {
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 0811cb4..55b7051 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -8332,5 +8332,63 @@
   }
 }
 
+TEST(SSLTest, EmptyWriteBlockedOnHandshakeData) {
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  bssl::UniquePtr<SSL_CTX> server_ctx =
+      CreateContextWithTestCertificate(TLS_method());
+  ASSERT_TRUE(client_ctx);
+  ASSERT_TRUE(server_ctx);
+  // Configure only TLS 1.3. This test requires post-handshake NewSessionTicket.
+  ASSERT_TRUE(SSL_CTX_set_min_proto_version(client_ctx.get(), TLS1_3_VERSION));
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(client_ctx.get(), TLS1_3_VERSION));
+
+  // Connect a client and server with tiny buffer between the two.
+  bssl::UniquePtr<SSL> client(SSL_new(client_ctx.get())),
+      server(SSL_new(server_ctx.get()));
+  ASSERT_TRUE(client);
+  ASSERT_TRUE(server);
+  SSL_set_connect_state(client.get());
+  SSL_set_accept_state(server.get());
+  BIO *bio1, *bio2;
+  ASSERT_TRUE(BIO_new_bio_pair(&bio1, 1, &bio2, 1));
+  SSL_set_bio(client.get(), bio1, bio1);
+  SSL_set_bio(server.get(), bio2, bio2);
+  ASSERT_TRUE(CompleteHandshakes(client.get(), server.get()));
+
+  // We defer NewSessionTicket to the first write, so the server has a pending
+  // NewSessionTicket. See https://boringssl-review.googlesource.com/34948. This
+  // means an empty write will flush the ticket. However, the transport only
+  // allows one byte through, so this will fail with |SSL_ERROR_WANT_WRITE|.
+  int ret = SSL_write(server.get(), nullptr, 0);
+  ASSERT_EQ(ret, -1);
+  ASSERT_EQ(SSL_get_error(server.get(), ret), SSL_ERROR_WANT_WRITE);
+
+  // Attempting to write non-zero data should not trip |SSL_R_BAD_WRITE_RETRY|.
+  const uint8_t kData[] = {'h', 'e', 'l', 'l', 'o'};
+  ret = SSL_write(server.get(), kData, sizeof(kData));
+  ASSERT_EQ(ret, -1);
+  ASSERT_EQ(SSL_get_error(server.get(), ret), SSL_ERROR_WANT_WRITE);
+
+  // Byte by byte, the data should eventually get through.
+  uint8_t buf[sizeof(kData)];
+  for (;;) {
+    ret = SSL_read(client.get(), buf, sizeof(buf));
+    ASSERT_EQ(ret, -1);
+    ASSERT_EQ(SSL_get_error(client.get(), ret), SSL_ERROR_WANT_READ);
+
+    ret = SSL_write(server.get(), kData, sizeof(kData));
+    if (ret > 0) {
+      ASSERT_EQ(ret, 5);
+      break;
+    }
+    ASSERT_EQ(ret, -1);
+    ASSERT_EQ(SSL_get_error(server.get(), ret), SSL_ERROR_WANT_WRITE);
+  }
+
+  ret = SSL_read(client.get(), buf, sizeof(buf));
+  ASSERT_EQ(ret, static_cast<int>(sizeof(kData)));
+  ASSERT_EQ(Bytes(buf, ret), Bytes(kData));
+}
+
 }  // namespace
 BSSL_NAMESPACE_END