# REPRO-2026-00278: libcurl HTTP/2 stream-dependency tree use-after-free ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00278 CVE: CVE-2026-10536 ## Package Name: curl/libcurl Ecosystem: github Affected: 7.88.0 to 8.20.0 inclusive Fixed: curl 8.21.0 ## Root Cause # Root Cause Analysis: CVE-2026-10536 ## Summary CVE-2026-10536 is a heap use-after-free (UAF) in libcurl's HTTP/2 stream-dependency bookkeeping. When an application sets `CURLOPT_STREAM_DEPENDS` (or `CURLOPT_STREAM_DEPENDS_E`) on an easy handle, then calls `curl_easy_reset()` on that handle and later `curl_easy_cleanup()` on both handles, libcurl can dereference a dependency-tree node that was already freed. The vulnerability requires HTTP/2 support (`nghttp2`) and use of the now-deprecated dependency options. It is reachable through the public libcurl API (`curl_easy_setopt`, `curl_easy_reset`, `curl_easy_cleanup`) without any network traffic. ## Impact - **Package/component affected:** curl / libcurl, HTTP/2 stream priority/dependency code (`lib/url.c`, `priority_remove_child`, `data_priority_cleanup`). - **Affected versions:** curl 7.88.0 through 8.20.0 inclusive (per upstream advisory); introduced by commit `71b7e0161032927cdfb` and fixed by commit `bfbff7852f050232edd3e5ca5c6bf2021c340f5a`. - **Risk level and consequences:** High / memory corruption. The UAF occurs during easy-handle cleanup, so a controlled memory layout could potentially lead to further memory corruption. The upstream advisory classifies the issue as a use-after-free with CVSS severity Low, but the demonstrated impact is a real memory-safety violation in a widely used library. It does not affect the curl command-line tool directly; only applications that programmatically use the deprecated dependency options are exposed. ## Impact Parity - **Disclosed/claimed maximum impact:** memory corruption (use-after-free). - **Reproduced impact from this run:** A deterministic AddressSanitizer `heap-use-after-free` report in `priority_remove_child` at `lib/url.c:3516` when the parent handle is cleaned up after the child handle has been reset and freed. - **Parity:** `full` for the memory-safety claim. The reproduction proves the exact vulnerability class (UAF) and the exact API surface described in the advisory. - **Not demonstrated:** No remote code execution or full exploit chain was demonstrated; only the UAF crash/reachability is proven, which is sufficient for the claimed memory-corruption impact. ## Root Cause The bug is in the priority-child doubly-linked bookkeeping in `lib/url.c`: 1. `curl_easy_setopt(child, CURLOPT_STREAM_DEPENDS, parent)` adds a `Curl_data_prio_node` to `parent->set.priority.children`; that node stores a raw pointer to `child`. It also sets `child->set.priority.parent = parent`. 2. `curl_easy_reset(child)` executes `memset(&data->set, 0, sizeof(struct UserDefined))`, which zeros `child->set.priority` and therefore clears `child->set.priority.parent`. The child handle now "forgets" that it is a dependent of `parent`, but `parent->set.priority.children` still contains the node pointing at `child`. 3. `curl_easy_cleanup(child)` calls `data_priority_cleanup(child)`. Because `child->set.priority.parent` is now zero, the code does **not** remove the child node from its parent list. The `Curl_easy` struct for `child` is then freed. 4. `curl_easy_cleanup(parent)` calls `data_priority_cleanup(parent)`, which loops over `parent->set.priority.children`. The first node still points to the freed `child` struct, so `priority_remove_child(parent, child)` reads `child->set.priority.parent` (offset 2120 inside the freed `Curl_easy`) and crashes under AddressSanitizer. - **Fix commit:** https://github.com/curl/curl/commit/bfbff7852f050232edd3e5ca5c6bf2021c340f5a ("http2: remove stream dependency tracking"). The upstream fix removes the entire HTTP/2 stream-dependency feature, making `CURLOPT_STREAM_DEPENDS` and `CURLOPT_STREAM_DEPENDS_E` no-ops, which eliminates the faulty bookkeeping. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh`. It is self-contained: it installs build dependencies, clones or reuses the curl repository, builds two static ASan-enabled libcurl installations (vulnerable `curl-8_20_0` and fixed `bfbff7852f`), compiles a small C reproducer, and runs it against both. 2. The reproducer creates two easy handles, sets `CURLOPT_STREAM_DEPENDS` on the child so it depends on the parent, calls `curl_easy_reset(child)` and `curl_easy_cleanup(child)`, then calls `curl_easy_cleanup(parent)`. 3. Expected evidence: - Vulnerable build: AddressSanitizer reports `heap-use-after-free` in `priority_remove_child` (`lib/url.c:3516`). - Fixed build: reproducer exits cleanly with no sanitizer report. ## Evidence - `bundle/logs/reproduction_steps.log` — full script output. - `bundle/logs/reproducer_vuln.log` — ASan heap-use-after-free report for the vulnerable build. - `bundle/logs/reproducer_fixed.log` — clean run for the fixed build. - `bundle/repro/reproducer.c` — source of the minimal C harness. - `bundle/repro/runtime_manifest.json` — structured runtime evidence manifest. Key excerpt from the vulnerable run: ``` ==ERROR: AddressSanitizer: heap-use-after-free on address ... at pc ... in priority_remove_child lib/url.c:3516 READ of size 8 at ... thread T0 #0 priority_remove_child /.../lib/url.c:3516 #1 data_priority_cleanup /.../lib/url.c:3588 #2 Curl_close /.../lib/url.c:280 #3 curl_easy_cleanup /.../lib/easy.c:844 ``` The freed region was the `Curl_easy` struct allocated in `Curl_open` during `curl_easy_init`, and it was freed by `curl_easy_cleanup(child)` before `curl_easy_cleanup(parent)` accessed it through the stale dependency node. ## Recommendations / Next Steps - **Upgrade:** Upgrade libcurl to 8.21.0 or later, where the dependency feature has been removed and the option is a no-op. - **Workaround:** Applications that currently use `CURLOPT_STREAM_DEPENDS` or `CURLOPT_STREAM_DEPENDS_E` should stop using these deprecated options; they are no longer functional in 8.21.0+. - **Testing:** For any cherry-picked patch, verify that `curl_easy_setopt(..., CURLOPT_STREAM_DEPENDS, ...)` followed by `curl_easy_reset` and `curl_easy_cleanup` no longer produces a use-after-free under ASan/Valgrind. ## Additional Notes - **Idempotency:** The reproduction script was run twice consecutively and produced the same confirmed result both times. Cached builds in the project cache are reused when the recorded commit matches, making subsequent runs fast. - **Limitations:** The proof is a library/API harness that directly calls the affected libcurl functions. It does not exercise a remote network path because the advisory describes an application-level API misuse, not a network-triggered vulnerability. ## Reproduction Details Reproduced: 2026-07-09T19:32:40.514Z Duration: 1350 seconds Tool calls: 194 Turns: Unknown Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00278 pruva-verify CVE-2026-10536 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00278&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00278/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-10536 - Source: https://github.com/curl/curl ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 5704 bytes) - bundle/repro/rca_report.md (analysis, 6554 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 7771 bytes) - bundle/vuln_variant/rca_report.md (analysis, 9711 bytes) - bundle/coding/proposed_fix.diff (patch, 719 bytes) - bundle/artifact_promotion_manifest.json (other, 9267 bytes) - bundle/artifact_promotion_report.json (other, 9285 bytes) - bundle/vuln_variant/source_identity.json (other, 873 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1570 bytes) - bundle/logs/reproducer_vuln.log (log, 5111 bytes) - bundle/logs/reproducer_fixed.log (log, 2436 bytes) - bundle/repro/runtime_manifest.json (other, 763 bytes) - bundle/repro/reproducer.c (other, 582 bytes) - bundle/repro/validation_verdict.json (other, 812 bytes) - bundle/logs/vuln_variant/reproducer_vuln_duphandle.log (log, 5696 bytes) - bundle/logs/vuln_variant/reproducer_vuln_reset_parent.log (log, 5320 bytes) - bundle/vuln_variant/artifacts/variant_duphandle.c (other, 1111 bytes) - bundle/vuln_variant/artifacts/variant_reset_parent.c (other, 775 bytes) - bundle/vuln_variant/variant_manifest.json (other, 2935 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1436 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 4305 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1125 bytes) - bundle/logs/vuln_variant/reproducer_fixed_duphandle.log (log, 2561 bytes) - bundle/coding/summary_report.md (documentation, 3593 bytes) - bundle/coding/verify_fix.sh (other, 5095 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00278 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00278/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00278 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev