Fix crash when flushing an SSL BIO.

OpenSSL synchronizes bio->next_bio and ssl->rbio with a variety of
callbacks, so BIO_copy_next_retry worked. We do not, so attempting to
flush the BIO crashed.

The SSL BIO is a compatibility hack and intentionally much more limited,
so start by just copying things from the right BIO directly. Add a basic
unit test for SSL BIOs. If we need to, we can implement a more complex
synchronization later.

Additionally reject reconfiguring an SSL BIO because that will leak the
object right now.

Change-Id: I724c95ab6f1a3a1aa1889b0483c81ce3bdc534ae
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/43424
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/bio/bio.c b/crypto/bio/bio.c
index 7d97c3e..3d36e28 100644
--- a/crypto/bio/bio.c
+++ b/crypto/bio/bio.c
@@ -262,6 +262,8 @@
 
 int BIO_get_retry_reason(const BIO *bio) { return bio->retry_reason; }
 
+void BIO_set_retry_reason(BIO *bio, int reason) { bio->retry_reason = reason; }
+
 void BIO_clear_flags(BIO *bio, int flags) {
   bio->flags &= ~flags;
 }
diff --git a/include/openssl/bio.h b/include/openssl/bio.h
index da0dcdf..f25492a 100644
--- a/include/openssl/bio.h
+++ b/include/openssl/bio.h
@@ -199,6 +199,10 @@
 // retried. The return value is one of the |BIO_RR_*| values.
 OPENSSL_EXPORT int BIO_get_retry_reason(const BIO *bio);
 
+// BIO_set_retry_reason sets the special I/O operation that needs to be retried
+// to |reason|, which should be one of the |BIO_RR_*| values.
+OPENSSL_EXPORT void BIO_set_retry_reason(BIO *bio, int reason);
+
 // BIO_clear_flags ANDs |bio->flags| with the bitwise-complement of |flags|.
 OPENSSL_EXPORT void BIO_clear_flags(BIO *bio, int flags);
 
diff --git a/ssl/bio_ssl.cc b/ssl/bio_ssl.cc
index 61afee5..a249889 100644
--- a/ssl/bio_ssl.cc
+++ b/ssl/bio_ssl.cc
@@ -37,12 +37,12 @@
 
     case SSL_ERROR_WANT_ACCEPT:
       BIO_set_retry_special(bio);
-      bio->retry_reason = BIO_RR_ACCEPT;
+      BIO_set_retry_reason(bio, BIO_RR_ACCEPT);
       break;
 
     case SSL_ERROR_WANT_CONNECT:
       BIO_set_retry_special(bio);
-      bio->retry_reason = BIO_RR_CONNECT;
+      BIO_set_retry_reason(bio, BIO_RR_CONNECT);
       break;
 
     case SSL_ERROR_NONE:
@@ -77,7 +77,7 @@
 
     case SSL_ERROR_WANT_CONNECT:
       BIO_set_retry_special(bio);
-      bio->retry_reason = BIO_RR_CONNECT;
+      BIO_set_retry_reason(bio, BIO_RR_CONNECT);
       break;
 
     case SSL_ERROR_NONE:
@@ -98,6 +98,17 @@
 
   switch (cmd) {
     case BIO_C_SET_SSL:
+      if (ssl != NULL) {
+        // OpenSSL allows reusing an SSL BIO with a different SSL object. We do
+        // not support this.
+        OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
+        return 0;
+      }
+
+      // Note this differs from upstream OpenSSL, which synchronizes
+      // |bio->next_bio| with |ssl|'s rbio here, and on |BIO_CTRL_PUSH|. We call
+      // into the corresponding |BIO| directly. (We can implement the upstream
+      // behavior if it ends up necessary.)
       bio->shutdown = num;
       bio->ptr = ptr;
       bio->init = 1;
@@ -117,9 +128,11 @@
       return SSL_pending(ssl);
 
     case BIO_CTRL_FLUSH: {
+      BIO *wbio = SSL_get_wbio(ssl);
       BIO_clear_retry_flags(bio);
-      long ret = BIO_ctrl(SSL_get_wbio(ssl), cmd, num, ptr);
-      BIO_copy_next_retry(bio);
+      long ret = BIO_ctrl(wbio, cmd, num, ptr);
+      BIO_set_flags(bio, BIO_get_retry_flags(wbio));
+      BIO_set_retry_reason(bio, BIO_get_retry_reason(wbio));
       return ret;
     }
 
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 451b401..6584686 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -6529,5 +6529,64 @@
                                                     sizeof(kTicket)));
 }
 
+TEST(SSLTest, BIO) {
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  bssl::UniquePtr<SSL_CTX> server_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(client_ctx);
+  ASSERT_TRUE(server_ctx);
+
+  bssl::UniquePtr<X509> cert = GetTestCertificate();
+  bssl::UniquePtr<EVP_PKEY> key = GetTestKey();
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(key);
+  ASSERT_TRUE(SSL_CTX_use_certificate(server_ctx.get(), cert.get()));
+  ASSERT_TRUE(SSL_CTX_use_PrivateKey(server_ctx.get(), key.get()));
+
+  for (bool take_ownership : {true, false}) {
+    // For simplicity, get the handshake out of the way first.
+    bssl::UniquePtr<SSL> client, server;
+    ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                       server_ctx.get()));
+
+    // Wrap |client| in an SSL BIO.
+    bssl::UniquePtr<BIO> client_bio(BIO_new(BIO_f_ssl()));
+    ASSERT_TRUE(client_bio);
+    ASSERT_EQ(1, BIO_set_ssl(client_bio.get(), client.get(), take_ownership));
+    if (take_ownership) {
+      client.release();
+    }
+
+    // Flushing the BIO should not crash.
+    EXPECT_EQ(1, BIO_flush(client_bio.get()));
+
+    // Exchange some data.
+    EXPECT_EQ(5, BIO_write(client_bio.get(), "hello", 5));
+    uint8_t buf[5];
+    ASSERT_EQ(5, SSL_read(server.get(), buf, sizeof(buf)));
+    EXPECT_EQ(Bytes("hello"), Bytes(buf));
+
+    EXPECT_EQ(5, SSL_write(server.get(), "world", 5));
+    ASSERT_EQ(5, BIO_read(client_bio.get(), buf, sizeof(buf)));
+    EXPECT_EQ(Bytes("world"), Bytes(buf));
+
+    // |BIO_should_read| should work.
+    EXPECT_EQ(-1, BIO_read(client_bio.get(), buf, sizeof(buf)));
+    EXPECT_TRUE(BIO_should_read(client_bio.get()));
+
+    // Writing data should eventually exceed the buffer size and fail, reporting
+    // |BIO_should_write|.
+    int ret;
+    for (int i = 0; i < 1024; i++) {
+      std::vector<uint8_t> buffer(1024);
+      ret = BIO_write(client_bio.get(), buffer.data(), buffer.size());
+      if (ret <= 0) {
+        break;
+      }
+    }
+    EXPECT_EQ(-1, ret);
+    EXPECT_TRUE(BIO_should_write(client_bio.get()));
+  }
+}
+
 }  // namespace
 BSSL_NAMESPACE_END