Skip to content

Add H3 quiche traffic handling tests and provide fixes#13213

Open
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:h3-quiche-boringssl-test-fixes
Open

Add H3 quiche traffic handling tests and provide fixes#13213
bneradt wants to merge 1 commit into
apache:masterfrom
bneradt:h3-quiche-boringssl-test-fixes

Conversation

@bneradt
Copy link
Copy Markdown
Contributor

@bneradt bneradt commented May 29, 2026

Overview

This patch extends the HTTP/3 autest coverage, using curl, Go,
Python/aioquic, and Proxy Verifier HTTP/3 clients to generate their
implementations of H3 traffic. It also adds request and response bodies
of various sizes, including "large" 300k bodies to exercise multiple
packet, buffer, and flow control ATS HTTP/3 implementations. It also
exercises interesting requests and responses, such as HEAD, 204, PUT,
DELETE, OPTIONS, H3-to-H2 origin forwarding, range responses over cached
objects, and malformed HTTP/3 frame behavior. This patch also includes
the various production fixes needed for these tests.

Issues Found and their Fixes

UDP batches could stall large H3 transfers

Large request and response bodies exposed a UDP receive starvation bug
in the UDP read path. On systems using recvmmsg() with
edge-triggered readiness, ATS could read one full batch of datagrams
and then leave the rest queued in the kernel without another readable
event to wake the QUIC stack.

This changes
UDPNetProcessorInternal::read_multiple_messages_from_net() in
src/iocore/net/UnixUDPNet.cc to return whether the kernel supplied a
full batch. udp_read_from_net() now processes a bounded number of
full batches per event, preserving UDP batching for H3 while avoiding
both unread UDP bursts and unbounded net-thread monopolization under
sustained QUIC load.

QUIC stream writes consumed data before quiche accepted it

The stream write path consumed the QUICStreamVCAdapter write reader
inside _read(), before QUICStream::send_data() knew whether
quiche_conn_stream_send() had accepted the bytes. When quiche accepted
only a partial write or returned a flow-control error, ATS could lose
stream data and report write progress too early.

This makes QUICStream::send_data() keep a pending
IOBufferBlock/FIN pair until quiche reports successful consumption,
and only then calls the new QUICStreamAdapter::consume() hook. The
concrete reader accounting lives in QUICStreamVCAdapter::_consume(),
while QUICStream::has_data_to_send(), QUICStream::on_write(), and
QUICNetVConnection::on_stream_updated() make newly writable stream
data schedule packet writes again. This also treats completed finite
writes with only FIN left as writable stream state, so empty bodies and
fully consumed bodies still close the H3 stream cleanly.

QUIC stream reads could expose bytes beyond the VIO request

The new H3-to-H2 and large-body tests exposed that
QUICStreamVCAdapter::_read() could hand more data to the transaction
than the read VIO requested. That was usually hidden by small bodies,
but larger reads and protocol translation made finite request-body
accounting fragile.

This clamps cloned input blocks in QUICStreamVCAdapter::_read() to the
requested and available byte count before filling the read VIO. The
adapter now also checks for a missing reader before touching the read
buffer, which makes late stream cleanup paths more defensive.

H3 transaction cleanup raced with stream closure

The timeout and stream lifetime tests exposed cases where an
HQTransaction could be deleted while an event handler was still
active, or while the QUIC stream adapter still had read/write cleanup to
finish. That left later stream-close and timeout paths touching state
that had already been torn down.

This adds explicit transaction lifetime state in HQTransaction:
_closed, _stream_closed, _event_handler_active, and
_is_write_buffer_flushed(). Http3App::on_stream_close() now calls
HQTransaction::stream_closed() while holding the transaction mutex,
and HQTransaction::_delete_if_possible() waits until the transaction
is done, the stream is closed or no longer readable, and pending writes
have flushed before deleting the transaction.

Malformed H3 streams could leave transactions behind

