Skip to content

HTTP/3: compare stream ids at full width in HQSession lookup - #13635

Draft
brbzull0 wants to merge 1 commit into
apache:masterfrom
brbzull0:h3-full-width-stream-id-lookup
Draft

HTTP/3: compare stream ids at full width in HQSession lookup#13635
brbzull0 wants to merge 1 commit into
apache:masterfrom
brbzull0:h3-full-width-stream-id-lookup

Conversation

@brbzull0

@brbzull0 brbzull0 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

HQSession::get_transaction() (src/proxy/http3/Http3Session.cc:100) matched
transactions with:

if (t->get_transaction_id() == static_cast<int>(id)) {

HQTransaction::get_transaction_id() is declared int (it overrides
ProxyTransaction's, and exists mainly for compact log fields and probe
arguments), while QUICStreamId is a 62-bit value. So the left side narrows on
return and the right side narrows in the cast, and two live transactions whose
stream ids share the low 32 bits resolve to the same entry.

This adds HQTransaction::get_quic_stream_id() and compares against that
instead. It returns _stream_id, the QUICStreamId the constructor already
caches (src/proxy/http3/Http3Transaction.cc:68):

: super(session), _info(info), _stream_id(info.adapter.stream().id())

Reading the id back through _info.adapter.stream() at call time would
reintroduce the use-after-free that #13213 fixed, which is why the accessor uses
the cached member.

get_transaction_id() is left as-is; nothing else compares or routes by it.
QUICStreamId is already visible in Http3Transaction.h via
iocore/net/quic/QUICStreamVCAdapter.h, so no new include is needed.

I checked for other sites narrowing a stream id the same way; this was the only
one in src/proxy/http3/ and include/proxy/http3/.

Test

No new test, and no existing test fails without this change.

The aliasing needs two concurrent transactions whose stream ids differ only
above bit 32. Client-initiated bidirectional stream ids advance by 4, and
initial_max_streams_bidi_in defaults to 100, so a stock server never issues an
id above roughly 400 -- reaching the collision requires a configuration no
default deployment uses. I would rather submit this as hardening than add a test
that only passes under a hand-tuned stream limit.

Run as a no-regression check, 8/8 pass: h3_active_timeout, h3_flow_control,
h3_go_client, h3_proxy_verifier, h3_python_client, h3_sni_check,
h3_stream_lifetime, quic_no_activity_timeout. test_http3 is unchanged at
134 assertions in 15 cases, as expected -- the lookup is not unit tested.

HQSession::get_transaction() matched with
t->get_transaction_id() == static_cast<int>(id). get_transaction_id()
returns int and QUICStreamId is a 62-bit value, so both sides narrow and
two live transactions whose ids share the low 32 bits alias each other.

Add get_quic_stream_id(), returning the QUICStreamId already cached in
_stream_id by the constructor, and compare against that. Reaching
through _info.adapter.stream() instead would reintroduce the
use-after-free fixed by 13213.
@brbzull0 brbzull0 added the HTTP/3 label Sep 3, 2026
@brbzull0 brbzull0 self-assigned this Sep 3, 2026
@brbzull0 brbzull0 added this to the 11.0.0 milestone Sep 3, 2026
@brbzull0

brbzull0 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

[approve ci autest 2]

@brbzull0

brbzull0 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

[approve ci freebsd]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant