Test state machine asynchronous behavior.

Add a framework for testing the asynchronous codepath. Move some handshake
state machine coverage tests to cover a range of record-layer and
handshake-layer asynchronicity.

This adds tests for the previous two async bugs fixed.

Change-Id: I422ef33ba3eeb0ad04766871ed8bc59b677b169e
Reviewed-on: https://boringssl-review.googlesource.com/1410
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/CMakeLists.txt b/ssl/test/CMakeLists.txt
index 1b70e99..22c6619 100644
--- a/ssl/test/CMakeLists.txt
+++ b/ssl/test/CMakeLists.txt
@@ -3,6 +3,7 @@
 add_executable(
 	bssl_shim
 
+	async_bio.cc
 	bssl_shim.cc
 )
 
diff --git a/ssl/test/async_bio.cc b/ssl/test/async_bio.cc
new file mode 100644
index 0000000..f30f412
--- /dev/null
+++ b/ssl/test/async_bio.cc
@@ -0,0 +1,161 @@
+/* Copyright (c) 2014, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include "async_bio.h"
+
+#include <errno.h>
+#include <openssl/mem.h>
+
+namespace {
+
+extern const BIO_METHOD async_bio_method;
+
+struct async_bio {
+  size_t read_quota;
+  size_t write_quota;
+};
+
+async_bio *get_data(BIO *bio) {
+  if (bio->method != &async_bio_method) {
+    return NULL;
+  }
+  return (async_bio *)bio->ptr;
+}
+
+static int async_write(BIO *bio, const char *in, int inl) {
+  async_bio *a = get_data(bio);
+  if (a == NULL || bio->next_bio == NULL) {
+    return 0;
+  }
+
+  BIO_clear_retry_flags(bio);
+
+  if (a->write_quota == 0) {
+    BIO_set_retry_write(bio);
+    errno = EAGAIN;
+    return -1;
+  }
+
+  if ((size_t)inl > a->write_quota) {
+    inl = a->write_quota;
+  }
+  int ret = BIO_write(bio->next_bio, in, inl);
+  if (ret <= 0) {
+    BIO_copy_next_retry(bio);
+  } else {
+    a->write_quota -= ret;
+  }
+  return ret;
+}
+
+static int async_read(BIO *bio, char *out, int outl) {
+  async_bio *a = get_data(bio);
+  if (a == NULL || bio->next_bio == NULL) {
+    return 0;
+  }
+
+  BIO_clear_retry_flags(bio);
+
+  if (a->read_quota == 0) {
+    BIO_set_retry_read(bio);
+    errno = EAGAIN;
+    return -1;
+  }
+
+  if ((size_t)outl > a->read_quota) {
+    outl = a->read_quota;
+  }
+  int ret = BIO_read(bio->next_bio, out, outl);
+  if (ret <= 0) {
+    BIO_copy_next_retry(bio);
+  } else {
+    a->read_quota -= ret;
+  }
+  return ret;
+}
+
+static long async_ctrl(BIO *bio, int cmd, long num, void *ptr) {
+  if (bio->next_bio == NULL) {
+    return 0;
+  }
+  BIO_clear_retry_flags(bio);
+  int ret = BIO_ctrl(bio->next_bio, cmd, num, ptr);
+  BIO_copy_next_retry(bio);
+  return ret;
+}
+
+static int async_new(BIO *bio) {
+  async_bio *a = (async_bio *)OPENSSL_malloc(sizeof(*a));
+  if (a == NULL) {
+    return 0;
+  }
+  memset(a, 0, sizeof(*a));
+  bio->init = 1;
+  bio->ptr = (char *)a;
+  return 1;
+}
+
+static int async_free(BIO *bio) {
+  if (bio == NULL) {
+    return 0;
+  }
+
+  OPENSSL_free(bio->ptr);
+  bio->ptr = NULL;
+  bio->init = 0;
+  bio->flags = 0;
+  return 1;
+}
+
+static long async_callback_ctrl(BIO *bio, int cmd, bio_info_cb fp) {
+  if (bio->next_bio == NULL) {
+    return 0;
+  }
+  return BIO_callback_ctrl(bio->next_bio, cmd, fp);
+}
+
+const BIO_METHOD async_bio_method = {
+  BIO_TYPE_FILTER,
+  "async bio",
+  async_write,
+  async_read,
+  NULL /* puts */,
+  NULL /* gets */,
+  async_ctrl,
+  async_new,
+  async_free,
+  async_callback_ctrl,
+};
+
+}  // namespace
+
+BIO *async_bio_create() {
+  return BIO_new(&async_bio_method);
+}
+
+void async_bio_allow_read(BIO *bio, size_t bytes) {
+  async_bio *a = get_data(bio);
+  if (a == NULL) {
+    return;
+  }
+  a->read_quota += bytes;
+}
+
+void async_bio_allow_write(BIO *bio, size_t bytes) {
+  async_bio *a = get_data(bio);
+  if (a == NULL) {
+    return;
+  }
+  a->write_quota += bytes;
+}
diff --git a/ssl/test/async_bio.h b/ssl/test/async_bio.h
new file mode 100644
index 0000000..7bec5fe
--- /dev/null
+++ b/ssl/test/async_bio.h
@@ -0,0 +1,35 @@
+/* Copyright (c) 2014, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#ifndef HEADER_ASYNC_BIO
+#define HEADER_ASYNC_BIO
+
+#include <openssl/bio.h>
+
+
+// async_bio_create creates a filter BIO for testing asynchronous
+// state machines. Reads and writes will fail and return EAGAIN unless
+// explicitly allowed. Each async BIO has a read quota and a write
+// quota. Initially both are zero. As each is incremented, bytes are
+// allowed to flow through the BIO.
+BIO *async_bio_create();
+
+// async_bio_allow_read increments |bio|'s read quota by |bytes|.
+void async_bio_allow_read(BIO *bio, size_t bytes);
+
+// async_bio_allow_write increments |bio|'s write quota by |bytes|.
+void async_bio_allow_write(BIO *bio, size_t bytes);
+
+
+#endif  // HEADER_ASYNC_BIO
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 40bd533..5a76567 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -23,6 +23,8 @@
 #include <openssl/bytestring.h>
 #include <openssl/ssl.h>
 
+#include "async_bio.h"
+
 static int usage(const char *program) {
   fprintf(stderr, "Usage: %s (client|server) (normal|resume) [flags...]\n",
           program);
@@ -155,33 +157,22 @@
   return NULL;
 }
 
-static SSL *setup_ssl(SSL_CTX *ssl_ctx, int fd) {
-  SSL *ssl = NULL;
-  BIO *bio = NULL;
-
-  ssl = SSL_new(ssl_ctx);
-  if (ssl == NULL) {
-    goto err;
+static int retry_async(SSL *ssl, int ret, BIO *bio) {
+  // No error; don't retry.
+  if (ret >= 0) {
+    return 0;
   }
-
-
-  bio = BIO_new_fd(fd, 1 /* take ownership */);
-  if (bio == NULL) {
-    goto err;
+  // See if we needed to read or write more. If so, allow one byte through on
+  // the appropriate end to maximally stress the state machine.
+  int err = SSL_get_error(ssl, ret);
+  if (err == SSL_ERROR_WANT_READ) {
+    async_bio_allow_read(bio, 1);
+    return 1;
+  } else if (err == SSL_ERROR_WANT_WRITE) {
+    async_bio_allow_write(bio, 1);
+    return 1;
   }
-
-  SSL_set_bio(ssl, bio, bio);
-
-  return ssl;
-
-err:
-  if (bio != NULL) {
-    BIO_free(bio);
-  }
-  if (ssl != NULL) {
-    SSL_free(ssl);
-  }
-  return NULL;
+  return 0;
 }
 
 static int do_exchange(SSL_SESSION **out_session,
@@ -192,13 +183,14 @@
                        int is_resume,
                        int fd,
                        SSL_SESSION *session) {
+  int async = 0;
   const char *expected_certificate_types = NULL;
   const char *expected_next_proto = NULL;
   expected_server_name = NULL;
   early_callback_called = 0;
   advertise_npn = NULL;
 
-  SSL *ssl = setup_ssl(ssl_ctx, fd);
+  SSL *ssl = SSL_new(ssl_ctx);
   if (ssl == NULL) {
     BIO_print_errors_fp(stdout);
     return 1;
@@ -271,12 +263,26 @@
         return 1;
       }
       select_next_proto = argv[i];
+    } else if (strcmp(argv[i], "-async") == 0) {
+      async = 1;
     } else {
       fprintf(stderr, "Unknown argument: %s\n", argv[i]);
       return 1;
     }
   }
 
