# Variant Root Cause Analysis: CVE-2026-10536

## Summary

CVE-2026-10536 is a heap use-after-free in libcurl's HTTP/2 stream-dependency
bookkeeping. The original reproduction uses `curl_easy_setopt(CURLOPT_STREAM_DEPENDS)`
on a child handle, then `curl_easy_reset(child)` and a later cleanup order that
frees the child before the parent. This variant analysis identifies an
additional, distinct trigger: `curl_easy_duphandle(parent)` after a dependency
has been created. The clone copies the parent handle's `set.priority.children`
pointer, so both the original and the clone share the same `Curl_data_prio_node`.
When the original parent is cleaned up first, the node is freed; the clone's
later cleanup dereferences that freed node, producing the same heap-use-after-free
in `data_priority_cleanup()` / `priority_remove_child()`. A secondary variant
(resetting and cleaning up the parent before the child) also triggers the same
UAF. Both variants are confirmed on the vulnerable commit, and both are clean on
the fixed and latest releases because the upstream fix removes the dependency
tree entirely.

## Fix Coverage / Assumptions

The upstream fix (`bfbff7852f050232edd3e5ca5c6bf2021c340f5a`) removes the
stream-dependency tracking feature:

- `CURLOPT_STREAM_DEPENDS` and `CURLOPT_STREAM_DEPENDS_E` become no-ops.
- `Curl_data_priority_add_child()`, `priority_remove_child()`, and
  `data_priority_cleanup()` are deleted from `lib/url.c`.
- The only remaining priority code is `Curl_data_priority_clear_state()` and a
  `memset` of `set->priority`, both dealing with per-transfer state, not
  inter-handle links.

The fix assumes that the public `CURLOPT_STREAM_DEPENDS` / `E` options are the
sole source of dependency-tree links, and that removing those options and the
associated tree code closes every reachable UAF path. The variant tests below
confirm that assumption: no other tested API path can create a surviving
dependency link on the fixed or latest code.

## Variant / Alternate Trigger

### Primary variant: `curl_easy_duphandle()`

**Entry point:** `curl_easy_duphandle()` (public library API).

**Steps:**
1. `parent = curl_easy_init()`
2. `child = curl_easy_init()`
3. `curl_easy_setopt(child, CURLOPT_STREAM_DEPENDS, parent)` — creates the
   dependency node on `parent` and sets `child->set.priority.parent = parent`.
4. `parent_clone = curl_easy_duphandle(parent)` — `dupset()` does a shallow
   copy of `src->set` into `dst->set`, so `parent_clone->set.priority.children`
   points to the same heap-allocated `Curl_data_prio_node` as `parent`.
5. `curl_easy_cleanup(child)` — child is freed; the node still lives on
   `parent`.
6. `curl_easy_cleanup(parent)` — frees the shared `Curl_data_prio_node`.
7. `curl_easy_cleanup(parent_clone)` — dereferences the freed node in
   `data_priority_cleanup()`, triggering the heap-use-after-free.

**Code path involved:**
- `lib/easy.c:dupset()` — shallow copy of `set.priority`.
- `lib/url.c:data_priority_cleanup()` — iterates over stale children list.
- `lib/url.c:priority_remove_child()` — reads freed `Curl_data_prio_node`.

### Secondary variant: reset/cleanup parent before child

**Entry point:** `curl_easy_reset()` and `curl_easy_cleanup()` order.

**Steps:**
1. Set `CURLOPT_STREAM_DEPENDS` on child so `child->set.priority.parent = parent`.
2. `curl_easy_reset(parent)` — zeroes `parent->set.priority`, but does not
   clear `child->set.priority.parent`.
3. `curl_easy_cleanup(parent)` — frees the parent `Curl_easy` struct.
4. `curl_easy_cleanup(child)` — `data_priority_cleanup()` reads
   `child->set.priority.parent`, now a freed pointer, and UAFs in
   `priority_remove_child()`.

This is the same root cause as the original report but with the reset/cleanup
order reversed; it demonstrates that the bug is not tied to one specific sequence
of API calls.

## Impact

- **Package/component affected:** curl / libcurl, HTTP/2 stream priority/dependency code (`lib/url.c`, `lib/easy.c`).
- **Affected versions tested:** `curl-8_20_0` (`6e3f8dc1f173b47de9a68516ce4b95bf25598c2f`). The upstream advisory identifies affected range `7.88.0` through `8.20.0`.
- **Fixed version tested:** `bfbff7852f050232edd3e5ca5c6bf2021c340f5a` (fix commit) and release `curl-8_21_0` (`3f00a2f6fa97f7721b65606954aac979dcb6caac`).
- **Risk level and consequences:** Memory-safety violation (heap-use-after-free)
  during easy-handle cleanup. The demonstrated impact is a deterministic ASan
  crash; no exploit chain or read/write primitive was demonstrated.

## Impact Parity

- **Disclosed/claimed maximum impact:** memory corruption (use-after-free).
- **Reproduced impact from this variant run:** Deterministic ASan
  `heap-use-after-free` in `data_priority_cleanup()` on the vulnerable build
  for both the `duphandle` and `reset_parent` variants.
- **Parity:** `full` for the memory-safety claim. The alternate triggers prove
  the same vulnerability class and the same sink, just through a different API
  path or ordering.
- **Not demonstrated:** Arbitrary code execution, information disclosure, or a
  bypass of the fixed code.

## Root Cause

The vulnerable code maintains a doubly-linked (parent/children) dependency tree
of `Curl_easy` handles via `set.priority.parent` and `set.priority.children`.
The lifetime of a node is tied to the handle that owns it, but the pointers are
not invalidated when a related handle is reset or freed. The original trigger
frees the child handle while the parent still points to it; the `duphandle`
variant creates a second handle that points to the same node; the
`reset_parent` variant frees the parent while the child still points to it. In
all cases, a subsequent cleanup dereferences a freed dependency link.

The fix removes the tree entirely, so there is no node to share or free
incorrectly. This closes all variants simultaneously.

## Reproduction Steps

Run the variant script:

```bash
bash bundle/vuln_variant/reproduction_steps.sh
```

The script:
1. Reuses the cached vulnerable (`curl-8_20_0`) and fixed (`bfbff7852f`) builds
   from the project cache, plus the latest release (`curl-8_21_0`) built in
   `bundle/vuln_variant/artifacts/install-latest`.
2. Compiles two small harnesses:
   - `bundle/vuln_variant/artifacts/variant_duphandle.c`
   - `bundle/vuln_variant/artifacts/variant_reset_parent.c`
3. Runs each harness against the vulnerable, fixed, and latest builds.
4. Checks each run's log for `ERROR: AddressSanitizer: heap-use-after-free`.
5. Logs are written to `bundle/logs/vuln_variant/`.
6. Exits `1` because the variants only reproduce on the vulnerable version, not
   on the fixed or latest versions (i.e., no bypass).

Expected evidence on the vulnerable build:

```
ERROR: AddressSanitizer: heap-use-after-free ...
    #0 data_priority_cleanup lib/url.c:3587
    #1 Curl_close lib/url.c:280
    #2 curl_easy_cleanup lib/easy.c:844
```

Expected evidence on the fixed and latest builds: clean exit, no ASan error,
possibly a deprecation warning for `CURLOPT_STREAM_DEPENDS`.

## Evidence

- Variant script: `bundle/vuln_variant/reproduction_steps.sh`
- Harness sources:
  - `bundle/vuln_variant/artifacts/variant_duphandle.c`
  - `bundle/vuln_variant/artifacts/variant_reset_parent.c`
- Runtime logs:
  - `bundle/logs/vuln_variant/reproducer_vuln_duphandle.log` — UAF
  - `bundle/logs/vuln_variant/reproducer_vuln_reset_parent.log` — UAF
  - `bundle/logs/vuln_variant/reproducer_fixed_duphandle.log` — clean
  - `bundle/logs/vuln_variant/reproducer_fixed_reset_parent.log` — clean
  - `bundle/logs/vuln_variant/reproducer_latest_duphandle.log` — clean
  - `bundle/logs/vuln_variant/reproducer_latest_reset_parent.log` — clean
  - `bundle/logs/vuln_variant/variant_reproduction_steps.log` — run summary
- Compiled binaries under `bundle/vuln_variant/artifacts/`

Excerpt from `reproducer_vuln_duphandle.log`:

```
==...==ERROR: AddressSanitizer: heap-use-after-free on address ... at pc ...
READ of size 8 ...
    #0 data_priority_cleanup /.../lib/url.c:3587
    #1 Curl_close /.../lib/url.c:280
    #2 curl_easy_cleanup /.../lib/easy.c:844
    #3 main /.../variant_duphandle.c:33
...
freed by thread T0 here:
    #0 free
    #1 priority_remove_child /.../lib/url.c:3525
    #2 data_priority_cleanup /.../lib/url.c:3594
...
previously allocated by thread T0 here:
    #0 calloc
    #1 Curl_data_priority_add_child /.../lib/url.c:3544
```

## Recommendations / Next Steps

- The upstream fix is complete: removing the dependency tree eliminates both the
  original trigger and the alternate triggers identified here. No additional
  code change is required to close this specific bug class.
- For a hypothetical partial fix that tried to patch the tree lifetime (e.g.,
  clearing `child->set.priority.parent` on `curl_easy_reset()`), the
  `curl_easy_duphandle()` variant would remain a concern because the clone
  shares the `children` pointer. Any partial fix must therefore cover every
  path that copies or shares the dependency tree, which is why the upstream
  removal approach is safer.
- Applications should stop using `CURLOPT_STREAM_DEPENDS` and
  `CURLOPT_STREAM_DEPENDS_E`; they are deprecated no-ops as of curl 8.21.0.

## Additional Notes

- **Idempotency:** The variant script was run twice consecutively and produced
  the same results both times.
- **Source state:** The project cache repository was left at the vulnerable
  commit (`curl-8_20_0`) throughout. The fixed and latest builds used separate
  checkouts/worktrees, and the current repo state was not modified by the
  variant script.
- **No bypass found:** The fixed and latest builds both exit cleanly, so the
  upstream removal-based fix is sufficient.
