Skip to content

H3wire p2 mq h3 - #43

Merged
mp0rta merged 33 commits into
mainfrom
h3wire-p2-mq-h3
Oct 6, 2026
Merged

mp0rta merged 33 commits into
mainfrom
h3wire-p2-mq-h3

Conversation

@mp0rta

@mp0rta mp0rta commented Oct 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

mp0rta added 30 commits October 6, 2026 09:43
h3wire emits no StreamAborted once Finished went out, so h3_reset on a
request whose receive side had finished left the request un-aborted with
its DATA frame in flight; the next h3_send_body wrote to a stream h3wire
had reaped (data_written: UnknownStream, a debug_assert). h3_reset now
marks the request aborted itself, as it already did without h3wire state
(adoption spec §4.3). Found by the C7 property test.
A peer RESET_STREAM after the request's FIN (xqc_h3 sends one on any
cancel) makes h3wire reset our send side, but after Finished it emits no
StreamAborted. H3Wire kept its DATA frame in flight and the next
h3_send_body wrote to a reaped stream (data_written: UnknownStream, a
debug_assert), or got Stale from send_data. It now ends the send side as
SendStopped does: H3Writable, then the client discards and the server
gets Err(Reset) (adoption spec §4.5). Same root cause as the h3_reset
fix, reached from the peer.
random_ops_hold_invariants (adoption spec §6.1): 200 cases of up to 64
steps over the Rig, both roles. Steps: peer HEADERS / body / FIN / reset
/ raw reset / STOP_SENDING / GOAWAY, the gateway's h3_* ops (keeping its
re-offer contract), transport StreamWritable / StreamClosed / close, send
limits and pumps. After every step debug_bounds_hold() (new, behind
test-support: carry <= max(BOOT_READ, largest buf), h3wire buffered <=
bound); throughout, one H3Request and at most one H3Closed per request,
nothing after it, and H3Closed once the transport closed the stream and
the gateway has its receive end; at the end, after the close and a
drain, exactly one H3Closed per known request.

The Rig models what xquic can deliver: StreamClosed (after
StreamCloseStats) only once both directions are over, every stream's
close before ConnClosed on the close step, one ConnClosed, no traffic
after it, open_stream Stale after it, and w's resets / STOP_SENDING
relayed to the peer.
A response to a request the peer already reset (aborted before
Finished, or send side stopped after it) reached h3wire's send_headers
on a reaped stream: UnknownStream, mapped to Err(Stale) for a request
H3Wire still holds. send_headers now returns Err(Reset) once the request
is aborted or its send side stopped, as h3_send_body does (adoption spec
§4.5). Found by the property test's new Stale check.
- PROPTEST_CASES overrides the case count (default 200) for deep runs.
- peer_reset_after_finished_client_discards: client role of the peer
  RESET after Finished (spec §4.5): upload accepted and discarded,
  (0, true) still read, one H3Closed.
- The drain runs in request order, so a shrunk case replays the same.
- PeerRawReset mostly targets a stream past the peer's FIN, where h3wire
  may be Finished; a new check makes Err(Stale) before H3Closed a
  failure. Together they catch a revert of the peer-reset fix in the
  default 200 cases.
- Model: a synthetic peer RESET / STOP_SENDING only on a stream the
  peer knows; the gateway sends body and finish only after its headers
  and nothing after its FIN.
Passthrough H3 events and conn-level readiness/creation events (both modes) were popped
without a liveness check, so one that went stale between the inner drain and the pop reached
the app, unlike the bare transport. H3 events now probe h3_req_info in passthrough, conn
events probe conn_stats; close events still always pop (adoption spec §4.1).
`matrix!` runs a scenario once per (client, server) backend cell
(xx/xw/wx/ww). Every compliant pair_gateway scenario and pair_h3's
h3_40k_section_both_ways now run in all four cells (adoption spec §6.2).
Request paths carry the cell tag so no cell can satisfy another's log
marker; the 403 drain line (path "-") is keyed by a per-cell body length.

Per-cell expectations from spec §5.3 (6): on a Wire server,
content-length 10 with FIN on HEADERS is reset before the gateway sees
the request, so the cell asserts a nonzero stream_err and an untouched
origin. The direct-H3 CONNECT is now the valid authority form, which
reaches the gateway's 400 in every cell.
The malformed producers move to RawH3Peer and run against an xqc_h3 and
a Wire receiver (`receivers!`, cells x / w; adoption spec §6.2):

- a response DATA frame cut by FIN: the fetch socket is reset in both
  cells; h3wire closes the conn with H3_FRAME_ERROR (spec §5.3 (1));
- the same cut on a request: xqc_h3 logs the gateway's reset; on Wire
  the conn closes with H3_FRAME_ERROR and the fan-out logs it once;
- NUL in x-mq-auth / :path / x-mq-class and a CONNECT with :scheme /
  :path: xqc_h3 keeps today's 403 / 400 / `?` and 400; h3wire resets
  with H3_MESSAGE_ERROR and the gateway logs nothing (spec §5.3 (6)).

fetch_short_cl_response_aborts_client joins the 4-cell matrix.
EchoMode::CutResponseFrame, H3Script.truncate_after_partial, CUT_BODY
and H3Recorded.accepted lose their last users and are deleted.
Every matrixed pair_gateway request that the server logs now waits for
its mq.req line before shutdown and asserts the full record, field for
field (adoption spec §6.2). `assert_req` takes one expected record per
scenario: a timing field written `T` needs a value >= 0 (its stamps were
set), and -1 stays exact. The §5.3 (6) differences are written per cell:
`req_bytes` 50 on an xqc_h3 server and 0 on a Wire server for the short
and excess uploads.

