Closed Bug 2025401 Opened 4 months ago Closed 3 months ago

IndexedDB heap use-after-free in [@ FileInfo::UpdateReferences] — ReleaseSavepoint fails to reset mSavepointDelta

Categories

(Core :: Storage: IndexedDB, defect)

defect

Tracking

()

RESOLVED FIXED
151 Branch
Tracking Status
firefox-esr115 149+ fixed
firefox-esr140 149+ fixed
firefox149 + fixed
firefox150 + fixed
firefox151 + fixed

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)

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 ThreadSafeAutoRefCntnsrefcntuintptr_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)

  1. Setup (T0): Store a blob at key k0 with idx: "u0". The SQLite file table has refcount = 1, in-memory mDBRefCnt = 1, one row references file_id 1.

  2. Get file-backed blob: get("k0") returns a blob backed by RemoteLazyInputStream. When this blob is put() back, the parent finds it in mMappedBlobs and reuses the same file_id 1 — no new file is created.

  3. Op Aput({idx: "u1", b: fbBlob}, "k1") succeeds:

    • stmt->Execute() at ActorsParent.cpp:19328 fires object_data_insert_trigger.
    • OnFunctionCallProcessValue(Increment) on file_id 1FileInfoEntry created in mFileInfoEntries: mDelta = +1, mSavepointDelta = +1.
    • InsertIndexTableRows() succeeds ("u1" is unique).
    • autoSave.Commit()ReleaseSavepoint() → index cleared, mSavepointDelta remains +1.
  4. Op Badd({idx: "u0", b: fbBlob}, "k2") fails on the index:

    • stmt->Execute() succeeds (main table has no conflict at k2). Trigger fires again on same file_id 1.
    • Same FileInfoEntry found: IncDeltas(true)mDelta = +2, mSavepointDelta = +1 (stale) + 1 = +2.
    • InsertIndexTableRows() at line 19348 failsidx: "u0" already used by k0SQLITE_CONSTRAINT.
    • Early return skips autoSave.Commit(). ~AutoSavepoint() destructor runs RollbackSavepoint():
      • entry->DecBySavepointDelta()mDelta = 2 - 2 = 0 (should have been 2 - 1 = 1).
      • entry->ResetSavepointDelta().
  5. preventDefault() on Op B's error event → transaction is not aborted, it commits.

  6. Commit with mDelta = 0:

    • WillCommit(): mDelta == 0 → SQLite file.refcount stays 1.
    • DidCommit(): mDelta == 0 → in-memory mDBRefCnt stays 1.
    • Reset(): drops the SafeRefPtr normally (mRefCnt goes to 0 but mDBRefCnt = 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)

  1. Page reload destroys all content-process IDBDatabase objects → all PBackgroundIDBDatabaseFile actors torn down → all DatabaseFile parent actors release their SafeRefPtr<DatabaseFileInfo>mRefCnt drops to 0. The DatabaseFileManager is cached in IndexedDatabaseManager (ActorsParent.cpp:15517) and survives; mDBRefCnt = 1 persists in memory.

  2. Reopen DBGetFileManager() returns the cached instance (already Initialized(), so Init() is skipped — refcounts are not re-read from disk).

  3. clear() in T2: DELETE fires object_data_delete_trigger twice (once for k0, once for k1) on the same file_id 1.

    • First fire: FileInfoEntry created. Constructor takes a SafeRefPtr (GetFileInfo()LockedAddRef()) → mRefCnt = 1. DecDeltas(true)mDelta = -1.
    • Second fire: same entry, mDelta = -2.
  4. CommitOp::Run:

    • WillCommit(): updates SQLite file.refcount by -21 - 2 = -1 → row deleted (refcount <= 0 cleanup trigger).
    • SQLite COMMIT succeeds.
    • DidCommit() at line 17659MaybeUpdateDBRefs()UpdateDBRefs(-2)UpdateReferences(mDBRefCnt, -2):
      • mDBRefCnt = 1 + uintptr_t(-2) = 0xFFFFFFFFFFFFFFFF
      • mRefCnt + mDBRefCnt = 1 + 0xFFFFFFFFFFFFFFFF = 0 (unsigned wraparound)
      • 0 > 0false → fall through → RemoveFileInfo()Cleanup()delete this at FileInfoImpl.h:95.
    • The FileInfoEntry in mFileInfoEntries still holds the now-dangling SafeRefPtr.
    • FinishWriteTransaction() at line 17675Reset():
      • entry->ReleaseFileInfo().forget().take() extracts the raw dangling pointer.
      • fileInfo->Release(true) at line 7652 → UpdateReferences(mRefCnt, -1, true) → line 72 reads mRefCntheap-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 at CommitOp::Run:17675 — sequential calls in a single runnable.
  • 32-byte allocation matches FileInfo layout (8-byte mFileId + 8-byte mRefCnt + 8-byte mDBRefCnt + 8-byte SafeRefPtr 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.

Attached file Crash stack trace
Attached file Testcase: test.html
Group: core-security → dom-core-security
Severity: -- → S3
Severity: S3 → S2

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.

Whiteboard: [prefs-checked]

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_ASSERT that a negative delta does not exceed the current count.
  • The liveness check mRefCnt + mDBRefCnt > 0 is changed to mRefCnt > 0 || mDBRefCnt > 0. These are equivalent for well-formed non-negative counts, but the summed form wraps to 0 when one count has underflowed to UINTPTR_MAX and the other is 1. 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: nobody → hsingh
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Attached file (secure)

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
Attachment #9559260 - Flags: sec-approval?

[Tracking Requested - why for this release]:

Comment on attachment 9559260 [details]
(secure)

Approved to land and request uplift

Attachment #9559260 - Flags: sec-approval? → sec-approval+
Pushed by hsingh@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/3a8949494b34 https://hg.mozilla.org/integration/autoland/rev/1d32e83e5b21 Fixing a bug in IDB blob files refcouting where stale count could carried over across savepoints.r=dom-storage-reviewers,asuth.
Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

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 DidCommitFileInfoImpl.h:98, use in FinishWriteTransactionFileInfoImpl.h:75).
  • Patch grafts cleanly — only line offset adjustments. ESR115's ReleaseSavepoint() / ResetSavepointDelta() / mSavepointEntriesIndex are 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.

The patch landed in nightly and beta is affected.
:hsingh, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(hsingh)

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
Attachment #9563705 - Flags: approval-mozilla-beta?
Attached file (secure)

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
Attachment #9563706 - Flags: approval-mozilla-esr115?
Attached file (secure)

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
Attachment #9563707 - Flags: approval-mozilla-esr140?
Attached file (secure)

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
Attachment #9563708 - Flags: approval-mozilla-release?
Attached file (secure)
Flags: needinfo?(hsingh)
Attachment #9563705 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9563707 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]
Attachment #9563706 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9563708 - Flags: approval-mozilla-release? → approval-mozilla-release+
Whiteboard: [prefs-checked] → [prefs-checked][adv-main149+r]
Whiteboard: [prefs-checked][adv-main149+r] → [prefs-checked][adv-main149+r][adv-esr115.34.1+r]
Whiteboard: [prefs-checked][adv-main149+r][adv-esr115.34.1+r] → [prefs-checked][adv-main149+r][adv-esr115.34.1+r][adv-esr140.9.1+r]
Whiteboard: [prefs-checked][adv-main149+r][adv-esr115.34.1+r][adv-esr140.9.1+r] → [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r]
Whiteboard: [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r] → [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r][adv-esr149.0.2+r]
Whiteboard: [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r][adv-esr149.0.2+r] → [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r]
Whiteboard: [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r] → [prefs-checked][adv-esr115.34.1+r][adv-esr140.9.1+r][adv-main149.0.2+r]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: