Closed Bug 2052908 Opened 5 days ago Closed 2 days ago

nsHttpChannel waits forever in `AwaitingCacheCallbacks`

Categories

(Core :: Networking: Cache, defect, P1)

defect

Tracking

()

RESOLVED FIXED
154 Branch
Tracking Status
firefox-esr140 --- unaffected
firefox152 --- wontfix
firefox153 --- affected
firefox154 --- fixed

People

(Reporter: kershaw, Assigned: jesup)

References

(Regression)

Details

(Keywords: regression, Whiteboard: [necko-triaged] [necko-priority-queue])

Attachments

(3 files)

Some loads hang permanently. The channel logs nsHttpChannel::Connect … AwaitingCacheCallbacks forces async, then never gets its OnCacheEntryAvailable callback, so the load neither proceeds nor
times out.

These turn out to be address-bar search suggestions (DuckDuckGo autocomplete,
https://ac.duckduckgo.com/ac/?type=list&q=…). The channel attaches as a consumer to a cache entry
that's READY but still flagged "write in progress (stream active)". Since the response is
Cache-Control: no-cache, the channel must revalidate, and cache2 makes it wait until the writer
finishes. But in this session the writer is gone — the entry stays "being written" with no live
transaction behind it and no writer-close event ever firing — so the wait never ends.

Details

Three channels hang end-to-end, all with the same signature (DuckDuckGo autocomplete, no-cache,
entry still "being written"):

Channel q= Cache entry Parked at Resolved?
6b64011800 p 6ae28f1bc0 line 55164, 14:38:35 never
6b64012500 po 6ae28f25e0 line 55317, 14:38:35 never
6b64013f00 pou 6ae28efe80 line 55434, 14:38:35 never

Sequence for 6b64011800 (lines 55098–55164): the entry is already in memory (AsyncOpen [state=READY]);
OnCacheEntryCheck sees cache-control: no-cacheMustValidate() TRUEdoValidation=1; but the
entry reports write in progress (stream active), so cache2 replies not the writer /
consumer will check on the entry again after write is done; the channel then parks at
AwaitingCacheCallbacks forces async. From here it can only be woken by OnWriterClosed → InvokeCallbacks.

That wake-up never comes:

  • No OnWriterClosed, output-stream close, SetValid, Doom, or OnStopRequest appears anywhere in
    the 324k-line log for any of the three entries.
  • The ac.duckduckgo.com:443 connection entry (ent=6b2fa428d0) shows active=0 idle=0 pending=0
    the whole time — there's no live transaction writing these entries. They're effectively zombie
    writers.
  • The Cache2 I/O thread keeps trying to Purge 6ae28f1bc0 for ~50s but it never frees.
  • The three channels stay parked from 14:38:35 to end-of-log (~54s) and never reappear.

Most likely an earlier suggestion request for each URL became the cache writer, then got cancelled as
the user kept typing — without closing its output stream or dooming the entry. That leaves the entry
permanently "being written", and because suggestions are no-cache, every later request for the same
URL has to revalidate, can't use the pending entry, and waits on a write that never completes. There's
also no timeout on a channel stuck in the cache-wait state, so nothing breaks the deadlock.

(Note: 6af5e58600, 6ae2d8d600, 6ae2d87b00 briefly hit the same bypass around 14:39:00 but
recovered when fresh writers ran, so they aren't counted here.)

Suspected code (leads, not a confirmed fix)

  • netwerk/cache2/CacheEntry.cpp — the concurrent-write bypass in InvokeCallback, and the writer
    teardown path (OnWriterClosed / output-stream close) which doesn't seem to run when a writer is
    cancelled, so the entry is never released or doomed.
  • netwerk/protocol/http/nsHttpChannel.cpp — the Connect / OnCacheEntryAvailable cache-wait path
    has no timeout on mCacheEntriesToWaitFor (AwaitingCacheCallbacks()), so a dead writer wedges the
    consumer forever.

Appendix — key excerpt (6b64011800, entry 6ae28f1bc0, lines 55098–55164)

55098 nsHttpChannel::OpenCacheEntry [this=6b64011800]
55099 AddStorageEntry [entryKey=:https://ac.duckduckgo.com/ac/?type=list&q=p]
55101 CacheEntry::AsyncOpen [this=6ae28f1bc0, state=READY, flags=16, callback=6b64012208]
55108 CacheEntry::InvokeCallback [this=6ae28f1bc0, state=READY, cb=6b64012208]
      ... cached response: HTTP/2 200, cache-control: no-cache ...
55140 write in progress (stream active)
55149 Must validate since response contains 'no-cache' header
55151 OnCacheEntryCheck exit [this=6b64011800 doValidation=1 result=1]
55155 not the writer
55157 consumer will check on the entry again after write is done
55164 nsHttpChannel::Connect 6b64011800 AwaitingCacheCallbacks forces async

The q=po (6b64012500) and q=pou (6b64013f00) blocks are identical apart from IDs.

104 ConnectionEntry::TimeoutTick() host=ac.duckduckgo.com idle=0 active=0 pending=0   (same throughout)

Bug 2030021 (FF 152, May 2026) introduced a new suspend-after-examine-response window for tracker channels that made a suspended-and-cancelled cache writer readily reachable. The LOAD_BACKGROUND flip (Bug 2046419, FF 153, Jun 2026) is a plausible aggravator of timing but is later and secondary; the 152 trigger is Bug 2030021.

nsHttpChannel::CancelInternal gates the teardown on suspension:

if (needAsyncAbort && !mCallOnResume && !mSuspendCount) {
  CloseCacheEntry(false);   // dooms a write-only entry
  (void)AsyncAbort(status); // drives OnStopRequest -> tee closes the output stream
}

If the channel is suspended when aborted, both are deferred to Resume(). CancelNetworkRequest cancels the transaction, but the suspended pump delivers no OnStopRequest, so the cache output stream stays registered, so DataSize() keeps returning IN_PROGRESS, so the entry is stuck "being written." If Resume() never fires, it's a permanent zombie writer.

What suspends the writer mid-write: Bug 2030021, part 7 — "Allow network request to proceed while annotating trackers", landed May 11 in 152. For ETP-classified channels (a DuckDuckGo autocomplete host plausibly qualifies), ProcessResponse now calls MaybeSuspendAfterExamineResponse() which Suspend()s right after http-on-examine-response, while the async tracker annotation completes; CancelSuspendOrResumeAfterExamineResponse() later Resume()s. The synchronous unwind through ContinueProcessResponse1 still opens the cache output stream, so the writer ends up READY + stream-active and suspended -- the precondition CancelInternal fails to tear down. The user types the next character, the controller abort()s the XHR, and if the annotation Resume is lost, the writer wedges forever.

This feature is part of a rollout, so that may explain why some people never see it, and others do. We could check if users hitting the problem are part of the rollout (privacy.trackingprotection.defer_annotation.enabled = true).

The base vulnerability predates that; bug 1966494 partially patched the hole, but only WRITING/REVALIDATING, leaving the READY/DataSize path uncovered. I think the vulnerability goes way back: The RECHECK_AFTER_WRITE_FINISHED wait keyed solely on CacheFile::mOutput, with no consumer-side timeout, is from the original cache2 design. It has always been possible to wedge this; it just required a writer to vanish without closing its output stream

Keywords: regression
Regressed by: 2046419

A channel that opens a cache entry asynchronously parks in AwaitingCacheCallbacks
until OnCacheEntryAvailable fires. Nothing bounds that wait, so if the entry is
left perpetually "being written" (e.g. by a writer that was suspended or
cancelled without ever closing its output stream), the load neither proceeds nor
times out.

Add a one-shot backstop timer, armed while the channel is parked waiting for the
cache entry and cancelled once the callback arrives. When it fires the channel
stops waiting and races to the network so the load can make progress instead of
hanging forever. The timeout is controlled by network.cache.entry_wait_timeout_ms
(default 30s, 0 disables) and is kept generous so it never fires for legitimately
slow concurrent reads/writes of large resources.

Assignee: nobody → rjesup
Status: NEW → ASSIGNED

When a cache entry writer is suspended or stalled for too long, the suspend
timer sets the entry writer-lock bypass (SetBypassWriterLock). That already lets
new callbacks through the WRITING/REVALIDATING gate, but a consumer that has
already passed OnCacheEntryCheck and is waiting for the write to finish
(RECHECK_AFTER_WRITE_FINISHED) is gated solely on the data being complete, so it
keeps getting re-queued and never wakes -- the load hangs forever.

Honor mBypassWriterLock on that path too: when the bypass is set and the data is
still in progress, hand the consumer the entry as "not wanted"
(NS_ERROR_CACHE_KEY_NOT_FOUND) so it falls back to the network instead of
deadlocking.

Adds gtests for the cache-layer behavior (a bypassed writer lock releases a
parked consumer; dooming a being-written entry releases a parked consumer) and
an xpcshell test that reproduces the end-to-end hang: a no-cache writer stranded
mid-write must not wedge a later same-URL request.

When a channel is suspended after examining the response to await asynchronous
tracking-protection annotation (bug 2030021) and is then cancelled while still
suspended, nsHttpChannel::CancelInternal defers the terminal teardown
(CloseCacheEntry / AsyncAbort) to a Resume() that may never arrive. For a cache
writer this strands the entry perpetually "being written" -- its output stream
never closed and the entry never doomed -- wedging every later same-URL
revalidating consumer.

Undo the annotation suspension in CancelInternal so the cancelled pump delivers
OnStopRequest and the write-only entry is closed/doomed through the normal path.
This is a no-op if the channel only primed but never actually suspended.

Regressed by: 2030021
No longer regressed by: 2046419
See Also: → 2030021

Set release status flags based on info from the regressing bug 2030021

Attachment #9604945 - Attachment description: Bug 2052908 - Add a backstop timeout for a channel wedged waiting on a cache entry. r=#necko-reviewers → Bug 2052908 - Add a backstop timeout for a channel wedged waiting on a cache entry. r=#necko-reviewers!
Attachment #9604946 - Attachment description: Bug 2052908 - Release cache consumers parked behind a bypassed writer lock. r=#necko-reviewers → Bug 2052908 - Release cache consumers parked behind a bypassed writer lock. r=#necko-reviewers!
Attachment #9604947 - Attachment description: Bug 2052908 - Tear down a suspended cache writer on cancel instead of stranding it. r=#necko-reviewers → Bug 2052908 - Tear down a suspended cache writer on cancel instead of stranding it. r=#necko-reviewers!
Pushed by rjesup@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/32cd886d24f7 https://hg.mozilla.org/integration/autoland/rev/24c1fb2846bd Add a backstop timeout for a channel wedged waiting on a cache entry. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/79b80fa439c6 https://hg.mozilla.org/integration/autoland/rev/0314e6f940c0 Release cache consumers parked behind a bypassed writer lock. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/6534b43a8571 https://hg.mozilla.org/integration/autoland/rev/54ee0a472b49 Tear down a suspended cache writer on cancel instead of stranding it. r=necko-reviewers,kershaw
Pushed by amarc@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/0e0ffb0c8371 https://hg.mozilla.org/integration/autoland/rev/8ea9affe668f Revert "Bug 2052908 - Tear down a suspended cache writer on cancel instead of stranding it. r=necko-reviewers,kershaw" for causing build bustages @ nsHttpChannel

Backed out for causing bustages + lint failure

Flags: needinfo?(rjesup)
Pushed by rjesup@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/db1058fc7b7c https://hg.mozilla.org/integration/autoland/rev/1b7354298da8 Add a backstop timeout for a channel wedged waiting on a cache entry. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/e33401288a0b https://hg.mozilla.org/integration/autoland/rev/2d0b98cb3c12 Release cache consumers parked behind a bypassed writer lock. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/8fe56c934ac9 https://hg.mozilla.org/integration/autoland/rev/e619d761407e Tear down a suspended cache writer on cancel instead of stranding it. r=necko-reviewers,kershaw
Pushed by pstanciu@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/7aaaf9a1605b https://hg.mozilla.org/integration/autoland/rev/5d0c6c6f1eb8 Revert "Bug 2052908 - Tear down a suspended cache writer on cancel instead of stranding it. r=necko-reviewers,kershaw" for causing es lint failures @ test_cache2-42-no_cache_writer_hang.js

Backed out for causing es lint failures @ test_cache2-42-no_cache_writer_hang.js

(In reply to pstanciu from comment #11)

Backed out for causing es lint failures @ test_cache2-42-no_cache_writer_hang.js

Also xpschells failures
bc failures

Pushed by rjesup@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/d7cb51189708 https://hg.mozilla.org/integration/autoland/rev/523b4bff3f68 Add a backstop timeout for a channel wedged waiting on a cache entry. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/9b8aebe2a0a5 https://hg.mozilla.org/integration/autoland/rev/37b559f94f11 Release cache consumers parked behind a bypassed writer lock. r=necko-reviewers,kershaw https://github.com/mozilla-firefox/firefox/commit/3f62e7e8d0ca https://hg.mozilla.org/integration/autoland/rev/5bf042dd3c53 Tear down a suspended cache writer on cancel instead of stranding it. r=necko-reviewers,kershaw
Flags: needinfo?(rjesup)
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: