# Variant Patch Analysis: CVE-2026-33630 c-ares

## Patch identity and inspected scope

- Repository: `https://github.com/c-ares/c-ares`
- Vulnerable baseline used by the parent reproduction: tag `v1.34.6`, commit `3ac47ee46edd8ea40370222f91613fc16c434853`.
- Fixed baseline inspected/tested: tag `v1.34.7`, commit `d63b7de3a3568d14ab1351374b205365aed51cca`, containing fix commit `d823199b688052dcdc1646f2ab4cb8c16b1c644a`.
- Primary changed file: `src/lib/ares_process.c`.
- Regression tests added by the fix commit: `test/ares-test-mock.cc`.
- Other relevant reviewed callback/free paths: `src/lib/ares_getaddrinfo.c`, `src/lib/ares_query.c`, `src/lib/ares_search.c`, `src/lib/ares_cancel.c`, `src/lib/ares_destroy.c`, `src/lib/ares_close_sockets.c`.

## Target security/threat model scope

`SECURITY.md` documents that c-ares treats security vulnerabilities as reportable through `c-ares-security@haxx.se` and publishes accepted vulnerabilities on the c-ares security page. It does not exclude remote DNS peer attacks or memory-safety issues. `README.md` states that c-ares has a strong security focus and implements safe parsers/builders. The parent ticket's trust boundary is therefore in scope: an application using public c-ares resolver APIs receives attacker-controlled DNS responses from a malicious or compromised recursive/authoritative DNS peer over UDP/TCP.

## What the fix changes

The fix consolidates deferred retry/end-query handling in `src/lib/ares_process.c`:

1. It adds `ares_flush_requeue(channel, now, &requeue)`, an iterative drain for deferred `REQUEUE_REQUEUE` and `REQUEUE_ENDQUERY` entries.
2. For `REQUEUE_ENDQUERY`, it now calls `ares_detach_query(query)` before invoking `query->callback(...)`. This removes the query from the connection list, `queries_by_qid`, and `all_queries` before user or wrapper callbacks can reenter c-ares.
3. It changes `read_answers()` to use `ares_flush_requeue()` instead of locally draining the requeue array with callback-before-free behavior.
4. It changes `process_timeouts()` to pass a requeue array into `ares_requeue_query()` and drain it with `ares_flush_requeue()` instead of ending/retrying inline.
5. It splits `ares_send_query()` into a public wrapper plus `ares_send_query_int()`. The wrapper owns a requeue list and flushes it iteratively, avoiding recursive `ares_requeue_query()` -> `ares_send_query()` chains and ensuring callback-ending failures in the send path also use the same detach-before-callback pattern.
6. It updates call sites in `process_answer()` and `ares_send_query_int()` so terminal `end_query()` calls and retryable `ares_requeue_query()` calls receive the active requeue list where possible.

## Fix assumptions

The patch assumes the dangerous stale-use condition arises when a query remains discoverable in `queries_by_qid`/`all_queries` while its completion callback is executing. That assumption is consistent with the parent exploit evidence: a malicious TCP DNS peer triggered a deferred ENDQUERY callback while the query was still linked, then reentrant resolver activity freed associated higher-level query state (`host_query`) before stale callback completion consumed reclaimed fields.

The patch also assumes all asynchronous network-driven query completions should funnel through `ares_flush_requeue()` when they can be deferred. That is now true for the two important remote completion surfaces covered by CVE-2026-33630: answer processing (`read_answers()` / `process_answer()`) and timeout processing (`process_timeouts()`), plus send-path retry recursion.

## Code paths/inputs not fully covered by the new helper

Some callback/free paths intentionally remain outside `ares_flush_requeue()`:

- `ares_cancel()` directly cancels `channel->all_queries` by swapping out the list, claiming each node, invoking `query->callback(... ARES_ECANCELLED ...)`, then freeing the query.
- `ares_destroy()` directly claims all query nodes, invokes callbacks with `ARES_EDESTRUCTION`, then frees queries.
- `ares_close_connection()` calls `ares_requeue_query(..., NULL)` for queries on the closing connection.

These paths are different trust/entry surfaces. `ares_cancel()` and `ares_destroy()` are application-initiated public API calls, not direct remote DNS response processing. `ares_close_connection()` can be reached during connection cleanup, but in the tested duplicate-QID/TCP-reset sequence on the fixed build it did not recreate the parent stale callback consumption, because the relevant ENDQUERY processing had already detached the query before callback.

The most plausible sibling candidate was `process_timeouts()`: it had the same pre-fix inline retry/end behavior and was explicitly named in the fix commit as sharing the double-free root cause. However, in v1.34.7 this path is also routed through `ares_flush_requeue()`, so it is covered by the fix and is not a bypass.

## Behavior before vs. after the fix

Before the fix (v1.34.6):

- `read_answers()` stored `REQUEUE_ENDQUERY` entries by QID and later looked the query up in `queries_by_qid`.
- It invoked `query->callback(query->arg, ...)` before calling `ares_free_query(query)`.
- During the callback, the query was still linked. Reentrant c-ares activity could find/cancel/free it or free higher-level wrapper state. The parent reproduction showed this can lead to vulnerable-only consumption of a forged reclaimed `host_query` callback pointer and execution of an in-process sentinel.

After the fix (v1.34.7):

- `ares_flush_requeue()` detaches the query from all lookup structures before invoking callbacks for deferred ENDQUERY entries.
- `process_timeouts()` and send-path retry/end logic use the same centralized requeue drain.
- The same malicious TCP DNS boundary and heap-reclaim setup no longer causes stale consumption in fixed builds, and the variant script's bounded candidate checks did not find an alternate patched entry point that reaches the same sink.

## Completeness assessment

For the parent remote DNS stale-use/code-execution primitive, the fix appears complete across the materially distinct remote query completion surfaces found in this analysis. It covers the originally exploited `read_answers()` deferred ENDQUERY path and the related `process_timeouts()` path. Remaining direct callback/free functions are application lifecycle/cancellation APIs, not independent attacker-controlled DNS response entry points; they may still require care for API reentrancy, but they do not constitute a bypass of this remote-DNS fix under the ticket's trust boundary.
