Commit dca561d4cd for openssl.org
commit dca561d4cdb84c7e813e9b28352928edd5b2cfd8
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date: Tue Apr 28 19:22:46 2026 +0200
Integrate mfail functionality to fuzz tests
Run the fuzz corpora under mfail in addition to the normal path, so the
existing inputs also exercise malloc-failure handling. The fuzz.pl harness
sizes the mfail runs to a time budget and, on a leak, bisects down to the
exact file and injection point. Adds a budgeted asan/ubsan CI job to run it.
Assisted-by: Claude:claude-opus-4-6
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Wed Jul 15 16:01:43 2026
(Merged from https://github.com/openssl/openssl/pull/30944)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b10ab496c5..597501112f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -462,6 +462,40 @@ jobs:
path: artifacts.tar.gz
if-no-files-found: ignore
+ fuzz_tests_mfail:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+ - name: checkout fuzz/corpora submodule
+ run: git submodule update --init --depth 1 fuzz/corpora
+ - name: Adjust ASLR for sanitizer
+ run: sudo sysctl -w vm.mmap_rnd_bits=28
+ - name: config
+ run: |
+ ./config --strict-warnings --banner=Configured --debug \
+ -DPEDANTIC -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION \
+ enable-asan enable-ec_explicit_curves enable-ubsan \
+ enable-rc5 enable-md2 enable-ec_nistp_64_gcc_128 \
+ enable-weak-ssl-ciphers enable-nextprotoneg
+ perl configdata.pm --dump
+ - name: make
+ run: make -s -j4
+ - name: make test (fuzz with mfail)
+ env:
+ OSSL_FUZZ_TEST_BUDGET: 1200
+ OSSL_FUZZ_TEST_JOBS: 4
+ run: .github/workflows/make-test OPENSSL_TEST_RAND_ORDER=0 TESTS="test_fuzz*"
+ - name: save artifacts
+ if: success() || failure()
+ uses: actions/upload-artifact@v5
+ with:
+ name: "ci@fuzz_tests_mfail"
+ path: artifacts.tar.gz
+ if-no-files-found: ignore
+
memory_sanitizer:
runs-on: ubuntu-latest
steps:
diff --git a/fuzz/build.info b/fuzz/build.info
index 3770c2df7e..5c410260e4 100644
--- a/fuzz/build.info
+++ b/fuzz/build.info
@@ -176,6 +176,8 @@ IF[{- !$disabled{"fuzz-afl"} || !$disabled{"fuzz-libfuzzer"} -}]
ENDIF
IF[{- !$disabled{tests} -}]
+ $FUZZTESTSRC=test-corpus.c ../test/mfail/mfail.c
+
PROGRAMS{noinst}=asn1-test asn1parse-test bignum-test bndiv-test client-test conf-test crl-test server-test smime-test
PROGRAMS{noinst}=pkcs12-test punycode-test pem-test decoder-test hashtable-test acert-test
PROGRAMS{noinst}=v3name-test
@@ -218,131 +220,131 @@ IF[{- !$disabled{tests} -}]
PROGRAMS{noinst}=dtlsclient-test dtlsserver-test
ENDIF
- SOURCE[asn1-test]=asn1.c test-corpus.c fuzz_rand.c
- INCLUDE[asn1-test]=../include
- DEPEND[asn1-test]=../libcrypto ../libssl
+ SOURCE[asn1-test]=asn1.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[asn1-test]=../include ../test/mfail
+ DEPEND[asn1-test]=../libcrypto.a ../libssl.a
- SOURCE[asn1parse-test]=asn1parse.c test-corpus.c
- INCLUDE[asn1parse-test]=../include
- DEPEND[asn1parse-test]=../libcrypto
+ SOURCE[asn1parse-test]=asn1parse.c $FUZZTESTSRC
+ INCLUDE[asn1parse-test]=../include ../test/mfail
+ DEPEND[asn1parse-test]=../libcrypto.a
- SOURCE[bignum-test]=bignum.c test-corpus.c
- INCLUDE[bignum-test]=../include
- DEPEND[bignum-test]=../libcrypto
+ SOURCE[bignum-test]=bignum.c $FUZZTESTSRC
+ INCLUDE[bignum-test]=../include ../test/mfail
+ DEPEND[bignum-test]=../libcrypto.a
- SOURCE[bndiv-test]=bndiv.c test-corpus.c
- INCLUDE[bndiv-test]=../include
- DEPEND[bndiv-test]=../libcrypto
+ SOURCE[bndiv-test]=bndiv.c $FUZZTESTSRC
+ INCLUDE[bndiv-test]=../include ../test/mfail
+ DEPEND[bndiv-test]=../libcrypto.a
- SOURCE[client-test]=client.c test-corpus.c fuzz_rand.c
- INCLUDE[client-test]=../include
- DEPEND[client-test]=../libcrypto ../libssl
+ SOURCE[client-test]=client.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[client-test]=../include ../test/mfail
+ DEPEND[client-test]=../libcrypto.a ../libssl.a
- SOURCE[cmp-test]=cmp.c test-corpus.c fuzz_rand.c
- INCLUDE[cmp-test]=../include
+ SOURCE[cmp-test]=cmp.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[cmp-test]=../include ../test/mfail
DEPEND[cmp-test]=../libcrypto.a
# referring to static lib allows using non-exported functions
- SOURCE[ml-kem-test]=ml-kem.c test-corpus.c fuzz_rand.c
- INCLUDE[ml-kem-test]=../include
+ SOURCE[ml-kem-test]=ml-kem.c $FUZZTESTSRC
+ INCLUDE[ml-kem-test]=../include ../test/mfail
DEPEND[ml-kem-test]=../libcrypto.a
# referring to static lib allows using non-exported functions
- SOURCE[ml-dsa-test]=ml-dsa.c test-corpus.c fuzz_rand.c
- INCLUDE[ml-dsa-test]=../include
+ SOURCE[ml-dsa-test]=ml-dsa.c $FUZZTESTSRC
+ INCLUDE[ml-dsa-test]=../include ../test/mfail
DEPEND[ml-dsa-test]=../libcrypto.a
# referring to static lib allows using non-exported functions
- SOURCE[slh-dsa-test]=slh-dsa.c test-corpus.c fuzz_rand.c
- INCLUDE[slh-dsa-test]=../include
+ SOURCE[slh-dsa-test]=slh-dsa.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[slh-dsa-test]=../include ../test/mfail
DEPEND[slh-dsa-test]=../libcrypto.a
# referring to static lib allows using non-exported functions
- SOURCE[cms-test]=cms.c test-corpus.c
- INCLUDE[cms-test]=../include
- DEPEND[cms-test]=../libcrypto
+ SOURCE[cms-test]=cms.c $FUZZTESTSRC
+ INCLUDE[cms-test]=../include ../test/mfail
+ DEPEND[cms-test]=../libcrypto.a
- SOURCE[pkcs12-test]=pkcs12.c test-corpus.c
- INCLUDE[pkcs12-test]=../include
- DEPEND[pkcs12-test]=../libcrypto
+ SOURCE[pkcs12-test]=pkcs12.c $FUZZTESTSRC
+ INCLUDE[pkcs12-test]=../include ../test/mfail
+ DEPEND[pkcs12-test]=../libcrypto.a
- SOURCE[conf-test]=conf.c test-corpus.c
- INCLUDE[conf-test]=../include
- DEPEND[conf-test]=../libcrypto
+ SOURCE[conf-test]=conf.c $FUZZTESTSRC
+ INCLUDE[conf-test]=../include ../test/mfail
+ DEPEND[conf-test]=../libcrypto.a
- SOURCE[crl-test]=crl.c test-corpus.c
- INCLUDE[crl-test]=../include
- DEPEND[crl-test]=../libcrypto
+ SOURCE[crl-test]=crl.c $FUZZTESTSRC
+ INCLUDE[crl-test]=../include ../test/mfail
+ DEPEND[crl-test]=../libcrypto.a
- SOURCE[ct-test]=ct.c test-corpus.c
- INCLUDE[ct-test]=../include
- DEPEND[ct-test]=../libcrypto
+ SOURCE[ct-test]=ct.c $FUZZTESTSRC
+ INCLUDE[ct-test]=../include ../test/mfail
+ DEPEND[ct-test]=../libcrypto.a
- SOURCE[dtlsclient-test]=dtlsclient.c test-corpus.c fuzz_rand.c
- INCLUDE[dtlsclient-test]=../include
- DEPEND[dtlsclient-test]=../libcrypto ../libssl
+ SOURCE[dtlsclient-test]=dtlsclient.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[dtlsclient-test]=../include ../test/mfail
+ DEPEND[dtlsclient-test]=../libcrypto.a ../libssl.a
- SOURCE[dtlsserver-test]=dtlsserver.c test-corpus.c fuzz_rand.c
- INCLUDE[dtlsserver-test]=../include
- DEPEND[dtlsserver-test]=../libcrypto ../libssl
+ SOURCE[dtlsserver-test]=dtlsserver.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[dtlsserver-test]=../include ../test/mfail
+ DEPEND[dtlsserver-test]=../libcrypto.a ../libssl.a
- SOURCE[pem-test]=pem.c test-corpus.c
- INCLUDE[pem-test]=../include
+ SOURCE[pem-test]=pem.c $FUZZTESTSRC
+ INCLUDE[pem-test]=../include ../test/mfail
DEPEND[pem-test]=../libcrypto.a
- SOURCE[decoder-test]=decoder.c test-corpus.c fuzz_rand.c
- INCLUDE[decoder-test]=../include
- DEPEND[decoder-test]=../libcrypto
+ SOURCE[decoder-test]=decoder.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[decoder-test]=../include ../test/mfail
+ DEPEND[decoder-test]=../libcrypto.a
- SOURCE[hashtable-test]=hashtable.c test-corpus.c fuzz_rand.c
- INCLUDE[hashtable-test]=../include
+ SOURCE[hashtable-test]=hashtable.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[hashtable-test]=../include ../test/mfail
DEPEND[hashtable-test]=../libcrypto.a
- SOURCE[acert-test]=acert.c test-corpus.c
- INCLUDE[acert-test]=../include
- DEPEND[acert-test]=../libcrypto
+ SOURCE[acert-test]=acert.c $FUZZTESTSRC
+ INCLUDE[acert-test]=../include ../test/mfail
+ DEPEND[acert-test]=../libcrypto.a
- SOURCE[punycode-test]=punycode.c test-corpus.c
- INCLUDE[punycode-test]=../include
+ SOURCE[punycode-test]=punycode.c $FUZZTESTSRC
+ INCLUDE[punycode-test]=../include ../test/mfail
DEPEND[punycode-test]=../libcrypto.a
- SOURCE[smime-test]=smime.c test-corpus.c
- INCLUDE[smime-test]=../include
- DEPEND[smime-test]=../libcrypto ../libssl
+ SOURCE[smime-test]=smime.c $FUZZTESTSRC
+ INCLUDE[smime-test]=../include ../test/mfail
+ DEPEND[smime-test]=../libcrypto.a ../libssl.a
- SOURCE[v3name-test]=v3name.c test-corpus.c
- INCLUDE[v3name-test]=../include
+ SOURCE[v3name-test]=v3name.c $FUZZTESTSRC
+ INCLUDE[v3name-test]=../include ../test/mfail
DEPEND[v3name-test]=../libcrypto.a
- SOURCE[quic-client-test]=quic-client.c test-corpus.c fuzz_rand.c
- INCLUDE[quic-client-test]=../include
+ SOURCE[quic-client-test]=quic-client.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[quic-client-test]=../include ../test/mfail
DEPEND[quic-client-test]=../libcrypto.a ../libssl.a
- SOURCE[quic-server-test]=quic-server.c test-corpus.c fuzz_rand.c
- INCLUDE[quic-server-test]=../include
+ SOURCE[quic-server-test]=quic-server.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[quic-server-test]=../include ../test/mfail
DEPEND[quic-server-test]=../libcrypto.a ../libssl.a
- SOURCE[quic-srtm-test]=quic-srtm.c test-corpus.c fuzz_rand.c
- INCLUDE[quic-srtm-test]=../include
+ SOURCE[quic-srtm-test]=quic-srtm.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[quic-srtm-test]=../include ../test/mfail
DEPEND[quic-srtm-test]=../libcrypto.a ../libssl.a
- SOURCE[quic-lcidm-test]=quic-lcidm.c test-corpus.c fuzz_rand.c
- INCLUDE[quic-lcidm-test]=../include
+ SOURCE[quic-lcidm-test]=quic-lcidm.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[quic-lcidm-test]=../include ../test/mfail
DEPEND[quic-lcidm-test]=../libcrypto.a ../libssl.a
- SOURCE[quic-rcidm-test]=quic-rcidm.c test-corpus.c fuzz_rand.c
- INCLUDE[quic-rcidm-test]=../include
+ SOURCE[quic-rcidm-test]=quic-rcidm.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[quic-rcidm-test]=../include ../test/mfail
DEPEND[quic-rcidm-test]=../libcrypto.a ../libssl.a
- SOURCE[server-test]=server.c test-corpus.c fuzz_rand.c
- INCLUDE[server-test]=../include
- DEPEND[server-test]=../libcrypto ../libssl
+ SOURCE[server-test]=server.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[server-test]=../include ../test/mfail
+ DEPEND[server-test]=../libcrypto.a ../libssl.a
- SOURCE[x509-test]=x509.c test-corpus.c fuzz_rand.c
- INCLUDE[x509-test]=../include
- DEPEND[x509-test]=../libcrypto
+ SOURCE[x509-test]=x509.c $FUZZTESTSRC fuzz_rand.c
+ INCLUDE[x509-test]=../include ../test/mfail
+ DEPEND[x509-test]=../libcrypto.a
- SOURCE[provider-test]=provider.c test-corpus.c
- INCLUDE[provider-test]=../include
- DEPEND[provider-test]=../libcrypto
+ SOURCE[provider-test]=provider.c $FUZZTESTSRC
+ INCLUDE[provider-test]=../include ../test/mfail
+ DEPEND[provider-test]=../libcrypto.a
ENDIF
diff --git a/fuzz/test-corpus.c b/fuzz/test-corpus.c
index 783df1a076..a908f69644 100644
--- a/fuzz/test-corpus.c
+++ b/fuzz/test-corpus.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2020 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.
@@ -17,10 +17,13 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <time.h>
#include <sys/stat.h>
#include <openssl/crypto.h>
+#include <openssl/err.h>
#include "fuzzer.h"
#include "internal/o_dir.h"
+#include "mfail.h"
#if defined(_WIN32) && defined(_MAX_PATH) && !defined(PATH_MAX)
#define PATH_MAX _MAX_PATH
@@ -34,7 +37,45 @@
#define S_ISREG(m) ((m) & S_IFREG)
#endif
-static void testfile(const char *pathname)
+static double secs_since(clock_t start)
+{
+ return (double)(clock() - start) / CLOCKS_PER_SEC;
+}
+
+static void run_baseline(const unsigned char *buf, size_t s)
+{
+ FuzzerTestOneInput(buf, s);
+}
+
+static void run_mfail(const unsigned char *buf, size_t s,
+ const char *path, int file_idx)
+{
+ mfail_init(file_idx, MFAIL_FLAG_COUNT);
+ while (mfail_has_next()) {
+ if (mfail_get_phase() == MFAIL_PHASE_COUNTING)
+ fprintf(stderr,
+ "# MFAIL_BEGIN file_idx=%d phase=count\n", file_idx);
+ else
+ fprintf(stderr,
+ "# MFAIL_BEGIN file_idx=%d point=%d/%d\n",
+ file_idx, mfail_get_point(), mfail_get_total());
+
+ mfail_start();
+ FuzzerTestOneInput(buf, s);
+ mfail_end();
+
+ if (mfail_get_phase() == MFAIL_PHASE_COUNTING) {
+ fprintf(stderr, "# %s: %d allocations\n", path, mfail_get_count());
+ } else {
+ fprintf(stderr, "# %s: point %d/%d %s\n", path,
+ mfail_get_point(), mfail_get_total(),
+ mfail_was_triggered() ? "hit" : "unreached");
+ }
+ ERR_clear_error();
+ }
+}
+
+static void testfile(const char *pathname, int file_idx)
{
struct stat st;
FILE *f;
@@ -43,26 +84,52 @@ static void testfile(const char *pathname)
if (stat(pathname, &st) < 0 || !S_ISREG(st.st_mode))
return;
- printf("# %s\n", pathname);
- fflush(stdout);
+
+ fprintf(stderr, "# CORPUS_FILE file_idx=%d size=%lld path=%s\n",
+ file_idx, (long long)st.st_size, pathname);
f = fopen(pathname, "rb");
if (f == NULL)
return;
buf = malloc(st.st_size);
- if (buf != NULL) {
- s = fread(buf, 1, st.st_size, f);
- OPENSSL_assert(s == (size_t)st.st_size);
- FuzzerTestOneInput(buf, s);
- free(buf);
+ if (buf == NULL) {
+ fclose(f);
+ return;
}
+ s = fread(buf, 1, st.st_size, f);
+ OPENSSL_assert(s == (size_t)st.st_size);
+
+ if (mfail_is_installed())
+ run_mfail(buf, s, pathname, file_idx);
+ else
+ run_baseline(buf, s);
+
+ free(buf);
fclose(f);
}
int main(int argc, char **argv)
{
- int n;
+ int n, mfi_rc;
+ int file_idx = 0;
+ clock_t corpus_start;
- FuzzerInitialize(&argc, &argv);
+ mfi_rc = mfail_install(1);
+ if (mfi_rc < 0) {
+ fprintf(stderr, "mfail: failed to install allocator hooks\n");
+ return 1;
+ } else if (mfi_rc > 0) {
+ /* Disable buffering for better crash analysis */
+ setvbuf(stdout, NULL, _IOLBF, 0);
+ setvbuf(stderr, NULL, _IOLBF, 0);
+ }
+
+ if (FuzzerInitialize(&argc, &argv) < 0) {
+ if (mfail_is_installed())
+ return 0; /* init failure under mfail is expected */
+ return 1;
+ }
+
+ corpus_start = clock();
for (n = 1; n < argc; ++n) {
size_t dirname_len = strlen(argv[n]);
@@ -88,18 +155,20 @@ int main(int argc, char **argv)
pathname[dirname_len] = '\0';
}
strcpy(pathname + dirname_len, filename);
- testfile(pathname);
+ testfile(pathname, file_idx++);
}
OPENSSL_DIR_end(&ctx);
/* If it wasn't a directory, treat it as a file instead */
if (!wasdir)
- testfile(argv[n]);
+ testfile(argv[n], file_idx++);
free(pathname);
}
- FuzzerCleanup();
+ if (!mfail_is_installed() || mfail_is_count_only())
+ fprintf(stderr, "# corpus_time: %.6f\n", secs_since(corpus_start));
+ FuzzerCleanup();
return 0;
}
diff --git a/test/recipes/fuzz.pl b/test/recipes/fuzz.pl
index 3f03eef4f7..e5182a362c 100644
--- a/test/recipes/fuzz.pl
+++ b/test/recipes/fuzz.pl
@@ -1,4 +1,4 @@
-# Copyright 2016-2020 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
@@ -8,18 +8,295 @@
use strict;
use warnings;
+use Cwd qw/abs_path/;
+
use OpenSSL::Glob;
-use OpenSSL::Test qw/:DEFAULT srctop_dir/;
+use OpenSSL::Test qw/:DEFAULT srctop_dir bldtop_file result_file/;
-sub fuzz_ok {
- die "Only one argument accepted" if scalar @_ != 1;
+# print logs
+sub fuzz_dump_log {
+ my ($log) = @_;
+ return unless $ENV{HARNESS_VERBOSE} && open(my $fh, '<', $log);
+ print STDERR <$fh>;
+ close $fh;
+}
+
+# execute test and print backtrace
+sub fuzz_print_backtrace {
+ my ($f, $path, $point) = @_;
+ my $bt_log = result_file("$f-backtrace.stderr.log");
+ local $ENV{OPENSSL_TEST_MFAIL_BACKTRACE} = 1;
+ local $ENV{OPENSSL_TEST_MFAIL_POINT} = $point if defined $point;
+ run(fuzz(["$f-test", $path], stderr => $bt_log));
+
+ diag("- backtrace at injection point:");
+ if (open(my $fh, '<', $bt_log)) {
+ while (my $line = <$fh>) {
+ chomp $line;
+ diag(" $line");
+ }
+ close $fh;
+ }
+}
+
+# check fuzz test logs
+sub fuzz_check_log {
+ my ($f, $log, $exit_ok, $silent_leak) = @_;
+
+ my $has_leaks = 0;
+ my $corpus_time = 0;
+ my @tail;
+ my ($last_path, $last_point);
+
+ # collect info from the executed fuzz log
+ if (open(my $fh, '<', $log)) {
+ while (my $line = <$fh>) {
+ chomp $line;
+ if ($line =~ /^#\s/) {
+ $corpus_time = $1
+ if $line =~ /^#\s*corpus_time:\s*([\d.]+)/;
+ $last_path = $1 if $line =~ /\bpath=(.+?)\s*$/;
+ $last_point = $1 if $line =~ /\bpoint=(\d+)\//;
+ @tail = ($line);
+ } else {
+ push @tail, $line;
+ $has_leaks = 1
+ if $line =~ /^(?:Direct|Indirect) leak of \d+ byte/;
+ }
+ }
+ close $fh;
+ }
+
+ # return if the test passed and there is no leak
+ return (1, 0, $corpus_time) if $exit_ok && !$has_leaks;
+ # return if there is a leak but it's silent (should not be reported)
+ return (0, $has_leaks, $corpus_time) if $has_leaks && $silent_leak;
+
+ # log leak info
+ my $why = !$exit_ok ? "non-zero exit" : "leaks (clean exit)";
+ diag("fuzz $f failed: $why");
+ diag("- full stderr: $log");
+ if (defined $last_path) {
+ my $bin = abs_path(bldtop_file('fuzz', "$f-test"));
+ my $path = abs_path($last_path);
+ my $env = defined $last_point
+ ? "OPENSSL_TEST_MFAIL_POINT=$last_point " : "";
+ diag("- reproduce: $env$bin $path");
+ }
+ diag($_) for @tail;
+ fuzz_print_backtrace($f, $last_path, $last_point)
+ if defined $last_path && !$has_leaks;
+ return (0, $has_leaks, $corpus_time);
+}
+
+# run fuzz test and dump logs
+sub fuzz_run {
+ my ($f, $d, $log_name, $silent_leak) = @_;
+ my $log = result_file($log_name);
+ my $exit_ok = run(fuzz(["$f-test", $d], stderr => $log));
+ fuzz_dump_log($log);
+ my ($passed, $leaks, $corpus_time) = fuzz_check_log($f, $log, $exit_ok,
+ $silent_leak);
+ return ($passed, $leaks, $corpus_time, $log);
+}
+
+# run fuzz test in counting mode and no fail run
+sub fuzz_run_count_only {
+ my ($f, $d) = @_;
+ my $log = result_file("$f-count.stderr.log");
+ local $ENV{OPENSSL_TEST_MFAIL_COUNT_ONLY} = 1;
+ my $exit_ok = run(fuzz(["$f-test", $d], stderr => $log));
+ fuzz_dump_log($log);
+
+ my ($corpus_time, $cur, @allocs) = (0, undef);
+ if (open(my $fh, '<', $log)) {
+ while (my $line = <$fh>) {
+ $corpus_time = $1 if $line =~ /^#\s*corpus_time:\s*([\d.]+)/;
+ $cur = $1 if $line =~ /^#\s*CORPUS_FILE\s+file_idx=(\d+)/;
+ push @allocs, $1 + 0 if defined $cur
+ && $line =~ /:\s*(\d+)\s+allocations\s*$/;
+ }
+ close $fh;
+ }
+ return ($exit_ok, $corpus_time, \@allocs);
+}
+
+# find path and point of the reported leak for easy recreation
+sub fuzz_mfail_bisect {
+ my ($f, $log) = @_;
+
+ # collect all paths and executed points from the output
+ my (%path, %points);
+ if (open(my $fh, '<', $log)) {
+ while (my $line = <$fh>) {
+ $path{$1} = $2
+ if $line =~
+ /CORPUS_FILE\s+file_idx=(\d+)\s+size=\d+\s+path=(\S+)/;
+ push @{$points{$1}}, $2
+ if $line =~ /MFAIL_BEGIN\s+file_idx=(\d+)\s+point=(\d+)\/\d+/;
+ }
+ close $fh;
+ }
+
+ # reset current envs so they don't get used in bisect run
+ delete local $ENV{OPENSSL_TEST_MFAIL_COUNT};
+ delete local $ENV{OPENSSL_TEST_MFAIL_START};
+ delete local $ENV{OPENSSL_TEST_MFAIL_POINT};
+
+ diag("bisecting mfail leak across isolated point reruns");
+
+ # go through all executed corpus files
+ for my $idx (sort { $a <=> $b } keys %path) {
+ # go trhout all executed points in path
+ for my $p (@{$points{$idx} || []}) {
+ local $ENV{OPENSSL_TEST_MFAIL_POINT} = $p;
+ my $plog = result_file("$f-bisect-$idx-$p.stderr.log");
+ my $exit_ok = run(fuzz(["$f-test", $path{$idx}], stderr => $plog),
+ quiet => 1);
+ # silently skip runs without a leak
+ my (undef, $leaks) = fuzz_check_log($f, $plog, $exit_ok, 1);
+ next unless $leaks;
+ # report exact leak location
+ my $bin = abs_path(bldtop_file('fuzz', "$f-test"));
+ my $abs = abs_path($path{$idx});
+ diag("isolated leak: file_idx=$idx point=$p path=$path{$idx}");
+ diag("- log: $plog");
+ diag("- reproduce: OPENSSL_TEST_MFAIL_POINT=$p $bin $abs");
+ fuzz_print_backtrace($f, $path{$idx}, $p);
+ return;
+ }
+ }
+ diag("bisection did not reproduce the leak");
+}
- my $f = $_[0];
+# get all test_fuzz tests calling this
+sub fuzz_test_names {
+ my @names;
+ for my $p (glob(srctop_dir('test', 'recipes') . '/[0-9][0-9]-test_fuzz_*.t')) {
+ # push the actual name of the test used in TESTS filtering
+ push @names, $1 if $p =~ m{/\d+-(test_fuzz_\S+)\.t$};
+ }
+ return @names;
+}
+
+# match a test name against TESTS env filter (test/run_tests.pl semantics)
+sub fuzz_match_tests_filter {
+ my ($name, $filter) = @_;
+ return 1 unless defined $filter && $filter ne '';
+
+ my @pats = grep { length } split /\s+/, $filter;
+ return 1 unless @pats;
+
+ # a leading negative implies a starting "alltests"
+ my $included = $pats[0] =~ /^-/ ? 1 : 0;
+
+ for my $pat (@pats) {
+ # alltests resets the set to all, ignoring everything before
+ if ($pat eq 'alltests') {
+ $included = 1;
+ next;
+ }
+
+ my $neg = $pat =~ s/^-//;
+
+ # glob -> regex
+ (my $re = quotemeta $pat) =~ s/\\\*/.*/g;
+ $re =~ s/\\\?/./g;
+ next unless $name =~ /\A$re\z/;
+
+ $included = $neg ? 0 : 1;
+ }
+
+ return $included;
+}
+
+# get budget per test
+sub fuzz_per_test_budget {
+ my $budget = $ENV{OSSL_FUZZ_TEST_BUDGET} or return 0;
+ my $jobs = $ENV{OSSL_FUZZ_TEST_JOBS} || 1;
+ my $filter = $ENV{TESTS};
+
+ my @active = grep { fuzz_match_tests_filter($_, $filter) } fuzz_test_names();
+ my $count = scalar(@active) || 1;
+
+ # we don't need all jobs if there are less tests
+ $jobs = $count if $jobs > $count;
+
+ my $per_test = $budget * $jobs / $count;
+ diag(sprintf("budget=%ss jobs=%d active=%d -> per-test=%.3fs",
+ $budget, $jobs, $count, $per_test));
+ return $per_test;
+}
+
+sub fuzz_ok {
+ my ($f, %opts) = @_;
my $d = srctop_dir('fuzz', 'corpora', $f);
SKIP: {
skip "No directory $d", 1 unless -d $d;
- ok(run(fuzz(["$f-test", $d])), "Fuzzing $f");
+
+ my $per_test = fuzz_per_test_budget();
+ my $safety = 0.8;
+ my $target = $per_test * $safety;
+
+ # no budget configured, just run the corpus
+ unless ($per_test > 0) {
+ ok(run(fuzz(["$f-test", $d])), "Fuzzing $f");
+ return;
+ }
+
+ # baseline run to measure the corpus run time
+ my ($ok, $corpus_time, $allocs) = fuzz_run_count_only($f, $d);
+ unless ($ok) {
+ ok(0, "Fuzzing $f (count-only)");
+ return;
+ }
+
+ # get the maximum allocations in instance and count total
+ my $total_allocs = 0;
+ my $max_k = 0;
+ for (@$allocs) {
+ $total_allocs += $_;
+ $max_k = $_ if $_ > $max_k;
+ }
+ my $num_files = scalar @$allocs;
+ diag(sprintf("%s: count-only %.3fs, allocs=%d, files=%d, max=%d",
+ $f, $corpus_time, $total_allocs, $num_files, $max_k));
+
+ # baseline alone consumed the budget, nothing left for mfail
+ if ($corpus_time <= 0 || $corpus_time >= $target) {
+ ok(1, "Fuzzing $f (no mfail budget; "
+ . "corpus=${corpus_time}s, target=${target}s)");
+ return;
+ }
+
+ # no allocations counted, can't size the mfail run
+ if ($total_allocs <= 0 || $num_files <= 0) {
+ ok(1, "Fuzzing $f (no allocations counted)");
+ return;
+ }
+
+ # number of mfail iterations that fit alongside the baseline:
+ # ~corpus_time * (1 + count / 2) <= target
+ my $count = int(2 * ($target - $corpus_time) / $corpus_time);
+ # never exceed max(K_i); injections beyond that are wasted
+ $count = $max_k if $count > $max_k;
+ if ($count <= 0) {
+ ok(1, "Fuzzing $f (budget too small for mfail)");
+ return;
+ }
+ diag("$f: running mfail with count=$count");
+
+ local $ENV{OPENSSL_TEST_MFAIL_COUNT} = $count;
+ my $main_log = "$f-mfail.stderr.log";
+ my ($passed, $leaks, undef, $log) = fuzz_run($f, $d, $main_log, 1);
+
+ unless ($passed) {
+ fuzz_mfail_bisect($f, $log) if $leaks;
+ ok(0, "Fuzzing $f (mfail count=$count, per-test=${per_test}s)");
+ return;
+ }
+ ok(1, "Fuzzing $f (mfail count=$count, per-test=${per_test}s)");
}
}
diff --git a/util/perl/OpenSSL/Test.pm b/util/perl/OpenSSL/Test.pm
index 369d4f2b9a..0cdb41259c 100644
--- a/util/perl/OpenSSL/Test.pm
+++ b/util/perl/OpenSSL/Test.pm
@@ -448,6 +448,14 @@ If used, B<VARREF> must be a reference to a scalar variable. It will be
assigned a boolean indicating if the command succeeded or not. This is
particularly useful together with B<capture>.
+=item B<quiet =E<gt> 0|1>
+
+If true, suppress the diagnostic line that C<run> normally prints to STDERR
+after the command completes (the C<command => exitcode> line) when running
+under a non-verbose test harness. Has no effect outside a harness or under
+C<HARNESS_VERBOSE>. Useful for loops that invoke many commands where that
+line would be noise.
+
=back
Usually 1 indicates that the command was successful and 0 indicates failure.
@@ -522,19 +530,20 @@ sub run {
${$opts{statusvar}} = $r;
}
- # Restore STDOUT / STDERR on VMS
+ my $harness_quiet = $ENV{HARNESS_ACTIVE} && !$ENV{HARNESS_VERBOSE};
if ($^O eq 'VMS') {
- if ($ENV{HARNESS_ACTIVE} && !$ENV{HARNESS_VERBOSE}) {
+ # Restore STDOUT / STDERR on VMS
+ if ($harness_quiet) {
close STDOUT;
close STDERR;
open STDOUT, '>&', $save_STDOUT or die "Can't restore STDOUT: $!";
open STDERR, '>&', $save_STDERR or die "Can't restore STDERR: $!";
}
- print STDERR "$prefix$display_cmd => $e\n"
- if !$ENV{HARNESS_ACTIVE} || $ENV{HARNESS_VERBOSE};
+ print STDERR "$prefix$display_cmd => $e\n" unless $harness_quiet;
} else {
- print STDERR "$prefix$display_cmd => $e\n";
+ print STDERR "$prefix$display_cmd => $e\n"
+ unless $opts{quiet} && $harness_quiet;
}
# At this point, $? stops being interesting, and unfortunately,