Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 73 additions & 24 deletions contrib/devtools/deterministic-fuzz-coverage/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://opensource.org/license/mit/.

use std::collections::VecDeque;
use std::env;
use std::fs::{read_dir, File};
use std::fs::{read_dir, DirEntry, File};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::str;
use std::thread;

/// A type for a complete and readable error message.
type AppError = String;
Expand All @@ -16,12 +18,14 @@ const LLVM_PROFDATA: &str = "llvm-profdata";
const LLVM_COV: &str = "llvm-cov";
const GIT: &str = "git";

const DEFAULT_PAR: usize = 1;

fn exit_help(err: &str) -> AppError {
format!(
r#"
Error: {err}

Usage: program ./build_dir ./qa-assets/fuzz_corpora fuzz_target_name
Usage: program ./build_dir ./qa-assets/fuzz_corpora fuzz_target_name [parallelism={DEFAULT_PAR}]

Refer to the devtools/README.md for more details."#
)
Expand Down Expand Up @@ -63,7 +67,14 @@ fn app() -> AppResult {
// Require fuzz target for now. In the future it could be optional and the tool could
// iterate over all compiled fuzz targets
.ok_or(exit_help("Must set fuzz target"))?;
if args.get(4).is_some() {
let par = match args.get(4) {
Some(s) => s
.parse::<usize>()
.map_err(|e| exit_help(&format!("Could not parse parallelism as usize ({s}): {e}")))?,
None => DEFAULT_PAR,
}
.max(1);
if args.get(5).is_some() {
Err(exit_help("Too many args"))?;
}

Expand All @@ -73,7 +84,7 @@ fn app() -> AppResult {

sanity_check(corpora_dir, &fuzz_exe)?;

deterministic_coverage(build_dir, corpora_dir, &fuzz_exe, fuzz_target)
deterministic_coverage(build_dir, corpora_dir, &fuzz_exe, fuzz_target, par)
}

fn using_libfuzzer(fuzz_exe: &Path) -> Result<bool, AppError> {
Expand All @@ -94,10 +105,14 @@ fn deterministic_coverage(
corpora_dir: &Path,
fuzz_exe: &Path,
fuzz_target: &str,
par: usize,
) -> AppResult {
let using_libfuzzer = using_libfuzzer(fuzz_exe)?;
let profraw_file = build_dir.join("fuzz_det_cov.profraw");
let profdata_file = build_dir.join("fuzz_det_cov.profdata");
if using_libfuzzer {
println!("Warning: The fuzz executable was compiled with libFuzzer as sanitizer.");
println!("This tool may be tripped by libFuzzer misbehavior.");
println!("It is recommended to compile without libFuzzer.");
}
Comment on lines +111 to +115
Copy link
Copy Markdown
Contributor

@hodlinator hodlinator Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would be nice to get

Check if using libFuzzer ... NO

For the negative case. Right now one only gets the ... and then just the omission of this warning.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May do if I have to re-touch for other reasons.

let corpus_dir = corpora_dir.join(fuzz_target);
let mut entries = read_dir(&corpus_dir)
.map_err(|err| {
Expand All @@ -110,10 +125,12 @@ fn deterministic_coverage(
.map(|entry| entry.expect("IO error"))
.collect::<Vec<_>>();
entries.sort_by_key(|entry| entry.file_name());
let run_single = |run_id: u8, entry: &Path| -> Result<PathBuf, AppError> {
let cov_txt_path = build_dir.join(format!("fuzz_det_cov.show.{run_id}.txt"));
if !{
{
let run_single = |run_id: char, entry: &Path, thread_id: usize| -> Result<PathBuf, AppError> {
let cov_txt_path = build_dir.join(format!("fuzz_det_cov.show.t{thread_id}.{run_id}.txt"));
let profraw_file = build_dir.join(format!("fuzz_det_cov.t{thread_id}.{run_id}.profraw"));
let profdata_file = build_dir.join(format!("fuzz_det_cov.t{thread_id}.{run_id}.profdata"));
{
let output = {
let mut cmd = Command::new(fuzz_exe);
if using_libfuzzer {
cmd.arg("-runs=1");
Expand All @@ -123,11 +140,15 @@ fn deterministic_coverage(
.env("LLVM_PROFILE_FILE", &profraw_file)
.env("FUZZ", fuzz_target)
.arg(entry)
.status()
.map_err(|e| format!("fuzz failed with {e}"))?
.success()
} {
Err("fuzz failed".to_string())?;
.output()
.map_err(|e| format!("fuzz failed: {e}"))?;
if !output.status.success() {
Err(format!(
"fuzz failed!\nstdout:\n{}\nstderr:\n{}\n",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
))?;
}
}
if !Command::new(LLVM_PROFDATA)
.arg("merge")
Expand All @@ -149,6 +170,8 @@ fn deterministic_coverage(
"--show-line-counts-or-regions",
"--show-branches=count",
"--show-expansions",
"--show-instantiation-summary",
"-Xdemangler=llvm-cxxfilt",
&format!("--instr-profile={}", profdata_file.display()),
])
.arg(fuzz_exe)
Expand Down Expand Up @@ -187,20 +210,46 @@ The coverage was not deterministic between runs.
//
// Also, This can catch issues where several fuzz inputs are non-deterministic, but the sum of
// their overall coverage trace remains the same across runs and thus remains undetected.
println!("Check each fuzz input individually ...");
for entry in entries {
println!(
"Check each fuzz input individually ... ({} inputs with parallelism {par})",
entries.len()
);
let check_individual = |entry: &DirEntry, thread_id: usize| -> AppResult {
let entry = entry.path();
if !entry.is_file() {
Err(format!("{} should be a file", entry.display()))?;
}
let cov_txt_base = run_single(0, &entry)?;
let cov_txt_repeat = run_single(1, &entry)?;
let cov_txt_base = run_single('a', &entry, thread_id)?;
let cov_txt_repeat = run_single('b', &entry, thread_id)?;
check_diff(
&cov_txt_base,
&cov_txt_repeat,
&format!("The fuzz target input was {}.", entry.display()),
)?;
}
Ok(())
};
thread::scope(|s| -> AppResult {
let mut handles = VecDeque::with_capacity(par);
let mut res = Ok(());
for (i, entry) in entries.iter().enumerate() {
println!("[{}/{}]", i + 1, entries.len());
handles.push_back(s.spawn(move || check_individual(entry, i % par)));
while handles.len() >= par || i == (entries.len() - 1) || res.is_err() {
Comment thread
maflcko marked this conversation as resolved.
Outdated
if let Some(th) = handles.pop_front() {
let thread_result = match th.join() {
Err(_e) => Err("A scoped thread panicked".to_string()),
Ok(r) => r,
};
if thread_result.is_err() {
res = thread_result;
}
} else {
return res;
}
}
}
res
})?;
// Finally, check that running over all fuzz inputs in one process is deterministic as well.
// This can catch issues where mutable global state is leaked from one fuzz input execution to
// the next.
Expand All @@ -209,23 +258,23 @@ The coverage was not deterministic between runs.
if !corpus_dir.is_dir() {
Err(format!("{} should be a folder", corpus_dir.display()))?;
}
let cov_txt_base = run_single(0, &corpus_dir)?;
let cov_txt_repeat = run_single(1, &corpus_dir)?;
let cov_txt_base = run_single('a', &corpus_dir, 0)?;
let cov_txt_repeat = run_single('b', &corpus_dir, 0)?;
check_diff(
&cov_txt_base,
&cov_txt_repeat,
&format!("All fuzz inputs in {} were used.", corpus_dir.display()),
)?;
}
println!("Coverage test passed for {fuzz_target}.");
println!("Coverage test passed for {fuzz_target}.");
Ok(())
}

fn main() -> ExitCode {
match app() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{}", err);
eprintln!("⚠️\n{}", err);
ExitCode::FAILURE
}
}
Expand Down
12 changes: 7 additions & 5 deletions contrib/devtools/deterministic-unittest-coverage/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fn app() -> AppResult {
fn deterministic_coverage(build_dir: &Path, test_exe: &Path, filter: &str) -> AppResult {
let profraw_file = build_dir.join("test_det_cov.profraw");
let profdata_file = build_dir.join("test_det_cov.profdata");
let run_single = |run_id: u8| -> Result<PathBuf, AppError> {
let run_single = |run_id: char| -> Result<PathBuf, AppError> {
println!("Run with id {run_id}");
let cov_txt_path = build_dir.join(format!("test_det_cov.show.{run_id}.txt"));
if !Command::new(test_exe)
Expand Down Expand Up @@ -104,6 +104,8 @@ fn deterministic_coverage(build_dir: &Path, test_exe: &Path, filter: &str) -> Ap
"--show-line-counts-or-regions",
"--show-branches=count",
"--show-expansions",
"--show-instantiation-summary",
"-Xdemangler=llvm-cxxfilt",
&format!("--instr-profile={}", profdata_file.display()),
])
.arg(test_exe)
Expand All @@ -129,18 +131,18 @@ fn deterministic_coverage(build_dir: &Path, test_exe: &Path, filter: &str) -> Ap
}
Ok(())
};
let r0 = run_single(0)?;
let r1 = run_single(1)?;
let r0 = run_single('a')?;
let r1 = run_single('b')?;
check_diff(&r0, &r1)?;
println!("The coverage was deterministic across two runs.");
println!("The coverage was deterministic across two runs.");
Ok(())
}

fn main() -> ExitCode {
match app() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{}", err);
eprintln!("⚠️\n{}", err);
ExitCode::FAILURE
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/test/util/setup_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
#include <walletinitinterface.h>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Inline comment thread to not spam main)

What is your clang version and do you mind sharing the cmake configure command and the full diff? I just used the tip of qa-assets.

₿ cmake -B build_fuzz
CMake Warning at CMakeLists.txt:216 (message):
  BUILD_FOR_FUZZING=ON will disable all other targets and force
  BUILD_FUZZ_BINARY=ON.



Configuring secp256k1 subtree...
-- Could NOT find Valgrind (missing: Valgrind_INCLUDE_DIR Valgrind_WORKS) 


secp256k1 configure summary
===========================
Build artifacts:
  library type ........................ Static
Optional modules:
  ECDH ................................ OFF
  ECDSA pubkey recovery ............... ON
  extrakeys ........................... ON
  schnorrsig .......................... ON
  musig ............................... OFF
  ElligatorSwift ...................... ON
Parameters:
  ecmult window size .................. 15
  ecmult gen table size ............... 86 KiB
Optional features:
  assembly ............................ x86_64
  external callbacks .................. OFF
Optional binaries:
  benchmark ........................... OFF
  noverify_tests ...................... OFF
  tests ............................... OFF
  exhaustive tests .................... OFF
  ctime_tests ......................... OFF
  examples ............................ OFF

Cross compiling ....................... FALSE
Valgrind .............................. OFF
Preprocessor defined macros ........... ENABLE_MODULE_ELLSWIFT=1 ENABLE_MODULE_SCHNORRSIG=1 ENABLE_MODULE_EXTRAKEYS=1 ENABLE_MODULE_RECOVERY=1 ECMULT_WINDOW_SIZE=15 COMB_BLOCKS=43 COMB_TEETH=6 USE_ASM_X86_64=1
C compiler ............................ Clang 19.1.7, /nix/store/ls0g67nsklb2vn3vc9dnksa1adfgq32a-clang-wrapper-19.1.7/bin/clang
CFLAGS ................................ -ftrivial-auto-var-init=pattern
Compile options ....................... -pedantic -Wall -Wcast-align -Wconditional-uninitialized -Wextra -Wnested-externs -Wno-long-long -Wno-overlength-strings -Wno-unused-function -Wreserved-identifier -Wshadow -Wstrict-prototypes -Wundef
Build type:
 - CMAKE_BUILD_TYPE ................... RelWithDebInfo
 - CFLAGS ............................. -O2 -g 
 - LDFLAGS for executables ............ 
 - LDFLAGS for shared libraries ....... 
SECP256K1_APPEND_CFLAGS ............... -fsanitize=undefined,address,fuzzer
SECP256K1_APPEND_LDFLAGS .............. -fsanitize=undefined,address,fuzzer



Configure summary
=================
Executables:
  bitcoind ............................ OFF
  bitcoin-node (multiprocess) ......... OFF
  bitcoin-qt (GUI) .................... OFF
  bitcoin-gui (GUI, multiprocess) ..... OFF
  bitcoin-cli ......................... OFF
  bitcoin-tx .......................... OFF
  bitcoin-util ........................ OFF
  bitcoin-wallet ...................... OFF
  bitcoin-chainstate (experimental) ... OFF
  libbitcoinkernel (experimental) ..... OFF
Optional features:
  wallet support ...................... ON
   - legacy wallets (Berkeley DB) ..... OFF
  external signer ..................... OFF
  ZeroMQ .............................. OFF
  USDT tracing ........................ OFF
  QR code (GUI) ....................... OFF
  DBus (GUI, Linux only) .............. OFF
Tests:
  test_bitcoin ........................ OFF
  test_bitcoin-qt ..................... OFF
  bench_bitcoin ....................... OFF
  fuzz binary ......................... ON

Cross compiling ....................... FALSE
C++ compiler .......................... Clang 19.1.7, /nix/store/ls0g67nsklb2vn3vc9dnksa1adfgq32a-clang-wrapper-19.1.7/bin/clang++
CMAKE_BUILD_TYPE ...................... RelWithDebInfo
Preprocessor defined macros ........... FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
C++ compiler flags .................... -fprofile-instr-generate -fcoverage-mapping -O2 -g -std=c++20 -fPIC -fdebug-prefix-map=/home/hodlinator/bitcoin/src=. -fmacro-prefix-map=/home/hodlinator/bitcoin/src=. -fsanitize=undefined,address,fuzzer -Wall -Wextra -Wgnu -Wformat -Wformat-security -Wvla -Wshadow-field -Wthread-safety -Wloop-analysis -Wredundant-decls -Wunused-member-function -Wdate-time -Wconditional-uninitialized -Woverloaded-virtual -Wsuggest-override -Wimplicit-fallthrough -Wunreachable-code -Wdocumentation -Wself-assign -Wundef -Wno-unused-parameter -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstack-protector -fstack-protector-all -fcf-protection=full -fstack-clash-protection
Linker flags .......................... -fprofile-instr-generate -fcoverage-mapping -O2 -g -fsanitize=undefined,address,fuzzer -fstack-protector-all -fcf-protection=full -fstack-clash-protection -Wl,-z,relro -Wl,-z,now -Wl,-z,separate-code -fPIE -pie

NOTE: The summary above may not exactly match the final applied build flags
      if any additional CMAKE_* or environment variables have been modified.
      To see the exact flags applied, build with the --verbose option.

Attempt to harden executables ......... ON
Treat compiler warnings as errors ..... OFF
Use ccache for compiling .............. ON


-- Configuring done (0.4s)
-- Generating done (0.1s)
-- Build files have been written to: /home/hodlinator/bitcoin/build_fuzz

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmake -B build_fuzz

I guess this is an unclean build, because it the command doesn't set the cxx flags, as explained in contrib/devtools/README.md, however they do end up in configure summary, so this may be fine.

Also, this is building with sanitizers, which may or may not have an effect on runtime behavior and thus make the non-determinism manifest differently.

Also, could you please share the full diff --git a/home/hodlinator/bitcoin/build_fuzz/fuzz_det_cov.show.tN.r0.txt b/home/hodlinator/bitcoin/build_fuzz/fuzz_det_cov.show.tN.r1.txt, if you don't mind?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log/diff with above build config: without_fix_400t.gz

Will try with a fresh build directory. Would make sense that sanitizers change behavior.

Copy link
Copy Markdown
Member Author

@maflcko maflcko Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this may be a bug in libFuzzer where an input is run twice, instead of once, when it should only be run once?

It is not visible in the output, as that is now hidden after your suggestion to hide the output, but it could make sense to reject libFuzzer in the script completely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not visible in the output, as that is now hidden after your suggestion to hide the input

Maybe one could return the std::process::Output objects from run_single and pass them into check_diff to be printed upon failure?


First run with fresh build tree and without the C++ thread fix from this PR failed (no determinism-issue detected).

✨ Second run did produce a diff containing both scriptch. and m_service_thread = .... ✨

(Third run again detected no determinism).

✨ Fourth run again produced diff with expected substrings. ✨

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was using > file 2> file and the output is a big garbled: without_fix_400t_libFuzzOutput.gz

Had it output the thread id when printing the fuzz-output (fuzz success thread <thread-id>), so one can correlate with t<thead id> in the diff filenames.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the output, it seems to indicate that the fuzz target ran twice:

    46|       |FUZZ_TARGET(partially_downloaded_block, .init = initialize_pdb)
-   47|      2|{
-   48|      2|    SeedRandomStateForTest(SeedRand::ZEROS);
-   49|      2|    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
-   50|      2|    SetMockTime(ConsumeTime(fuzzed_data_provider));
+   47|      1|{
+   48|      1|    SeedRandomStateForTest(SeedRand::ZEROS);
+   49|      1|    FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
+   50|      1|    SetMockTime(ConsumeTime(fuzzed_data_provider));
    51|       |

Thus, it seems there are two options:

  • There is a bug in the code here and two processes are started by the same thread for the same run-id (or some other bug)
  • There is a bug in libFuzzer and the fuzz target is run twice in the same process (most likely)

If you want to check this further, and if you want to check if the fuzz process itself is at fault, it should be possible by aborting it, if the process runs the target twice. (Also, could mock out the meat of the logic here):

diff --git a/contrib/devtools/deterministic-fuzz-coverage/src/main.rs b/contrib/devtools/deterministic-fuzz-coverage/src/main.rs
index 9c1738396b..d5b7181c80 100644
--- a/contrib/devtools/deterministic-fuzz-coverage/src/main.rs
+++ b/contrib/devtools/deterministic-fuzz-coverage/src/main.rs
@@ -150,40 +150,6 @@ fn deterministic_coverage(
                 ))?;
             }
         }
-        if !Command::new(LLVM_PROFDATA)
-            .arg("merge")
-            .arg("--sparse")
-            .arg(&profraw_file)
-            .arg("-o")
-            .arg(&profdata_file)
-            .status()
-            .map_err(|e| format!("{LLVM_PROFDATA} merge failed with {e}"))?
-            .success()
-        {
-            Err(format!("{LLVM_PROFDATA} merge failed. This can be a sign of compiling without code coverage support."))?;
-        }
-        let cov_file = File::create(&cov_txt_path)
-            .map_err(|e| format!("Failed to create coverage txt file ({e})"))?;
-        if !Command::new(LLVM_COV)
-            .args([
-                "show",
-                "--show-line-counts-or-regions",
-                "--show-branches=count",
-                "--show-expansions",
-                "--show-instantiation-summary",
-                "-Xdemangler=llvm-cxxfilt",
-                &format!("--instr-profile={}", profdata_file.display()),
-            ])
-            .arg(fuzz_exe)
-            .stdout(cov_file)
-            .spawn()
-            .map_err(|e| format!("{LLVM_COV} show failed with {e}"))?
-            .wait()
-            .map_err(|e| format!("{LLVM_COV} show failed with {e}"))?
-            .success()
-        {
-            Err(format!("{LLVM_COV} show failed"))?;
-        };
         Ok(cov_txt_path)
     };
     let check_diff = |a: &Path, b: &Path, err: &str| -> AppResult {
@@ -221,11 +187,6 @@ The coverage was not deterministic between runs.
         }
         let cov_txt_base = run_single('a', &entry, thread_id)?;
         let cov_txt_repeat = run_single('b', &entry, thread_id)?;
-        check_diff(
-            &cov_txt_base,
-            &cov_txt_repeat,
-            &format!("The fuzz target input was {}.", entry.display()),
-        )?;
         Ok(())
     };
     thread::scope(|s| -> AppResult {
@@ -254,18 +215,6 @@ The coverage was not deterministic between runs.
     // This can catch issues where mutable global state is leaked from one fuzz input execution to
     // the next.
     println!("Check all fuzz inputs in one go ...");
-    {
-        if !corpus_dir.is_dir() {
-            Err(format!("{} should be a folder", corpus_dir.display()))?;
-        }
-        let cov_txt_base = run_single('a', &corpus_dir, 0)?;
-        let cov_txt_repeat = run_single('b', &corpus_dir, 0)?;
-        check_diff(
-            &cov_txt_base,
-            &cov_txt_repeat,
-            &format!("All fuzz inputs in {} were used.", corpus_dir.display()),
-        )?;
-    }
     println!("✨ Coverage test passed for {fuzz_target}. ✨");
     Ok(())
 }
diff --git a/src/test/fuzz/partially_downloaded_block.cpp b/src/test/fuzz/partially_downloaded_block.cpp
index 8c6565e48b..466548ee35 100644
--- a/src/test/fuzz/partially_downloaded_block.cpp
+++ b/src/test/fuzz/partially_downloaded_block.cpp
@@ -45,6 +45,9 @@ PartiallyDownloadedBlock::CheckBlockFn FuzzedCheckBlock(std::optional<BlockValid
 
 FUZZ_TARGET(partially_downloaded_block, .init = initialize_pdb)
 {
+static bool g_once{true};
+Assert(g_once);
+g_once=false;
     SeedRandomStateForTest(SeedRand::ZEROS);
     FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
     SetMockTime(ConsumeTime(fuzzed_data_provider));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Result with provided patch:
../src/test/fuzz/partially_downloaded_block.cpp:49 partially_downloaded_block_fuzz_target: Assertion `g_once' failed.

So it's within the same process. libFuzzer doesn't take -runs=1 too seriously. Seems weird that it would repeat when only given 1 input... I guess they don't expect deterministic tests.

Full error
⚠️
fuzz failed!
stdout:

stderr:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 921745561
INFO: Loaded 1 modules   (543062 inline 8-bit counters): 543062 [0x563af8979360, 0x563af89fdcb6), 
INFO: Loaded 1 PC tables (543062 PCs): 543062 [0x563af89fdcb8,0x563af9247218), 
/home/hodlinator/bitcoin/build_fuzz/bin/fuzz: Running 1 inputs 1 time(s) each.
Running: ../qa-assets/fuzz_corpora/partially_downloaded_block/4eccfaddc420749f641d81b122425972c4f5108e
../src/test/fuzz/partially_downloaded_block.cpp:49 partially_downloaded_block_fuzz_target: Assertion `g_once' failed.
==91561== ERROR: libFuzzer: deadly signal
    #0 0x563af549e44a in __sanitizer_print_stack_trace (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x192744a)
    #1 0x563af539cb90 in fuzzer::PrintStackTrace() (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x1825b90)
    #2 0x563af5377d16 in fuzzer::Fuzzer::CrashCallback() (.part.0) FuzzerLoop.cpp.o
    #3 0x563af5377dda in fuzzer::Fuzzer::StaticCrashSignalCallback() (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x1800dda)
    #4 0x7f0d6b440f2f  (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x40f2f) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #5 0x7f0d6b49916b in __pthread_kill_implementation (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x9916b) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #6 0x7f0d6b440e85 in gsignal (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x40e85) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #7 0x7f0d6b428939 in abort (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x28939) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #8 0x563af60b3c74 in assertion_fail(std::basic_string_view<char, std::char_traits<char>>, int, std::basic_string_view<char, std::char_traits<char>>, std::basic_string_view<char, std::char_traits<char>>) /home/hodlinator/bitcoin/build_fuzz/../src/util/check.cpp:34:5
    #9 0x563af5950a77 in bool& inline_assertion_check<true, bool&>(bool&, char const*, int, char const*, char const*) /home/hodlinator/bitcoin/build_fuzz/../src/util/check.h:59:13
    #10 0x563af59db569 in partially_downloaded_block_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>) /home/hodlinator/bitcoin/build_fuzz/../src/test/fuzz/partially_downloaded_block.cpp:49:1
    #11 0x563af5cff4c0 in std::function<void (std::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::span<unsigned char const, 18446744073709551615ul>) const /nix/store/dih8vf5naf93c0wcfxqa9pll3k7iv9bm-gcc-14-20241116/include/c++/14-20241116/bits/std_function.h:591:9
    #12 0x563af5cff4c0 in test_one_input(std::span<unsigned char const, 18446744073709551615ul>) /home/hodlinator/bitcoin/build_fuzz/../src/test/fuzz/fuzz.cpp:86:5
    #13 0x563af5cff4c0 in LLVMFuzzerTestOneInput /home/hodlinator/bitcoin/build_fuzz/../src/test/fuzz/fuzz.cpp:205:5
    #14 0x563af53785d8 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x18015d8)
    #15 0x563af5378b5e in fuzzer::Fuzzer::TryDetectingAMemoryLeak(unsigned char const*, unsigned long, bool) (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x1801b5e)
    #16 0x563af5353e9f in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x17dce9f)
    #17 0x563af535f904 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x17e8904)
    #18 0x563af52a9242 in main (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x1732242)
    #19 0x7f0d6b42a1fd in __libc_start_call_main (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x2a1fd) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #20 0x7f0d6b42a2b8 in __libc_start_main@GLIBC_2.2.5 (/nix/store/6q2mknq81cyscjmkv72fpcsvan56qhmg-glibc-2.40-66/lib/libc.so.6+0x2a2b8) (BuildId: d9765725d929c713f4481765fa13ed815149985f)
    #21 0x563af5349574 in _start (/home/hodlinator/bitcoin/build_fuzz/bin/fuzz+0x17d2574)

NOTE: libFuzzer has rudimentary signal handlers.
      Combine libFuzzer with AddressSanitizer or similar for better crash reports.
SUMMARY: libFuzzer: deadly signal

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it's within the same process. libFuzzer doesn't take -runs=1 too seriously. Seems weird that it would repeat when only given 1 input... I guess they don't expect deterministic tests.

Thanks for checking. I'd say it would be nice to report this upstream to llvm (with a minimal reproducer), and then eventually fix it. Though, google stopped maintaining it, so they providing the fix is unlikely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the minimal reproducer would be:

cat << EOF > /tmp/fuzz_once.cpp
#include <cstddef>
#include <cstdint>
#include <cstdlib>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
  static bool g_once{true};
  if (g_once) {
    g_once = false;
    return 0;
  }
  std::abort(); // Ran twice :(
}
EOF

and then:

clang++ -fsanitize=fuzzer -g -o /tmp/fuzz_once.exe /tmp/fuzz_once.cpp && while ( /tmp/fuzz_once.exe /tmp/fuzz_once.cpp ) ; do true ; done

However, I can't reproduce. So it would be good if someone else checked this.

#include <algorithm>
#include <future>
#include <functional>
#include <stdexcept>

Expand Down Expand Up @@ -230,6 +231,12 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
// Use synchronous task runner while fuzzing to avoid non-determinism
G_FUZZING ? std::make_unique<ValidationSignals>(std::make_unique<util::ImmediateTaskRunner>()) :
std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(*m_node.scheduler));
{
// Ensure deterministic coverage by waiting for m_service_thread to be running
std::promise<void> promise;
m_node.scheduler->scheduleFromNow([&promise] { promise.set_value(); }, 0ms);
promise.get_future().wait();
}
}

bilingual_str error{};
Expand All @@ -247,7 +254,8 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
.check_block_index = 1,
.notifications = *m_node.notifications,
.signals = m_node.validation_signals.get(),
.worker_threads_num = 2,
// Use no worker threads while fuzzing to avoid non-determinism
.worker_threads_num = G_FUZZING ? 0 : 2,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried this means we get decreased fuzz-coverage. Maybe could introduce another var?

Suggested change
.worker_threads_num = G_FUZZING ? 0 : 2,
.worker_threads_num = G_FUZZING && G_DETERMINISTIC? 0 : 2,

Although being able to reproduce fuzz failures is also nice. :\

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. If someone wants to fuzz test the script worker threads, a dedicated fuzz target seems ideal (See ./src/test/fuzz/checkqueue.cpp). Relying on unrelated fuzz targets to achieve coverage here seems brittle at best, because those targets may change at any time, dropping the coverage silently. Also, I am not aware of any meaningful coverage here in any fuzz target that would be more than what the unit tests and functional tests already achieve.

};
if (opts.min_validation_cache) {
chainman_opts.script_execution_cache_bytes = 0;
Expand Down
Loading