MOZ_ASSERT(mState == State::Open) on late loser Finish0RTT after Cleanup
Categories
(Core :: Networking: HTTP, defect, P2)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr140 | --- | unaffected |
| firefox-esr153 | --- | disabled |
| firefox152 | --- | disabled |
| firefox153 | --- | disabled |
| firefox154 | --- | fixed |
People
(Reporter: tsmith, Assigned: kershaw)
References
(Blocks 2 open bugs, Regression)
Details
(Keywords: assertion, regression, Whiteboard: [necko-triaged])
Attachments
(1 file)
Initially found with m-c 20260624-3fbd071a3248. I have not been able to reproduce the issue.
Crash signature: MOZ_ASSERT(mState == State::Open) "Finish0RTT declaring winner on a non-Open handle" at netwerk/protocol/http/ZeroRttHandle.cpp:198 (reported as SIGSEGV / SEGV_MAPERR @ 0x0 on Android, which is how MOZ_CRASH manifests there).
Crash stack (socket thread):
mozilla::net::ZeroRttHandle::Finish0RTT ZeroRttHandle.cpp:198
mozilla::net::nsHttpConnection::HandshakeDoneInternal nsHttpConnection.cpp:2648
mozilla::net::TlsHandshaker::HandshakeDone (runnable)
mozilla::net::nsSocketTransportService::Run
Root cause:
ZeroRttHandle is the shared 0-RTT coordinator for one Happy Eyeballs race. Every racer's nsHttpConnection calls Finish0RTT() on the same handle when its TLS handshake completes, so the handle must tolerate a late Finish0RTT from a losing racer after a winner has already been declared and the handle cleaned up.
The loser short-circuit guard checks the wrong field:
// ZeroRttHandle.cpp:188
if (mWinner) {
// Late Finish0RTT on a loser ... ignore
return NS_OK;
}
MOZ_ASSERT(mState == State::Open, ...); // :198 fires
The normal winner path calls Cleanup() (line 285), and Cleanup() -> Transition(State::CleanedUp) nulls mWinner (line 352) to break the RefPtr cycle, while leaving mState == State::CleanedUp. So once a winner has been declared and cleaned up:
mWinneris nullptr, so the guard at line 188 is not takenmStateisCleanedUp, so the assert at line 198 fires
The header documents the field that should have been used: mHadWinner is described as outliving mWinner precisely so callers can distinguish "no winner yet" from "winner declared and then cleaned up". The guard consults mWinner instead.
Two ways to reach it:
-
Winner then loser (most likely): racer A wins ->
WinnerDeclared->Cleanup()nullsmWinner,mState = CleanedUp. Racer B's handshake completes ->Finish0RTT->mWinnernull,mState == CleanedUp-> assert. -
"Real txn gone" path: the first
Finish0RTTtakes the!realTxnbranch (line 243) and callsCleanup()at line 250 without ever settingmHadWinner. A subsequentFinish0RTTthen also passes themWinnerguard and asserts. This variant would NOT be caught by anmHadWinner-based guard; only anmState != State::Opencheck covers both.
Suggested fix:
Key the loser short-circuit off lifecycle state rather than the transient mWinner pointer:
if (mState != State::Open) {
// Winner already declared (and possibly cleaned up), or the handle
// was torn down on the real-txn-gone path. Late loser Finish0RTT: no-op.
return NS_OK;
}
This subsumes the existing mWinner guard and closes both entry paths, making the MOZ_ASSERT(mState == State::Open) at line 198 genuinely unreachable.
I believe this was introduced in Bug 2025276. Adding severity S3, given that Happy Eyeballs is in Nightly only.
@Kershaw would you mind taking a look? Please let me know if you would prefer me to prepare a patch.
| Assignee | ||
Updated•10 days ago
|
Comment 2•10 days ago
|
||
Set release status flags based on info from the regressing bug 2025276
| Assignee | ||
Comment 3•10 days ago
|
||
Updated•10 days ago
|
Updated•10 days ago
|
| Assignee | ||
Updated•9 days ago
|
Comment 8•8 days ago
|
||
| bugherder | ||
Description
•