Lines with `path="-"` are keyed by a per-cell `req_bytes`: the 403 drain
as before, and the direct-H3 CONNECT, which now carries a `cell + 1` byte
body. `cell_index(cells)` is the one source of that index. On a Wire
server the client's `stream_err` is asserted as H3_MESSAGE_ERROR, and the
cl-with-FIN cell also asserts that no `:status` and no FIN arrived.
Every call that takes `now` serviced every H3 conn, so a tunnel
stream op or a UDP-lane datagram paid O(H3 conns x stream map). Only
conns whose h3wire state an op touched, or that still wait on the
transport (a pending FIN, a short core-bytes write), are swept now;
event handlers keep servicing their own conn. The outer queue keeps its
level events as (kind, slot) flags instead of scanning the queue.
A gateway that offered `fin` has ended its body: a re-offer after a
Blocked or partial h3_send_body keeps the fin and adds no bytes. The
model dropped it, so H3Wire finished the stream the gateway still
believed open and a later contract-breaking h3_send_headers got Stale
before H3Closed (a PROPTEST_CASES=30000 run hit it at d4ff0b6).
spawn_mitm_on takes the (client, server) backends; spawn_mitm keeps the
xqc_h3 pair. Every pair_mitm scenario becomes a matrix! test. The cells
of a test that waits for an opaque-route log line run one at a time,
each waiting for a line logged after it started, since the line is the
same in every cell.

mq-h3: the MITM's empty-buffer terminal probe gets Rig tests: a bare
FIN gives (0, true) as on xqc_h3; framing alone before the FIN stays
Blocked (no byte fits an empty slice), which xqc_h3 answers (0, true).
No request exists for it on a client, so h3wire was never fed, no
H3_STREAM_CREATION_ERROR was raised (RFC 9114 §6.1) and the stream never
retired. Its NewStream now feeds h3wire an empty read, which closes the
connection.
A peer RESET that arrived after the transport FIN was read into the
carry, but before h3wire reached Finished, aborted a complete response;
one h3_recv_body later the same reset was handled as after Finished
(adoption spec §5.3 (7)). The code is now held and passed to h3wire
once the feed has given it the FIN.
`reqs_of` collects a conn's requests for both fan-outs; `Req::stop_send`
queues the H3Writable that every caller pushed after it; Req has one
impl block. The latched-finish comment in `flush` now says that Closed
and GoingAway drop it silently.
adoption spec §6.2 compares status and headers. Every matrixed
pair_gateway scenario now asserts the full response header list, the
fetch reply's and the direct H3 response's, against one list with
`date` normalised; h3_40k_section_both_ways compares both sections
exactly. All four cells agree: no difference to write in.
A raw peer sends a request HEADERS frame over 64 KiB encoded (h3wire
closes the connection with H3_EXCESSIVE_LOAD; xqc_h3 resets the stream
with H3_MESSAGE_ERROR), a request stream with a FIN and no HEADERS
(h3wire resets with H3_REQUEST_INCOMPLETE; xqc_h3 drops it silently),
and a response that ends without a final response, bare or after a 103
(h3wire aborts it and the fetch client answers 502 upstream-reset;
xqc_h3 relays the 103 as the final response).
…port

The origin connect succeeds, so origin_tls=connect_fail and
origin_connect_ms=-1 are an existing gateway misreport outside this
plan; a later fix to them is not a regression.
Tiny DATA frames made every feed memmove the rest of the carry. The carry is
refilled only when empty, so a cursor needs no compaction; the bound is
unchanged.
service() called h3wire's sendable(), a scan of every live stream, on each
request event and body op. h3wire's core bytes come from bind_uni,
send_headers and GOAWAY, so keep a per-conn set (our uni streams plus
requests with undrained HEADERS) and flush just those.
mp0rta added 3 commits October 6, 2026 14:09
H3Wire::poll_event used conn_stats (hash lookup, snprintf, malloc on xquic) to
check conn liveness of every conn-level pop. conn_live is the slot-table
check; Transport, ScriptedTransport and H3Wire implement it. No default
body: no impl lives under mq-proxy/src, so the gateway is untouched.
… requests

assert_eq!(c.handle.lock().x, v, "{:?}", c.handle.lock()) held the guard
through the failure branch and relocked it: a wrong code hung. One guard now,
scoped to the assert.

The malformed-request checks searched the log for the submitted path, which a
gateway line with path="-" (before intake) does not carry. spawn_gateway_on
pairs now count the H3Request events the server app is handed; the h3wire
receiver must have none. xqc_h3 does hand over the stream before decoding, so
it keeps the path search.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Deploying mqproxy with  Cloudflare Pages  Cloudflare Pages

Latest commit: fff9b3c
Status: ✅  Deploy successful!
Preview URL: https://c9e40f5d.mqproxy.pages.dev
Branch Preview URL: https://h3wire-p2-mq-h3.mqproxy.pages.dev

View logs

@mp0rta
mp0rta merged commit e5751d5 into main Oct 6, 2026
44 of 45 checks passed
@mp0rta
mp0rta deleted the h3wire-p2-mq-h3 branch October 6, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant