# RCA Report — CVE-2026-XRING (Alibaba XQUIC HTTP/3/QPACK ring buffer resize underflow)

## Summary

Alibaba XQUIC (all versions through v1.9.4, latest at time of disclosure) contains a
remote, unauthenticated denial-of-service vulnerability in its HTTP/3 QPACK dynamic
table implementation. The bug is in `xqc_ring_mem_resize()` (`src/common/utils/ringmem/xqc_ring_mem.c`).
When the QPACK dynamic-table ring buffer has wrapped (data is split across the end and
the beginning of the buffer — the "both-truncated" resize case) and the QPACK decoder
receives a `Set Dynamic Table Capacity` instruction that grows the table, the resize
code sizes the first block of the *original* buffer using the **new** capacity (`mcap`)
instead of the **old** capacity (`rmem->capacity`). This produces a heap out-of-bounds
read of the old buffer followed by a `size_t` underflow (`rmem->used - ori_sz1`) into the
third `xqc_memcpy`, which glibc `_FORTIFY_SOURCE` aborts (`*** buffer overflow detected ***:
terminated`). A remote, unauthenticated QUIC/HTTP3 client can trigger this with
spec-compliant QPACK encoder-stream instructions, crashing the server process.

## Impact

- **Package/component affected:** `xquic` — `src/common/utils/ringmem/xqc_ring_mem.c`
  (`xqc_ring_mem_resize`), reached via `xqc_dtable_set_capacity` (`src/http3/qpack/dtable/xqc_dtable.c`)
  ← `xqc_decoder_set_dtable_cap` (`src/http3/qpack/xqc_decoder.c`) when the decoder
  processes a `Set Dynamic Table Capacity` instruction on the QPACK encoder stream.
- **Affected versions:** All published XQUIC versions through v1.9.4. The disclosure
  states no upstream patch is available. Reproduced here against XQUIC v1.9.4
  (commit `96155cf`, tag `v1.9.4`).
- **Risk level / consequences:** High. Remote, unauthenticated, deterministic process
  crash (DoS) of any XQUIC HTTP/3 endpoint that allows the QPACK dynamic table
  (default configuration). The OOB read + underflowed memcpy also imply potential heap
  memory-safety impact beyond a simple crash, although only the crash (DoS) is claimed
  and reproduced.

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote unauthenticated crash (DoS); the blog
  additionally describes a 64-byte heap OOB read and a `size_t` underflow into `memcpy`.
  No code execution is claimed.
- **Reproduced impact from this run:** Deterministic remote crash of the real XQUIC
  `demo_server` (HTTP/3 over QUIC/UDP) via a real `quic-go` client sending spec-compliant
  QPACK encoder-stream instructions. Observed: server process terminated with exit code
  `134` (SIGABRT) and glibc message `*** buffer overflow detected ***: terminated`
  (the `_FORTIFY_SOURCE` abort of the underflowed `memcpy`). 2/2 vulnerable attempts
  crashed; 2/2 negative-control (one-line-fix) attempts stayed alive.
- **Parity:** `full` for the claimed DoS (remote unauthenticated crash) via the claimed
  network protocol surface.
- **Not demonstrated:** Code execution / arbitrary heap write. Only the crash (DoS) and
  the underlying OOB-read + underflow symptom are demonstrated, matching the claim.

## Root Cause

In `xqc_ring_mem_resize(xqc_ring_mem_t *rmem, size_t cap)`, when `cap > rmem->capacity`
the function allocates a new power-of-two buffer `mcap = xqc_pow2_upper(cap)` and copies
the used bytes from the old buffer into the new one. The copy has four shape cases. In
the "both-truncated" case (data wraps in **both** the old and the new buffer,
i.e. `soffset_ori >= eoffset_ori` and `soffset_new >= eoffset_new`):

```c
size_t new_sz1 = mcap - soffset_new;    /* size of first block in new buffer  */
size_t ori_sz1 = mcap - soffset_ori;    /* BUG: uses NEW cap; should be rmem->capacity */
```

`soffset_ori` is computed with the **old** mask (`rmem->sidx & rmem->mask`), so it is an
offset inside the old buffer of size `rmem->capacity`. The first block of the old buffer
that must be copied therefore has size `rmem->capacity - soffset_ori`, **not**
`mcap - soffset_ori`. Because `mcap > rmem->capacity`, `ori_sz1` is too large. In the
"first block of new buffer is smaller than original buffer" sub-branch this causes:

1. `xqc_memcpy(buf, rmem->buf + soffset_ori + new_sz1, ori_sz1 - new_sz1)` — reads
   `ori_sz1 - new_sz1` bytes starting past the end of the old buffer (64-byte OOB read
   in the reproduced scenario);
2. `xqc_memcpy(buf + ori_sz1 - new_sz1, rmem->buf, rmem->used - ori_sz1)` —
   `rmem->used - ori_sz1` underflows (unsigned), producing a huge `size_t`, so glibc's
   `__memcpy_chk` (`_FORTIFY_SOURCE`) aborts the process.

**Concrete reproduced arithmetic** (v1.9.4, payload from FoxIO `xring-poc`):
initial `Set Dynamic Table Capacity = 64` → `rmem->capacity = 64` (mask 63). 61×
`Insert "x":"y"` then `Insert "AAAAA":"BBBBB"` evict entries and wrap the 64-byte ring,
leaving `used = 10`, `sidx = 122` (`soffset_ori = 122 & 63 = 58`), `eidx = 132`
(`eoffset_ori = 132 & 63 = 4`): old buffer is truncated. `Set Dynamic Table Capacity = 65`
→ `xqc_ring_mem_resize(rmem, 65)`: `mcap = xqc_pow2_upper(65) = 128`, `soffset_new = 122
& 127 = 122`, `eoffset_new = 132 & 127 = 4`: new buffer also truncated → "both-truncated"
branch. `new_sz1 = 128 - 122 = 6`; buggy `ori_sz1 = 128 - 58 = 70` (correct: `64 - 58 =
6`). `new_sz1 < ori_sz1` → reads `70 - 6 = 64` bytes OOB, then `used(10) - 70` underflows
→ `memcpy_chk` abort (exit 134).

**Fix (no upstream patch exists; applied here as the negative control):**
```c
- size_t ori_sz1 = mcap - soffset_ori;    /* size of first block in original buffer */
+ size_t ori_sz1 = rmem->capacity - soffset_ori;    /* size of first block in original buffer */
```
With this one-line change the identical malicious payload no longer crashes the server
(verified: 2/2 fixed attempts stay alive).

## Reproduction Steps

1. Script: `bundle/repro/reproduction_steps.sh` (self-contained; reuses the durable
   project cache when present, otherwise clones/builds XQUIC v1.9.4 + Tongsuo 8.4-stable
   + a `quic-go` client from source).
2. What it does:
   - Locates the durable project cache via `bundle/project_cache_context.json`; uses
     prebuilt `demo_server_vuln` (XQUIC v1.9.4, `-Werror` removed, vulnerable line
     intact) and `demo_server_fixed` (same tree with the one-line `ori_sz1` fix) plus a
     prebuilt `quic-go` client that reproduces the FoxIO XRING payload.
   - Generates a self-signed EC cert, starts the real XQUIC `demo_server` (HTTP/3 over
     QUIC/UDP on `127.0.0.1:8443`), then runs the `quic-go` client which performs a real
     QUIC/TLS handshake, opens the HTTP/3 control stream (`0x00 0x04 0x00`) and the QPACK
     encoder unidirectional stream (`0x02` + payload), and sends: `Set Dynamic Table
     Capacity=64`, 61× `Insert "x":"y"`, `Insert "AAAAA":"BBBBB"`,
     `Set Dynamic Table Capacity=65`.
   - Runs **2 vulnerable** attempts and **2 fixed** attempts with per-attempt logs, then
     writes `bundle/repro/runtime_manifest.json`.
3. Expected evidence: vulnerable server exits `134` (SIGABRT) with stderr
   `*** buffer overflow detected ***: terminated`; fixed server stays alive. Script
   exits `0` when vulnerable crashes ≥1/2 and fixed alive ≥1/2.

## Evidence

- `bundle/repro/reproduction_steps.sh` — the reproducer.
- `bundle/repro/runtime_manifest.json` — runtime evidence manifest (`entrypoint_kind=tcp_peer`,
  `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`,
  proof artifact list).
