fuzz: check http_request body matches framing #35759

pull Ameen-Alam wants to merge 1 commits into bitcoin:master from Ameen-Alam:fuzz-http-request-body-assert changing 1 files +19 −1
  1. Ameen-Alam commented at 9:42 PM on July 20, 2026: contributor

    The http_request target asserted that ReadBody() returns an empty string. That held for the libevent-based http_libevent::HTTPRequest, where the harness only parsed the request line and headers and never populated a body. Commit 9c20859b5f (PR #35182) replaced libevent with http_bitcoin::HTTPRequest, and the target was switched over in e427c227fa; its LoadBody() now decodes Content-Length and chunked bodies per RFC 9112, so any fully-parsed request carrying a body trips the stale assertion (e.g. "POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc").

    Replace the emptiness check with a framing-consistency check that mirrors LoadBody()'s own branch logic: a chunked body is bounded by MAX_BODY_SIZE, a Content-Length body is exactly that many bytes, and a request with neither framing header has no body. This strengthens the target instead of dropping the assertion.

    Steps to reproduce (old assertion): Build the fuzz binary and pass this input as a file to the http_request target: POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabctest/fuzz/http_request.cpp:49: Assertion 'body.empty()' failed

    Testing the fix: Ran the updated target ~16 min under libFuzzer with ASAN/UBSAN (14.2M execs, no crashes), plus targeted inputs for each branch: Content-Length body, chunked, Transfer-Encoding: identity + Content-Length, no framing headers, and Content-Length: 0. Happy to contribute the repro input to qa-assets as a follow-up.

  2. fuzz: check http_request body matches framing
    The http_request target asserted that ReadBody() returns an empty
    string. That held for the libevent-based http_libevent::HTTPRequest,
    where the harness only parsed the request line and headers and never
    populated a body. Commit 9c20859b5f (PR #35182) replaced libevent with
    http_bitcoin::HTTPRequest, and the target was switched over in
    e427c227fa; its LoadBody() now decodes Content-Length and chunked
    bodies per RFC 9112, so any fully-parsed request carrying a body trips
    the stale assertion (e.g. "POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc").
    
    Replace the emptiness check with a framing-consistency check that
    mirrors LoadBody()'s own branch logic: a chunked body is bounded by
    MAX_BODY_SIZE, a Content-Length body is exactly that many bytes, and a
    request with neither framing header has no body. This strengthens the
    target instead of dropping the assertion.
    7502b9ddba
  3. DrahtBot added the label Fuzzing on Jul 20, 2026
  4. DrahtBot commented at 9:42 PM on July 20, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35759.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK pinheadmz, marcofleon

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. pinheadmz commented at 10:13 PM on July 20, 2026: member

    concept ack on improving the http fuzz test but I'd like to wait for #35735 which I think might also fix this same issue or otherwise require a fuzz test change.

    My assumption in #35182 was the readbody assertion was safe because of the unlikelihood that fuzz data produces a valid HTTP request. I fuzzed for days without a crash (https://github.com/bitcoin-core/qa-assets/pull/280)

    But I think the authors intention here is correct, the assertion was safe not because of luck but because of libevent's architecture.

  6. Ameen-Alam commented at 10:22 PM on July 20, 2026: contributor

    Thanks Mr. Matthew Zipkin, that makes sense. I am happy to wait for #35735, then rebase and adapt this change based on the LoadBody semantics that land there. The reproducer currently triggers deterministically on master, so I will rerun it against #35735 to check whether a body assertion is still needed. I can also share the test results on that PR.

  7. maflcko commented at 3:22 PM on July 22, 2026: member

    My assumption in #35182 was the readbody assertion was safe because of the unlikelihood that fuzz data produces a valid HTTP request. I fuzzed for days without a crash (bitcoin-core/qa-assets#280)

    Did a quick check with libFuzzer and a week of CPU, but it doesn't seem to be escaping the coverage hole. Haven't tried AFL/honggfuzz or other engines.

    I guess this was found with an LLM? E.g:

    # cdx -c model=gpt-5.4-mini exec 'Review this file: src/test/fuzz/http_request.cpp' 2> /dev/null
    
    **Findings**
    - Medium: http_request.cpp:48-49 unconditionally asserts `body.empty()` after a successful parse. That is false for any valid request with a real body, including normal `POST` or chunked requests, so the fuzzer will report crashes on inputs the parser is supposed to accept. It also prevents the harness from exploring body-bearing cases, which is the main thing this target should cover.
    
    No other correctness issues stood out in this file. The main fix is to remove the assertion or replace it with an invariant that holds for both empty and non-empty bodies.
    
  8. Ameen-Alam commented at 4:29 PM on July 22, 2026: contributor

    Yes, AI assistance helped me spot the stale assumption, then I reproduced it, traced it back to the libevent removal in #35182 and the harness change in e427c227fa, and wrote and verified the fix.

    I think the fuzzer missed it because the input needs a valid request line, complete headers, and a Content-Length that exactly matches the remaining body. Random mutation is unlikely to produce all three together, so this seems more like a corpus reachability issue. I can add the repro input to qa-assets as a seed.

    I tested this on top of fb0a085 from #35735. Before applying this change, I could still trigger assert(body.empty()) with POST / HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc.

    The cherry-pick applied without any conflicts. All five framing cases passed afterward, and I also ran the HTTP fuzz target for 10 minutes without a crash. From what I tested, the two changes seem independent, so either one should be able to merge first.

    Related testing and findings: #35735 (comment)

  9. maflcko commented at 9:44 AM on July 31, 2026: member

    I fuzzed for days without a crash (bitcoin-core/qa-assets#280)

    Looks like a dict really helps here. Added in https://github.com/bitcoin-core/qa-assets/pull/284

  10. pinheadmz approved
  11. pinheadmz commented at 5:30 PM on July 31, 2026: member

    ACK 7502b9ddba740a2b2250764e340b17a23b15ece8

    Built and tested on macos/arm64. Also built and run the fuzz test in a Debian container, along with the HTTP dict added to qa_assets. Confirmed the crash and the fix. I would like this to get merged as soon as possible before the 32 release, along with adding the http dict to CI (if possible?)

    I'm going to add this commit to my http followups branch and run the fuzzer there as well.

    <details><summary>Show Signature</summary>

    -----BEGIN PGP SIGNED MESSAGE-----
    Hash: SHA256
    
    ACK 7502b9ddba740a2b2250764e340b17a23b15ece8
    -----BEGIN PGP SIGNATURE-----
    
    iQJPBAEBCAA5FiEE5hdzzW4BBA4vG9eM5+KYS2KJyToFAmps2vgbFIAAAAAABAAO
    bWFudTIsMi41KzEuMTIsMCwzAAoJEOfimEtiick6cIsP/1ekHJpd5j1VwIM/M3rd
    ctjqWbnfoq9Qi2+y80PHizrmlkP/FL7vHCPbDVaCuRXlAjryJpiP//RKojknAG0S
    ORTFONOekCiOb6qcooEDQyZyZSWwr7ljsb13U1ladqnivWSFTO4Bq2ksI1PQva8s
    im7n2MF7/rZMMWNCCFCvL9q93PBg1QDUaaBL5v7Gn6R3absvGEi2Wr/NQBvn4gi/
    oEM7EdvTFdoeNmOCxe1coAuw91eY2F0LfFg+T1+EQNdHCmM+K7vpFasU+bh6/mMV
    +JkbW9Qe+iMgMztH0XTrgCZJrMVHE+ZVL0dCwTuhL34lW8nyxvciwY2NUz4u7yPh
    IQhWVO4uhUbGLEFLHcX7SMFCUMYdmPNCJUDWgrkaDwnI5pvphngmv6N2tFSIkEL2
    xt2M/tAAvotmFlEm6hygiXZP3Mgkka1sKbuqQUpLJLfZIj3/7ncyByi38ySLAg1F
    fT5pGlZRWt6iFTDqJ9CeXIO5aiLR+ECDjZVRCpvbuSCkc+GDqy9d5N3uAIAzf+SL
    sxrtzH0kcW1QzQ/p08+JG0GCxHEjOu41XAdQxUrp53xVS2fo10+V0nlqSw0sCBP7
    FvoQmHrsybFt8Xh1wFXDCjX1QnuTmfYQ7mmLHGRav5uM+JxMLwcJacUBd12O7Sf1
    DJzF//azPdeRRBpF8IHCrAhc
    =PQGd
    -----END PGP SIGNATURE-----
    

    pinheadmz's public key is on openpgp.org

    </details>

  12. maflcko added this to the milestone 32.0 on Aug 1, 2026
  13. sedited requested review from marcofleon on Aug 4, 2026
  14. marcofleon commented at 2:12 PM on August 6, 2026: contributor

    tACK 7502b9ddba740a2b2250764e340b17a23b15ece8

    Got the crash on master using the dictionary. Been fuzzing a while with this fix and so far so good.

  15. pinheadmz commented at 2:18 PM on August 6, 2026: member

    Been fuzzing a while with this fix and so far so good.

    me too ;-)

    [#84260184793](/bitcoin-bitcoin/84260184793/): cov: 634 ft: 2490 corp: 413 exec/s: 7320 oom/timeout/crash: 0/0/0 time: 505368s job: 40382 dft_time: 0
    
  16. fanquake merged this on Aug 6, 2026
  17. fanquake closed this on Aug 6, 2026


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-08-11 08:51 UTC

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