CVE-2026-16411
Overview
Background
- IndexedDB transaction
- an atomic unit of database work in the DOM
IndexedDBAPI, represented byIDBTransaction, that moves through lifecycle states as requests run. - `ReadyState`
- the
IDBTransactionlifecycle enum whose values (Inactive,Active,Finished) gate whether new requests may be placed against the transaction and whether post-abort safety guards apply. - `TransitionToActive`
- the
IDBTransactionmethod that drains deferred responses and then flips the transaction back to theActivestate so queued work can proceed. - Deferred responses
- request completions queued while the transaction was
Inactiveand replayed byDrainDeferredResponses, whose handlers can run author (page) script.
Root Cause Analysis
The vulnerable path was IDBTransaction::TransitionToActive, which called DrainDeferredResponses() and then unconditionally executed mReadyState = ReadyState::Active. Draining deferred responses can invoke JavaScript event handlers that abort or otherwise finish the transaction, driving mReadyState to Finished as a side effect. The invariant violated is that a transaction which has already been aborted/finished must never be resurrected to Active, because the post-abort guards that reject further operations rely on the state not being Active. By clobbering mReadyState back to Active after such a reentrant abort, the code reopened a finished transaction to further requests, defeating those guards and leading to memory corruption.
The fix asserts the state is Inactive or Finished and only sets Active when it is still Inactive, so a transaction finished during draining stays Finished.
Active state after DrainDeferredResponses could reentrantly run script and abort the transaction; the fix makes the transition conditional on the transaction still being Inactive, preserving the terminal Finished state and keeping post-abort guards effective.Attack Path
- Open a transaction and queue work
Author script starts an
IDBTransactionand issues requests so that responses become deferred while the transaction isInactive. - Trigger draining
The transaction re-activates, entering
TransitionToActiveand callingDrainDeferredResponses(), which replays deferred completions. - Abort during a drained handler
A completion handler runs attacker script that aborts (finishes) the transaction, setting
mReadyStatetoFinishedmid-drain. - State clobbered back to Active
Control returns to
TransitionToActive, which unconditionally setsmReadyState = ReadyState::Active, resurrecting the finished transaction. - Use freed/finished state Subsequent requests bypass post-abort guards and operate on a transaction whose backing state was already torn down, producing memory corruption.
Impact Assessment
IDBTransaction through a reentrant abort during response draining. Exploitation requires no special privileges beyond loading attacker-controlled web content that scripts IndexedDB, and Mozilla presumes such memory corruption could, with effort, be leveraged toward arbitrary code execution.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/indexedDB/IDBTransaction.cpp |
modified |
Files Changed
dom/indexedDB/IDBTransaction.cpp
Audit Directions
- Reentrancy after script-running callsAudit any code that reads or restores object state after a call (like
DrainDeferredResponses) that can dispatch events or run author script, since the object may have been finished or freed during the call. - Unconditional state transitionsFlag lifecycle transitions that write a state value unconditionally rather than guarding on the expected prior state, especially where a terminal state (
Finished) must be sticky. - IndexedDB lifecycle guardsReview other
IDBTransactionand IndexedDB actor methods for post-abort guards that assume the transaction cannot re-enterActive, and verify no path re-activates a finished transaction.
Patch
diff --git a/dom/indexedDB/IDBTransaction.cpp b/dom/indexedDB/IDBTransaction.cpp
index e5afac2e2e4..00216a36798 100644
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -345,7 +345,13 @@ void IDBTransaction::TransitionToActive() {
DrainDeferredResponses();
- mReadyState = ReadyState::Active;
+ // Draining can run script that aborts the transaction; only become
+ // Active again if we're still Inactive so post-abort guards stay effective.
+ MOZ_DIAGNOSTIC_ASSERT(mReadyState == ReadyState::Inactive ||
+ mReadyState == ReadyState::Finished);
+ if (mReadyState == ReadyState::Inactive) {
+ mReadyState = ReadyState::Active;
+ }
}
void IDBTransaction::TransitionToInactiveWithDeferral() {