- `bundle/logs/repro_run1.log`, `bundle/logs/repro_run2.log` — full logs of two
  consecutive successful runs.
- `bundle/logs/vuln_attempt{1,2}_server.stderr` — contain
  `*** buffer overflow detected ***: terminated`.
- `bundle/logs/vuln_attempt{1,2}_result.txt` — `RESULT: server CRASHED exit=134`.
- `bundle/logs/fixed_attempt{1,2}_result.txt` — `RESULT: server ALIVE (no crash)`.
- `bundle/logs/vuln_attempt{1,2}_client.stdout` — `payload len 260` / `payload sent`.

Key excerpts (run 1):
```
[vuln#1] server pid=22035 listening on udp/8443
[vuln#1] RESULT: server CRASHED exit=134
[vuln#2] server pid=22062 listening on udp/8443
[vuln#2] RESULT: server CRASHED exit=134
[fixed#1] server pid=22089 listening on udp/8443
[fixed#1] RESULT: server ALIVE (no crash)
[fixed#2] server pid=22112 listening on udp/8443
[fixed#2] RESULT: server ALIVE (no crash)
[*] vulnerable crashes: 2 / 2
[*] fixed alive (no crash): 2 / 2
[*] glibc _FORTIFY_SOURCE abort observed: 2 / 2
```
Server stderr (vulnerable): `*** buffer overflow detected ***: terminated`

Environment: Ubuntu 26.04 LTS, GCC 15.2.0, XQUIC v1.9.4 (commit `96155cf`), Tongsuo
8.4-stable (BabaSSL) as the QUIC TLS backend, `quic-go` v0.60.0 client, Go 1.26.5,
glibc `_FORTIFY_SOURCE` enabled in the Release build. No sanitizers used for the
primary proof (`sanitizer_used=false`); the crash is the native product-visible abort.

## Recommendations / Next Steps

- **Fix:** In `xqc_ring_mem_resize`, change the both-truncated branch to size the
  original-buffer block with the old capacity:
  `ori_sz1 = rmem->capacity - soffset_ori;`. (This is the negative-control patch
  applied and verified here.) Audit the other resize sub-branches for the same
  old-vs-new-capacity confusion and add unit tests covering wrapped-ring grow scenarios.
- **Mitigation (deploy now):** Advertise `SETTINGS_QPACK_MAX_TABLE_CAPACITY = 0` to
  disable the QPACK dynamic table, preventing the decoder from ever creating/growing the
  ring memory that contains the bug.
- **Upgrade guidance:** No patched upstream release exists at disclosure time; users
  should apply the one-line fix or the mitigation until an official patch is published.
- **Testing:** Add regression tests that fill the QPACK dynamic table until the ring
  wraps, then grow via `Set Dynamic Table Capacity`, asserting no OOB read / underflow
  (e.g., under ASAN/UBSAN and with `_FORTIFY_SOURCE`).

## Additional Notes

- **Idempotency:** Confirmed — the script was run twice consecutively; both runs exited
  `0` with identical results (vuln 2/2 crash, fixed 2/2 alive, fortify abort 2/2).
- **Surface match:** The claim surface is `network_protocol` /
  `required_entrypoint_kind=tcp_peer`. The proof drives a real QUIC/HTTP3 peer over UDP
  (a `quic-go` client dialing the real XQUIC `demo_server`), exercising the real QPACK
  decoder encoder-stream path end-to-end. QUIC uses UDP datagrams rather than a TCP
  byte stream, but the peer/listener boundary and protocol exchange are real.
- **No sanitizers in the primary proof:** The crash is glibc `_FORTIFY_SOURCE` aborting
  the underflowed `memcpy` in a normal Release build — a product-visible native crash,
  not an ASAN/UBSAN report.
- **Claimed vs observed impact:** Only DoS (crash) is claimed and reproduced. The
  underlying 64-byte OOB read + `size_t` underflow suggest possible heap memory-safety
  impact beyond DoS, but code execution was not claimed and is not demonstrated here.
- **Limitations:** Reproduced against the `demo_server` sample shipped with XQUIC; any
  XQUIC-based HTTP/3 server exposing the QPACK decoder to remote clients is expected to
  be similarly affected (default dynamic-table configuration).
