Commit 783388e3c6 for openssl.org

commit 783388e3c68cb3a49b815b5a8c34c0857d698eb4
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Mon May 11 17:57:16 2026 +0200

    Extend and separate mfail test framework

    Separate the mfail framework so it can be used beyond testutil.
    Specifically, this is a step toward using it in fuzzing.

    This change also improves the way mfail tests are executed. It first
    counts the number of allocations and then iterates through them.

    This has a couple of advantages:
    - It allows removal of MFAIL_SLOW_TEST by identifying slow tests based
      on the number of allocations.
    - It allows non-failing tests to be ignored.

    In addition, it adds a new environment variable to print a backtrace on
    memory failure.

    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    MergeDate: Mon May 18 07:23:52 2026
    (Merged from https://github.com/openssl/openssl/pull/31144)

diff --git a/test/README.md b/test/README.md
index 07beb56952..bc4bd73f49 100644
--- a/test/README.md
+++ b/test/README.md
@@ -190,15 +190,19 @@ 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:
+run repeatedly, failing one allocation later each iteration. The
+`ADD_MFAIL_NO_CHECK_TEST` variant relaxes the requirement that the test
+return 0 when a failure was triggered. Behavior is 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_SKIP_SLOW=1  Skip mfail tests whose allocation count
+                                    exceeds the slow threshold.
+
+    OPENSSL_TEST_MFAIL_SLOW=N       Slow threshold (default 1000).

     OPENSSL_TEST_MFAIL_POINT=N      Run only failure point N (0-indexed),
                                     useful for debugging a specific failure.
@@ -206,6 +210,8 @@ controlled with the following environment variables:
     OPENSSL_TEST_MFAIL_START=N      Start iteration from point N, skipping
                                     earlier points that are already fixed.

+    OPENSSL_TEST_MFAIL_BACKTRACE=1  Print a backtrace at each injection point.
+
 For example, to debug a failure at allocation point 42:

     $ OPENSSL_TEST_MFAIL_POINT=42 ./test/crltest -test test_crl_diff_mfail
diff --git a/test/build.info b/test/build.info
index 16266b594b..955a0ace58 100644
--- a/test/build.info
+++ b/test/build.info
@@ -32,8 +32,8 @@ 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 testutil/mfail.c $LIBAPPSSRC
-  INCLUDE[libtestutil.a]=../include ../apps/include ..
+          testutil/compare.c mfail/mfail.c $LIBAPPSSRC
+  INCLUDE[libtestutil.a]=../include ../apps/include .. mfail
   DEPEND[libtestutil.a]=../libcrypto

   PROGRAMS{noinst}= \
diff --git a/test/mfail/mfail.c b/test/mfail/mfail.c
new file mode 100644
index 0000000000..034d9881da
--- /dev/null
+++ b/test/mfail/mfail.c
@@ -0,0 +1,367 @@
+/*
+ * 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 "mfail.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <openssl/crypto.h>
+
+#if defined(__has_feature)
+#if __has_feature(address_sanitizer)
+#define MFAIL_HAVE_ASAN 1
+#endif
+#endif
+#if defined(__SANITIZE_ADDRESS__) && !defined(MFAIL_HAVE_ASAN)
+#define MFAIL_HAVE_ASAN 1
+#endif
+
+#ifdef MFAIL_HAVE_ASAN
+extern void __sanitizer_print_stack_trace(void);
+#elif defined(__GLIBC__) || defined(__APPLE__)
+#include <execinfo.h>
+#define MFAIL_HAVE_BACKTRACE
+#define MFAIL_BT_MAX 128
+#endif
+
+static struct {
+    int installed;
+    int skip_all;
+    int skip_slow;
+    int single_point;
+    int start_point;
+    int env_count;
+    int slow_threshold;
+    int print_bt;
+    int mode;
+    int phase;
+    int seq;
+    int next_point;
+    int iter_index;
+    int n;
+    int total;
+    int iterations;
+    int started;
+    int fail_after;
+    int alloc_count;
+    int triggered;
+    int counting;
+    int slow_skipped;
+} mf;
+
+static int env_is_true(const char *name)
+{
+    const char *value = getenv(name);
+    return value != NULL && *value != '\0' && strcmp(value, "0") != 0;
+}
+
+static int env_int(const char *name, int dflt)
+{
+    const char *value = getenv(name);
+    return (value != NULL && *value != '\0') ? atoi(value) : dflt;
+}
+
+static void mfail_print_bt(void)
+{
+#ifdef MFAIL_HAVE_ASAN
+    fprintf(stderr, "# MFAIL_BT (failure injection point)\n");
+    __sanitizer_print_stack_trace();
+#elif defined(MFAIL_HAVE_BACKTRACE)
+    void *buf[MFAIL_BT_MAX];
+    char **syms;
+    int n, i;
+
+    n = backtrace(buf, MFAIL_BT_MAX);
+    syms = backtrace_symbols(buf, n);
+    if (syms == NULL)
+        return;
+
+    fprintf(stderr, "# MFAIL_BT (failure injection point)\n");
+    /* Skip frame 0 (this function) */
+    for (i = 1; i < n; i++)
+        fprintf(stderr, "#   %s\n", syms[i]);
+
+    free(syms);
+#endif
+}
+
+static int should_fail(void)
+{
+    int idx;
+
+    if (!mf.counting)
+        return 0;
+
+    idx = mf.alloc_count++;
+
+    if (mf.fail_after < 0 || mf.triggered)
+        return 0;
+    if (idx == mf.fail_after) {
+        mf.triggered = 1;
+        if (mf.print_bt)
+            mfail_print_bt();
+        return 1;
+    }
+    return 0;
+}
+
+static void *mf_malloc(size_t num, const char *file, int line)
+{
+    if (num == 0)
+        return NULL;
+    if (should_fail())
+        return NULL;
+    return malloc(num);
+}
+
+static void *mf_realloc(void *addr, size_t num, const char *file, int line)
+{
+    if (addr == NULL)
+        return mf_malloc(num, file, line);
+    if (num == 0) {
+        free(addr);
+        return NULL;
+    }
+    if (should_fail())
+        return NULL;
+    return realloc(addr, num);
+}
+
+static void mf_free(void *addr, const char *file, int line)
+{
+    free(addr);
+}
+
+int mfail_install(int optional)
+{
+    if (mf.installed)
+        return 1;
+    if (env_is_true("OPENSSL_TEST_MFAIL_DISABLE"))
+        return 0;
+
+    mf.skip_all = env_is_true("OPENSSL_TEST_MFAIL_SKIP_ALL");
+    mf.skip_slow = env_is_true("OPENSSL_TEST_MFAIL_SKIP_SLOW");
+    mf.single_point = env_int("OPENSSL_TEST_MFAIL_POINT", -1);
+    mf.start_point = env_int("OPENSSL_TEST_MFAIL_START", 0);
+    mf.env_count = env_int("OPENSSL_TEST_MFAIL_COUNT", 0);
+    mf.slow_threshold = env_int("OPENSSL_TEST_MFAIL_SLOW", 1000);
+    mf.print_bt = env_is_true("OPENSSL_TEST_MFAIL_BACKTRACE");
+
+    /* if optional and nothing configured, then no point installing hooks */
+    if (optional && mf.env_count <= 0 && mf.single_point < 0)
+        return 0;
+
+    if (!CRYPTO_set_mem_functions(mf_malloc, mf_realloc, mf_free))
+        return -1;
+
+    mf.installed = 1;
+    mf.fail_after = -1;
+    return 1;
+}
+
+int mfail_is_installed(void)
+{
+    return mf.installed;
+}
+
+int mfail_env_skip_all(void)
+{
+    return !mf.installed || mf.skip_all;
+}
+
+int mfail_env_skip_slow(void)
+{
+    return !mf.installed || mf.skip_slow;
+}
+
+/* The i-th of n points distributed over [start, total), rotated by seq */
+static int compute_point(int i, int total, int n, int seq, int start)
+{
+    int range = total - start;
+    int stride_int, stride_rem_x2, stride_rnd, offset, p;
+
+    if (range <= 0 || n <= 0)
+        return start;
+
+    stride_int = range / n;
+    stride_rem_x2 = (range - stride_int * n) * 2;
+    stride_rnd = stride_int + (stride_rem_x2 >= n ? 1 : 0);
+    offset = (stride_rnd > 0) ? (seq % stride_rnd) : 0;
+
+    p = (int)(((long)i * range + n / 2) / n) + offset;
+    if (p >= range)
+        p = range - 1;
+    if (p < 0)
+        p = 0;
+    return start + p;
+}
+
+void mfail_init(int seq, int flags)
+{
+    mf.seq = seq;
+    mf.iter_index = 0;
+    mf.n = 0;
+    mf.total = 0;
+    mf.iterations = 0;
+    mf.started = 0;
+    mf.fail_after = -1;
+    mf.alloc_count = 0;
+    mf.triggered = 0;
+    mf.counting = 0;
+    mf.slow_skipped = 0;
+
+    if (mf.single_point >= 0) {
+        mf.mode = MFAIL_MODE_SINGLE;
+    } else if ((flags & MFAIL_FLAG_COUNT) && mf.env_count > 0) {
+        mf.mode = MFAIL_MODE_SAMPLED;
+    } else {
+        mf.mode = MFAIL_MODE_EXHAUSTIVE;
+    }
+    mf.phase = MFAIL_PHASE_COUNTING;
+    mf.next_point = -1;
+}
+
+int mfail_has_next(void)
+{
+    if (mf.started) {
+        mf.iterations++;
+        switch (mf.phase) {
+        case MFAIL_PHASE_COUNTING:
+            mf.total = mf.alloc_count;
+            if (mf.skip_slow && mf.total > mf.slow_threshold) {
+                mf.slow_skipped = 1;
+                mf.phase = MFAIL_PHASE_DONE;
+                break;
+            }
+            if (mf.mode == MFAIL_MODE_SINGLE) {
+                mf.phase = MFAIL_PHASE_INJECTING;
+                mf.next_point = mf.single_point;
+            } else if (mf.mode == MFAIL_MODE_EXHAUSTIVE) {
+                if (mf.total > mf.start_point) {
+                    mf.phase = MFAIL_PHASE_INJECTING;
+                    mf.next_point = mf.start_point;
+                } else {
+                    mf.phase = MFAIL_PHASE_DONE;
+                }
+            } else { /* mf.mode is MFAIL_MODE_SAMPLED */
+                mf.n = mf.env_count;
+                if (mf.n > mf.total)
+                    mf.n = mf.total;
+                if (mf.n > 0 && mf.total > mf.start_point) {
+                    mf.phase = MFAIL_PHASE_INJECTING;
+                    mf.iter_index = 0;
+                    mf.next_point = compute_point(0, mf.total, mf.n, mf.seq,
+                        mf.start_point);
+                } else {
+                    mf.phase = MFAIL_PHASE_DONE;
+                }
+            }
+            break;
+        case MFAIL_PHASE_INJECTING:
+            if (mf.mode == MFAIL_MODE_SINGLE) {
+                mf.phase = MFAIL_PHASE_DONE;
+            } else if (mf.mode == MFAIL_MODE_EXHAUSTIVE) {
+                if (++mf.next_point >= mf.total)
+                    mf.phase = MFAIL_PHASE_DONE;
+            } else { /* SAMPLED */
+                if (++mf.iter_index >= mf.n)
+                    mf.phase = MFAIL_PHASE_DONE;
+                else
+                    mf.next_point = compute_point(mf.iter_index, mf.total,
+                        mf.n, mf.seq, mf.start_point);
+            }
+            break;
+        case MFAIL_PHASE_DONE:
+        default:
+            break;
+        }
+    } else {
+        mf.started = 1;
+    }
+
+    if (mf.phase == MFAIL_PHASE_DONE)
+        return 0;
+
+    mf.alloc_count = 0;
+    mf.counting = 0;
+    mf.triggered = 0;
+    mf.fail_after = (mf.phase == MFAIL_PHASE_INJECTING) ? mf.next_point : -1;
+    return 1;
+}
+
+void mfail_start(void)
+{
+    mf.alloc_count = 0;
+    mf.counting = 1;
+}
+
+void mfail_end(void)
+{
+    mf.counting = 0;
+}
+
+void mfail_arm_once(int point)
+{
+    mf.fail_after = point;
+    mf.alloc_count = 0;
+    mf.triggered = 0;
+}
+
+void mfail_disarm(void)
+{
+    mf.fail_after = -1;
+    mf.alloc_count = 0;
+    mf.triggered = 0;
+}
+
+int mfail_was_triggered(void)
+{
+    return mf.triggered;
+}
+
+int mfail_was_slow_skipped(void)
+{
+    return mf.slow_skipped;
+}
+
+int mfail_get_count(void)
+{
+    return mf.alloc_count;
+}
+
+int mfail_get_total(void)
+{
+    return mf.total;
+}
+
+int mfail_get_phase(void)
+{
+    return mf.phase;
+}
+
+int mfail_get_mode(void)
+{
+    return mf.mode;
+}
+
+int mfail_iterations(void)
+{
+    return mf.iterations;
+}
+
+int mfail_get_slow_threshold(void)
+{
+    return mf.slow_threshold;
+}
+
+int mfail_get_point(void)
+{
+    return (mf.phase == MFAIL_PHASE_INJECTING) ? mf.next_point : -1;
+}
diff --git a/test/mfail/mfail.h b/test/mfail/mfail.h
new file mode 100644
index 0000000000..35888423f8
--- /dev/null
+++ b/test/mfail/mfail.h
@@ -0,0 +1,67 @@
+/*
+ * 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
+ */
+
+#ifndef OSSL_TEST_MFAIL_H
+#define OSSL_TEST_MFAIL_H
+
+/* Flags for mfail_init(). */
+#define MFAIL_FLAG_COUNT (1 << 0)
+
+/* Modes */
+#define MFAIL_MODE_EXHAUSTIVE 0
+#define MFAIL_MODE_SAMPLED 1
+#define MFAIL_MODE_SINGLE 2
+
+/* Phases */
+#define MFAIL_PHASE_DONE 0
+#define MFAIL_PHASE_COUNTING 1
+#define MFAIL_PHASE_INJECTING 2
+
+/* Install mem hooks */
+int mfail_install(int optional);
+/* Check if hooks installed */
+int mfail_is_installed(void);
+/* Initialize the mfail for test case runs */
+void mfail_init(int seq, int flags);
+/* Check for the failure loop if another fail execution should be done */
+int mfail_has_next(void);
+/* Start the failure triggering block */
+void mfail_start(void);
+/* End the failure triggering block */
+void mfail_end(void);
+/* Check if the failure was triggered in the block */
+int mfail_was_triggered(void);
+/* Check if the inject phase was skipped because it got over slow threshold */
+int mfail_was_slow_skipped(void);
+/* If the counting was executed, get the total number of allocations */
+int mfail_get_count(void);
+/* Get the total number of failure points */
+int mfail_get_total(void);
+/* Get the number of iterations that run */
+int mfail_iterations(void);
+/* Get the current failure point */
+int mfail_get_point(void);
+/* Get execution phase */
+int mfail_get_phase(void);
+/* Get execution mode */
+int mfail_get_mode(void);
+/* Get the configured slow threshold */
+int mfail_get_slow_threshold(void);
+
+/* Low level arming at specific point (instead of start) */
+void mfail_arm_once(int point);
+/* Low level disarming (similar to end) */
+void mfail_disarm(void);
+
+/* Check whether to skip all tests */
+int mfail_env_skip_all(void);
+/* Check whether to skip only slow tests */
+int mfail_env_skip_slow(void);
+
+#endif /* OSSL_TEST_MFAIL_H */
diff --git a/test/testutil.h b/test/testutil.h
index 459f09e915..31e5f87412 100644
--- a/test/testutil.h
+++ b/test/testutil.h
@@ -20,6 +20,7 @@
 #include <openssl/bn.h>
 #include <openssl/x509.h>
 #include "opt.h"
