From: Jakub Zelenka Date: Tue, 28 Apr 2026 17:22:46 +0000 (+0200) Subject: Integrate mfail functionality to fuzz tests X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=dca561d4cdb84c7e813e9b28352928edd5b2cfd8;p=thirdparty%2Fopenssl.git 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 Reviewed-by: Tomas Mraz 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 b10ab496c50..597501112fb 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 3770c2df7e3..5c410260e4c 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 783df1a0768..a908f69644c 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 #include #include +#include #include #include +#include #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 3f03eef4f72..e5182a362ca 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 369d4f2b9a7..0cdb41259c9 100644 --- a/util/perl/OpenSSL/Test.pm +++ b/util/perl/OpenSSL/Test.pm @@ -448,6 +448,14 @@ If used, B 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. +=item B 0|1> + +If true, suppress the diagnostic line that C normally prints to STDERR +after the command completes (the C exitcode> line) when running +under a non-verbose test harness. Has no effect outside a harness or under +C. 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,