IndexedDB heap use-after-free in [@ FileInfo::UpdateReferences] — ReleaseSavepoint fails to reset mSavepointDelta
Categories
(Core :: Storage: IndexedDB, defect)
Tracking
()
People
(Reporter: bugmon, Assigned: hsingh)
Details
(6 keywords, Whiteboard: [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r][adv-main149.0.2+r])
Attachments
(9 files)
|
3.75 KB,
text/plain
|
Details | |
|
3.05 KB,
text/html
|
Details | |
|
2.06 KB,
patch
|
Details | Diff | Splinter Review | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
1.27 KB,
patch
|
Details | Diff | Splinter Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr115+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-release+
|
Details | Review |
Heap Use-After-Free in FileInfo<DatabaseFileManager> via Savepoint Delta Desynchronization
Vulnerability Class
Heap use-after-free in the Firefox parent process (sandbox-escape class), triggered by unprivileged web content using only standard IndexedDB APIs. No patches, no custom prefs, no external services required.
Root Cause
DatabaseFileInfo.cpp instantiates template class FileInfo<DatabaseFileManager>. The vulnerable function is FileInfo::UpdateReferences in FileInfoImpl.h:
void FileInfo<FileManager>::UpdateReferences(ThreadSafeAutoRefCnt& aRefCount,
const int32_t aDelta, ...) {
{
AutoLockType lock(FileManager::Mutex());
aRefCount = aRefCount + aDelta; // line 72: 1 + uintptr_t(-2) = UINTPTR_MAX
if (mRefCnt + mDBRefCnt > 0) { // line 74: 1 + UINTPTR_MAX wraps to 0 → false
return;
}
mFileManager->RemoveFileInfo(Id(), lock);
...
}
...
delete this; // line 95: premature free, SafeRefPtr still alive
}
mRefCnt and mDBRefCnt are ThreadSafeAutoRefCnt → nsrefcnt → uintptr_t (unsigned). If aDelta is more negative than the current count, arithmetic underflows silently. When the sum of the two unsigned counters wraps to exactly zero, the guard at line 74 fails and the object is deleted — even though mRefCnt == 1 indicates a live SafeRefPtr still exists.
How a Negative Over-Delta Reaches This Function
The primitive is a logic bug in ActorsParent.cpp's savepoint handling:
void DatabaseConnection::UpdateRefcountFunction::ReleaseSavepoint() {
mSavepointEntriesIndex.Clear(); // BUG: does NOT reset mSavepointDelta on entries
mInSavepoint = false;
}
Compare with the correct RollbackSavepoint(), which loops over entries and calls entry->ResetSavepointDelta() before clearing. ReleaseSavepoint() only clears the index (raw pointers), but the FileInfoEntry objects persist in mFileInfoEntries with stale non-zero mSavepointDelta values. When the next operation in the same transaction starts a new savepoint and touches the same file, IncDeltas(true) adds to the stale value. If that operation then fails and rolls back, DecBySavepointDelta() subtracts the inflated stale delta from mDelta, over-correcting it.
Full Exploit Chain
Phase 1 — Desync Creation (Transaction T1)
-
Setup (T0): Store a blob at key
k0withidx: "u0". The SQLitefiletable hasrefcount = 1, in-memorymDBRefCnt = 1, one row referencesfile_id 1. -
Get file-backed blob:
get("k0")returns a blob backed byRemoteLazyInputStream. When this blob isput()back, the parent finds it inmMappedBlobsand reuses the samefile_id 1— no new file is created. -
Op A —
put({idx: "u1", b: fbBlob}, "k1")succeeds:stmt->Execute()at ActorsParent.cpp:19328 firesobject_data_insert_trigger.OnFunctionCall→ProcessValue(Increment)onfile_id 1→FileInfoEntrycreated inmFileInfoEntries:mDelta = +1,mSavepointDelta = +1.InsertIndexTableRows()succeeds ("u1"is unique).autoSave.Commit()→ReleaseSavepoint()→ index cleared,mSavepointDeltaremains+1.
-
Op B —
add({idx: "u0", b: fbBlob}, "k2")fails on the index:stmt->Execute()succeeds (main table has no conflict atk2). Trigger fires again on samefile_id 1.- Same
FileInfoEntryfound:IncDeltas(true)→mDelta = +2,mSavepointDelta = +1 (stale) + 1 = +2. InsertIndexTableRows()at line 19348 fails —idx: "u0"already used byk0→SQLITE_CONSTRAINT.- Early return skips
autoSave.Commit().~AutoSavepoint()destructor runsRollbackSavepoint():entry->DecBySavepointDelta()→mDelta = 2 - 2 = 0(should have been2 - 1 = 1).entry->ResetSavepointDelta().
-
preventDefault() on Op B's error event → transaction is not aborted, it commits.
-
Commit with
mDelta = 0:WillCommit():mDelta == 0→ SQLitefile.refcountstays1.DidCommit():mDelta == 0→ in-memorymDBRefCntstays1.Reset(): drops theSafeRefPtrnormally (mRefCntgoes to 0 butmDBRefCnt = 1 > 0, object survives).
Resulting persistent desync: object_data now has 2 rows (k0, k1) whose file_ids both reference file_id 1, but file.refcount = 1 and mDBRefCnt = 1. This corruption is written to SQLite on disk.
Phase 2 — UAF Trigger (after page reload)
-
Page reload destroys all content-process
IDBDatabaseobjects → allPBackgroundIDBDatabaseFileactors torn down → allDatabaseFileparent actors release theirSafeRefPtr<DatabaseFileInfo>→mRefCntdrops to0. TheDatabaseFileManageris cached inIndexedDatabaseManager(ActorsParent.cpp:15517) and survives;mDBRefCnt = 1persists in memory. -
Reopen DB →
GetFileManager()returns the cached instance (alreadyInitialized(), soInit()is skipped — refcounts are not re-read from disk). -
clear()in T2: DELETE firesobject_data_delete_triggertwice (once fork0, once fork1) on the samefile_id 1.- First fire:
FileInfoEntrycreated. Constructor takes aSafeRefPtr(GetFileInfo()→LockedAddRef()) →mRefCnt = 1.DecDeltas(true)→mDelta = -1. - Second fire: same entry,
mDelta = -2.
- First fire:
-
CommitOp::Run:
WillCommit(): updates SQLitefile.refcountby-2→1 - 2 = -1→ row deleted (refcount <= 0cleanup trigger).- SQLite
COMMITsucceeds. DidCommit()at line 17659 →MaybeUpdateDBRefs()→UpdateDBRefs(-2)→UpdateReferences(mDBRefCnt, -2):mDBRefCnt = 1 + uintptr_t(-2) = 0xFFFFFFFFFFFFFFFFmRefCnt + mDBRefCnt = 1 + 0xFFFFFFFFFFFFFFFF = 0(unsigned wraparound)0 > 0→ false → fall through →RemoveFileInfo()→Cleanup()→delete thisat FileInfoImpl.h:95.
- The
FileInfoEntryinmFileInfoEntriesstill holds the now-danglingSafeRefPtr. FinishWriteTransaction()at line 17675 →Reset():entry->ReleaseFileInfo().forget().take()extracts the raw dangling pointer.fileInfo->Release(true)at line 7652 →UpdateReferences(mRefCnt, -1, true)→ line 72 readsmRefCnt→ heap-use-after-free.
Crash Confirmation
==175411==ERROR: AddressSanitizer: heap-use-after-free on address 0x77dd48423ba8
READ of size 8 at 0x77dd48423ba8 thread T45
#3 FileInfo<DatabaseFileManager>::UpdateReferences(...) FileInfoImpl.h:72:17
#5 DatabaseConnection::UpdateRefcountFunction::Reset() ActorsParent.cpp:7652:15
#6 DatabaseConnection::FinishWriteTransaction() ActorsParent.cpp:7001:30
#7 TransactionBase::CommitOp::Run() ActorsParent.cpp:17675:19
freed by thread T45 here:
#1 FileInfo<DatabaseFileManager>::UpdateReferences(...) FileInfoImpl.h:95:3
#3 FileInfoEntry::MaybeUpdateDBRefs() ActorsParent.cpp:1333:18
#4 DatabaseConnection::UpdateRefcountFunction::DidCommit() ActorsParent.cpp:7584:12
#5 TransactionBase::CommitOp::Run() ActorsParent.cpp:17659:37
- Parent process (
crashed_parent: true) — sandbox escape class. - Same thread (T45) for free and use — fully deterministic, not a race.
- Free at
CommitOp::Run:17659, use atCommitOp::Run:17675— sequential calls in a single runnable. - 32-byte allocation matches
FileInfolayout (8-bytemFileId+ 8-bytemRefCnt+ 8-bytemDBRefCnt+ 8-byteSafeRefPtr mFileManager). Offset 8 =mRefCnt.
Verifier Verdict
{
"verdict": "approved",
"reasoning": "Confirmed heap-use-after-free in the Firefox parent process triggered by pure web content JavaScript with no patches, prefs, or external services. The root cause is a clear logic bug: ReleaseSavepoint() fails to reset mSavepointDelta on FileInfoEntry objects that persist in mFileInfoEntries, causing refcount desynchronization that later underflows an unsigned refcount and triggers premature deletion while a SafeRefPtr still holds the pointer. Free and use occur deterministically on the same thread in the same Run() call.",
"concerns": []
}
Suggested Fix
void DatabaseConnection::UpdateRefcountFunction::ReleaseSavepoint() {
MOZ_ASSERT(mConnection);
mConnection->AssertIsOnConnectionThread();
MOZ_ASSERT(mInSavepoint);
for (const auto& entry : mSavepointEntriesIndex.Values()) {
entry->ResetSavepointDelta(); // ← ADD: mirror RollbackSavepoint's cleanup
}
mSavepointEntriesIndex.Clear();
mInSavepoint = false;
}
A defense-in-depth fix in FileInfoImpl.h would also assert that aRefCount >= -aDelta before applying negative deltas, and use a signed intermediate or saturating arithmetic for the mRefCnt + mDBRefCnt > 0 check.
| Reporter | ||
Comment 1•4 months ago
|
||
| Reporter | ||
Comment 2•4 months ago
|
||
Updated•4 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 4•3 months ago
|
||
The testcase uses FuzzingFunctions.garbageCollect()/cycleCollect() but only to reliably tear down the content-side IDB actors between phases — the underlying savepoint-delta / refcount-underflow bug is default-on IndexedDB parent code with no pref gate, and db.close() + reload + natural GC would achieve the same precondition. Not an unsupported configuration.
Updated•3 months ago
|
Comment 5•3 months ago
|
||
Comment 6•3 months ago
|
||
Reproduced the parent-process heap-use-after-free on current origin/main with the attached testcase; no longer reproduces with this patch applied.
Root cause fix (ActorsParent.cpp): UpdateRefcountFunction::ReleaseSavepoint() cleared mSavepointEntriesIndex (raw pointers) without resetting mSavepointDelta on the underlying FileInfoEntry objects, which persist in mFileInfoEntries across savepoints. A subsequent savepoint that touches the same file adds to the stale value; if that savepoint rolls back, DecBySavepointDelta() over-subtracts. The fix mirrors RollbackSavepoint(): iterate the index and call ResetSavepointDelta() before clearing. All other mSavepointEntriesIndex mutation sites were audited — no other path leaves mSavepointDelta stale.
Defense-in-depth (FileInfoImpl.h):
MOZ_DIAGNOSTIC_ASSERTthat a negative delta does not exceed the current count.- The liveness check
mRefCnt + mDBRefCnt > 0is changed tomRefCnt > 0 || mDBRefCnt > 0. These are equivalent for well-formed non-negative counts, but the summed form wraps to 0 when one count has underflowed toUINTPTR_MAXand the other is1. The disjunctive form makes an underflowed count keep the object alive — degrading any future bug of this class from UAF to leak.
This is the analysis tool's suggested fix. Feel welcome to adopt it as a starting point and evolve it as needed to meet our coding standards.
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 7•3 months ago
|
||
| Assignee | ||
Comment 8•3 months ago
|
||
Comment on attachment 9559260 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: It's fairly easy and can be done by running a js attached on the bug.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: ESR, release and beta
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: No
- If not, how different, hard to create, and risky will they be?: should be easy to backport.
- How likely is this patch to cause regressions; how much testing does it need?: low risk.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
| Assignee | ||
Comment 9•3 months ago
|
||
[Tracking Requested - why for this release]:
Updated•3 months ago
|
Comment 10•3 months ago
|
||
Comment on attachment 9559260 [details]
(secure)
Approved to land and request uplift
Comment 11•3 months ago
|
||
Comment 12•3 months ago
|
||
Comment 13•3 months ago
|
||
Comment 14•3 months ago
|
||
ESR115 backport of the landed patch (3a8949494b34).
- Reproduced the parent-process heap-use-after-free on a clean ESR115 ASAN build with the attached testcase (same signature: free in
DidCommit→FileInfoImpl.h:98, use inFinishWriteTransaction→FileInfoImpl.h:75). - Patch grafts cleanly — only line offset adjustments. ESR115's
ReleaseSavepoint()/ResetSavepointDelta()/mSavepointEntriesIndexare structurally identical to central. - Build succeeds, crash no longer reproduces with the patch applied.
This is the analysis tool's suggested fix. Feel welcome to adopt it as a starting point and evolve it as needed to meet our coding standards.
Comment 15•3 months ago
|
||
The patch landed in nightly and beta is affected.
:hsingh, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox150towontfix.
For more information, please visit BugBot documentation.
Comment 16•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Sandbox escape and use after free.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: NA
- Risk associated with taking this patch: low
- Explanation of risk level: Low risk
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 17•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290463
Comment 18•3 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined/Reason for urgency: Sandbox escape and use after free.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: NA
- Risk associated with taking this patch: low
- Explanation of risk level: Low risk
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 19•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290463
Comment 20•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Sandbox escape and use after free.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: NA
- Risk associated with taking this patch: low
- Explanation of risk level: Low risk
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 21•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290463
Comment 22•3 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined/Reason for urgency: Sandbox escape and use after free.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing: NA
- Risk associated with taking this patch: low
- Explanation of risk level: Low risk
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 23•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290463
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 24•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 25•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 26•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 27•3 months ago
|
||
| uplift | ||
Comment 28•3 months ago
|
||
| 140.9.1 uplift | ||
Comment 29•3 months ago
|
||
| 115.34.1 uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•1 month ago
|
Description
•