+  BIO *bio = BIO_new_fd(fd, 1 /* take ownership */);
+  if (bio == NULL) {
+    BIO_print_errors_fp(stdout);
+    return 1;
+  }
+  if (async) {
+    BIO *async = async_bio_create();
+    BIO_push(async, bio);
+    bio = async;
+  }
+  SSL_set_bio(ssl, bio, bio);
+
   if (session != NULL) {
     if (SSL_set_session(ssl, session) != 1) {
       fprintf(stderr, "failed to set session\n");
@@ -285,11 +291,13 @@
   }
 
   int ret;
-  if (is_server) {
-    ret = SSL_accept(ssl);
-  } else {
-    ret = SSL_connect(ssl);
-  }
+  do {
+    if (is_server) {
+      ret = SSL_accept(ssl);
+    } else {
+      ret = SSL_connect(ssl);
+    }
+  } while (async && retry_async(ssl, ret, bio));
   if (ret != 1) {
     SSL_free(ssl);
     BIO_print_errors_fp(stdout);
@@ -342,7 +350,10 @@
 
   for (;;) {
     uint8_t buf[512];
-    int n = SSL_read(ssl, buf, sizeof(buf));
+    int n;
+    do {
+      n = SSL_read(ssl, buf, sizeof(buf));
+    } while (async && retry_async(ssl, n, bio));
     if (n < 0) {
       SSL_free(ssl);
       BIO_print_errors_fp(stdout);
@@ -353,7 +364,10 @@
       for (int i = 0; i < n; i++) {
         buf[i] ^= 0xff;
       }
-      int w = SSL_write(ssl, buf, n);
+      int w;
+      do {
+        w = SSL_write(ssl, buf, n);
+      } while (async && retry_async(ssl, w, bio));
       if (w != n) {
         SSL_free(ssl);
         BIO_print_errors_fp(stdout);
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 4f34ce4..eb1d57c 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -391,6 +391,11 @@
 	// SendFallbackSCSV causes the client to include
 	// TLS_FALLBACK_SCSV in the ClientHello.
 	SendFallbackSCSV bool
+
+	// MaxHandshakeRecordLength, if non-zero, is the maximum size of a
+	// handshake record. Handshake messages will be split at the record
+	// layer.
+	MaxHandshakeRecordLength int
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 02ed8f0..52582ad 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -719,6 +719,9 @@
 		if m > maxPlaintext {
 			m = maxPlaintext
 		}
+		if typ == recordTypeHandshake && c.config.Bugs.MaxHandshakeRecordLength > 0 && m > c.config.Bugs.MaxHandshakeRecordLength {
+			m = c.config.Bugs.MaxHandshakeRecordLength
+		}
 		explicitIVLen := 0
 		explicitIVIsSeq := false
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index cf9e44c..bdd3566 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -218,17 +218,6 @@
 		expectedError: ":UNEXPECTED_MESSAGE:",
 	},
 	{
-		testType: serverTest,
-		name:     "NPNServerTest",
-		config: Config{
-			NextProtos: []string{"bar"},
-		},
-		flags: []string{
-			"-advertise-npn", "\x03foo\x03bar\x03baz",
-			"-expect-next-proto", "bar",
-		},
-	},
-	{
 		name: "SkipChangeCipherSpec-Client",
 		config: Config{
 			Bugs: ProtocolBugs{
@@ -323,19 +312,6 @@
 		expectedError: ":CCS_RECEIVED_EARLY:",
 	},
 	{
-		name: "SessionTicketsDisabled-Client",
-		config: Config{
-			SessionTicketsDisabled: true,
-		},
-	},
-	{
-		testType: serverTest,
-		name:     "SessionTicketsDisabled-Server",
-		config: Config{
-			SessionTicketsDisabled: true,
-		},
-	},
-	{
 		name: "SkipNewSessionTicket",
 		config: Config{
 			Bugs: ProtocolBugs{
@@ -346,18 +322,6 @@
 		expectedError: ":CCS_RECEIVED_EARLY:",
 	},
 	{
-		name: "FalseStart",
-		config: Config{
-			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
-			NextProtos:   []string{"foo"},
-		},
-		flags: []string{
-			"-false-start",
-			"-select-next-proto", "foo",
-		},
-		resumeSession: true,
-	},
-	{
 		name: "FalseStart-SessionTicketsDisabled",
 		config: Config{
 			CipherSuites:           []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
@@ -824,6 +788,135 @@
 	}
 }
 
+// Adds tests that try to cover the range of the handshake state machine, under
+// various conditions. Some of these are redundant with other tests, but they
+// only cover the synchronous case.
+func addStateMachineCoverageTests(async bool, splitHandshake bool) {
+	var suffix string
+	var flags []string
+	var maxHandshakeRecordLength int
+	if async {
+		suffix = "-Async"
+		flags = append(flags, "-async")
+	} else {
+		suffix = "-Sync"
+	}
+	if splitHandshake {
+		suffix += "-SplitHandshakeRecords"
+		maxHandshakeRecordLength = 10
+	}
+
+	// Basic handshake, with resumption. Client and server.
+	testCases = append(testCases, testCase{
+		name: "Basic-Client" + suffix,
+		config: Config{
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: flags,
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "Basic-Server" + suffix,
+		config: Config{
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: flags,
+	})
+
+	// No session ticket support; server doesn't send NewSessionTicket.
+	testCases = append(testCases, testCase{
+		name: "SessionTicketsDisabled-Client" + suffix,
+		config: Config{
+			SessionTicketsDisabled: true,
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: flags,
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "SessionTicketsDisabled-Server" + suffix,
+		config: Config{
+			SessionTicketsDisabled: true,
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: flags,
+	})
+
+	// NPN on client and server; results in post-handshake message.
+	testCases = append(testCases, testCase{
+		name: "NPN-Client" + suffix,
+		config: Config{
+			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+			NextProtos:   []string{"foo"},
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: append(flags, "-select-next-proto", "foo"),
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "NPN-Server" + suffix,
+		config: Config{
+			NextProtos: []string{"bar"},
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: append(flags,
+			"-advertise-npn", "\x03foo\x03bar\x03baz",
+			"-expect-next-proto", "bar"),
+	})
+
+	// Client does False Start and negotiates NPN.
+	testCases = append(testCases, testCase{
+		name: "FalseStart" + suffix,
+		config: Config{
+			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+			NextProtos:   []string{"foo"},
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: append(flags,
+			"-false-start",
+			"-select-next-proto", "foo"),
+		resumeSession: true,
+	})
+
+	// TLS client auth.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "ClientAuth-Client" + suffix,
+		config: Config{
+			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+			ClientAuth:   RequireAnyClientCert,
+			Bugs: ProtocolBugs{
+				MaxHandshakeRecordLength: maxHandshakeRecordLength,
+			},
+		},
+		flags: append(flags,
+			"-cert-file", rsaCertificateFile,
+			"-key-file", rsaKeyFile),
+	})
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "ClientAuth-Server" + suffix,
+		config: Config{
+			Certificates: []Certificate{rsaCertificate},
+		},
+		flags: append(flags, "-require-any-client-certificate"),
+	})
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, buildDir string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -874,6 +967,11 @@
 	addBadECDSASignatureTests()
 	addCBCPaddingTests()
 	addClientAuthTests()
+	for _, async := range []bool{false, true} {
+		for _, splitHandshake := range []bool{false, true} {
+			addStateMachineCoverageTests(async, splitHandshake)
+		}
+	}
 
 	var wg sync.WaitGroup