{
 "number": 36631,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36631",
 "title": "fuzz: add a custom mutator for PSBT",
 "author": "brunoerg",
 "author_association": "MEMBER",
 "created_at": "2026-10-23T19:12:34Z",
 "updated_at": "2026-10-31T16:38:35Z",
 "description_hash": "ace88bd4c33176c3",
 "age_days": 9,
 "draft": true,
 "labels": [
  "Fuzzing"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "3f6be480edd1f1c010267ae91984efab4959e746",
 "head_ref": "2026-10-fuzz-psbt-custom-mutator",
 "head_repo": "brunoerg/bitcoin",
 "head_history": [
  {
   "t": "2026-10-23T19:23:32Z",
   "sha": "6e20db3402af3c90adfd4b09a15c51dec6ebf1e1"
  },
  {
   "t": "2026-10-23T19:57:42Z",
   "sha": "f34b7ff64f19397fe73e264d4f8f1dca5d62f4cd"
  },
  {
   "t": "2026-10-28T17:10:19Z",
   "sha": "3f6be480edd1f1c010267ae91984efab4959e746"
  }
 ],
 "additions": 570,
 "deletions": 3,
 "changed_files": 7,
 "commit_count": 2,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "ekzyis"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-30T17:14:26Z",
  "last_reviewer_activity": "2026-10-31T16:38:34Z",
  "last_reviewer": "ekzyis",
  "author_silent_days": 2,
  "waiting_on_author_days": 1,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/test/fuzz/fuzz.cpp",
  "src/test/fuzz/util/psbt.cpp"
 ],
 "body": "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.\n\nThis 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\n`FuzzTargetOptions`.\n\nnote: LLM assisted on porting the custom mutator.",
 "commits": [
  {
   "sha": "825c18f07da217e019ca878a7342e181cdec296d",
   "date": "2026-10-28T17:06:03Z",
   "message": "fuzz: allow targets to provide a libFuzzer custom mutator\n\nAdd an optional `mutator` field to FuzzTargetOptions. When the fuzz\nbinary is linked against libFuzzer, LLVMFuzzerCustomMutator dispatches\nto the selected target's mutator, falling back to LLVMFuzzerMutate for\ntargets that don't set one.\n\nCustom mutators can call FuzzDefaultMutate() to apply libFuzzer's\ndefault mutations to parts of the input.\n\nlibFuzzer disables -len_control by default whenever\nLLVMFuzzerCustomMutator is linked in, which would otherwise affect every\ntarget. Targets without a custom mutator get -len_control=100 restored,\nunless it was set explicitly.\n\nWhether the engine provides LLVMFuzzerMutate is detected at configure\ntime, so builds using other engines or the built-in main are unaffected."
  },
  {
   "sha": "3f6be480edd1f1c010267ae91984efab4959e746",
   "date": "2026-10-28T17:06:33Z",
   "message": "fuzz: add structure-aware PSBT mutator\n\nAdd a custom mutator for raw PSBTs, ported from bitcoinfuzz, and use it\nfor the partially_signed_transaction_deserialize target."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-23T19:14:56Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/fuzz.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": null,
   "text": "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.",
   "id": "rc:4363341583",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4363341583",
   "hash": "a36989000c3cbd84",
   "edited": null
  },
  {
   "t": "2026-10-23T19:23:32Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "6e20db3402af3c90adfd4b09a15c51dec6ebf1e1",
   "id": "push:6e20db3402af3c90adfd4b09a15c51dec6ebf1e1"
  },
  {
   "t": "2026-10-23T19:57:42Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "f34b7ff64f19397fe73e264d4f8f1dca5d62f4cd",
   "id": "push:f34b7ff64f19397fe73e264d4f8f1dca5d62f4cd"
  },
  {
   "t": "2026-10-28T17:10:19Z",
   "kind": "force_push",
   "who": "brunoerg",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "id": "push:3f6be480edd1f1c010267ae91984efab4959e746"
  },
  {
   "t": "2026-10-28T18:14:07Z",
   "kind": "comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "text": "Rebased",
   "id": "c:6476945390",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#issuecomment-6476945390",
   "hash": "f87a71af18167b87",
   "edited": null
  },
  {
   "t": "2026-10-30T01:21:29Z",
   "kind": "review_comment",
   "who": "ekzyis",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/fuzz.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": 4363341583,
   "text": "I think this will break fuzzing with afl++ in OSS-Fuzz. It defines `LLVMFuzzerMutate` as a symbol [here](https://github.com/AFLplusplus/AFLplusplus/blob/d0070729206a80a9af297b2341edd63f68fdac35/utils/aflpp_driver/aflpp_driver.c#L251) and then crashes [here](https://github.com/AFLplusplus/AFLplusplus/blob/d0070729206a80a9af297b2341edd63f68fdac35/utils/aflpp_driver/aflpp_driver.c#L437) because of argv manipulation. You can reproduce with this:\n\n```\n$ cmake -B build_fuzz_afl -DBUILD_FOR_FUZZING=ON -DCMAKE_C_COMPILER=afl-clang-lto -DCMAKE_CXX_COMPILER=afl-clang-lto++ -DSANITIZERS=fuzzer\n$ cmake --build build_fuzz_afl -j$(nproc)\n$ FUZZ=process_message ./build_fuzz_afl/bin/fuzz\n============================== INFO ================================\nThis binary is built for afl++.\nTo run the target function on individual input(s) execute:\n  ./build_fuzz_afl/bin/fuzz INPUT_FILE1 [INPUT_FILE2 ... ]\nTo fuzz with afl-fuzz execute:\n  afl-fuzz [afl-flags] -- ./build_fuzz_afl/bin/fuzz [-N]\nafl-fuzz will run N iterations before re-spawning the process (default: INT_MAX)\nYou can also use AFL_FUZZER_LOOPCOUNT to set N\nFor stdin input processing, pass '-' as single command line option.\nFor file input processing, pass '@@' as single command line option.\nTo use with afl-cmin or afl-cmin.bash pass '-' as single command line option\n===================================================================\nRunning LLVMFuzzerInitialize ...\ncontinue...\nfuzz: aflpp_driver.c:422: int LLVMFuzzerRunDriver(int *, char ***, int (*)(const uint8_t *, size_t)): Assertion `N > 0' failed.\nAborted                    (core dumped) FUZZ=process_message ./build_fuzz_afl/bin/fuzz\n```\n\nThis doesn't happen on master (6ed5aad061).",
   "id": "rc:4419570555",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4419570555",
   "hash": "c3eb29c91144fa9a",
   "edited": "2026-10-30T01:22:28Z"
  },
  {
   "t": "2026-10-30T01:22:02Z",
   "kind": "review_comment",
   "who": "ekzyis",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/util/psbt.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": null,
   "text": "The key gets _erased_ if `FuzzDefaultMutate` returns 0 (which is [the case](https://github.com/AFLplusplus/AFLplusplus/blob/d0070729206a80a9af297b2341edd63f68fdac35/utils/aflpp_driver/aflpp_driver.c#L258) for `LLVMFuzzerMutate` in afl++) . Is that expected to happen in this branch for key _mutations_?",
   "id": "rc:4419573290",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4419573290",
   "hash": "036b048d50b71ab0",
   "edited": "2026-10-30T01:22:28Z"
  },
  {
   "t": "2026-10-30T01:24:51Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/fuzz.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": 4363341583,
   "text": "Yea, noticed this, I had tested only on libFuzzer. I've moved this to draft while fixing it.",
   "id": "rc:4419586585",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4419586585",
   "hash": "55cad6d1a5418a5a",
   "edited": "2026-10-30T01:24:52Z"
  },
  {
   "t": "2026-10-30T17:14:26Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/util/psbt.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": 4419573290,
   "text": "this would only happen with AFL++'s weak stub when PROVIDE_FUZZ_CUSTOM_MUTATOR is defined, isn't it?",
   "id": "rc:4426404831",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4426404831",
   "hash": "73f9b5e58db957b4",
   "edited": "2026-10-30T17:14:27Z"
  },
  {
   "t": "2026-10-31T16:38:34Z",
   "kind": "review_comment",
   "who": "ekzyis",
   "assoc": "CONTRIBUTOR",
   "path": "src/test/fuzz/util/psbt.cpp",
   "commit": "3f6be480edd1f1c010267ae91984efab4959e746",
   "in_reply_to": 4419573290,
   "text": "Ah yes, that is my understanding as well after seeing `#if defined(PROVIDE_FUZZ_CUSTOM_MUTATOR)` at fuzz.cpp:224.",
   "id": "rc:4432455324",
   "url": "https://github.com/bitcoin/bitcoin/pull/36631#discussion_r4432455324",
   "hash": "55494ac83aa2252a",
   "edited": "2026-10-31T16:38:35Z"
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-23T19:12:39Z",
   "action": "labeled",
   "label": "Fuzzing",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-23T19:25:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-23T21:04:34Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-28T16:40:39Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-28T18:56:47Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-23T19:18:36Z",
   "kind": "convert_to_draft",
   "who": "brunoerg"
  },
  {
   "t": "2026-10-26T09:48:43Z",
   "kind": "ready_for_review",
   "who": "brunoerg"
  },
  {
   "t": "2026-10-30T01:23:45Z",
   "kind": "convert_to_draft",
   "who": "brunoerg"
  }
 ],
 "text_chars": 4241,
 "text_tokens_estimate": 1060,
 "changed_paths": [
  "CMakeLists.txt",
  "src/test/fuzz/deserialize.cpp",
  "src/test/fuzz/fuzz.cpp",
  "src/test/fuzz/fuzz.h",
  "src/test/fuzz/util/CMakeLists.txt",
  "src/test/fuzz/util/psbt.cpp",
  "src/test/fuzz/util/psbt.h"
 ],
 "files": [
  {
   "path": "CMakeLists.txt",
   "add": 11,
   "del": 0
  },
  {
   "path": "src/test/fuzz/deserialize.cpp",
   "add": 8,
   "del": 3
  },
  {
   "path": "src/test/fuzz/fuzz.cpp",
   "add": 48,
   "del": 0
  },
  {
   "path": "src/test/fuzz/fuzz.h",
   "add": 5,
   "del": 0
  },
  {
   "path": "src/test/fuzz/util/CMakeLists.txt",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/test/fuzz/util/psbt.cpp",
   "add": 481,
   "del": 0
  },
  {
   "path": "src/test/fuzz/util/psbt.h",
   "add": 13,
   "del": 0
  }
 ],
 "test_lines": 562,
 "git": {
  "head": "3f6be480edd1f1c010267ae91984efab4959e746",
  "head_matches_backup": true,
  "base": "0f27bed5b2924420d65caeca439e6b036d3c6cb2",
  "patch_id": "338c6c10cf45c468",
  "commits": [
   {
    "sha": "825c18f07d",
    "subject": "fuzz: allow targets to provide a libFuzzer custom mutator",
    "files": 4,
    "add": 67,
    "del": 0
   },
   {
    "sha": "3f6be480ed",
    "subject": "fuzz: add structure-aware PSBT mutator",
    "files": 4,
    "add": 503,
    "del": 3
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "338c6c10cf45c468",
 "input_hash": "eedc6d215d26d817",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}