+#include "mfail/mfail.h"

 /*-
  * Simple unit tests should implement setup_tests().
@@ -64,13 +65,19 @@
  * 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.
+ * The NO_CHECK variant disables the assertion that failed tests must
+ * result in function failure.
  *
  * 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)
+
+/* Per-test flags for add_mfail_test() */
+#define MFAIL_TEST_NO_CHECK (1 << 0)
+
+#define ADD_MFAIL_TEST(test_fn) \
+    add_mfail_test(#test_fn, test_fn, 0)
+#define ADD_MFAIL_NO_CHECK_TEST(test_fn) \
+    add_mfail_test(#test_fn, test_fn, MFAIL_TEST_NO_CHECK)

 /*
  * A variant of the same without TAP output.
@@ -242,17 +249,8 @@ 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);
+void add_mfail_test(const char *test_case_name, int (*test_fn)(void),
+    int flags);

 #define MFAIL_start mfail_start
 #define MFAIL_end mfail_end
diff --git a/test/testutil/driver.c b/test/testutil/driver.c
index 0b8391eaa6..2742cc3908 100644
--- a/test/testutil/driver.c
+++ b/test/testutil/driver.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2023 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2016-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
@@ -19,6 +19,9 @@

 #include "platform.h" /* From libapps */

+#include "mfail.h"
+#include <time.h>
+
 #if defined(_WIN32) && !defined(__BORLANDC__)
 #define strdup _strdup
 #endif
@@ -35,7 +38,7 @@ typedef struct test_info {
     /* flags */
     unsigned int subtest : 1;
     unsigned int mfail : 1;
-    unsigned int mfail_slow : 1;
+    int mfail_flags;
 } TEST_INFO;

 static TEST_INFO all_tests[1024];
@@ -46,7 +49,6 @@ 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.
@@ -82,17 +84,16 @@ 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)
+void add_mfail_test(const char *test_case_name, int (*test_fn)(void), int flags)
 {
     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;
+    all_tests[num_tests].mfail_flags = flags;
     ++num_tests;
     ++num_test_cases;
-    mfail_added = 1;
 }

 static int gcd(int a, int b)
@@ -294,6 +295,64 @@ static void test_verdict(int verdict,
     test_flush_tapout();
 }

+static double mfail_elapsed_secs(clock_t start)
+{
+    return (double)(clock() - start) / CLOCKS_PER_SEC;
+}
+
+static int mfail_should_skip(void)
+{
+    if (!mfail_is_installed())
+        return 1;
+    return mfail_env_skip_all();
+}
+
+static int mfail_run_test(const char *test_case_name,
+    int (*test_fn)(void), int flags)
+{
+    int ret = 1;
+    int no_check = (flags & MFAIL_TEST_NO_CHECK) != 0;
+    clock_t start = clock();
+
+    mfail_init(0, 0);
+
+    while (mfail_has_next()) {
+        int rv;
+
+        ERR_clear_error();
+        rv = test_fn();
+
+        if (mfail_was_triggered()) {
+            if (!no_check && !TEST_int_eq(rv, 0)) {
+                TEST_error("mfail test '%s': allocation failure at point %d "
+                           "not handled",
+                    test_case_name, mfail_get_point());
+                ret = 0;
+            }
+        } else if (mfail_get_mode() == MFAIL_MODE_SINGLE) {
+            TEST_info("mfail test '%s': point %d is beyond the last "
+                      "allocation point, test %s",
+                test_case_name, mfail_get_point(),
+                rv == 1 ? "succeeded" : "failed");
+        } else if (!TEST_int_eq(rv, 1)) {
+            TEST_error("mfail test '%s': no injection but test failed",
+                test_case_name);
+            ret = 0;
+        }
+    }
+
+    if (ret != 0 && mfail_was_slow_skipped())
+        return TEST_skip("mfail test '%s': %d allocations exceeds slow "
+                         "threshold %d",
+            test_case_name, mfail_get_total(),
+            mfail_get_slow_threshold());
+
+    TEST_info("mfail test '%s': %d allocations, %d iterations, %.6f seconds",
+        test_case_name, mfail_get_total(), mfail_iterations(),
+        mfail_elapsed_secs(start));
+    return ret;
+}
+
 int run_tests(const char *test_prog_name)
 {
     int num_failed = 0;
@@ -321,9 +380,6 @@ 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)
@@ -353,11 +409,11 @@ int run_tests(const char *test_prog_name)
             set_test_title(all_tests[i].test_case_name);
             ERR_clear_error();
             if (all_tests[i].mfail)
-                if (mfail_should_skip(all_tests[i].mfail_slow))
+                if (mfail_should_skip())
                     verdict = TEST_skip("mfail test skipped");
                 else
                     verdict = mfail_run_test(all_tests[i].test_case_name,
-                        all_tests[i].test_fn);
+                        all_tests[i].test_fn, all_tests[i].mfail_flags);
             else
                 verdict = all_tests[i].test_fn();
             finalize(verdict != 0);
diff --git a/test/testutil/main.c b/test/testutil/main.c
index 80ca7062be..940d25f707 100644
--- a/test/testutil/main.c
+++ b/test/testutil/main.c
@@ -31,7 +31,10 @@ int main(int argc, char *argv[])
     int setup_res;
     int gi_ret;

-    mfail_install();
+    if (mfail_install(0) < 0) {
+        test_printf_stderr("MFAIL installation failed - aborting\n");
+        return ret;
+    }

     gi_ret = global_init();

diff --git a/test/testutil/mfail.c b/test/testutil/mfail.c
deleted file mode 100644
index e002eafe03..0000000000
--- a/test/testutil/mfail.c
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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 b7169f8a97..6b900d19b4 100644
--- a/test/testutil/tu_local.h
+++ b/test/testutil/tu_local.h
@@ -49,11 +49,6 @@ 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);