test: add libFuzzer harness #1956

pull fametrano wants to merge 11 commits into bitcoin-core:master from fametrano:fuzz-harness changing 24 files +1055 −1
  1. fametrano commented at 1:07 PM on October 10, 2026: contributor

    This takes over #744 with @elichai's go-ahead: his three targets, ported to the current API, with his authorship kept on the first commit. As @sipa suggested there, it is one binary, fuzz, that picks the target from the FUZZ environment variable, as in Bitcoin Core. There is a target for the core API, ecdsa, and one per module: ecdh, recovery, extrakeys, schnorrsig, musig, ellswift and silentpayments. It is built with --enable-fuzz or -DSECP256K1_BUILD_FUZZ=ON, and with VERIFY, like tests, so internal checks run. The instrumentation comes from CFLAGS and the engine from FUZZ_LIBS (SECP256K1_FUZZ_LIBS in CMake), so an OSS-Fuzz build can pass its own. A CI job runs each target under ASan and UBSan, from an empty corpus, for 10000 runs or 20 seconds; a crash it finds is public in its log at once.

    The targets check what the public headers document, and that forged or tampered signatures do not verify. Where tweak-add or silent payments output creation fails, the target checks that the failure has its documented cause. #744 asserted that tweak-add always succeeds, but it returns 0, as documented, when the two keys sum to 0 mod n; its consume_pubkey also overwrote the prefix byte. Both are fixed. ellswift and silentpayments take what an attacker controls from the input: the peer's 64 bytes and decoy outputs to scan. Internal targets, which @sipa suggested, can come on top of this; #1407 and @erickcestari's diffsecp have field, scalar and group ones. Background and a roadmap, with this PR as its first step: https://claude.ai/artifact/68binXRWqAZewaZABsEqBz

    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.

  2. test: add libFuzzer harness with ecdsa, recovery and ecdh targets
    This is the harness of PR #744, rebased on the current tree. It is
    built as one binary, `fuzz`, which picks the target from the FUZZ
    environment variable, as Bitcoin Core does. Like the tests, it includes
    secp256k1.c, so that targets for internal functions can be added later.
    Module targets live in src/modules/<module>/fuzz_impl.h.
    
    The code is updated to the current API (secp256k1_context_static,
    SECP256K1_CONTEXT_NONE, secp256k1_ec_seckey_*), uses
    secp256k1_memcmp_var, and is C89 clean under the project's warning
    flags. The checks are those of #744, with these changes:
    
    - consume_pubkey copies the x coordinate after the prefix byte instead
      of over it.
    - The Pubkey(seckey1+seckey2) check accepts seckey1 + seckey2 == 0 mod
      n, where seckey_tweak_add, pubkey_combine and pubkey_tweak_add all
      return 0. It checks that they agree and fail exactly in that case.
    - secret_msg and predefined_pubkey are one byte longer, for the NUL: a
      32-byte literal in a 32-byte array fails -Werror with
      -Wunterminated-string-initialization (clang 23, GCC 15).
    - The scratch space block is dropped: secp256k1_scratch_space_create
      left the API in 0.6.0.
    - A NULL data pointer with size 0 is replaced by a non-NULL one, since
      the targets do arithmetic on it.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    e873b25516
  3. build: add fuzz target to autotools and CMake
    The fuzz target is built with --enable-fuzz or -DSECP256K1_BUILD_FUZZ=ON
    and is off by default. Like tests, it is compiled with VERIFY, so the
    library's internal VERIFY_CHECKs run. It is not a test and does not run
    in make check or ctest.
    
    The fuzzing engine is linked from FUZZ_LIBS (SECP256K1_FUZZ_LIBS in
    CMake), default -fsanitize=fuzzer, into the fuzz target only.
    Instrumentation comes from CFLAGS, e.g.
    -fsanitize=fuzzer-no-link,address,undefined. This is the split Bitcoin
    Core and OSS-Fuzz use, so that LIB_FUZZING_ENGINE can be passed as
    FUZZ_LIBS.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    abc1649335
  4. doc: describe fuzzing
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    cdcbd85002
  5. test: add extrakeys and schnorrsig fuzz targets
    extrakeys checks that keypair and x-only keys agree with
    ec_pubkey_create, that x-only keys round-trip through serialization,
    and that keypair_xonly_tweak_add, xonly_pubkey_tweak_add and
    xonly_pubkey_tweak_add_check agree. Where the two tweak functions
    fail, it checks the cause: a tweak >= n, or one that cancels the even-y
    secret key.
    
    schnorrsig checks that signing succeeds and verifies, that sign_custom
    with a 32-byte message equals sign32, that a NULL aux_rand32 equals an
    all-zero one, that a signature with one bit flipped does not verify,
    and that a garbage signature does not verify for a key whose secret
    nobody knows.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    2067a2f710
  6. test: add ellswift fuzz target
    It checks that decode(encode(pubkey)) is pubkey, that every 64-byte
    string decodes to a key that re-parses, that ellswift_create decodes to
    ec_pubkey_create's key, and that both parties of ellswift_xdh get the
    same secret. Against 64 arbitrary bytes from the peer, as in BIP324, it
    checks that the shared x is the x of our secret key times the decoded
    peer key.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    38b1a164d3
  7. test: add musig fuzz target
    Each input runs one two-signer session. It checks that nonce_gen
    succeeds for a non-zero session_secrand32, that sorted key aggregation
    does not depend on the key order, that both partial signatures verify,
    that the aggregate signature verifies with schnorrsig_verify, and that
    a tampered partial signature does not verify.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    c1d5660951
  8. test: add silentpayments fuzz target
    One sender input pays one recipient. The recipient scans the sent
    output among decoys: its own spend key and up to two x-only keys from
    the fuzz input. The target checks that the sent output is found, and
    that every found output is spendable with the spend key plus its tweak.
    
    create_outputs fails, with one valid input key, only for a spend key
    equal to minus the output tweak. There the target retries with the
    negated spend key and checks that cause.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    c7b5420e2f
  9. test: check at fuzz startup that valid inputs are accepted
    The targets skip inputs that the library rejects as invalid. A library
    that rejected every valid secret key, public key or signature would
    then pass every run without reaching a check. At startup, the fuzz
    binary now aborts unless secret key 1, the generator, a compact ECDSA
    signature and, where their modules are enabled, the generator's
    x-only key and a partial signature are accepted.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    2bfecf6feb
  10. ci: add libFuzzer job
    The job builds with clang, ASan and UBSan, and runs every fuzz target
    on an empty corpus for 10000 runs or 20 seconds, whichever comes first.
    
    It passes -len_control=0, so inputs have full length from the first
    run. Otherwise libFuzzer lengthens them slowly, and in 10000 runs they
    stay under 100 bytes, too short for the musig, ellswift and
    silentpayments targets to reach their last checks.
    
    CFLAGS carry -fsanitize=fuzzer-no-link, not -fsanitize=fuzzer: on Linux
    clang links the latter as a whole archive, libFuzzer's main included,
    into every program it links, which clashes with the main of configure's
    checks, the tests and the examples.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    fec3ea735a
  11. in src/modules/recovery/fuzz_impl.h:21 in fec3ea735a
      16 | +    int recid;
      17 | +    int success = 0;
      18 | +    for (recid = 0; recid < 4; recid++) {
      19 | +        int ret = secp256k1_ecdsa_recoverable_signature_parse_compact(fuzz_ctx, &sig, data64, recid);
      20 | +        CHECK(ret == 0 || ret == 1);
      21 | +        if (ret) {
    


    erickcestari commented at 2:48 PM on October 10, 2026:

    nit: If we want to assert that every API function returns exactly 0 or 1, perhaps we could introduce a small helper that checks the return value and returns it, allowing us to inline the check into conditions.

    /* Checks that ret is 0 or 1, and returns it for use in conditions. */
    static int fuzz_check_bool(int ret) {
        CHECK(ret == 0 || ret == 1);
        return ret;
    }
    

    So this code would be:

    if (fuzz_check_bool(secp256k1_ecdsa_recoverable_signature_parse_compact(fuzz_ctx, &sig, data64, recid))) {
    

    fametrano commented at 5:27 AM on October 14, 2026:

    Done in 26b9224: fuzz_check_bool is added and used wherever a target branches on an API result.

  12. in src/fuzz.c:263 in fec3ea735a outdated
     258 | +            ret |= secp256k1_ec_pubkey_parse(fuzz_ctx, &pubkey, random65, 65);
     259 | +            random65[0] = 0x06;
     260 | +            ret |= secp256k1_ec_pubkey_parse(fuzz_ctx, &pubkey, random65, 65);
     261 | +            random65[0] = 0x07;
     262 | +            ret |= secp256k1_ec_pubkey_parse(fuzz_ctx, &pubkey, random65, 65);
     263 | +        }
    


    erickcestari commented at 3:01 PM on October 10, 2026:

    nit: These parse calls on raw input only check that nothing crashes. Their results go into ret, and CHECK(ret == 0 || ret == 1) can't fail. A round trip would catch a parser that accepts a non-canonical encoding.

    /* If input parses, checks that it serializes back to input; hybrid 0x06/0x07 serializes as 0x04. */
    static void fuzz_check_pubkey_parse(const unsigned char *input, size_t inputlen) {
        secp256k1_pubkey pubkey;
        unsigned char ser[65];
        size_t serlen = sizeof(ser);
        if (!fuzz_check_bool(secp256k1_ec_pubkey_parse(fuzz_ctx, &pubkey, input, inputlen))) {
            return;
        }
        CHECK(secp256k1_ec_pubkey_serialize(fuzz_ctx, ser, &serlen, &pubkey, inputlen == 33 ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED) == 1);
        CHECK(serlen == inputlen);
        CHECK(ser[0] == ((input[0] == 0x06 || input[0] == 0x07) ? 0x04 : input[0]));
        CHECK(secp256k1_memcmp_var(ser + 1, input + 1, inputlen - 1) == 0);
    }
    
    tmp = fuzz_consume_bytes(provider, 33);
            if (tmp) {
                memcpy(random33, tmp, sizeof(random33));
                fuzz_check_pubkey_parse(random33, 33);
                random33[0] = 0x02;
                fuzz_check_pubkey_parse(random33, 33);
                random33[0] = 0x03;
                fuzz_check_pubkey_parse(random33, 33);
            }
            tmp = fuzz_consume_bytes(provider, 65);
            if (tmp) {
                memcpy(random65, tmp, sizeof(random65));
                fuzz_check_pubkey_parse(random65, 65);
                random65[0] = 0x04;
                fuzz_check_pubkey_parse(random65, 65);
                random65[0] = 0x06;
                fuzz_check_pubkey_parse(random65, 65);
                random65[0] = 0x07;
                fuzz_check_pubkey_parse(random65, 65);
            }
    

    fametrano commented at 5:27 AM on October 14, 2026:

    Done in 26b9224: fuzz_check_pubkey_parse round-trips the 33- and 65-byte inputs.

  13. in src/fuzz.c:242 in fec3ea735a
     237 | +        const unsigned char *tmp;
     238 | +        unsigned char random33[33], random65[65];
     239 | +        int ret = 0;
     240 | +        random32 = fuzz_consume_bytes(provider, 32);
     241 | +        if (random32) {
     242 | +            ret |= secp256k1_ec_seckey_verify(fuzz_ctx, random32);
    


    erickcestari commented at 3:03 PM on October 10, 2026:

    nit: check if both secp256k1_ec_pubkey_create and secp256k1_ec_seckey_verify function agree.

    CHECK(secp256k1_ec_pubkey_create(fuzz_ctx, &pubkey, random32) == fuzz_check_bool(secp256k1_ec_seckey_verify(fuzz_ctx, random32)));
    

    fametrano commented at 5:27 AM on October 14, 2026:

    Done in 26b9224: the raw 32-byte input checks that ec_pubkey_create and ec_seckey_verify agree.

  14. Yudis-bit commented at 3:20 PM on October 10, 2026: contributor

    Tested ACK fec3ea7.

    In fuzz_silentpayments, the spend_seckey == -t fallback (lines 46-54) is practically unreachable under random inputs because t is derived from input_hash while spend_seckey is consumed independently. Forcing -t directly or varying the sent output position across decoys would exercise that path.

  15. in ci/ci.sh:148 in fec3ea735a outdated
     143 | +    targets=$(FUZZ='' ./fuzz 2>&1 | tail -n +2)
     144 | +    test -n "$targets"
     145 | +    for target in $targets; do
     146 | +        FUZZ="$target" $EXEC ./fuzz -runs=10000 -max_total_time=20 -len_control=0 >> fuzz.log 2>&1
     147 | +    done
     148 | +fi
    


    erickcestari commented at 5:50 PM on October 10, 2026:

    nit: The CI won't print the crash input as base64 if it's larger than 256 bytes.

    As a follow-up, we could seed the CI fuzzing with corpus inputs once we have a corpus, which would also help catch regressions.


    fametrano commented at 5:27 AM on October 14, 2026:

    Left to a follow-up, together with a CI corpus.

  16. in src/fuzz.c:391 in fec3ea735a
     386 | +        CHECK(secp256k1_xonly_pubkey_parse(fuzz_ctx, &xonly, g33 + 1) == 1);
     387 | +#endif
     388 | +#ifdef ENABLE_MODULE_MUSIG
     389 | +        CHECK(secp256k1_musig_partial_sig_parse(fuzz_ctx, &partial_sig, one) == 1);
     390 | +#endif
     391 | +    }
    


    erickcestari commented at 6:22 PM on October 10, 2026:

    nit: this test probably isn't necessary in its current form. It checks the library's parsers, which the unit tests already cover. The more likely way for a target to go quiet is a harness bug: #744's consume_pubkey copied the x coordinate over the prefix byte, so almost no input became a key, and this check would still pass because it never calls the helpers. I put that bug back and confirmed it.

    Testing the harness functions instead would catch it; with the bug back, the version below aborts at startup. It also drops the module #ifdefs. Removing the test entirely would also be fine.

        /* The targets skip inputs the consume helpers reject, so check that known-valid input gets through. */
        {
            static const unsigned char g33[33] = {
                0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, 0x07,
                0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98
            };
            unsigned char one[32] = {0};
            fuzz_data_provider provider;
            secp256k1_pubkey pubkey, g;
            one[31] = 1;
            provider.data = one;
            provider.remaining = sizeof(one);
            CHECK(fuzz_consume_seckey(&provider) == one && provider.remaining == 0);
            provider.data = g33;
            provider.remaining = sizeof(g33);
            CHECK(fuzz_consume_pubkey(&provider, &pubkey) == 1 && provider.remaining == 0);
            CHECK(secp256k1_ec_pubkey_create(fuzz_ctx, &g, one) == 1);
            CHECK(secp256k1_memcmp_var(&pubkey, &g, sizeof(g)) == 0);
        }
    

    fametrano commented at 5:27 AM on October 14, 2026:

    Done in 26b9224: the startup check now runs fuzz_consume_seckey and fuzz_consume_pubkey, with your version except that it compares the keys with ec_pubkey_cmp.

  17. erickcestari commented at 6:42 PM on October 10, 2026: none

    Tested ACK fec3ea735a75fd1b882d628205bec786a785c9dc.

    I ran all 8 targets in parallel for 1 hour each (commit fec3ea7, clang 23.1.1, ASan + UBSan, -len_control=0), about ~ 9M executions. The targets look good to me.

    I left a few nits. They don't change coverage, but they catch bugs the current checks miss:

    • A round-trip check on the garbage key parses. I patched the library so that the key parser accepts x >= p: the current harness passes, and the suggested check aborts.
    • The startup check tests the library's parsers, which the unit tests already cover. Testing the consume helpers instead would catch a harness bug like #744's consume_pubkey, which made almost no input a key.

    <details><summary>Coverage (source-based llvm-cov, replaying the saved test cases)</summary>

    File Lines Functions Branches
    src/assumptions.h 0.0% (0/59) 0.0% (0/1) -
    src/ecdsa_impl.h 88.5% (170/192) 100.0% (6/6) 80.6% (87/108)
    src/eckey_impl.h 93.1% (27/29) 100.0% (4/4) 75.0% (3/4)
    src/ecmult_const_impl.h 88.1% (89/101) 100.0% (4/4) 58.3% (49/84)
    src/ecmult_gen_impl.h 93.2% (110/118) 85.7% (6/7) 73.7% (28/38)
    src/ecmult_impl.h 41.1% (231/562) 44.0% (11/25) 33.9% (114/336)
    src/field_5x52_impl.h 97.5% (317/325) 96.7% (29/30) 59.7% (74/124)
    src/field_5x52_int128_impl.h 100.0% (177/177) 100.0% (2/2) 50.0% (178/356)
    src/field_impl.h 96.6% (285/295) 96.8% (30/31) 62.7% (94/150)
    src/fuzz.c 94.8% (274/289) 100.0% (7/7) 55.3% (177/320)
    src/group_impl.h 88.1% (765/868) 89.4% (84/94) 61.5% (252/410)
    src/hash_impl.h 79.3% (230/290) 83.3% (15/18) 77.5% (100/129)
    src/hsort_impl.h 81.8% (45/55) 100.0% (6/6) 55.6% (20/36)
    src/int128_native_impl.h 91.2% (62/68) 89.5% (17/19) 57.5% (23/40)
    src/modinv64_impl.h 99.2% (481/485) 100.0% (14/14) 56.4% (252/447)
    src/modules/ecdh/fuzz_impl.h 100.0% (40/40) 100.0% (2/2) 58.3% (28/48)
    src/modules/ecdh/main_impl.h 93.2% (41/44) 66.7% (2/3) 64.7% (11/17)
    src/modules/ellswift/fuzz_impl.h 100.0% (64/64) 100.0% (2/2) 57.1% (56/98)
    src/modules/ellswift/main_impl.h 91.9% (273/297) 83.3% (15/18) 64.6% (128/198)
    src/modules/extrakeys/fuzz_impl.h 100.0% (54/54) 100.0% (1/1) 56.7% (59/104)
    src/modules/extrakeys/main_impl.h 88.0% (184/209) 94.1% (16/17) 59.3% (105/177)
    src/modules/musig/fuzz_impl.h 100.0% (66/66) 100.0% (1/1) 62.5% (55/88)
    src/modules/musig/keyagg_impl.h 68.5% (126/184) 69.2% (9/13) 38.8% (38/98)
    src/modules/musig/session_impl.h 75.7% (421/556) 85.7% (30/35) 50.3% (171/340)
    src/modules/recovery/fuzz_impl.h 100.0% (61/61) 100.0% (2/2) 58.3% (42/72)
    src/modules/recovery/main_impl.h 86.4% (108/125) 100.0% (8/8) 59.1% (55/93)
    src/modules/schnorrsig/fuzz_impl.h 100.0% (47/47) 100.0% (1/1) 60.6% (40/66)
    src/modules/schnorrsig/main_impl.h 92.5% (160/173) 90.0% (9/10) 67.6% (50/74)
    src/modules/silentpayments/fuzz_impl.h 90.0% (63/70) 100.0% (1/1) 51.1% (45/88)
    src/modules/silentpayments/main_impl.h 51.8% (249/481) 52.6% (10/19) 38.8% (113/291)
    src/scalar_4x64_impl.h 98.5% (596/605) 92.6% (25/27) 57.6% (83/144)
    src/scalar_impl.h 100.0% (72/72) 100.0% (5/5) 56.2% (18/32)
    src/scratch_impl.h 0.0% (0/80) 0.0% (0/6) 0.0% (0/19)
    src/secp256k1.c 77.5% (447/577) 76.0% (38/50) 50.7% (206/406)
    src/selftest.h 83.3% (20/24) 100.0% (2/2) 33.3% (2/6)
    src/util.h 48.8% (103/211) 65.0% (13/20) 64.7% (22/34)
    Total 81.2% (6458/7953) 83.6% (427/511) 54.7% (2778/5075)

    Headers with no executable code are left out.

    </details>

  18. fametrano referenced this in commit 7ba9a8516b on Oct 10, 2026
  19. fametrano commented at 7:12 PM on October 10, 2026: contributor

    Thanks for testing, @Yudis-bit. You're right: with this PR's target, the fallback is never reached in my coverage run (-seed=1 -runs=20000). I've added a commit to #1957 that reaches it and varies the sent output's position among the decoys. #1957 is stacked on this PR and collects such paths, so this PR keeps the head you tested.

  20. test: tighten fuzz checks after review
    fuzz_check_bool checks that a library function returns 0 or 1, as the
    API documents.
    
    Public keys parsed from raw input, compressed and uncompressed, must
    serialize back to the input, with a hybrid 0x06/0x07 prefix as 0x04.
    This catches a parser that accepts a non-canonical encoding.
    
    Raw 32-byte input also checks that ec_pubkey_create and
    ec_seckey_verify accept the same secret keys.
    
    The startup check now runs the consume helpers on secret key 1 and the
    generator, instead of the library's parsers, which the unit tests
    cover. It aborts on a harness bug like the one in #744's
    consume_pubkey, which copied the x coordinate over the prefix byte.
    
    Suggested by erickcestari in #1956.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    26b9224d54
  21. fametrano commented at 9:23 PM on October 10, 2026: contributor

    Thanks @erickcestari for the review, the hour-long runs and the patches to try.

    I applied nits 1, 2, 3 and 5 in 26b9224:

    • fuzz_check_bool checks for 0 or 1 wherever a target branches on an API result.
    • fuzz_check_pubkey_parse round-trips the 33- and 65-byte keys parsed from raw input.
    • The raw 32-byte input now checks that ec_pubkey_create and ec_seckey_verify agree.
    • The startup check now runs the consume helpers, as you suggested. With #744's consume_pubkey bug put back, it aborts at startup. I compare the keys with ec_pubkey_cmp rather than memcmp, as secp256k1.h asks.

    A fault I planted for each nit makes its check fail. One limit: the 65-byte round trip needs a raw input that is a point on the curve. A 60 s run did not hit the fault I planted there; a crafted input did.

    I'll leave nit 4, printing crash inputs over 256 bytes, and a CI corpus, to a follow-up.

  22. fametrano referenced this in commit 5de46b118a on Oct 10, 2026
  23. in src/fuzz.c:112 in 26b9224d54 outdated
     107 | +    {
     108 | +        const unsigned char *randomize;
     109 | +        randomize = fuzz_consume_bytes(provider, 32);
     110 | +        if (randomize) {
     111 | +            CHECK(secp256k1_context_randomize(fuzz_ctx, randomize) == 1);
     112 | +        }
    


    erickcestari commented at 4:56 PM on October 13, 2026:

    The fuzz_ctx is shared across inputs and context_randomize builds on its current blinding. Reset it first, otherwise the result depends on earlier inputs and crashes may not reproduce.

            const unsigned char *randomize;
            /* context_randomize builds on the current blinding, so reset it to not depend on earlier inputs. */
            CHECK(secp256k1_context_randomize(fuzz_ctx, NULL) == 1);
            randomize = fuzz_consume_bytes(provider, 32);
            if (randomize) {
                CHECK(secp256k1_context_randomize(fuzz_ctx, randomize) == 1);
            }
    

    fametrano commented at 5:27 AM on October 14, 2026:

    Thanks, applied in f5d37b7. A debug print of the blinding confirmed that it no longer depends on earlier inputs.

  24. erickcestari commented at 5:04 PM on October 13, 2026: none

    On another pass through this PR, I noticed that each target exercises many different functions in a fixed sequence. This seems like a good fit for the interleaved fuzzing technique, where a selector byte from the input decides which operation runs next.

    Why it would help:

    • Decoupled inputs: All operations currently consume from the same stream in a fixed order, so a mutation that helps an early operation shifts the bytes seen by every later one, which throws away progress on them.
    • Faster execs: To reach the last operations, every input has to pay for all the earlier ones. With a selector, each exec runs only the operations the fuzzer picked.
    • Better coverage of combinations: The fuzzer can choose which operations run and in what order, reaching sequences and states the fixed order never produces.
    • Smaller, clearer crash inputs: Minimized crashing inputs would contain only the operations needed to trigger the bug, making them easier to triage.

    Since the main goal of this PR is adding libFuzzer support and the initial targets, I think this is better as a follow-up, and I'd be happy to take it on.

  25. test: reset fuzz context blinding before randomizing
    The fuzz context is shared across inputs, and secp256k1_context_randomize
    builds on the current blinding. The state used for an input therefore
    depended on the inputs run before it, and a crash might not reproduce
    from its input alone.
    
    Reset the blinding with a NULL seed first. Suggested by erickcestari.
    f5d37b7545
  26. fametrano commented at 5:27 AM on October 14, 2026: contributor

    Thanks @erickcestari. I agree interleaved fuzzing fits these targets and belongs in a follow-up.

  27. theStack added the label assurance on Oct 18, 2026

github-metadata-mirror

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

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