ClientQUIC destructor closes the connection before its streams finish shutting down (intermittent abort) #7
Labels
No labels
claude:blocked
claude:done
claude:failed
claude:in-progress
claude:ready
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Catcrafts/Crafter.Network#7
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
~ClientQUICcallsMsQuicConnectionClosewithout waiting for its streams tofinish shutting down. When a connection is destroyed while stream shutdowns are
still in flight, msquic trips a bugcheck and aborts the process.
Reproduces on
master(0827e1f) about 45% of runs with the test below.Impact
Any client that tears a connection down and builds another in the same process
— a reconnect loop, most obviously — can abort instead of reconnecting. It does
not affect a connection that simply stays up, which is probably why it has gone
unnoticed: every QUIC test in this repo ends in
std::_Exit(0)and so neverexercises the teardown path.
I hit this building a long-lived QUIC ingress client, where the reconnect-with-
backoff path destroys a
ClientQUICper attempt.Reproducer
tests/ShouldSurviveConnectionChurn/main.cpp, registered withcfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg });The blocked readers matter. An otherwise identical loop whose
OnStreamhandler reads once and returns passed 20/20 for me — the failure needs threads
parked inside
RecieveSyncwhen the connection goes away, which is the normalshape for a streaming client.
Stack
Diagnosis
QUICStream::Stoponly initiates a graceful shutdown — the comment says sodirectly (
implementations/Crafter.Network-ClientQUIC.cpp:197-205):The real
StreamClosehappens later, inFinalizeClose(
Crafter.Network-ClientQUIC.cpp:91-97), driven by the terminalQUIC_STREAM_EVENT_SHUTDOWN_COMPLETEcallback.~ClientQUIC(Crafter.Network-ClientQUIC.cpp:553-559) does not wait for anyof that:
So
ConnectionClosecan run while streams belonging to that connection arestill open on the msquic side.
Supporting evidence: inserting a 50 ms sleep immediately before the connection
is destroyed took a variant of this from 7/15 to 2/15 failures. It narrows the
window without closing it, which is what you would expect if the race is
"streams have not reached SHUTDOWN_COMPLETE yet".
Suggested fix
Have
ClientQUIC::Impltrack outstanding streams — increment where aQUICStreamis created (ClientQUIC::OpenStream, and the peer-initiated pathin the connection callback), decrement in
FinalizeClose— and have~ClientQUICwait on a condition variable, bounded, for that count to reachzero before
ConnectionShutdown/ConnectionClose.A bounded wait matters: the decrement arrives on an msquic worker thread, so an
unbounded wait would turn a dropped peer into a hung destructor.
Environment
master@0827e1fmain, built by Crafter.Build into the external cachex86_64-pc-linux-gnuRelated, lower priority
ClientQUIC's constructor waits onimpl->cv.wait(...)with no timeout(
Crafter.Network-ClientQUIC.cpp:524), andQUIC_SETTINGS.HandshakeIdleTimeoutMsis never set. Dialling a host that does not answer therefore blocks for minutes
rather than failing. Setting
HandshakeIdleTimeoutMsalongside the existingIdleTimeoutMsinOpenClientConfigurationbounds it. Happy to split this intoits own issue if you would rather keep them separate.