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