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 <evanstade@microsoft.com>
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 <evanstade@microsoft.com>
Reviewed-by: Evan Stade <evanstade@microsoft.com>
Reviewed-by: Nate Chapin <japhet@chromium.org>
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