# REPRO-2026-00284: XRING: Alibaba XQUIC QPACK ring-buffer resize underflow ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00284 ## Package Name: Alibaba XQUIC Ecosystem: github Affected: All XQUIC versions through v1.9.4 (since first public release Jan 2022) Fixed: Unknown ## Root Cause # 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). ## Reproduction Details Reproduced: 2026-07-11T13:15:02.325Z Duration: 1253 seconds Tool calls: 242 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00284 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00284&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00284/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 - Source: https://github.com/alibaba/xquic ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 14719 bytes) - bundle/repro/rca_report.md (analysis, 11166 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 16862 bytes) - bundle/vuln_variant/rca_report.md (analysis, 11525 bytes) - bundle/artifact_promotion_manifest.json (other, 17112 bytes) - bundle/artifact_promotion_report.json (other, 17130 bytes) - bundle/vuln_variant/source_identity.json (other, 2664 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 4735 bytes) - bundle/repro/runtime_manifest.json (other, 1134 bytes) - bundle/repro/validation_verdict.json (other, 1216 bytes) - bundle/logs/vuln_attempt1_server.stderr (other, 45 bytes) - bundle/logs/repro_run1.log (log, 1977 bytes) - bundle/logs/vuln_attempt1_client.stdout (other, 29 bytes) - bundle/logs/vuln_attempt1_result.txt (other, 89 bytes) - bundle/logs/fixed_attempt1_server.stderr (other, 0 bytes) - bundle/logs/fixed_attempt1_client.stdout (other, 29 bytes) - bundle/logs/fixed_attempt1_result.txt (other, 91 bytes) - bundle/logs/vuln_attempt2_server.stderr (other, 45 bytes) - bundle/logs/vuln_attempt2_client.stdout (other, 29 bytes) - bundle/logs/vuln_attempt2_result.txt (other, 89 bytes) - bundle/logs/fixed_attempt2_server.stderr (other, 0 bytes) - bundle/logs/fixed_attempt2_client.stdout (other, 29 bytes) - bundle/logs/fixed_attempt2_result.txt (other, 91 bytes) - bundle/logs/vuln_variant/variant_matrix_summary.txt (other, 394 bytes) - bundle/logs/vuln_variant/vuln_P1_server.stderr (other, 9172 bytes) - bundle/logs/vuln_variant/vuln_P3_server.stderr (other, 9173 bytes) - bundle/logs/vuln_variant/fixed_P1_server.stderr (other, 0 bytes) - bundle/vuln_variant/validation_verdict.json (other, 4479 bytes) - bundle/vuln_variant/variant_manifest.json (other, 5148 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 9194 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 2223 bytes) - bundle/logs/vuln_variant/vuln_P1_client.stdout (other, 57 bytes) - bundle/logs/vuln_variant/vuln_P1_result.txt (other, 88 bytes) - bundle/logs/vuln_variant/fixed_P1_client.stdout (other, 57 bytes) - bundle/logs/vuln_variant/fixed_P1_result.txt (other, 90 bytes) - bundle/logs/vuln_variant/vuln_P2_server.stderr (other, 9172 bytes) - bundle/logs/vuln_variant/vuln_P2_client.stdout (other, 57 bytes) - bundle/logs/vuln_variant/vuln_P2_result.txt (other, 88 bytes) - bundle/logs/vuln_variant/fixed_P2_server.stderr (other, 0 bytes) - bundle/logs/vuln_variant/fixed_P2_client.stdout (other, 57 bytes) - bundle/logs/vuln_variant/fixed_P2_result.txt (other, 90 bytes) - bundle/logs/vuln_variant/vuln_P3_client.stdout (other, 59 bytes) - bundle/logs/vuln_variant/vuln_P3_result.txt (other, 88 bytes) - bundle/logs/vuln_variant/fixed_P3_server.stderr (other, 0 bytes) - bundle/logs/vuln_variant/fixed_P3_client.stdout (other, 59 bytes) - bundle/logs/vuln_variant/fixed_P3_result.txt (other, 90 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00284 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00284/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00284 ## 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