CVE-2026-5291
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTthird_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.cc |
modified | |
CORE_EXPORTthird_party/blink/renderer/bindings/core/v8/pass_as_span.h |
modified |
Files Changed
third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.hthird_party/blink/renderer/bindings/core/v8/native_value_traits_impl_test.ccthird_party/blink/renderer/bindings/core/v8/pass_as_span.h
Patch
From 861187c9a286e6250a3fe7608ca62578b6af6dd4 Mon Sep 17 00:00:00 2001 From: Tiago Vignatti <[email protected]> Date: Wed, 08 Apr 2026 07:41:17 -0700 Subject: [PATCH] [M138-LTS][bindings] Perform detach check when passing span instead of retaining underlying array buffer This CL renames and repurposes kSupportReentry that used to retain the underlying storage for an array buffer so that we now check if an AB has been detached and return an empty span in such case, as the previous behavior turned out to be not spec compliant. Also use this this for fall-back generated bindings of [NoAllocDirectCall] operations. (cherry picked from commit 0e84e9b32b74e72de294c58fa03042cb6cffc715) Bug: 490118036 Change-Id: I17603e4f4345beb6959f1f578a73ec540795811f Fixed: 490810422, 489325431 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7677065 Reviewed-by: Nate Chapin <[email protected]> Commit-Queue: Andrey Kosyakov <[email protected]> Cr-Original-Original-Commit-Position: refs/heads/main@{#1601433} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7690510 Auto-Submit: Andrey Kosyakov <[email protected]> Cr-Original-Commit-Position: refs/branch-heads/7680@{#3111} Cr-Original-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7716642 Reviewed-by: Victor Gabriel Savu <[email protected]> Reviewed-by: Andrey Kosyakov <[email protected]> Commit-Queue: Tiago Vignatti (xWF) <[email protected]> Cr-Commit-Position: refs/branch-heads/7204@{#3532} Cr-Branched-From: d5de512dc9dc8ddfe4e6d71b0637578bb6158683-refs/heads/main@{#1465706} --- 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 f4819919..1869c82 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 @@ -1768,15 +1768,16 @@ v8::Local<v8::Value> value, ExceptionState& exception_state) = delete; - static bindings::internal::ByteSpanWithInlineStorage<T::support_reentry> + static bindings::internal::ByteSpanWithInlineStorage<T::perform_detach_check> ArgumentValue(v8::Isolate* isolate, int argument_index, v8::Local<v8::Value> value, ExceptionState& exception_state) { - bindings::internal::ByteSpanWithInlineStorage<T::support_reentry> result; + bindings::internal::ByteSpanWithInlineStorage<T::perform_detach_check> + result; if (value->IsArrayBuffer()) { v8::Local<v8::ArrayBuffer> array_buffer = value.As<v8::ArrayBuffer>(); - result.MaybeSetBackingStore(array_buffer); + result.MaybeSetArrayBuffer(array_buffer); result.Assign(bindings::internal::GetArrayData(array_buffer)); return result; } @@ -1794,7 +1795,7 @@ "The provided ArrayBufferView value must not be shared."); return result; } - result.MaybeSetBackingStore(view->Buffer()); + result.MaybeSetArrayBuffer(view->Buffer()); } result.Assign(view->GetContents(result.GetInlineStorage())); return result; @@ -1829,7 +1830,7 @@ "The provided ArrayBufferView value must not be shared."); return result; } - result.MaybeSetBackingStore(view->Buffer()); + result.MaybeSetArrayBuffer(view->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 a0cbb62..b01bc4c 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 @@ -749,12 +749,12 @@ std::numeric_limits<double>::infinity(), 42)); } -using PassAsSpanWithReentry = - PassAsSpan<PassAsSpanMarkerBase::Flags::kSupportReentry, void>; +using PassAsSpanWithDetachCheck = + PassAsSpan<PassAsSpanMarkerBase::Flags::kPerformDetachCheck, void>; template <typename T> -using TypedPassAsSpanWithReentry = - PassAsSpan<PassAsSpanMarkerBase::Flags::kSupportReentry, T>; +using TypedPassAsSpanWithDetachCheck = + PassAsSpan<PassAsSpanMarkerBase::Flags::kPerformDetachCheck, T>; TEST(NativeValueTraitsImplTest, TypedPassAsSpanDetach) { test::TaskEnvironment task_environment; @@ -765,32 +765,34 @@ v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"( self.arrbuf = new Uint8Array(10000).fill(42).buffer; )"); - auto converted = NativeValueTraits<PassAsSpanWithReentry>::ArgumentValue( - scope.GetIsolate(), 0, v8_object, exception_state); + auto converted = + NativeValueTraits<PassAsSpanWithDetachCheck>::ArgumentValue( + scope.GetIsolate(), 0, v8_object, exception_state); EvaluateScriptForObject(scope, "self.arrbuf.transfer(0)"); - EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000)); + EXPECT_THAT(converted.as_span(), testing::IsEmpty()); } { v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"( self.arr1 = new Uint8Array(10000).fill(42); )"); - auto converted = NativeValueTraits<PassAsSpanWithReentry>::ArgumentValue( - scope.GetIsolate(), 0, v8_object, exception_state); + auto converted = + NativeValueTraits<PassAsSpanWithDetachCheck>::ArgumentValue( + scope.GetIsolate(), 0, v8_object, exception_state); EvaluateScriptForObject(scope, "self.arr1.buffer.transfer(0)"); - EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000)); + EXPECT_THAT(converted.as_span(), testing::IsEmpty()); } { v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"( self.arr2 = new Uint16Array(10000).fill(42); )"); auto converted = - NativeValueTraits<TypedPassAsSpanWithReentry<uint16_t>>::ArgumentValue( - scope.GetIsolate(), 0, v8_object, exception_state); + NativeValueTraits<TypedPassAsSpanWithDetachCheck<uint16_t>>:: + ArgumentValue(scope.GetIsolate(), 0, v8_object, exception_state); EvaluateScriptForObject(scope, "self.arr2.buffer.transfer(0)"); - EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000)); + EXPECT_THAT(converted.as_span(), testing::IsEmpty()); } } diff --git a/third_party/blink/renderer/bindings/core/v8/pass_as_span.h b/third_party/blink/renderer/bindings/core/v8/pass_as_span.h index 17ea6a35e..70bd75f 100644 --- a/third_party/blink/renderer/bindings/core/v8/pass_as_span.h +++ b/third_party/blink/renderer/bindings/core/v8/pass_as_span.h @@ -15,7 +15,7 @@ namespace bindings::internal { -template <bool kSupportReentry> +template <bool kPerformDetachCheck> class CORE_EXPORT ByteSpanWithInlineStorage { STACK_ALLOCATED(); @@ -32,18 +32,16 @@ span_ = span.first(r.span_.size()); } else { span_ = r.span_; - backing_store_ = r.backing_store_; + orig_buffer_for_detach_check_ = r.orig_buffer_for_detach_check_; } return *this; } void Assign(base::span<const uint8_t> span) { span_ = span; } void Assign(v8::MemorySpan<const uint8_t> span) { span_ = span; } - void MaybeSetBackingStore(v8::Local<v8::ArrayBuffer> array_buffer) { - if constexpr (kSupportReentry) { - if (array_buffer->IsDetachable()) { - backing_store_ = array_buffer->GetBackingStore(); - } + void MaybeSetArrayBuffer(v8::Local<v8::ArrayBuffer> array_buffer) { + if constexpr (kPerformDetachCheck) { + orig_buffer_for_detach_check_ = array_buffer; } } @@ -51,9 +49,17 @@ // class tightly coupled to the bindings generator that knows how to use it. // Note rvalue conversion is explicitly disabled. // NOLINTNEXTLINE(google-explicit-constructor) - operator base::span<const uint8_t>() const& { return span_; } + operator base::span<const uint8_t>() const& { return as_span(); } operator base::span<const uint8_t>() const&& = delete; - const base::span<const uint8_t> as_span() const { return span_; } + const base::span<const uint8_t> as_span() const { + if constexpr (kPerformDetachCheck) { + if (!orig_buffer_for_detach_check_.IsEmpty() && + orig_buffer_for_detach_check_->WasDetached()) { + return {}; + } + } + return span_; + } v8::MemorySpan<uint8_t> GetInlineStorage() { return inline_storage_; } @@ -61,8 +67,8 @@ base::span<const uint8_t> span_; uint8_t inline_storage_[kInlineStorageSize]; struct Void {}; - std::conditional_t<kSupportReentry, std::shared_ptr<v8::BackingStore>, Void> - backing_store_; + std::conditional_t<kPerformDetachCheck, v8::Local<v8::ArrayBuffer>, Void>
Regression Test / PoC
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 a0cbb62..b01bc4c 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
@@ -749,12 +749,12 @@
std::numeric_limits<double>::infinity(), 42));
}
-using PassAsSpanWithReentry =
- PassAsSpan<PassAsSpanMarkerBase::Flags::kSupportReentry, void>;
+using PassAsSpanWithDetachCheck =
+ PassAsSpan<PassAsSpanMarkerBase::Flags::kPerformDetachCheck, void>;
template <typename T>
-using TypedPassAsSpanWithReentry =
- PassAsSpan<PassAsSpanMarkerBase::Flags::kSupportReentry, T>;
+using TypedPassAsSpanWithDetachCheck =
+ PassAsSpan<PassAsSpanMarkerBase::Flags::kPerformDetachCheck, T>;
TEST(NativeValueTraitsImplTest, TypedPassAsSpanDetach) {
test::TaskEnvironment task_environment;
@@ -765,32 +765,34 @@
v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
self.arrbuf = new Uint8Array(10000).fill(42).buffer;
)");
- auto converted = NativeValueTraits<PassAsSpanWithReentry>::ArgumentValue(
- scope.GetIsolate(), 0, v8_object, exception_state);
+ auto converted =
+ NativeValueTraits<PassAsSpanWithDetachCheck>::ArgumentValue(
+ scope.GetIsolate(), 0, v8_object, exception_state);
EvaluateScriptForObject(scope, "self.arrbuf.transfer(0)");
- EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000));
+ EXPECT_THAT(converted.as_span(), testing::IsEmpty());
}
{
v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
self.arr1 = new Uint8Array(10000).fill(42);
)");
- auto converted = NativeValueTraits<PassAsSpanWithReentry>::ArgumentValue(
- scope.GetIsolate(), 0, v8_object, exception_state);
+ auto converted =
+ NativeValueTraits<PassAsSpanWithDetachCheck>::ArgumentValue(
+ scope.GetIsolate(), 0, v8_object, exception_state);
EvaluateScriptForObject(scope, "self.arr1.buffer.transfer(0)");
- EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000));
+ EXPECT_THAT(converted.as_span(), testing::IsEmpty());
}
{
v8::Local<v8::Object> v8_object = EvaluateScriptForObject(scope, R"(
self.arr2 = new Uint16Array(10000).fill(42);
)");
auto converted =
- NativeValueTraits<TypedPassAsSpanWithReentry<uint16_t>>::ArgumentValue(
- scope.GetIsolate(), 0, v8_object, exception_state);
+ NativeValueTraits<TypedPassAsSpanWithDetachCheck<uint16_t>>::
+ ArgumentValue(scope.GetIsolate(), 0, v8_object, exception_state);
EvaluateScriptForObject(scope, "self.arr2.buffer.transfer(0)");
- EXPECT_THAT(converted.as_span(), testing::Contains(42).Times(10000));
+ EXPECT_THAT(converted.as_span(), testing::IsEmpty());
}
}
diff --git a/third_party/blink/web_tests/external/wpt/encoding/textdecoder-arguments.any.js b/third_party/blink/web_tests/external/wpt/encoding/textdecoder-arguments.any.js
index 74a52fa..283dc5f5 100644
--- a/third_party/blink/web_tests/external/wpt/encoding/textdecoder-arguments.any.js
+++ b/third_party/blink/web_tests/external/wpt/encoding/textdecoder-arguments.any.js
@@ -60,6 +60,6 @@
}
};
assert_equals(
- decoder.decode(arr, options), Array(10000 + 1).join('*'),
- 'Decoding should work with underlying array buffer detached during options conversion');
+ decoder.decode(arr, options), '',
+ 'Decoding should return an empty string with underlying array buffer detached during options conversion');
}, 'TextDecoder decode() with array buffer detached during arg conversion');
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/crashtests/GPURenderPassEncode.setBindGroup.https.html b/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/crashtests/GPURenderPassEncode.setBindGroup.https.html
new file mode 100644
index 0000000..bdaf5693
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/crashtests/GPURenderPassEncode.setBindGroup.https.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+promise_test(async t => {
+ const adapter = await navigator.gpu.requestAdapter();
+ assert_true(adapter instanceof GPUAdapter, 'Failed to request WebGPU adapter');
+ const device = await adapter.requestDevice();
+ assert_true(device instanceof GPUDevice, 'Failed to request WebGPU device');
+ const buf = device.createBuffer({size: 256, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});
+ const bgl = device.createBindGroupLayout({
+ entries: [{binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: {type: 'uniform', hasDynamicOffset: true}}]
+ });
+ const bg = device.createBindGroup({layout: bgl, entries: [{binding: 0, resource: {buffer: buf, offset: 0, size: 256}}]});
+
+ let arrayBuffer = new ArrayBuffer(4, {maxByteLength: 4});
+ let offsets = new Uint32Array(arrayBuffer);
+
+ const startObj = {
+ valueOf() {
+ arrayBuffer.transfer(0);
+ return 0;
+ }
+ };
+
+ const encoder = device.createCommandEncoder();
+ const pass = encoder.beginComputePass();
+
+ assert_throws_js(RangeError, () => pass.setBindGroup(0, bg, offsets, startObj, 1));
+})
+</script>
+</body>
+</html>
Original Bug Report
Arbitrary read via valueOf reentry in multiDrawElementsWEBGL
Vulnerability Details
This is a renderer-side memory-safety bug reachable from JavaScript via WEBGL_multi_draw.
The bug is caused by passing Int32List arguments as PassAsSpan into a [NoAllocDirectCall] WebGL entrypoint. For these bindings, Blink deliberately disables reentry-safe backing-store retention, so the generated conversion passes raw spans without pinning detachable ArrayBuffer storage. A user-controlled valueOf() on the final drawcount argument runs after the typed-array arguments have already been converted, and can detach those backing stores before the C++ implementation consumes them.
WebGLMultiDraw::multiDrawElementsWEBGL() then forwards stale raw pointers into gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(), which copies drawcount * sizeof(GLsizei) bytes from those stale addresses into a transfer buffer. The copy happens in gpu::CopyArraysToBuffer() via memcpy, leading to an ASan SEGV on a renderer READ.
=================================================================
==79018==ERROR: AddressSanitizer: SEGV on unknown address 0x6de700000000 (pc 0x7327b4dadf0d bp 0x7ffd9e5b3ed0 sp 0x7ffd9e5b3688 T0)
==79018==The signal is caused by a READ memory access.
#0 0x7327b4dadf0d in __memcpy_avx512_unaligned_erms string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:265
#1 0x7327ad8a226f in std::__Cr::array<unsigned int, sizeof... (fp2)> gpu::CopyArraysToBuffer<int const, int const>(unsigned int, unsigned int, void*, int const*, int const*) gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:87:5
#2 0x7327ad818a2a in bool internal::TransferArraysAndExecute<gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(unsigned int, int const*, unsigned int, int const*, int)::$_1, gpu::ScopedTransferBufferPtr, int const, int const>(unsigned int, gpu::ScopedTransferBufferPtr*, gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(unsigned int, int const*, unsigned int, int const*, int)::$_1 const&, int const*, int const*) gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:177:9
#3 0x7327ad74fae2 in bool gpu::TransferArraysAndExecute<gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(unsigned int, int const*, unsigned int, int const*, int)::$_1, int const, int const>(unsigned int, gpu::ScopedTransferBufferPtr*, gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(unsigned int, int const*, unsigned int, int const*, int)::$_1 const&, int const*, int const*) gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:195:10
#4 0x7327ad74f7dd in gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper(unsigned int, int const*, unsigned int, int const*, int) gpu/command_buffer/client/gles2_implementation.cc:2277:8
#5 0x7327ad751e68 in gpu::gles2::GLES2Implementation::MultiDrawElementsWEBGL(unsigned int, int const*, unsigned int, int const*, int) gpu/command_buffer/client/gles2_implementation.cc:2441:3
#6 0x7327cd45b4fb in blink::WebGLMultiDraw::multiDrawElementsWEBGL(unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, int)::$_0::operator()() const third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:93:40
#7 0x7327cd458dc7 in void blink::WebGLRenderingContextBase::DrawWrapper<blink::WebGLMultiDraw::multiDrawElementsWEBGL(unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, int)::$_0>(char const*, blink::CanvasPerformanceMonitor::DrawType, blink::WebGLMultiDraw::multiDrawElementsWEBGL(unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, int)::$_0) third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.h:845:5
#8 0x7327cd45854f in blink::WebGLMultiDraw::multiDrawElementsWEBGL(unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, unsigned int, base::span<int const, 18446744073709551615ul, int const*>, unsigned int, int) third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:90:21
#9 0x7327c7a1ff9c in blink::(anonymous namespace)::v8_webgl_multi_draw::MultiDrawElementsWEBGLOperationCallback(v8::FunctionCallbackInfo<v8::Value> const&) gen/third_party/blink/renderer/bindings/modules/v8/v8_webgl_multi_draw.cc:378:17
#10 0x6f27669e9aaf (<unknown module>)
#11 0x6f27669e5c13 (<unknown module>)
#12 0x6f27669d8366 (<unknown module>)
#13 0x6f27669d80aa (<unknown module>)
#14 0x7327de15b584 in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/simulator.h:216:12
#15 0x7327de158dd6 in v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>, v8::base::Vector<v8::internal::DirectHandle<v8::internal::Object> const>) v8/src/execution/execution.cc:564:10
#16 0x7327dd911166 in v8::Function::Call(v8::Isolate*, v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) v8/src/api/api.cc:5584:27
#17 0x7328018c15a0 in blink::V8ScriptRunner::CallFunction(v8::Local<v8::Function>, blink::ExecutionContext*, v8::Local<v8::Value>, int, v8::Local<v8::Value>*, v8::Isolate*) third_party/blink/renderer/bindings/core/v8/v8_script_runner.cc:851:48
#18 0x7328013654e4 in blink::bindings::CallbackInvokeHelper<blink::CallbackFunctionBase, (blink::bindings::CallbackInvokeHelperMode)0, (blink::bindings::CallbackReturnTypeIsPromise)0>::CallInternal(int, v8::Local<v8::Value>*) third_party/blink/renderer/bindings/core/v8/callback_invoke_helper.cc:124:12
#19 0x7328013650b6 in blink::bindings::CallbackInvokeHelper<blink::CallbackFunctionBase, (blink::bindings::CallbackInvokeHelperMode)0, (blink::bindings::CallbackReturnTypeIsPromise)0>::Call(int, v8::Local<v8::Value>*) third_party/blink/renderer/bindings/core/v8/callback_invoke_helper.cc:148:10
#20 0x73280d7be2c8 in blink::V8Function::Invoke(blink::bindings::V8ValueOrScriptWrappableAdapter, blink::BasicHeapVector<(blink::internal::HeapCollectionType)1, blink::ScriptValue, 0u> const&) gen/third_party/blink/renderer/bindings/core/v8/v8_function.cc:73:13
#21 0x73280d7bf6bc in blink::V8Function::InvokeAndReportException(blink::bindings::V8ValueOrScriptWrappableAdapter, blink::BasicHeapVector<(blink::internal::HeapCollectionType)1, blink::ScriptValue, 0u> const&) gen/third_party/blink/renderer/bindings/core/v8/v8_function.cc:133:15
#22 0x73280a961834 in blink::ScheduledAction::Execute(blink::ExecutionContext*) third_party/blink/renderer/core/scheduler/scheduled_action.cc:145:18
#23 0x73280a93b2a0 in blink::DOMTimer::Fired() third_party/blink/renderer/core/scheduler/dom_timer.cc:446:11
#24 0x7327eb80ce6d in blink::TimerBase::RunInternal() third_party/blink/renderer/platform/timer.cc:166:3
#25 0x732807f6d9ea in void base::internal::DecayedFunctorTraits<void (blink::TimerBase::*)(), blink::TimerBase*>::Invoke<void (blink::TimerBase::*)(), blink::TimerBase*>(void (blink::TimerBase::*)(), blink::TimerBase*&&) base/functional/bind_internal.h:740:12
#26 0x732807f6d8ac in void base::internal::InvokeHelper<false, base::internal::FunctorTraits<void (blink::TimerBase::*&&)(), blink::TimerBase*>, void, 0ul>::MakeItSo<void (blink::TimerBase::*)(), std::__Cr::tuple<blink::UnretainedWrapper<blink::TimerBase>>>(void (blink::TimerBase::*&&)(), std::__Cr::tuple<blink::UnretainedWrapper<blink::TimerBase>>&&) base/functional/bind_internal.h:932:12
#27 0x732807f6d651 in void base::internal::Invoker<base::internal::FunctorTraits<void (blink::TimerBase::*&&)(), blink::TimerBase*>, base::internal::BindState<true, true, false, void (blink::TimerBase::*)(), blink::UnretainedWrapper<blink::TimerBase>>, void ()>::RunImpl<void (blink::TimerBase::*)(), std::__Cr::tuple<blink::UnretainedWrapper<blink::TimerBase>>, 0ul>(void (blink::TimerBase::*&&)(), std::__Cr::tuple<blink::UnretainedWrapper<blink::TimerBase>>&&, std::__Cr::integer_sequence<unsigned long, 0ul>) base/functional/bind_internal.h:1069:14
#28 0x732807f6d4e8 in base::internal::Invoker<base::internal::FunctorTraits<void (blink::TimerBase::*&&)(), blink::TimerBase*>, base::internal::BindState<true, true, false, void (blink::TimerBase::*)(), blink::UnretainedWrapper<blink::TimerBase>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:982:12
#29 0x732801518b12 in base::OnceCallback<void ()>::Run() && base/functional/callback.h:155:12
#30 0x732801518994 in blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::RunInternal(base::OnceCallback<void ()>*) third_party/blink/renderer/platform/wtf/functional.h:212:33
#31 0x7328015164c3 in blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::Run() third_party/blink/renderer/platform/wtf/functional.h:197:12
#32 0x732801518031 in void base::internal::DecayedFunctorTraits<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>&&>::Invoke<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>(void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>&&) base/functional/bind_internal.h:740:12
#33 0x732801517f77 in void base::internal::InvokeHelper<false, base::internal::FunctorTraits<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*&&)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>&&>, void, 0ul>::MakeItSo<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::tuple<std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>>(void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*&&)(), std::__Cr::tuple<std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>&&) base/functional/bind_internal.h:932:12
#34 0x732801517de1 in void base::internal::Invoker<base::internal::FunctorTraits<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*&&)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>&&>, base::internal::BindState<true, true, false, void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>, void ()>::RunImpl<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::tuple<std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>, 0ul>(void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*&&)(), std::__Cr::tuple<std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>&&, std::__Cr::integer_sequence<unsigned long, 0ul>) base/functional/bind_internal.h:1069:14
#35 0x732801517c78 in base::internal::Invoker<base::internal::FunctorTraits<void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*&&)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>&&>, base::internal::BindState<true, true, false, void (blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>::*)(), std::__Cr::unique_ptr<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>, std::__Cr::default_delete<blink::ThreadCheckingCallbackWrapper<base::OnceCallback<void ()>, void ()>>>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:982:12
#36 0x7328f5768482 in base::OnceCallback<void ()>::Run() && base/functional/callback.h:155:12
#37 0x7328f5c94abe in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/task/common/task_annotator.cc:229:34
#38 0x7328f5dc5317 in void base::TaskAnnotator::RunTask<base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*)::$_4>(perfetto::StaticString, base::PendingTask&, base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*)::$_4&&) base/task/common/task_annotator.h:112:5
#39 0x7328f5dc430e in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:475:23
#40 0x7328f5dc308a in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:346:40
#41 0x7328f5dc47e2 in non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc
#42 0x7328f58e3bea in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
#43 0x7328f5dc6482 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:650:12
#44 0x7328f5b1caa7 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
#45 0x7328ddf01955 in content::RendererMain(content::MainFunctionParams) content/renderer/renderer_main.cc:332:16
#46 0x7328de9ec1ba in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:664:14
#47 0x7328de9ed9ec in content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>> const&, content::MainFunctionParams, content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:771:12
#48 0x7328de9f11e6 in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1152:10
#49 0x7328de9e706f in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:358:36
#50 0x7328de9e7eb5 in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:371:10
#51 0x5f9acbff47e0 in ChromeMain chrome/app/chrome_main.cc:191:12
#52 0x5f9acbff4041 in main chrome/app/chrome_exe_main_aura.cc:17:10
#53 0x7327b4c2a577 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#54 0x7327b4c2a63a in __libc_start_main csu/../csu/libc-start.c:360:3
#55 0x5f9acbf0e049 in _start (/home/shou/coding/chromium/src/out/asan_debug_nodcheck/chrome+0xf14e049) (BuildId: 39154d2d2bd938c6)
==79018==Register values:
rax = 0x00006f2701432070 rbx = 0x00006f2701432070 rcx = 0x00000de56027e48e rdx = 0x0000000000000400
rdi = 0x00006f2701432070 rsi = 0x00006de700000000 rbp = 0x00007ffd9e5b3ed0 rsp = 0x00007ffd9e5b3688
r8 = 0x0000000000000000 r9 = 0x000000000000000f r10 = 0x000000000000000c r11 = 0x0000000000000001
r12 = 0x00006de700000000 r13 = 0x00000000000003bf r14 = 0x0000000000000400 r15 = 0x00006de700000000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:265 in __memcpy_avx512_unaligned_erms
==79018==ADDITIONAL INFO
==79018==Note: Please include this section with the ASan report.
Task trace:
#0 0x73280a936f0b in blink::DOMTimer::DOMTimer(blink::ExecutionContext&, blink::ScheduledAction*, base::TimeDelta, bool) third_party/blink/renderer/core/scheduler/dom_timer.cc:343:27
#1 0x73280cb0884a in blink::HTMLDocumentParser::SchedulePumpTokenizer(bool) third_party/blink/renderer/core/html/parser/html_document_parser.cc:858:7
#2 0x7328c4fc4106 in IPC::ChannelAssociatedGroupController::Accept(mojo::Message*) ipc/ipc_mojo_bootstrap.cc:1138:13
Command line: `/proc/self/exe --type=renderer --crashpad-handler-pid=78899 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/org.chromium.Chromium.scoped_dir.F2rMUY --change-stack-guard-on-fork=enable --disable-in-process-stack-traces --no-sandbox --disable-breakpad --ozone-platform=headless --lang=en-US --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=5 --time-ticks-at-unix-epoch=-1771447530144636 --launch-time-ticks=1325126586276 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,16554769914338161308,16393762372383704334,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,929498045835722835,11515194025570577038,4 --trace-process-track-uuid=2297823529537201119`
==79018==END OF ADDITIONAL INFO
==79018==ABORTING
Impact
This is an attacker-triggerable renderer memory-safety bug caused by a stale-pointer dereference after JavaScript reentry.
- The JavaScript caller controls the typed-array arguments passed into
multiDrawElementsWEBGL(). - The JavaScript caller controls
drawcount, which directly controls the number of bytes copied by the helper. In the attached minimized PoC, the helper attempts two 1024-byte reads in one call. - A user-defined
valueOf()ondrawcountruns after the typed-array arguments have already been converted, and can detach those backing stores before the C++ implementation consumes them. - The stale raw pointers are then dereferenced in
memcpy, producing an ASan READ-side crash in the renderer.
Version
- Chrome Version:
147.0.7719.0(local ASan Chromium build from source; dev/ToT-style build) - Operating System:
Ubuntu 25.04,Linux 6.14.0-37-generic x86_64
Reproduction Case
Attached files:
min_poc.html
Verified Reproduction Steps
- Launch the ASan Chromium build with:
ASAN_OPTIONS=detect_odr_violation=0 chrome \
--headless=new \
--ozone-platform=headless \
--no-sandbox \
--disable-breakpad \
--disable-crash-reporter \
--disable-in-process-stack-traces \
--enable-unsafe-swiftshader \
--use-angle=swiftshader-webgl \
file:///path/to/min_poc.html
- No interaction is required. The PoC triggers automatically on load.
- The renderer crashes with ASan reporting a SEGV on READ.
Notes on Flag
--enable-unsafe-swiftshader is not needed for this vulnerability - it is there simply for no GPU cases.
Detailed Root Cause
1. WebGL typed-array arguments use PassAsSpan
Int32List is defined as a [PassAsSpan] typedef in:
src/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl:43
2. multiDrawElementsWEBGL() is marked [NoAllocDirectCall]
The vulnerable WebGL entrypoint is declared in:
src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.idl:17
Because the method is [NoAllocDirectCall], Blink’s bindings generator disables reentry-safe span retention:
src/third_party/blink/renderer/bindings/scripts/bind_gen/blink_v8_bridge.py:506
Relevant logic:
support_reentry = "NoAllocDirectCall" not in argument.owner.extended_attributes
3. PassAsSpan only pins backing stores when reentry support is enabled
In ByteSpanWithInlineStorage::MaybeSetBackingStore(), the ArrayBuffer backing store is retained only when kSupportReentry is enabled:
src/third_party/blink/renderer/bindings/core/v8/pass_as_span.h:42
4. Blink forwards stale raw pointers into the GPU client helper
After validation, WebGLMultiDraw::multiDrawElementsWEBGL() forwards raw pointers derived from the spans:
src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:71src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:92
5. The GPU client helper copies from those stale pointers with memcpy
The data reaches:
src/gpu/command_buffer/client/gles2_implementation.cc:2260
which calls TransferArraysAndExecute(), and ultimately gpu::CopyArraysToBuffer() does:
src/gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:87
memcpy(static_cast<int8_t*>(buffer) + byte_offsets[i], byte_pointers[i],
copy_lengths[i]);
At this point byte_pointers[i] may refer to detached / stale typed-array backing storage that was invalidated by JavaScript reentry in drawcount.valueOf().
Vulnerable Code Snippets and Locations
Blink typedef and entrypoint
src/third_party/blink/renderer/modules/webgl/webgl_rendering_context_base.idl:43src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.idl:17
Reentry disabled for [NoAllocDirectCall]
src/third_party/blink/renderer/bindings/scripts/bind_gen/blink_v8_bridge.py:506
Backing-store pinning only with reentry support
src/third_party/blink/renderer/bindings/core/v8/pass_as_span.h:42
Raw-pointer forwarding into GLES2 client code
src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:71src/third_party/blink/renderer/modules/webgl/webgl_multi_draw.cc:92src/gpu/command_buffer/client/gles2_implementation.cc:2260
Faulting copy
src/gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:87
Crash Information
- Type of crash:
Renderer / tab crash - Crash site:
gpu/command_buffer/client/transfer_buffer_cmd_copy_helpers.h:87 - Crash state: stale-pointer READ during
memcpyfrom detachedInt32Arraybacking stores reached viagpu::gles2::GLES2Implementation::MultiDrawElementsWEBGLHelper - Client ID:
N/A
See attached asan_crash.log for the ASan signal.
Reporter Credit
- Reporter credit:
heapracer