The aioquic edge-case probes found malformed request streams that were
correctly rejected at the H3 layer but still left partially constructed
transactions attached to the session. Session teardown then either
asserted because the transaction list was not empty or touched the H3
session after Http3Session had already nulled its network connection.

This adds HQSession::_close_transactions() and drains any remaining
transactions before destroying the H3 session-specific state. It also
lets Http3App::on_stream_close() attach a cleanup callback to the
transaction so the application stream map is erased when the transaction
is actually destroyed, rather than when quiche first reports stream
closure.

H3 read completion could run before headers and DATA were settled

The H3 request read path could signal completion before asynchronous
QPACK header decode and buffered DATA delivery had finished updating the
sink VIO. That showed up around HEAD, 204, and stream-close timing
because the HTTP state machine needed a stable view of whether headers
were decoded and whether a request body existed.

This updates Http3HeaderVIOAdaptor::_on_qpack_decode_complete() to add
the printed header length to the sink VIO and notify
Http3Transaction::on_header_decode_complete(), which schedules the
appropriate read event. Http3StreamDataVIOAdaptor::finalize() now uses
a persistent reader, writes buffered DATA into the sink VIO exactly
once, and updates ndone/nbytes consistently before the transaction
is signaled.

Malformed H3 frames were not consistently enforced

The aioquic client can write raw QUIC stream data, which exposed gaps in
ATS's HTTP/3 frame validation. Reserved frames on request streams,
DATA-before-HEADERS, client-created push streams, and duplicate control
streams did not all reliably close the QUIC connection with an H3
application error.

This adds request-stream enforcement through Http3ProtocolEnforcer in
Http3Transaction, recognizes reserved HTTP/3 frame types in
Http3Frame, and routes connection-level errors through
Http3App::_handle_error() and Http3Transaction::_handle_error() to
close the QUIC connection. The transaction signal path now also avoids
calling the HTTP state machine through closed transactions or the
initial zero-byte write VIO created before the HTTP response handler is
installed.

H3-to-H2 origin traffic exposed H2 body accounting bugs

The H3-to-H2 origin coverage found that HEAD and large request-body
translation depended on HTTP/2 knowing both the original request method
and the exact remaining write VIO byte count. Without that, an H2 origin
stream could send DATA past the finite request body or mishandle
no-body HEAD semantics.

This records the sent request method in Http2Stream and uses it when
validating response body framing.
Http2ConnectionState::send_a_data_frame() now caps DATA payloads to
the write VIO ntodo() value and sends END_STREAM when a finite body
has been exhausted, even if the reader has additional buffered bytes.

The QPACK static table had drifted from the standard table

The HEAD, 204, and quic-go coverage exposed that ATS's static QPACK
table was not the table used by external HTTP/3 implementations. The
extra zstd entry and modified accept-encoding value in
src/proxy/http3/QPACK.cc shifted later static indexes, so an
externally encoded :status 204 could decode as a different status.

This restores the standard static table entries by using
accept-encoding: gzip, deflate, br and removing the non-standard
content-encoding: zstd entry. The new 204 cases in
tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml,
tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml, and
tests/gold_tests/h3/replays/h3_server_for_python_client.replay.yaml
cover this interoperability point with Proxy Verifier, quic-go, and
aioquic.

Copilot AI review requested due to automatic review settings May 29, 2026 04:04
@bneradt bneradt added this to the 11.0.0 milestone May 29, 2026
@bneradt bneradt self-assigned this May 29, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves HTTP/3 over QUIC handling for larger bodies and stream lifecycle edge cases, and adds H3 interoperability/timeout coverage around curl, Proxy Verifier, SNI, session tickets, and stream cleanup.

Changes:

  • Keeps QUIC stream write data pending until accepted, schedules packet writes on stream updates, and adjusts H3 transaction cleanup.
  • Drains UDP recvmmsg batches for edge-triggered sockets.
  • Adds/updates HTTP/3 gold tests and replay support for QUIC ports.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/iocore/net/quic/QUICStream.cc Adds pending send-block handling and delayed consumption.
src/iocore/net/quic/QUICStreamVCAdapter.cc Defers write consumption and schedules stream write updates.
src/iocore/net/quic/QUICStreamAdapter.cc Adds explicit consume path.
src/iocore/net/QUICNetVConnection.cc Schedules packet writes when streams update.
src/iocore/net/UnixUDPNet.cc Drains full recvmmsg batches.
src/proxy/http3/Http3Transaction.cc Updates H3 transaction read/write/close lifecycle.
src/proxy/http3/Http3StreamDataVIOAdaptor.cc Buffers and finalizes DATA delivery via persistent reader.
src/proxy/http3/Http3HeaderVIOAdaptor.cc Tracks decoded header bytes and signals decode completion.
src/proxy/http3/Http3App.cc Notifies transactions when streams close.
include/** QUIC/H3 headers Adds APIs and state needed by the implementation changes.
include/iocore/net/quic/Mock.h Updates QUIC mocks for new interfaces.
src/iocore/net/P_UDPNet.h Updates UDP batch-read return type.
src/iocore/net/P_QUICNetVConnection.h Declares stream update callback.
tests/gold_tests/autest-site/ats_replay.test.ext Adds HTTP/3 port wiring for replay tests.
tests/gold_tests/h3/* Adds H3 curl, Proxy Verifier, stream lifetime, timeout, SNI, and session ticket tests/replays.
tests/gold_tests/timeout/* Updates QUIC feature gating for timeout tests.

Comment thread src/iocore/net/quic/QUICStream.cc Outdated
Comment thread tests/gold_tests/h3/h3_proxy_verifier.test.py
Comment thread tests/gold_tests/h3/h3_stream_lifetime.test.py
Comment thread tests/gold_tests/h3/h3_active_timeout.test.py
Comment thread src/iocore/net/quic/QUICStreamVCAdapter.cc Outdated
@zwoop
Copy link
Copy Markdown
Contributor

zwoop commented May 29, 2026

[approve ci autest 2]

@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 778e8bb to f69c806 Compare May 29, 2026 20:10
Comment thread tests/gold_tests/h3/replays/h3_active_timeout.replay.yaml
Comment thread tests/gold_tests/h3/h3_active_timeout.test.py
@bneradt bneradt changed the title Fix H3 quiche large body handling Fix H3 quiche traffic handling May 29, 2026
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from f69c806 to ba62f1b Compare May 29, 2026 22:52
Copilot AI review requested due to automatic review settings May 29, 2026 22:52
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 2 comments.

Comment thread src/iocore/net/quic/QUICStreamVCAdapter.cc Outdated
Comment thread tests/gold_tests/h3/h3_session_ticket.test.py
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from ba62f1b to 6297520 Compare May 29, 2026 23:05
Copilot AI review requested due to automatic review settings May 30, 2026 17:01
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 6297520 to 061dc63 Compare May 30, 2026 17:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 41 changed files in this pull request and generated 2 comments.

Comment thread src/proxy/http3/Http3Session.cc Outdated
Comment thread src/iocore/net/quic/QUICStreamAdapter.cc
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 061dc63 to b9f298a Compare June 5, 2026 03:32
Copilot AI review requested due to automatic review settings June 5, 2026 04:22
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from b9f298a to 13b7dab Compare June 5, 2026 04:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 46 changed files in this pull request and generated 2 comments.

Comment thread tests/gold_tests/autest-site/conditions.test.ext Outdated
Comment thread tests/gold_tests/autest-site/conditions.test.ext
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 13b7dab to 0587b3a Compare June 6, 2026 00:47
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 0587b3a to 39ecc1a Compare June 6, 2026 01:42
Copilot AI review requested due to automatic review settings June 6, 2026 01:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 44 changed files in this pull request and generated 1 comment.

Comment thread src/proxy/http3/Http3App.cc Outdated
@bneradt
Copy link
Copy Markdown
Contributor Author

bneradt commented Jun 6, 2026

[approve ci freebsd]

@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch 2 times, most recently from 30cb52f to eb4e362 Compare June 7, 2026 04:02
Copilot AI review requested due to automatic review settings June 7, 2026 04:02
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 63 out of 64 changed files in this pull request and generated 1 comment.

Comment thread src/proxy/http3/Http3Transaction.cc
@bneradt bneradt changed the title Fix H3 quiche traffic handling Add H3 quiche traffic handling tests and provide fixes Jun 8, 2026
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from eb4e362 to 749b7c8 Compare June 8, 2026 21:14
# Overview
This patch extends the HTTP/3 autest coverage, using curl, Go,
Python/aioquic, and Proxy Verifier HTTP/3 clients to generate their
implementations of H3 traffic. It also adds request and response bodies
of various sizes, including "large" 300k bodies to exercise multiple
packet, buffer, and flow control ATS HTTP/3 implementations. It also
exercises interesting requests and responses, such as HEAD, 204, PUT,
DELETE, OPTIONS, H3-to-H2 origin forwarding, range responses over cached
objects, and malformed HTTP/3 frame behavior. This patch also includes
the various production fixes needed for these tests.

# Issues Found and their Fixes

## UDP batches could stall large H3 transfers
Large request and response bodies exposed a UDP receive starvation bug
in the UDP read path. On systems using `recvmmsg()` with
edge-triggered readiness, ATS could read one full batch of datagrams
and then leave the rest queued in the kernel without another readable
event to wake the QUIC stack.

This changes
`UDPNetProcessorInternal::read_multiple_messages_from_net()` in
`src/iocore/net/UnixUDPNet.cc` to return whether the kernel supplied a
full batch. `udp_read_from_net()` now processes a bounded number of
full batches per event, preserving UDP batching for H3 while avoiding
both unread UDP bursts and unbounded net-thread monopolization under
sustained QUIC load.

## QUIC stream writes consumed data before quiche accepted it
The stream write path consumed the `QUICStreamVCAdapter` write reader
inside `_read()`, before `QUICStream::send_data()` knew whether
`quiche_conn_stream_send()` had accepted the bytes. When quiche accepted
only a partial write or returned a flow-control error, ATS could lose
stream data and report write progress too early.

This makes `QUICStream::send_data()` keep a pending
`IOBufferBlock`/FIN pair until quiche reports successful consumption,
and only then calls the new `QUICStreamAdapter::consume()` hook. The
concrete reader accounting lives in `QUICStreamVCAdapter::_consume()`,
while `QUICStream::has_data_to_send()`, `QUICStream::on_write()`, and
`QUICNetVConnection::on_stream_updated()` make newly writable stream
data schedule packet writes again. This also treats completed finite
writes with only FIN left as writable stream state, so empty bodies and
fully consumed bodies still close the H3 stream cleanly.

## QUIC stream reads could expose bytes beyond the VIO request
The new H3-to-H2 and large-body tests exposed that
`QUICStreamVCAdapter::_read()` could hand more data to the transaction
than the read VIO requested. That was usually hidden by small bodies,
but larger reads and protocol translation made finite request-body
accounting fragile.

This clamps cloned input blocks in `QUICStreamVCAdapter::_read()` to the
requested and available byte count before filling the read VIO. The
adapter now also checks for a missing reader before touching the read
buffer, which makes late stream cleanup paths more defensive.

## H3 transaction cleanup raced with stream closure
The timeout and stream lifetime tests exposed cases where an
`HQTransaction` could be deleted while an event handler was still
active, or while the QUIC stream adapter still had read/write cleanup to
finish. That left later stream-close and timeout paths touching state
that had already been torn down.

This adds explicit transaction lifetime state in `HQTransaction`:
`_closed`, `_stream_closed`, `_event_handler_active`, and
`_is_write_buffer_flushed()`. `Http3App::on_stream_close()` now calls
`HQTransaction::stream_closed()` while holding the transaction mutex,
and `HQTransaction::_delete_if_possible()` waits until the transaction
is done, the stream is closed or no longer readable, and pending writes
have flushed before deleting the transaction.

## Malformed H3 streams could leave transactions behind
The aioquic edge-case probes found malformed request streams that were
correctly rejected at the H3 layer but still left partially constructed
transactions attached to the session. Session teardown then either
asserted because the transaction list was not empty or touched the H3
session after `Http3Session` had already nulled its network connection.

This adds `HQSession::_close_transactions()` and drains any remaining
transactions before destroying the H3 session-specific state. It also
lets `Http3App::on_stream_close()` attach a cleanup callback to the
transaction so the application stream map is erased when the transaction
is actually destroyed, rather than when quiche first reports stream
closure.

## H3 read completion could run before headers and DATA were settled
The H3 request read path could signal completion before asynchronous
QPACK header decode and buffered DATA delivery had finished updating the
sink VIO. That showed up around HEAD, 204, and stream-close timing
because the HTTP state machine needed a stable view of whether headers
were decoded and whether a request body existed.

This updates `Http3HeaderVIOAdaptor::_on_qpack_decode_complete()` to add
the printed header length to the sink VIO and notify
`Http3Transaction::on_header_decode_complete()`, which schedules the
appropriate read event. `Http3StreamDataVIOAdaptor::finalize()` now uses
a persistent reader, writes buffered DATA into the sink VIO exactly
once, and updates `ndone`/`nbytes` consistently before the transaction
is signaled.

## Malformed H3 frames were not consistently enforced
The aioquic client can write raw QUIC stream data, which exposed gaps in
ATS's HTTP/3 frame validation. Reserved frames on request streams,
DATA-before-HEADERS, client-created push streams, and duplicate control
streams did not all reliably close the QUIC connection with an H3
application error.

This adds request-stream enforcement through `Http3ProtocolEnforcer` in
`Http3Transaction`, recognizes reserved HTTP/3 frame types in
`Http3Frame`, and routes connection-level errors through
`Http3App::_handle_error()` and `Http3Transaction::_handle_error()` to
close the QUIC connection. The transaction signal path now also avoids
calling the HTTP state machine through closed transactions or the
initial zero-byte write VIO created before the HTTP response handler is
installed.

## H3-to-H2 origin traffic exposed H2 body accounting bugs
The H3-to-H2 origin coverage found that HEAD and large request-body
translation depended on HTTP/2 knowing both the original request method
and the exact remaining write VIO byte count. Without that, an H2 origin
stream could send DATA past the finite request body or mishandle
no-body HEAD semantics.

This records the sent request method in `Http2Stream` and uses it when
validating response body framing.
`Http2ConnectionState::send_a_data_frame()` now caps DATA payloads to
the write VIO `ntodo()` value and sends END_STREAM when a finite body
has been exhausted, even if the reader has additional buffered bytes.

## The QPACK static table had drifted from the standard table
The HEAD, 204, and quic-go coverage exposed that ATS's static QPACK
table was not the table used by external HTTP/3 implementations. The
extra zstd entry and modified `accept-encoding` value in
`src/proxy/http3/QPACK.cc` shifted later static indexes, so an
externally encoded `:status 204` could decode as a different status.

This restores the standard static table entries by using
`accept-encoding: gzip, deflate, br` and removing the non-standard
`content-encoding: zstd` entry. The new 204 cases in
`tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml`,
`tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml`, and
`tests/gold_tests/h3/replays/h3_server_for_python_client.replay.yaml`
cover this interoperability point with Proxy Verifier, quic-go, and
aioquic.
@bneradt bneradt force-pushed the h3-quiche-boringssl-test-fixes branch from 749b7c8 to eb3339f Compare June 8, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants