CVE-2026-95365
Overview
Background
- `SharedArrayBuffer` (SAB)
- a JavaScript buffer whose backing memory is shared and can be mutated concurrently by other threads while script runs.
- `IDBKey`
- the internal Blink representation of an IndexedDB key, including a binary variant built via
IDBKey::CreateBinaryfrom a copiedVector<char>. - Buffer source type
- the spec category (
ArrayBuffer,ArrayBufferView, andSharedArrayBuffer) that IndexedDB accepts as a valid key, distinct from the structured-serialization path used for values that bans SAB. - `AtomicReadMemcpy`
- a WTF helper that copies from possibly-shared memory atomically, so a concurrent writer cannot tear the copy mid-read.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Commit frames this as spec-compliance (SAB is a valid key) plus atomic-read hardening against torn shared reads; a corruption-yielding type confusion is not demonstrated in the diff. Needs cross-origin isolation.
The key-creation path in v8_binding_for_modules.cc converted a V8 value into an IDBKey by branching on IsArrayBuffer and IsArrayBufferView, but had no branch for IsSharedArrayBuffer and did not distinguish shared from non-shared views. For a shared ArrayBufferView, the code fell through to the non-shared logic and read the bytes via view->ByteSpan() (and checked view->buffer()->IsDetached()), treating shared-backed memory as if it were an ordinary detached-checkable DOMArrayBuffer — a type/state confusion between shared and non-shared buffer sources. Because the copy out of shared memory was a plain, non-atomic read, another thread could mutate the SharedArrayBuffer during conversion, and the mismatched view/buffer typing meant the wrong invariants (detachment, single-threaded ownership) were assumed.
The fix adds an explicit IsSharedArrayBuffer branch that materializes a DOMSharedArrayBuffer and copies its bytes with AtomicReadMemcpy over ByteSpanMaybeShared(), and adds an IsShared() check inside the view branch that copies via BaseAddressMaybeShared() with AtomicReadMemcpy before the detached-buffer logic. This works because each buffer-source category is now routed to the code that matches its true type, and the shared cases are read atomically into an owned Vector<char> snapshot.
SharedArrayBuffer-backed inputs as their own buffer-source type, so shared memory was processed by the non-shared ArrayBufferView/ArrayBuffer path; the fix distinguishes them explicitly and snapshots the bytes with AtomicReadMemcpy.Attack Path
- Cross-origin isolate the page
Serve the document with COOP/COEP so
crossOriginIsolatedis true andSharedArrayBufferconstruction is allowed in script. - Use a SAB as an IndexedDB key
Create a
SharedArrayBuffer(or aTypedArrayview over one) and pass it as the key tostore.put(value, key), driving execution into the un-handled key-conversion path inCreateIDBKeyFromValue. - Trigger the type/state confusion
The pre-patch code routes the shared buffer through the non-shared
IsArrayBufferView/IsArrayBufferlogic, readingByteSpan()and applyingIsDetachedassumptions that do not hold for shared memory. (inferred) - Race a concurrent mutation
From a worker thread, mutate the same
SharedArrayBufferduring the non-atomic copy so the key bytes are read while changing, defeating the intended immutable-key snapshot. (inferred) - Reach the type-confusion sink The mismatched typing of shared vs non-shared buffer sources yields memory unsafety in the key-encoding path exploitable for further corruption. (inferred)
Impact Assessment
SharedArrayBuffer is available (cross-origin isolation) and used as an IndexedDB key. Beyond the confusion itself, any escalation to a controlled read/write primitive, sandbox escape, or code execution is (inferred) and not demonstrated by the patch. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.Files Changed
third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.ccthird_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headersthird_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html
Audit Directions
- Buffer-source branchingAudit other V8-to-Blink conversions that switch on
IsArrayBuffer/IsArrayBufferViewbut omit anIsSharedArrayBuffer/IsShared()case, since these silently misroute shared memory. - Non-atomic reads of shared memoryLook for
ByteSpan()/BaseAddress()reads that should useByteSpanMaybeShared()/BaseAddressMaybeShared()withAtomicReadMemcpyto avoid tearing when a concurrent thread mutates aSharedArrayBuffer. - Detachment assumptionsReview code that gates on
IsDetached()or single-threaded ownership before copying buffer bytes, as those invariants do not hold for shared buffers and can mask a state confusion.
Patch
From 8984e9c2ced8db0a71c5a7e6469087b70ee9041b Mon Sep 17 00:00:00 2001 From: Evan Stade <[email protected]> Date: Mon, 14 Sep 2026 18:36:19 -0700 Subject: [PATCH] IDB: correctly handle SharedArrayBuffer as key. The spec says any "buffer source type" is a valid key, and that includes SharedArrayBuffer. This is in contrast to values, which use the structured serialization algorithm, which specifically bans SAB when serializing for storage. Bug: 558764482 Change-Id: I790db66baf596154c3139cc6e9621b90f37d19b0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8393497 Commit-Queue: Evan Stade <[email protected]> Reviewed-by: Evan Stade <[email protected]> Reviewed-by: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1697719} --- diff --git a/third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc b/third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc index f303c965..9496f60 100644 --- a/third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc +++ b/third_party/blink/renderer/bindings/modules/v8/v8_binding_for_modules.cc @@ -47,6 +47,7 @@ #include "third_party/blink/renderer/core/frame/web_feature.h" #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h" +#include "third_party/blink/renderer/core/typed_arrays/dom_shared_array_buffer.h" #include "third_party/blink/renderer/modules/indexeddb/idb_any.h" #include "third_party/blink/renderer/modules/indexeddb/idb_cursor.h" #include "third_party/blink/renderer/modules/indexeddb/idb_cursor_with_value.h" @@ -57,6 +58,7 @@ #include "third_party/blink/renderer/modules/indexeddb/idb_value.h" #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" +#include "third_party/blink/renderer/platform/wtf/atomic_operations.h" #include "third_party/blink/renderer/platform/wtf/math_extras.h" #include "third_party/blink/renderer/platform/wtf/vector.h" @@ -106,19 +108,49 @@ Vector<char>(base::as_chars(buffer->ByteSpan())))); } + if (value->IsSharedArrayBuffer()) { + DOMSharedArrayBuffer* buffer = + NativeValueTraits<DOMSharedArrayBuffer>::NativeValue(isolate, value, + exception_state); + if (exception_state.HadException()) { + return IDBKey::CreateInvalid(); + } + base::span<uint8_t> shared_bytes = buffer->ByteSpanMaybeShared(); + SCOPED_CRASH_KEY_NUMBER("CreateIDBKey", "ArrayBuffer", shared_bytes.size()); + Vector<char> bytes(base::checked_cast<wtf_size_t>(shared_bytes.size())); + // SAFETY: `bytes` and `shared_bytes` have the same size. + UNSAFE_BUFFERS(AtomicReadMemcpy(bytes.data(), shared_bytes.data(), + shared_bytes.size())); + return IDBKey::CreateBinary( + base::MakeRefCounted<base::RefCountedData<Vector<char>>>( + std::move(bytes))); + } + if (value->IsArrayBufferView()) { DOMArrayBufferView* view = NativeValueTraits<MaybeShared<DOMArrayBufferView>>::NativeValue( isolate, value, exception_state) .Get(); - if (exception_state.HadException()) + if (exception_state.HadException()) { return IDBKey::CreateInvalid(); + } + SCOPED_CRASH_KEY_NUMBER("CreateIDBKey", "ArrayBuffer", view->byteLength()); + if (view->IsShared()) { + Vector<char> bytes(base::checked_cast<wtf_size_t>(view->byteLength())); + // SAFETY: `byteLength()` returns the number of bytes at + // `BaseAddressMaybeShared()`. + UNSAFE_BUFFERS(AtomicReadMemcpy( + bytes.data(), view->BaseAddressMaybeShared(), bytes.size())); + return IDBKey::CreateBinary( + base::MakeRefCounted<base::RefCountedData<Vector<char>>>( + std::move(bytes))); + } + if (view->buffer()->IsDetached()) { exception_state.ThrowDOMException(DOMExceptionCode::kDataError, "The viewed ArrayBuffer is detached."); return IDBKey::CreateInvalid(); } - SCOPED_CRASH_KEY_NUMBER("CreateIDBKey", "ArrayBuffer", view->byteLength()); return IDBKey::CreateBinary( base::MakeRefCounted<base::RefCountedData<Vector<char>>>( Vector<char>(base::as_chars(view->ByteSpan())))); diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html new file mode 100644 index 0000000..9de6a6f --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<title>IndexedDB: SharedArrayBuffer keys</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/support.js"></script> +<script> +'use strict'; + +const key_data = [1, 2, 3, 4]; + +function create_object_store(t, db) { + db.createObjectStore('store'); +} + +function create_shared_buffer() { + const buffer = new SharedArrayBuffer(key_data.length); + const view = new Uint8Array(buffer); + view.set(key_data); + return {buffer, view}; +} + +function put_and_validate_key(t, db, key, view_on_sab, key_copy) { + assert_true( + self.crossOriginIsolated, 'The page should be cross-origin isolated'); + + // Put the SAB-backed key in the store. + const tx = db.transaction('store', 'readwrite'); + const store = tx.objectStore('store'); + const put_request = store.put('value', key); + put_request.onerror = t.unreached_func('put should succeed'); + + // Modifying the SAB after put should have no effect. + view_on_sab.fill(0); + + // Reading the record back out of the key should work. + const get_request = store.get(key_copy); + get_request.onerror = t.unreached_func('get should succeed'); + get_request.onsuccess = t.step_func(() => { + assert_equals(get_request.result, 'value'); + }); + + tx.oncomplete = t.step_func_done(); +} + +indexeddb_test( + create_object_store, + (t, db) => { + const {buffer, view} = create_shared_buffer(); + put_and_validate_key( + t, db, buffer, view, new Uint8Array(key_data)); + }, + 'A SharedArrayBuffer can be used as an IndexedDB key'); + +indexeddb_test( + create_object_store, + (t, db) => { + const {view} = create_shared_buffer(); + put_and_validate_key( + t, db, view, view, new Uint8Array(key_data)); + }, + 'A SharedArrayBuffer-backed view can be used as an IndexedDB key'); + +indexeddb_test( + create_object_store, + (t, db) => { + const {buffer, view} = create_shared_buffer(); + put_and_validate_key( + t, db, [buffer], view, [new Uint8Array(key_data)]); + }, + 'A SharedArrayBuffer in an array can be used as an IndexedDB key'); +</script> diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers new file mode 100644 index 0000000..5f8621ef8 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html
new file mode 100644
index 0000000..9de6a6f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<title>IndexedDB: SharedArrayBuffer keys</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/support.js"></script>
+<script>
+'use strict';
+
+const key_data = [1, 2, 3, 4];
+
+function create_object_store(t, db) {
+ db.createObjectStore('store');
+}
+
+function create_shared_buffer() {
+ const buffer = new SharedArrayBuffer(key_data.length);
+ const view = new Uint8Array(buffer);
+ view.set(key_data);
+ return {buffer, view};
+}
+
+function put_and_validate_key(t, db, key, view_on_sab, key_copy) {
+ assert_true(
+ self.crossOriginIsolated, 'The page should be cross-origin isolated');
+
+ // Put the SAB-backed key in the store.
+ const tx = db.transaction('store', 'readwrite');
+ const store = tx.objectStore('store');
+ const put_request = store.put('value', key);
+ put_request.onerror = t.unreached_func('put should succeed');
+
+ // Modifying the SAB after put should have no effect.
+ view_on_sab.fill(0);
+
+ // Reading the record back out of the key should work.
+ const get_request = store.get(key_copy);
+ get_request.onerror = t.unreached_func('get should succeed');
+ get_request.onsuccess = t.step_func(() => {
+ assert_equals(get_request.result, 'value');
+ });
+
+ tx.oncomplete = t.step_func_done();
+}
+
+indexeddb_test(
+ create_object_store,
+ (t, db) => {
+ const {buffer, view} = create_shared_buffer();
+ put_and_validate_key(
+ t, db, buffer, view, new Uint8Array(key_data));
+ },
+ 'A SharedArrayBuffer can be used as an IndexedDB key');
+
+indexeddb_test(
+ create_object_store,
+ (t, db) => {
+ const {view} = create_shared_buffer();
+ put_and_validate_key(
+ t, db, view, view, new Uint8Array(key_data));
+ },
+ 'A SharedArrayBuffer-backed view can be used as an IndexedDB key');
+
+indexeddb_test(
+ create_object_store,
+ (t, db) => {
+ const {buffer, view} = create_shared_buffer();
+ put_and_validate_key(
+ t, db, [buffer], view, [new Uint8Array(key_data)]);
+ },
+ 'A SharedArrayBuffer in an array can be used as an IndexedDB key');
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers
new file mode 100644
index 0000000..5f8621ef8
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/IndexedDB/sharedarraybuffer-key.https.html.headers
@@ -0,0 +1,2 @@
+Cross-Origin-Embedder-Policy: require-corp
+Cross-Origin-Opener-Policy: same-origin