# CVE-2024-26582 Variant RCA: failed-backlog-decryption use-after-free (bypass of 32b55c5)

## Summary

A **confirmed bypass** of the CVE-2024-26582 fix commit `32b55c5ff9103b8508c1e04bfa5a08c64e7a925f`
(`net: tls: fix use-after-free with partial reads and async decrypt`, the `free_sgout` fix) was
found and reproduced on the **fixed** Linux v6.6.18 kernel. The `free_sgout` fix only corrects the
*page-freeing heuristic* in the async completion callback `tls_decrypt_done()` (replacing the buggy
`sgout != sgin` test with `dctx->free_sgout = !!pages`). It does **not** cover the **crypto-backlog
error path**: when an async kTLS decrypt request is enqueued to the crypto backlog
(`crypto_aead_decrypt()` returns `-EBUSY`) and a pending async decrypt fails with `-EBADMSG`
(e.g. an attacker-controlled malformed/mismatched-key TLS record), `tls_do_decryption()` waits for
the backlog to drain (`tls_decrypt_async_wait()`); during that wait `tls_decrypt_done()` already
runs and `kfree()`s the `aead_req`/`tls_decrypt_ctx` block (and frees the destination pages when
`free_sgout`). `tls_do_decryption()` then returns `-EBADMSG`, `tls_decrypt_sg()` jumps to
`exit_free_pages`/`exit_free` and frees the **same** memory again → **slab-use-after-free /
double-free**, confirmed by KASAN. This second UAF is fixed upstream by commit
`13114dc5543069f7b97991e3b79937b6da05f5b0` (`tls: fix use-after-free on failed backlog decryption`,
adds the `async_done` flag), which is **absent** in v6.6.18. We reproduced the UAF on v6.6.18
(`32b55c5` present, `13114dc` absent) and verified that adapting `13114dc` closes it.

## Fix Coverage / Assumptions

The original fix `32b55c5` relies on this invariant:

> The decision of whether to free the AEAD destination pages in the async completion callback
> `tls_decrypt_done()` can be made correctly at submit time by recording whether `tls_decrypt_sg()`
> actually allocated those pages (`dctx->free_sgout = !!pages`), instead of the fragile
> `sgout != sgin` pointer comparison.

Code paths `32b55c5` **explicitly covers**:
- `tls_decrypt_done()` (the async AEAD completion callback) — page-free condition changed to
  `if (dctx->free_sgout)`.
- All three `sgout` setup sub-cases in `tls_decrypt_sg()`: clear_skb (non-ZC, `pages=0`),
  `out_iov` (ZC, `pages>0`), and `out_sg` (ZC, `pages=0`).

What `32b55c5` does **NOT** cover:
- The **`-EBUSY` backlog error path** in `tls_do_decryption()`. When `crypto_aead_decrypt()` returns
  `-EBUSY`, `tls_do_decryption()` calls `tls_decrypt_async_wait()` and may return an error
  (`-EBADMSG`) **after** the async callback `tls_decrypt_done()` has already run and released the
  memory. `tls_decrypt_sg()`'s error cleanup (`exit_free_pages` → `put_page` loop, then
  `exit_free` → `kfree(mem)`) then releases the same memory a second time.
- `32b55c5` only governs *whether `tls_decrypt_done` frees pages*; it has no mechanism to tell
  `tls_decrypt_sg()`'s error path that the callback already ran. That mechanism (`async_done`) is
  exactly what `13114dc` adds.

