← All issues

[13] [WebCore] IndexedDB hash invariant fix for -0/+0 keys

Severity: High | Component: WebCore IndexedDB / NetworkProcess | 917854a

Rated High because the diff fixes a HashMap invariant violation in IDBKeyData that produces a UAF on IndexValueStore entries: add(Hasher&, ...) hashed the raw double by bit pattern while operator== used IEEE 754 equality, so -0.0 and +0.0 collided unequally and a later removal walked the wrong bucket and destroyed a live entry observed by an open cursor.

add(Hasher&, const IDBKeyData&) normalises -0.0 to +0.0 before hashing the Number/Date payload, restoring the a == b ⇒ hash(a) == hash(b) contract. MemoryIndex::transactionAborted now broadcasts cursor invalidation, and UniqueIDBDatabase::didFinishHandlingVersionChangeTransaction validates the transaction state before mutating m_versionChangeTransaction-related fields.

Source/WebCore/Modules/indexeddb/shared/IDBKeyData.cpp

- add(hasher, m_numberValue);
+ add(hasher, m_numberValue == 0.0 ? 0.0 : m_numberValue); // normalize -0

HashMap invariant violation: bit-pattern hash plus IEEE 754 equality on IDBKeyData corrupted the in-memory index store, destroying a live record observed by open cursors.

The hashing path for Number/Date keys normalizes -0 to +0. MemoryIndex::transactionAborted invalidates open cursors before rolling back. The NetworkProcess UniqueIDBDatabase::didFinishHandlingVersionChangeTransaction re-checks transaction state before acting on it.

IndexValueStore is a HashMap keyed on IDBKeyData. The in-memory backing store is the path used for ephemeral / private-browsing sessions; the IDB server runs in the NetworkProcess. The HashMap contract is that equal keys hash equally.

Pre-fix, add(Hasher&, const IDBKeyData&) forwarded the raw double for Number/Date keys; the hasher hashed by bit pattern, so -0.0 (sign bit set) and +0.0 (sign bit clear) produced different hashes. IDBKeyData::operator== used IEEE 754 numeric equality, where -0.0 == +0.0.

Concrete corruption sequence:

insert {v: -0}   → hash(-0) → bucket A, slot for -0
insert {v: +0}   → unique-constraint check via operator== matches -0,
                   ConstraintError reported correctly
remove key +0    → hash(+0) → bucket B, walk chain comparing with ==,
                   matches and destroys the -0 entry in bucket A

The map is now structurally inconsistent: an entry observed by an open MemoryIndexCursor has been destroyed. Subsequent cursor reads dereference freed records. A secondary lifetime bug exists in MemoryIndex::transactionAborted: rollback destroyed/replaced index records while live cursors still observed them. A third defense-in-depth defect: didFinishHandlingVersionChangeTransaction mutated state without verifying the transaction was finishing.

This weakens the IndexedDB in-memory store invariants; the exploit primitive is heap UAF on MemoryIndexRecord reachable through a script-held cursor.