]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
add a handshake memory failure test
authorNeil Horman <nhorman@openssl.org>
Wed, 23 Jul 2025 13:09:05 +0000 (09:09 -0400)
committerNeil Horman <nhorman@openssl.org>
Wed, 6 Aug 2025 13:07:40 +0000 (09:07 -0400)
We would like to be able to test our memory failure paths, but forcing
malloc to return NULL at certain points in time.

This test does that, by running a sepcific workload n+1 time.  In this
case the workload is a simple ssl handshake.

We run 1 test which sets our malloc wrapper into record mode, in which
it just acts as a pass through to the system malloc call and records the
number of times it was called.

Then we run a second test, which does the same handshake N times, where
N is the number of times malloc was called in the previous test.  For
each iteration in i=0..N we fail the ith malloc operation.

We don't check for functional failures in the second test (as we expect
failures), we just want to make sure that (a) we don't crash and (b)
asan doesn't report any errors.

Currently, we get _lots_ of asan failures, but we can use this test to
log issues for that and fix those up.

Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Paul Dale <ppzgs1@gmail.com>
(Merged from https://github.com/openssl/openssl/pull/28078)

Configure
INSTALL.md
test/build.info
test/handshake-memfail.c [new file with mode: 0644]
test/recipes/90-test_handshake-memfail.t [new file with mode: 0644]

index d1586e60a7e0f7a98db71671dfc148d2be31a72d..6db98231dfed7327ec3a48ee26ce59741babd901 100755 (executable)
--- a/Configure
+++ b/Configure
@@ -450,6 +450,7 @@ my @disablables = (
     "cms",
     "comp",
     "crypto-mdebug",
+    "allocfail-tests",
     "ct",
     "default-thread-pool",
     "demos",
@@ -593,6 +594,7 @@ our %disabled = ( # "what"         => "comment"
                   "brotli-dynamic"      => "default",
                   "buildtest-c++"       => "default",
                   "crypto-mdebug"       => "default",
+                  "allocfail-tests"     => "default",
                   "crypto-mdebug-backtrace" => "default",
                   "demos"               => "default",
                   "h3demo"              => "default",
@@ -667,7 +669,7 @@ my @disable_cascades = (
     "tls1_3"            => [ "quic" ],
     "quic"              => [ "unstable-qlog" ],
 
-    "crypto-mdebug"     => [ "crypto-mdebug-backtrace" ],
+    "crypto-mdebug"     => [ "crypto-mdebug-backtrace", "allocfail-tests" ],
 
     "module"            => [ "dynamic-engine", "fips" ],
 
index a4b5fadf55b8c760e75a8299fedb06f84bc9218c..f90b937cef682e9b71fbb8906da917c5b041f9cb 100644 (file)
@@ -745,6 +745,11 @@ This now only enables the `failed-malloc` feature.
 
 This is a no-op; the project uses the compiler's address/leak sanitizer instead.
 
+### enable-allocfail-tests
+
+This option enables testing that leverages the use of the crypto-mdebug feature
+to test error paths resulting from failed memory allocations.
+
 ### no-ct
 
 Don't build support for Certificate Transparency (CT).
index bd70cfa9756ac820da246de6f8bbd4cd18cb771d..e22c9005388286120568bf349bb2a5780dd8c5ef 100644 (file)
@@ -75,6 +75,10 @@ IF[{- !$disabled{tests} -}]
     PROGRAMS{noinst}=rpktest
   ENDIF
 
+  IF[{- !$disabled{'allocfail-tests'} -}]
+    PROGRAMS{noninst}=handshake-memfail
+  ENDIF
+
   IF[{- !$disabled{'deprecated-3.0'} -}]
     PROGRAMS{noinst}=enginetest
   ENDIF
@@ -582,6 +586,10 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[sslapitest]=../include ../apps/include ../providers/common/include ..
   DEPEND[sslapitest]=../libcrypto.a ../libssl.a libtestutil.a
 
+  SOURCE[handshake-memfail]=handshake-memfail.c helpers/ssltestlib.c
+  INCLUDE[handshake-memfail]=../include ../apps/include
+  DEPEND[handshake-memfail]=../libcrypto.a ../libssl.a libtestutil.a
+
   SOURCE[ssl_handshake_rtt_test]=ssl_handshake_rtt_test.c helpers/ssltestlib.c
   INCLUDE[ssl_handshake_rtt_test]=../include ../apps/include ..
   DEPEND[ssl_handshake_rtt_test]=../libcrypto.a ../libssl.a libtestutil.a
diff --git a/test/handshake-memfail.c b/test/handshake-memfail.c
new file mode 100644 (file)
index 0000000..0aa83ce
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include <openssl/crypto.h>
+#include <openssl/ssl.h>
+
+#include "helpers/ssltestlib.h"
+#include "testutil.h"
+
+/**
+ * @brief Global static variables for certificate and key handling.
+ *
+ * These variables store the paths and context used for managing
+ * certificates and private keys in the application.
+ *
+ * - certsdir: Directory containing trusted certificates.
+ * - cert:     Path to the certificate file in use.
+ * - privkey:  Path to the private key file.
+ * - mcount:   Number of mallocs counted
+ * - rcount:   Number of reallocs counted
+ * - fcount:   Number of frees counted
+ * - scount:   Number of mallocs counted prior to workload
+ */
+static char *cert = NULL;
+static char *privkey = NULL;
+static int mcount, rcount, fcount, scount;
+
+/**
+ * @brief Performs an SSL/TLS handshake between a test client and server.
+ *
+ * This function sets up SSL/TLS contexts and objects for both client and
+ * server, then initiates a handshake to verify successful connection
+ * establishment. It is intended for use in testing scenarios to validate
+ * handshake behavior using specified certificates and keys.
+ *
+ * @return 1 on successful handshake, 0 on failure.
+ *
+ * @note The function uses @c TEST_true() macros to validate intermediate
+ *       steps. All SSL objects and contexts are freed before returning.
+ */
+static int do_handshake(OSSL_LIB_CTX *libctx)
+{
+    SSL_CTX *cctx = NULL, *sctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL;
+    int testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+                                       TLS_client_method(),
+                                       TLS1_VERSION, 0,
+                                       &sctx, &cctx, cert, privkey)))
+        return 0;
+
+    /* Now do a handshake */
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+                                      &clientssl, NULL, NULL))
+            || !TEST_true(create_ssl_connection(serverssl, clientssl,
+                                                SSL_ERROR_NONE)))
+        goto end;
+
+    testresult = 1;
+
+end:
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+
+    return testresult;
+}
+
+/**
+ * @brief run our workload to count the number of allocations we make.
+ *
+ * Creates a new OpenSSL library context and performs a test SSL/TLS
+ * handshake. The number of malloc operations is recorded and printed for
+ * diagnostic purposes.
+ *
+ * @return 1 if the handshake succeeds, 0 otherwise.
+ */
+static int test_record_alloc_counts(void)
+{
+    int ret;
+    OSSL_LIB_CTX *libctx;
+
+    libctx = OSSL_LIB_CTX_new();
+    if (!TEST_ptr(libctx))
+        return 0;
+
+    ret = do_handshake(libctx);
+
+    OSSL_LIB_CTX_free(libctx);
+    libctx = NULL;
+
+    return ret;
+}
+
+/**
+ * @brief run our workload to count the number of allocations we make.
+ *
+ * Creates a new OpenSSL library context and performs a test SSL/TLS
+ * handshake.
+ *
+ * Note this is exactly the same as test_record_alloc_counts with 1 difference
+ * The test always returns 1.  We do this because with allocation failures
+ * in effect, we can't expect things to work, so we always return success
+ * so that the test keeps running.
+ */
+static int test_alloc_failures(void)
+{
+    OSSL_LIB_CTX *libctx;
+
+    libctx = OSSL_LIB_CTX_new();
+    if (!TEST_ptr(libctx))
+        return 1;
+
+    do_handshake(libctx);
+
+    OSSL_LIB_CTX_free(libctx);
+    libctx = NULL;
+
+    return 1;
+}
+
+static int test_report_alloc_counts(void)
+{
+    CRYPTO_get_alloc_counts(&mcount, &rcount, &fcount);
+    /*
+     * Report our memory allocations from the count run
+     * NOTE: We report a number of allocations to skip here
+     * (the scount value).  These are the allocations that took
+     * place while the test harness itself was getting setup
+     * (i.e. calling OPENSSL_init_crypto/etc).  We can't fail
+     * those allocations as they will cause the test to fail before
+     * we have even run the workload.  So report them so we can
+     * allow them to function before we start doing any real testing
+     */
+    TEST_info("skip: %d count %d\n", scount, mcount - scount);
+    return 1;
+}
+
+int setup_tests(void)
+{
+    char *opmode = NULL;
+    char *certsdir = NULL;
+
+    if (!TEST_ptr(opmode = test_get_argument(0)))
+        goto err;
+
+    if (!TEST_ptr(certsdir = test_get_argument(1)))
+        goto err;
+
+    cert = test_mk_file_path(certsdir, "servercert.pem");
+    if (cert == NULL)
+        goto err;
+
+    privkey = test_mk_file_path(certsdir, "serverkey.pem");
+    if (privkey == NULL)
+        goto err;
+
+    if (strcmp(opmode, "count") == 0) {
+        CRYPTO_get_alloc_counts(&scount, &rcount, &fcount);
+        ADD_TEST(test_record_alloc_counts);
+        ADD_TEST(test_report_alloc_counts);
+    } else {
+        ADD_TEST(test_alloc_failures);
+    }
+    return 1;
+
+ err:
+    OPENSSL_free(cert);
+    OPENSSL_free(privkey);
+    return 0;
+}
+
+void cleanup_tests(void)
+{
+    OPENSSL_free(cert);
+    OPENSSL_free(privkey);
+}
diff --git a/test/recipes/90-test_handshake-memfail.t b/test/recipes/90-test_handshake-memfail.t
new file mode 100644 (file)
index 0000000..bcb748b
--- /dev/null
@@ -0,0 +1,74 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test qw/:DEFAULT srctop_dir result_dir/;
+use OpenSSL::Test::Utils;
+use File::Temp qw(tempfile);
+use File::Path 2.00 qw(rmtree mkpath);
+
+setup("test_handshake_memfail");
+
+#
+# Don't run this test if mdebug isn't enabled, it won't work
+#
+plan skip_all => "$test_name requires allocfail-tests to be enabled"
+    if disabled("allocfail-tests");
+
+#
+# We need to know how many mallocs we plan to fail, so run the test in count mode
+# To tell us how many mallocs it executes
+# We capture the result of the test into countinfo.txt
+# and parse that to figure out what our values are
+#
+my $resultdir = result_dir();
+run(test(["handshake-memfail", "count", srctop_dir("test", "certs")], stderr => "$resultdir/countinfo.txt"));
+
+#
+# Read the result file into an array
+#
+open my $handle, '<', "$resultdir/countinfo.txt";
+chomp(my @lines = <$handle>);
+close $handle;
+
+#
+# some line contains our counts, find and split that into an array
+#
+my @vals;
+foreach(@lines) {
+    if ($_ =~/skip:/) {
+        @vals = split ' ', $_;
+        break;
+    }
+}
+
+#
+# The number of mallocs we need to skip is in entry two
+# The number of mallocs to test is in entry 4
+#
+my $skipcount = $vals[2];
+my $malloccount = $vals[4];
+
+#
+# Now we can plan our tests.  We plan to run malloccount iterations of this
+# test
+#
+plan tests => $malloccount;
+
+my @seq = (1..$malloccount);
+for my $idx (@seq) {
+    #
+    # We need to setup our openssl malloc failures env var to fail the target malloc
+    # the format of this string is a series of A@B;C@D tuples where A,C are the number
+    # of mallocs to consider, and B,D are the likelyhood that they should fail.
+    # We always skip the first "skip" allocations, then iteratively guarantee that 
+    # next <idx> mallocs pass, followed by the next single malloc failing, with the remainder
+    # passing
+    #
+    $ENV{OPENSSL_MALLOC_FAILURES} = "$skipcount\@0;$idx\@0;1\@100;0\@0"; 
+    ok(run(test(["handshake-memfail", "run", srctop_dir("test", "certs")])));
+}