The reproduction's own instrumentation already forces the async path (a `crypto/simd.c` patch that
defers all AEAD decrypts to the cryptd workqueue, simulating an async/hardware crypto accelerator),
and v6.6.18 already contains the `-EBUSY` backlog handling from `859054147318` ("net: tls: handle
backlogging of crypto requests"). Only the `13114dc` completion of that fix is missing.

## Variant / Alternate Trigger

**Entry point:** `recv()` / `recvmsg()` on a kTLS (TLS 1.2) software RX socket — the same syscall
surface as the original CVE, but driven through the **backlog error sub-path** instead of the
partial-read `process_rx_list()` path.

**Trigger conditions (all attacker-relevant):**
1. A kTLS TLS 1.2 session with an async-capable AEAD (`ctx->async_capable = true`; any cipher whose
   tfm is `CRYPTO_ALG_ASYNC`, e.g. `gcm(aes)` via the simd/cryptd wrapper, or a hardware crypto
   accelerator). TLS 1.3 disables async, so TLS 1.2 is required — same as the original CVE.
2. A decrypt request that is **backlogged** (`crypto_aead_decrypt()` → `-EBUSY`). This is the
   natural behavior under crypto backlog congestion (the `CRYPTO_TFM_REQ_MAY_BACKLOG` flag kTLS sets
   on every AEAD request). With a busy/congested async crypto queue (hardware accelerator or cryptd
   under load), `-EBUSY` is returned instead of `-EINPROGRESS`.
3. A **pending async decrypt that fails** with `-EBADMSG`. An attacker who controls the TLS record
   bytes on the wire (the trust boundary the original CVE crosses — network-received ciphertext) can
   send a malformed/corrupt record (bad AEAD tag) so decryption fails. In the reproducer this is
   simulated with mismatched TX/RX keys (every record fails `-EBADMSG`), which exercises the
   identical `tls_decrypt_done(... err=-EBADMSG)` code path.

**Exact code path (files / functions), all in `net/tls/tls_sw.c`:**
```
__x64_sys_recvfrom -> ... -> tls_sw_recvmsg()
  -> tls_rx_one_record() -> tls_decrypt_sw() -> tls_decrypt_sg()
     -> tls_do_decryption()
        crypto_aead_decrypt() == -EBUSY            [backlog congestion]
        -> tls_decrypt_async_wait()                [waits; tls_decrypt_done() runs on cryptd
                                                    workqueue, frees pages if free_sgout AND
                                                    kfree(aead_req)/dctx block]
        returns -EBADMSG                            [a pending decrypt failed]
     err = -EBADMSG -> goto exit_free_pages:
        for (; pages>0; ...) put_page(sg_page(&sgout[pages]))   [DOUBLE-FREE of ZC pages]
     exit_free: kfree(mem)                          [DOUBLE-FREE / UAF of aead_req||dctx block]
```
KASAN attributes the freed object to `tls_decrypt_done()` running on the cryptd workqueue
(`cryptd_aead_crypt -> cryptd_queue_worker -> process_one_work`), and the buggy re-access to
`tls_decrypt_sg()` — exactly the `13114dc` bug.

The reproduction additionally forces `-EINPROGRESS -> -EBUSY` for async decrypts
(`bundle/vuln_variant/ebusy_force.patch`) because the backlog-congestion condition is
non-deterministic in a single-CPU loopback QEMU. This is analogous to the reproduction's existing
`crypto/simd.c` patch that forces the cryptd async path; it does **not** alter the page-freeing
(`free_sgout`) logic — it only makes the backlog (`-EBUSY`) code path reachable on demand.

## Impact

- **Package / component affected:** Linux kernel kTLS software RX, `net/tls/tls_sw.c`
  (`tls_do_decryption`, `tls_decrypt_done`, `tls_decrypt_sg`).
- **Affected versions (as tested):** Linux v6.6.18 — i.e. any branch that has `32b55c5`
  (`free_sgout`) but **not** `13114dc` (`async_done`). Concretely v6.6.18 ships `32b55c5` and the
  backlog handling `859054147318`, but predates `13114dc` (committed 2024-02-29, after v6.6.18).
- **Risk level:** High — kernel-space slab use-after-free / double-free in the kTLS receive path,
  triggerable by an attacker who can send malformed TLS records to a kTLS peer whose async crypto
  queue is congested. Same impact class as the parent CVE (memory corruption; potential info leak /
  privilege escalation).

## Impact Parity

- **Disclosed / claimed maximum impact (parent CVE-2024-26582):** kernel-space use-after-free /
  memory corruption in the kTLS async decrypt receive path.
- **Reproduced impact from this variant run:** kernel-space **slab-use-after-free confirmed by
  KASAN** (`BUG: KASAN: slab-use-after-free in tls_decrypt_sg+0x2037/0x29c0`), where the freed
  `aead_req`/`dctx` block (kmalloc-512) was freed by `tls_decrypt_done()` on the cryptd workqueue
  and re-accessed by `tls_decrypt_sg()`'s error path. This is a memory-corruption primitive of the
  same class as the parent CVE, on the **fixed** (v6.6.18 + `32b55c5`) kernel.
- **Parity:** `full` — same component, same async-decrypt UAF class, same trust boundary
  (network-controlled TLS records), reproduced on the patched code path.
- **Not demonstrated:** no exploit chain to privilege escalation / code execution was built; the
  reproduction demonstrates the memory-corruption primitive (KASAN UAF) only, consistent with the
  parent CVE's reproduced scope.

## Root Cause

`tls_decrypt_sg()` allocates a single block `mem = kmalloc(aead_req || tls_decrypt_ctx)` that owns
both the AEAD request and the destination scatterlist/`free_sgout` flag. In the async case the
completion callback `tls_decrypt_done()` is responsible for freeing the destination pages (when
`free_sgout`) and for `kfree(aead_req)` (the `mem` block). The error path of `tls_decrypt_sg()`
(`exit_free_pages`/`exit_free`) **also** frees those pages and `kfree(mem)`.

For the normal async path (`crypto_aead_decrypt()` → `-EINPROGRESS`), `tls_do_decryption()` returns
`0` and `tls_decrypt_sg()` returns without hitting its error cleanup, so only the callback frees —
no double-free. `32b55c5` makes the callback's page-free decision correct.

For the **backlog** path (`-EBUSY`), `tls_do_decryption()` synchronously waits for the backlog to
drain (`tls_decrypt_async_wait()`). By the time the wait returns, `tls_decrypt_done()` has already
executed for the backlogged request and freed its memory. If the wait also observed a failure
(`ctx->async_wait.err == -EBADMSG`), `tls_do_decryption()` returns `-EBADMSG`, and `tls_decrypt_sg()`
runs its error cleanup on the already-freed memory → double-free / UAF. `32b55c5` does not provide
any signal that the callback already ran, so it cannot prevent this.

The missing fix is upstream commit **`13114dc5543069f7b97991e3b79937b6da05f5b0`** ("tls: fix
use-after-free on failed backlog decryption"), `Fixes: 859054147318`. It adds `bool async_done` to
`tls_decrypt_arg`; on the `-EBUSY` path `tls_do_decryption()` sets `async_done=true` and returns the
wait result, and `tls_decrypt_sg()` then skips `exit_free_pages`/`exit_free` (`goto exit_free_skb`)
when `async_done` is set, avoiding the double-free. We verified this exact behavior by adapting
`13114dc` to v6.6.18: the UAF disappears.

## Reproduction Steps

1. See `bundle/vuln_variant/reproduction_steps.sh`.
2. The script builds four v6.6.18 kernels side by side (all have the async-forcing
   `crypto/simd.c` patch from the parent reproduction):
   - `vuln+ebusy` (32b55c5 **reverted** + `ebusy_force.patch`) — variant on the vulnerable tree,
   - `fixed` (32b55c5 present, no backlog forcing) — baseline,
   - `fixed+ebusy` (32b55c5 + `ebusy_force.patch`: `-EINPROGRESS -> -EBUSY` for async decrypts) — bypass,
   - `fixed+ebusy+13114dc` (also adapts the `13114dc` `async_done` fix) — gap-closure.
   It builds a static init binary (`backlog_variant_test.c`) that sets up kTLS TLS 1.2 AES-128-GCM
   over loopback with **mismatched** TX/RX keys (so every record fails `-EBADMSG` during async
   decrypt), sends 16 records, and `recv()`s them with a large buffer (zero-copy full reads,
   `darg.zc=true`, `pages>0`, `free_sgout=true`). It runs each kernel in QEMU (KASAN + page
   poisoning) and counts `BUG: KASAN` reports.
3. **Expected evidence:** `vuln+ebusy` and `fixed+ebusy` both show
   `BUG: KASAN: slab-use-after-free in tls_decrypt_sg`; `fixed` (no backlog) shows none;
   `fixed+ebusy+13114dc` shows none. Exit 0 = bypass confirmed on the FIXED kernel. The
   `vuln+ebusy` result corroborates that the variant is present on the vulnerable tree too
   (both lack `13114dc`).

## Evidence

- `bundle/logs/vuln_variant/qemu-vuln-ebusy-variant.log` — **variant on vulnerable tree**
  (32b55c5 reverted + ebusy): `BUG: KASAN: slab-use-after-free in tls_decrypt_sg` (corroborates
  the variant is the same on the vulnerable tree, which also lacks `13114dc`).
- `bundle/logs/vuln_variant/qemu-fixed-ebusy-variant.log` — **bypass**: contains
  `BUG: KASAN: slab-use-after-free in tls_decrypt_sg+0x2037/0x29c0` (Read of size 8, 440 bytes into a
  freed kmalloc-512 region). Allocated by `__kmalloc` in `tls_decrypt_sg`; **Freed by task 26 via
  `tls_decrypt_done -> cryptd_aead_crypt -> cryptd_queue_worker`** (async workqueue). Program output:
  `recv returned -1 (errno=74 Bad message)`.
- `bundle/logs/vuln_variant/qemu-fixed-nobacklog-baseline.log` — **baseline**: same test, no
  backlog forcing → `EBADMSG` returned, **no KASAN** (proves `32b55c5` holds for the normal async
  path and the `-EBUSY` condition is the trigger).
- `bundle/logs/vuln_variant/qemu-fixed-ebusy-13114dc.log` — **gap-closure**: same test with
  `13114dc` adapted → `EBADMSG` returned, **no KASAN** (proves `13114dc` closes the bypass).
- `bundle/logs/vuln_variant/kasan_report_excerpt.txt` — extracted KASAN report.
- `bundle/logs/vuln_variant/fixed_version.txt` — tested source identity (v6.6.18 tarball;
  `free_sgout` present, `async_done` absent).
- `bundle/vuln_variant/ebusy_force.patch`, `bundle/vuln_variant/apply_fix13114dc.py`,
  `bundle/vuln_variant/backlog_variant_test.c` — instrumentation / test program.

## Recommendations / Next Steps

- **Close the gap:** backport / apply commit `13114dc5543069f7b97991e3b79937b6da05f5b0`
  ("tls: fix use-after-free on failed backlog decryption") to every branch that carries the CVE
  2024-26582 `free_sgout` fix (`32b55c5`) but not `13114dc` (e.g. v6.6.18 / v6.6.19-only branches
  that took only the first patch). The `32b55c5` fix alone is **incomplete** for the async-decrypt
  UAF class. The fix must add an `async_done`-style signal so `tls_decrypt_sg()`'s error path skips
  `exit_free_pages`/`exit_free` when the async completion callback has already released the memory.
- **Rule-out matrix (other surfaces inspected, all covered/sync — not bypasses):**
  - `tls_sw_splice_read()` (splice syscall): uses `darg.async = false` (sync) → uses
    `crypto_req_done`, never `tls_decrypt_done` → not reachable. Not a variant.
  - `tls_sw_read_sock()` (sendfile / `tcp_read_sock`): likewise sync. Not a variant.
  - `decrypt_skb()` / `out_sg` path (Case C, device fallback): `darg = { .zc = true }` with
    `async = false` (sync) → never `tls_decrypt_done`. Not a variant.
  - Other ciphers (CHACHA20-POLY1305, AES-256-GCM, AES-128-CCM): the bug is cipher-agnostic (all
    flow through `tls_decrypt_sg`/`tls_do_decryption`/`tls_decrypt_done`); `32b55c5`'s `free_sgout`
    and the missing `13114dc` `async_done` apply identically. (CHACHA20-POLY1305 exercises a
    different IV-prep branch at `tls_sw.c:1531` but the same sink.) These are alternate triggers of
    the *same* sink, all covered by `32b55c5`+`13114dc`; not independent bypasses.
- **Hardening:** treat the two CVE-2024-26582 patches (`32b55c5` and `13114dc`) as a single
  mandatory unit when backporting; add a regression test that exercises the async decrypt backlog +
  failure combination (the reproducer in this bundle is a starting point).

## Additional Notes

- **Idempotency:** `bundle/vuln_variant/reproduction_steps.sh` was run twice; both runs produced
  identical results (`fixed(no ebusy) KASAN=0 | fixed+ebusy KASAN=1 | fixed+ebusy+13114dc KASAN=0`)
  and exit 0. Kernel builds and the initramfs are cached and skipped on repeat runs; the shared
  kernel source tree is always restored to pristine (`tls_sw.c.orig`) after each variant build.
- **Trust boundary:** the attacker controls the TLS record bytes received over the network
  (malformed record → `-EBADMSG`); the `-EBUSY` backlog congestion is an environmental condition
  (busy async crypto), not attacker-controlled, but it is a normal operating condition for systems
  using async/hardware crypto accelerators. The reproducer forces `-EBUSY` deterministically because
  loopback single-CPU QEMU cannot naturally congest the crypto backlog; this mirrors the parent
  reproduction's `crypto/simd.c` async-forcing approach.
- **Instrumentation caveat:** `ebusy_force.patch` converts `-EINPROGRESS -> -EBUSY` for async
  decrypts to make the backlog path reachable on demand. It does **not** change `free_sgout` or any
  page-freeing logic; the double-free is the genuine v6.6.18 code path, not an artifact of the
  patch. The `13114dc`-adapted kernel shows no UAF under the same forcing, proving the gap is
  exactly the missing `async_done` fix.
- The v6.6.18 source is a kernel.org release tarball (no git metadata); source identity is recorded
  in `bundle/vuln_variant/source_identity.json` via release-tag resolution.
