fuzz: add a custom mutator for PSBT #36631

pull brunoerg wants to merge 2 commits into bitcoin:master from brunoerg:2026-10-fuzz-psbt-custom-mutator changing 7 files +570 −3
  1. brunoerg commented at 7:12 PM on October 23, 2026: contributor

    The partially_signed_transaction_deserialize target feeds raw bytes to the PSBT parser. Byte-level mutations rarely produce well-formed key-value maps with valid keydata (pubkeys, xpubs, control blocks, DER signatures, etc), so most inputs are rejected early and the deeper per-field parsing is rarely reached.

    This PR adds a custom mutator for PSBT which is ported from bitcoinfuzz (with some changes). The mutator splits the input into global/input/output maps of key-value records, applies the mutations and serializes the result again. The first commit is a preparation to allow targets to provide a custom mutator by adding an optional mutator field to FuzzTargetOptions.

    note: LLM assisted on porting the custom mutator.

  2. DrahtBot added the label Fuzzing on Oct 23, 2026
  3. DrahtBot commented at 7:12 PM on October 23, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. in src/test/fuzz/fuzz.cpp:237 in b44c41bd44 outdated
     232 | +}
     233 | +
     234 | +// libFuzzer disables -len_control by default whenever LLVMFuzzerCustomMutator
     235 | +// is linked in, even for targets that don't provide a mutator. Restore
     236 | +// libFuzzer's usual default for those targets, unless the user set it.
     237 | +static void restore_default_len_control(int* argc, char*** argv)
    


    brunoerg commented at 7:14 PM on October 23, 2026:

    note: since we have one binary for everything, a custom mutator would disable -len_control even for targets that do not use it. This function solves it but libFuzzer will still log the disabling.


    ekzyis commented at 1:21 AM on October 30, 2026:

    I think this will break fuzzing with afl++ in OSS-Fuzz. It defines LLVMFuzzerMutate as a symbol here and then crashes here because of argv manipulation. You can reproduce with this:

    $ cmake -B build_fuzz_afl -DBUILD_FOR_FUZZING=ON -DCMAKE_C_COMPILER=afl-clang-lto -DCMAKE_CXX_COMPILER=afl-clang-lto++ -DSANITIZERS=fuzzer
    $ cmake --build build_fuzz_afl -j$(nproc)
    $ FUZZ=process_message ./build_fuzz_afl/bin/fuzz
    ============================== INFO ================================
    This binary is built for afl++.
    To run the target function on individual input(s) execute:
      ./build_fuzz_afl/bin/fuzz INPUT_FILE1 [INPUT_FILE2 ... ]
    To fuzz with afl-fuzz execute:
      afl-fuzz [afl-flags] -- ./build_fuzz_afl/bin/fuzz [-N]
    afl-fuzz will run N iterations before re-spawning the process (default: INT_MAX)
    You can also use AFL_FUZZER_LOOPCOUNT to set N
    For stdin input processing, pass '-' as single command line option.
    For file input processing, pass '@@' as single command line option.
    To use with afl-cmin or afl-cmin.bash pass '-' as single command line option
    ===================================================================
    Running LLVMFuzzerInitialize ...
    continue...
    fuzz: aflpp_driver.c:422: int LLVMFuzzerRunDriver(int *, char ***, int (*)(const uint8_t *, size_t)): Assertion `N > 0' failed.
    Aborted                    (core dumped) FUZZ=process_message ./build_fuzz_afl/bin/fuzz
    

    This doesn't happen on master (6ed5aad061).


    brunoerg commented at 1:24 AM on October 30, 2026:

    Yea, noticed this, I had tested only on libFuzzer. I've moved this to draft while fixing it.

  5. brunoerg marked this as a draft on Oct 23, 2026
  6. brunoerg force-pushed on Oct 23, 2026
  7. DrahtBot added the label CI failed on Oct 23, 2026
  8. DrahtBot commented at 7:25 PM on October 23, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task NetBSD Cross: https://github.com/bitcoin/bitcoin/actions/runs/40009584622/job/120606204787</sub> <sub>LLM reason (✨ experimental): CI failed during the fuzz-test build because g_custom_mutator in src/test/fuzz/fuzz.cpp is set but unused, triggering -Werror.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  9. brunoerg force-pushed on Oct 23, 2026
  10. DrahtBot removed the label CI failed on Oct 23, 2026
  11. brunoerg marked this as ready for review on Oct 26, 2026
  12. DrahtBot added the label Needs rebase on Oct 28, 2026
  13. fuzz: allow targets to provide a libFuzzer custom mutator
    Add an optional `mutator` field to FuzzTargetOptions. When the fuzz
    binary is linked against libFuzzer, LLVMFuzzerCustomMutator dispatches
    to the selected target's mutator, falling back to LLVMFuzzerMutate for
    targets that don't set one.
    
    Custom mutators can call FuzzDefaultMutate() to apply libFuzzer's
    default mutations to parts of the input.
    
    libFuzzer disables -len_control by default whenever
    LLVMFuzzerCustomMutator is linked in, which would otherwise affect every
    target. Targets without a custom mutator get -len_control=100 restored,
    unless it was set explicitly.
    
    Whether the engine provides LLVMFuzzerMutate is detected at configure
    time, so builds using other engines or the built-in main are unaffected.
    825c18f07d
  14. fuzz: add structure-aware PSBT mutator
    Add a custom mutator for raw PSBTs, ported from bitcoinfuzz, and use it
    for the partially_signed_transaction_deserialize target.
    3f6be480ed
  15. brunoerg force-pushed on Oct 28, 2026
  16. brunoerg commented at 6:14 PM on October 28, 2026: contributor

    Rebased

  17. DrahtBot removed the label Needs rebase on Oct 28, 2026
  18. in src/test/fuzz/util/psbt.cpp:429 in 3f6be480ed
     424 | +        Record& rec{map[rng.randrange(map.size())]};
     425 | +        if (rec.key.empty()) break;
     426 | +        // Leave room to grow, so keydata of other lengths can be reached.
     427 | +        const size_t old_size{rec.key.size()};
     428 | +        rec.key.resize(old_size + 16);
     429 | +        rec.key.resize(FuzzDefaultMutate(rec.key.data(), old_size, rec.key.size()));
    


    ekzyis commented at 1:22 AM on October 30, 2026:

    The key gets erased if FuzzDefaultMutate returns 0 (which is the case for LLVMFuzzerMutate in afl++) . Is that expected to happen in this branch for key mutations?

  19. brunoerg marked this as a draft on Oct 30, 2026
Labels

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-30 15:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me