Commit 3cff7c2181 for openssl.org
commit 3cff7c2181794aafa670e0858dd6213855614f9e
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Thu Apr 16 18:17:59 2026 +0200
Add memory allocation failure testing framework
Introduce ADD_MFAIL_TEST for exhaustive testing of allocation failure
handling in individual functions. The framework repeatedly calls the
test function, each time failing one allocation later within the
section bracketed by mfail_start() and mfail_end(), verifying that
every failure path returns 0 without crashing or leaking.
Custom allocators are installed once at startup via
CRYPTO_set_mem_functions(). When not armed, they pass through to
malloc/realloc/free. Installation can be disabled by setting
OPENSSL_TEST_MFAIL_DISABLE for tests that need the default allocator
(e.g. those using OPENSSL_MALLOC_FAILURES).
Additional environment variables control test execution:
OPENSSL_TEST_MFAIL_SKIP_ALL, OPENSSL_TEST_MFAIL_SKIP_SLOW,
OPENSSL_TEST_MFAIL_POINT, and OPENSSL_TEST_MFAIL_START.
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.foundation>
MergeDate: Thu Apr 23 20:23:34 2026
(Merged from https://github.com/openssl/openssl/pull/30871)
diff --git a/CHANGES.md b/CHANGES.md
index 4fa0eebf3c..871e4d36a3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -31,6 +31,10 @@ OpenSSL Releases
### Changes between 4.0 and 4.1 [xx XXX xxxx]
+ * Added test framework for testing function memory allocation failures.
+
+ *Jakub Zelenka*
+
* Improved DTLS handshake robustness under UDP reordering by buffering and
replaying early ChangeCipherSpec (CCS) records at the expected state.
diff --git a/test/README.md b/test/README.md
index 9865933edb..07beb56952 100644
--- a/test/README.md
+++ b/test/README.md
@@ -185,6 +185,35 @@ To run the tests using the order defined by the random seed `42`:
$ make OPENSSL_TEST_RAND_ORDER=42 test
+Memory Allocation Failure Tests
+-------------------------------
+
+Some tests use the `ADD_MFAIL_TEST` framework to exhaustively verify that
+functions handle every possible allocation failure gracefully. These tests
+run repeatedly, failing one allocation later each iteration, and can be
+controlled with the following environment variables:
+
+ OPENSSL_TEST_MFAIL_DISABLE=1 Disable mfail custom allocator installation.
+
+ OPENSSL_TEST_MFAIL_SKIP_ALL=1 Skip all mfail tests.
+
+ OPENSSL_TEST_MFAIL_SKIP_SLOW=1 Skip only slow mfail tests
+ (registered with ADD_MFAIL_SLOW_TEST).
+
+ OPENSSL_TEST_MFAIL_POINT=N Run only failure point N (0-indexed),
+ useful for debugging a specific failure.
+
+ OPENSSL_TEST_MFAIL_START=N Start iteration from point N, skipping
+ earlier points that are already fixed.
+
+For example, to debug a failure at allocation point 42:
+
+ $ OPENSSL_TEST_MFAIL_POINT=42 ./test/crltest -test test_crl_diff_mfail
+
+Or to skip already-fixed points and collect remaining failures:
+
+ $ OPENSSL_TEST_MFAIL_START=13 make TESTS=test_crl test
+
Running Tests under Valgrind
----------------------------
diff --git a/test/build.info b/test/build.info
index 678139e453..f599b3aff8 100644
--- a/test/build.info
+++ b/test/build.info
@@ -32,7 +32,7 @@ IF[{- !$disabled{tests} -}]
testutil/test_cleanup.c testutil/main.c testutil/testutil_init.c \
testutil/options.c testutil/test_options.c testutil/provider.c \
testutil/apps_shims.c testutil/random.c testutil/helper.c \
- testutil/compare.c $LIBAPPSSRC
+ testutil/compare.c testutil/mfail.c $LIBAPPSSRC
INCLUDE[libtestutil.a]=../include ../apps/include ..
DEPEND[libtestutil.a]=../libcrypto
diff --git a/test/crltest.c b/test/crltest.c
index 24ab5d0cb5..d2f2a125fb 100644
--- a/test/crltest.c
+++ b/test/crltest.c
@@ -855,6 +855,31 @@ static int test_crl_idp_malformed2(void)
return test;
}
+static int test_crl_diff_mfail(void)
+{
+ X509_CRL *base_crl = NULL, *newer_crl = NULL, *delta = NULL;
+ int ret = 0;
+
+ base_crl = CRL_from_strings(kBasicCRL);
+ newer_crl = CRL_from_strings(kRevokedCRL);
+ if (!TEST_ptr(base_crl) || !TEST_ptr(newer_crl))
+ goto err;
+
+ MFAIL_start();
+ delta = X509_CRL_diff(base_crl, newer_crl, NULL, NULL, 0);
+ MFAIL_end();
+
+ if (delta == NULL)
+ goto err;
+
+ ret = 1;
+err:
+ X509_CRL_free(delta);
+ X509_CRL_free(base_crl);
+ X509_CRL_free(newer_crl);
+ return ret;
+}
+
int setup_tests(void)
{
if (!TEST_ptr(test_root = X509_from_strings(kCRLTestRoot))
@@ -878,6 +903,7 @@ int setup_tests(void)
ADD_TEST(test_unknown_critical_crl1);
ADD_TEST(test_unknown_critical_crl2);
ADD_ALL_TESTS(test_reuse_crl, 6);
+ ADD_MFAIL_TEST(test_crl_diff_mfail);
return 1;
}
diff --git a/test/recipes/02-test_mem_alloc.t b/test/recipes/02-test_mem_alloc.t
index aa3c7518cc..d75a2c1112 100644
--- a/test/recipes/02-test_mem_alloc.t
+++ b/test/recipes/02-test_mem_alloc.t
@@ -14,6 +14,7 @@ plan skip_all => "This test should not be run under valgrind"
if ( defined $ENV{OSSL_USE_VALGRIND} );
{
+ local $ENV{"OPENSSL_TEST_MFAIL_DISABLE"} = 1;
local $ENV{"ASAN_OPTIONS"} = "allocator_may_return_null=true";
local $ENV{"MSAN_OPTIONS"} = "allocator_may_return_null=true";
diff --git a/test/recipes/02-test_mem_alloc_custom_fns.t b/test/recipes/02-test_mem_alloc_custom_fns.t
index 4580d0fb8c..b402634ab3 100644
--- a/test/recipes/02-test_mem_alloc_custom_fns.t
+++ b/test/recipes/02-test_mem_alloc_custom_fns.t
@@ -14,6 +14,7 @@ plan skip_all => "This test should not be run under valgrind"
if ( defined $ENV{OSSL_USE_VALGRIND} );
{
+ local $ENV{"OPENSSL_TEST_MFAIL_DISABLE"} = 1;
local $ENV{"ASAN_OPTIONS"} = "allocator_may_return_null=true";
local $ENV{"MSAN_OPTIONS"} = "allocator_may_return_null=true";
diff --git a/test/recipes/90-test_memfail.t b/test/recipes/90-test_memfail.t
index d8bf1e7a4b..01ee4925fa 100644
--- a/test/recipes/90-test_memfail.t
+++ b/test/recipes/90-test_memfail.t
@@ -68,6 +68,8 @@ plan skip_all => "could not get malloc counts (one or more count runs failed or
#
plan tests => $total_malloccount;
+$ENV{OPENSSL_TEST_MFAIL_DISABLE} = "1";
+
sub run_memfail_test {
my $skipcount = $_[0];
my @mallocseq = (1..$_[1]);
diff --git a/test/testutil.h b/test/testutil.h
index 10bbaf3727..cea90df6d9 100644
--- a/test/testutil.h
+++ b/test/testutil.h
@@ -57,6 +57,21 @@
*/
#define ADD_ALL_TESTS(test_function, num) \
add_all_tests(#test_function, test_function, num, 1)
+
+/*
+ * Memory failure exhaustive test. Runs test_fn repeatedly, each time
+ * injecting an allocation failure one step later. When a failure is
+ * injected, asserts test_fn returns 0. When no failure is injected
+ * (all allocation points exhausted), asserts test_fn returns 1 and stops.
+ *
+ * The slow variant is for marking the slow test that can be skipped using
+ * environment variable.
+ *
+ * test_fn has no parameters and returns 1 on success, 0 on failure.
+ */
+#define ADD_MFAIL_TEST(test_fn) add_mfail_test(#test_fn, test_fn, 0)
+#define ADD_MFAIL_SLOW_TEST(test_fn) add_mfail_test(#test_fn, test_fn, 1)
+
/*
* A variant of the same without TAP output.
*/
@@ -227,6 +242,20 @@ int test_arg_libctx(OSSL_LIB_CTX **libctx, OSSL_PROVIDER **default_null_prov,
void add_test(const char *test_case_name, int (*test_fn)(void));
void add_all_tests(const char *test_case_name, int (*test_fn)(int idx), int num,
int subtest);
+void add_mfail_test(const char *test_case_name, int (*test_fn)(void), int slow);
+
+/*
+ * Start the memory allocation failure counter.
+ */
+void mfail_start(void);
+
+/*
+ * Stop the memory allocation failure counter.
+ */
+void mfail_end(void);
+
+#define MFAIL_start mfail_start
+#define MFAIL_end mfail_end
/*
* Declarations for user defined functions.
diff --git a/test/testutil/driver.c b/test/testutil/driver.c
index 1097c68fc3..0b8391eaa6 100644
--- a/test/testutil/driver.c
+++ b/test/testutil/driver.c
@@ -33,7 +33,9 @@ typedef struct test_info {
int num;
/* flags */
- int subtest : 1;
+ unsigned int subtest : 1;
+ unsigned int mfail : 1;
+ unsigned int mfail_slow : 1;
} TEST_INFO;
static TEST_INFO all_tests[1024];
@@ -44,6 +46,7 @@ static int single_iter = -1;
static int level = 0;
static int seed = 0;
static int rand_order = 0;
+static int mfail_added = 0;
/*
* A parameterised test runs a loop of test cases.
@@ -79,6 +82,19 @@ void add_all_tests(const char *test_case_name, int (*test_fn)(int idx),
num_test_cases += num;
}
+void add_mfail_test(const char *test_case_name, int (*test_fn)(void), int slow)
+{
+ assert(num_tests != OSSL_NELEM(all_tests));
+ all_tests[num_tests].test_case_name = test_case_name;
+ all_tests[num_tests].test_fn = test_fn;
+ all_tests[num_tests].num = -1;
+ all_tests[num_tests].mfail = 1;
+ all_tests[num_tests].mfail_slow = slow ? 1 : 0;
+ ++num_tests;
+ ++num_test_cases;
+ mfail_added = 1;
+}
+
static int gcd(int a, int b)
{
while (b != 0) {
@@ -305,6 +321,9 @@ int run_tests(const char *test_prog_name)
test_flush_tapout();
+ if (mfail_added)
+ mfail_init();
+
for (i = 0; i < num_tests; i++)
permute[i] = i;
if (rand_order != 0)
@@ -333,7 +352,14 @@ int run_tests(const char *test_prog_name)
} else if (all_tests[i].num == -1) {
set_test_title(all_tests[i].test_case_name);
ERR_clear_error();
- verdict = all_tests[i].test_fn();
+ if (all_tests[i].mfail)
+ if (mfail_should_skip(all_tests[i].mfail_slow))
+ verdict = TEST_skip("mfail test skipped");
+ else
+ verdict = mfail_run_test(all_tests[i].test_case_name,
+ all_tests[i].test_fn);
+ else
+ verdict = all_tests[i].test_fn();
finalize(verdict != 0);
test_verdict(verdict, "%d - %s", test_case_count + 1, test_title);
if (verdict == 0)
diff --git a/test/testutil/main.c b/test/testutil/main.c
index 921d02a2cb..80ca7062be 100644
--- a/test/testutil/main.c
+++ b/test/testutil/main.c
@@ -31,6 +31,8 @@ int main(int argc, char *argv[])
int setup_res;
int gi_ret;
+ mfail_install();
+
gi_ret = global_init();
test_open_streams();
diff --git a/test/testutil/mfail.c b/test/testutil/mfail.c
new file mode 100644
index 0000000000..e002eafe03
--- /dev/null
+++ b/test/testutil/mfail.c
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2026 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 "../testutil.h"
+#include "tu_local.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <openssl/crypto.h>
+
+static int mfail_fail_after = -1;
+static int mfail_alloc_count = 0;
+static int mfail_triggered = 0;
+static int mfail_counting = 0;
+static int mfail_do_skip_all = 0;
+static int mfail_do_skip_slow = 0;
+static int mfail_single_point = -1;
+static int mfail_start_point = 0;
+static int mfail_installed = 0;
+
+static int should_fail(void)
+{
+ if (mfail_fail_after < 0 || !mfail_counting || mfail_triggered)
+ return 0;
+ if (mfail_alloc_count++ == mfail_fail_after) {
+ mfail_triggered = 1;
+ return 1;
+ }
+ return 0;
+}
+
+static void *mfail_malloc(size_t num, const char *file, int line)
+{
+ if (num == 0)
+ return NULL;
+ if (should_fail())
+ return NULL;
+ return malloc(num);
+}
+
+static void *mfail_realloc(void *addr, size_t num, const char *file, int line)
+{
+ if (addr == NULL)
+ return mfail_malloc(num, file, line);
+ if (num == 0) {
+ free(addr);
+ return NULL;
+ }
+ if (should_fail())
+ return NULL;
+ return realloc(addr, num);
+}
+
+static void mfail_free(void *addr, const char *file, int line)
+{
+ free(addr);
+}
+
+static int env_is_true(const char *name)
+{
+ const char *val = getenv(name);
+
+ return val != NULL && *val != '\0' && strcmp(val, "0") != 0;
+}
+
+void mfail_install(void)
+{
+ if (env_is_true("OPENSSL_TEST_MFAIL_DISABLE"))
+ return;
+ if (!CRYPTO_set_mem_functions(mfail_malloc, mfail_realloc, mfail_free))
+ return;
+ mfail_installed = 1;
+}
+
+void mfail_start(void)
+{
+ mfail_alloc_count = 0;
+ mfail_counting = 1;
+}
+
+void mfail_end(void)
+{
+ mfail_counting = 0;
+}
+
+static void mfail_arm(int fail_after)
+{
+ mfail_fail_after = fail_after;
+ mfail_alloc_count = 0;
+ mfail_triggered = 0;
+ mfail_counting = 0;
+}
+
+static void mfail_disarm(void)
+{
+ mfail_fail_after = -1;
+ mfail_alloc_count = 0;
+ mfail_triggered = 0;
+ mfail_counting = 0;
+}
+
+static double elapsed_secs(clock_t start)
+{
+ return (double)(clock() - start) / CLOCKS_PER_SEC;
+}
+
+void mfail_init(void)
+{
+ const char *env;
+
+ mfail_do_skip_all = env_is_true("OPENSSL_TEST_MFAIL_SKIP_ALL");
+ mfail_do_skip_slow = env_is_true("OPENSSL_TEST_MFAIL_SKIP_SLOW");
+
+ env = getenv("OPENSSL_TEST_MFAIL_POINT");
+ if (env != NULL && *env != '\0')
+ mfail_single_point = atoi(env);
+
+ env = getenv("OPENSSL_TEST_MFAIL_START");
+ if (env != NULL && *env != '\0')
+ mfail_start_point = atoi(env);
+}
+
+int mfail_should_skip(int slow)
+{
+ if (!mfail_installed)
+ return 1;
+ return mfail_do_skip_all || (slow && mfail_do_skip_slow);
+}
+
+int mfail_run_test(const char *test_case_name, int (*test_fn)(void))
+{
+ int alloc_point, ret = 1;
+ clock_t start;
+
+ start = clock();
+
+ if (mfail_single_point >= 0) {
+ int rv, triggered;
+
+ ERR_clear_error();
+ mfail_arm(mfail_single_point);
+ rv = test_fn();
+ triggered = mfail_triggered;
+ mfail_disarm();
+
+ if (!triggered) {
+ TEST_info("mfail test '%s': point %d is beyond the last "
+ "allocation point, test %s",
+ test_case_name, mfail_single_point,
+ rv == 1 ? "succeeded" : "failed");
+ } else if (!TEST_int_eq(rv, 0)) {
+ TEST_error("mfail test '%s': allocation failure at point %d "
+ "not handled",
+ test_case_name, mfail_single_point);
+ ret = 0;
+ }
+ } else {
+ for (alloc_point = mfail_start_point;; alloc_point++) {
+ int rv, triggered;
+
+ ERR_clear_error();
+ mfail_arm(alloc_point);
+ rv = test_fn();
+ triggered = mfail_triggered;
+ mfail_disarm();
+
+ if (!triggered) {
+ if (!TEST_int_eq(rv, 1)) {
+ TEST_error("mfail test '%s': no injection but test failed",
+ test_case_name);
+ ret = 0;
+ }
+ break;
+ }
+
+ if (!TEST_int_eq(rv, 0)) {
+ TEST_error("mfail test '%s': allocation failure at point %d "
+ "not handled",
+ test_case_name, alloc_point);
+ ret = 0;
+ }
+ }
+ TEST_info("mfail test '%s': points %d..%d, %d iterations, %.6f seconds",
+ test_case_name, mfail_start_point, alloc_point,
+ alloc_point - mfail_start_point + 1, elapsed_secs(start));
+ }
+
+ return ret;
+}
diff --git a/test/testutil/tu_local.h b/test/testutil/tu_local.h
index 6b900d19b4..b7169f8a97 100644
--- a/test/testutil/tu_local.h
+++ b/test/testutil/tu_local.h
@@ -49,6 +49,11 @@ void test_fail_memory_message(const char *prefix, const char *file,
__owur int setup_test_framework(int argc, char *argv[]);
__owur int pulldown_test_framework(int ret);
+void mfail_install(void);
+void mfail_init(void);
+int mfail_should_skip(int slow);
+int mfail_run_test(const char *test_case_name, int (*test_fn)(void));
+
__owur int run_tests(const char *test_prog_name);
void set_test_title(const char *title);