Chrome · Bindings
CVE-2026-110170
UAF in Bindings
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/bindings/core/v8/native_value_traits_impl.cc |
modified | |
ifthird_party/blink/renderer/platform/bindings/string_resource.h |
modified | |
ifthird_party/blink/renderer/platform/bindings/to_blink_string.cc |
modified |
Files Changed
third_party/blink/renderer/platform/bindings/to_blink_string.ccthird_party/blink/renderer/platform/bindings/to_blink_string.hthird_party/blink/renderer/bindings/core/v8/native_value_traits_impl.hthird_party/blink/renderer/core/css/css_style_declaration.ccthird_party/blink/renderer/bindings/core/v8/native_value_traits_impl.ccthird_party/blink/renderer/platform/bindings/string_resource.h
Patch
From 711ebfd2fd5240901f96ffcb3e03581dd0746d1e Mon Sep 17 00:00:00 2001
From: Andrey Kosyakov <caseq@chromium.org>
Date: Mon, 14 Sep 2026 21:29:16 -0700
Subject: [PATCH] Inline ToBlinkStringView() in NativeValueTraitsStringAdapter::ToStringView(), its only caller
No intended changes to either logic or performance, see stacked CL (https://chromium-review.git.corp.google.com/c/chromium/src/+/8393538) for the actual change.
Bug: 496308073
Change-Id: I661774478228a2e3a2659849872123dfef835aab
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8392220
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1697797}
---
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 59bc4ba..f3a87682 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
@@ -8,12 +8,120 @@
#include "third_party/blink/renderer/bindings/core/v8/v8_ctype_traits.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
+#include "third_party/blink/renderer/platform/bindings/string_resource.h"
+#include "third_party/blink/renderer/platform/bindings/to_blink_string.h"
#include "v8/include/v8-fast-api-calls.h"
namespace blink {
namespace bindings {
+StringView NativeValueTraitsStringAdapter::ToStringView() const& {
+ if (v8_string_.IsEmpty()) [[unlikely]] {
+ return wtf_string_;
+ }
+
+ // Be very careful in this code to ensure it is RVO friendly. Accidentally
+ // breaking RVO will degrade some of the blink_perf benchmarks by a few
+ // percent. This includes moving the StringTraits<>::FromStringResource() call
+ // into GetExternalizedString() as it becomes impossible for the calling code
+ // to satisfy all RVO constraints.
+ if (StringResourceBase* string_resource =
+ StringResourceBase::GetExternalizedString(isolate_, v8_string_)) {
+ return string_resource->GetAtomicString(isolate_).Impl();
+ }
+
+ uint32_t length = v8_string_->Length();
+ if (!length) [[unlikely]] {
+ return StringView(g_empty_atom);
+ }
+
+ // Note that this code path looks very similar to ToBlinkString(). The
+ // critical difference in ToStringView(), if `can_externalize` is false,
+ // there is no attempt to create either an AtomicString or an String. This
+ // can very likely avoid a heap allocation and definitely avoids refcount
+ // churn which can be significantly faster in some hot paths.
+ const bool is_one_byte = v8_string_->IsOneByte();
+ const v8::String::Encoding requested_encoding =
+ is_one_byte ? v8::String::ONE_BYTE_ENCODING
+ : v8::String::TWO_BYTE_ENCODING;
+ bool can_externalize = v8_string_->CanMakeExternal(requested_encoding);
+ if (can_externalize) [[likely]] {
+ // An AtomicString is always used here for externalization. Using a String
+ // would avoid the AtomicStringTable insert however it also means APIs
+ // consuming the returned StringView must do O(l) operations on equality
+ // checking.
+ //
+ // Given that externalization implies reuse of the string, taking the single
+ // O(l) hit to insert into the AtomicStringTable ends up being faster in
+ // most cases.
+ //
+ // If the caller explicitly wants a String, then using ToBlinkString<String>
+ // is the better option.
+ //
+ // If the caller wants a disposable serialization where it knows the
+ // v8::String is unlikely to be re-projected into Blink (seems rare?) then
+ // calling this with kDoNotExternalize and relying on the
+ // StringView::StackBackingStore yields the most efficient code.
+ AtomicString blink_string =
+ ToBlinkString<AtomicString>(isolate_, v8_string_, kExternalize);
+ if (v8_string_->IsExternal()) {
+ return StringView(blink_string.Impl());
+ }
+ }
+
+ // The string has not been externalized. Serialize into
+ // `string_view_backing_store_` and return.
+ //
+ // Note on platforms with v8 pointer compression, this is the hot path
+ // for short strings like "id" as those are never externalized whereas on
+ // platforms without pointer compression GetExternalizedString() is the hot
+ // path.
+ //
+ // This is particularly important when optimizing for blink_perf.bindings as
+ // x64 vs ARM performance will have very different behavior; x64 has
+ // pointer compression but ARM does not. Since a common string used in the
+ // {get,set}-attribute benchmarks is "id", this means optimizations
+ // that affect the microbenchmark in one architecture likely have no effect
+ // (or even a negative effect due to different expectations in branch
+ // prediction) in the other.
+ //
+ // When pointer compression is on, short strings always cause a
+ // serialization to Blink and thus if there are 1000 runs of an API
+ // asking to convert the same `v8_string` to a Blink string, each run will
+ // behavior similarly.
+ //
+ // When pointer compression is off, the first run will externalize the string
+ // going through this path, but subsequent runs will enter the
+ // GetExternalizedString() path and be much faster as it is just extracting
+ // a pointer.
+ //
+ // Confusingly, the ARM and x64 absolute numbers for the benchmarks look
+ // similar (80-90 runs/s on a pixel2 and a Lenovo P920). This can give the
+ // mistaken belief that they are related numbers even though they are
+ // testing almost entirely completely different codepaths. When optimizing
+ // this code, it is instructive to increase the test attribute name string
+ // length. Using something like something like "abcd1234" will make all
+ // platforms externalize and x64 will likely run much much faster (local
+ // test sees 260 runs/s on a x64 P920).
+ //
+ // TODO(ajwong): Revisit if the length restriction on externalization makes
+ // sense. It's odd that pointer compression changes externalization
+ // behavior.
+ if (is_one_byte) {
+ base::span<LChar> lchar = string_view_backing_store_.Realloc<LChar>(length);
+ v8_string_->WriteOneByteV2(isolate_, 0, length, lchar.data());
+ return StringView(lchar);
+ }
+
+ base::span<UChar> uchar = string_view_backing_store_.Realloc<UChar>(length);
+ static_assert(sizeof(UChar) == sizeof(uint16_t),
+ "UChar isn't the same as uint16_t");
+ v8_string_->WriteV2(isolate_, 0, length,
+ reinterpret_cast<uint16_t*>(uchar.data()));
+ return StringView(uchar);
+}
+
static_assert(static_cast<IntegerConversionConfiguration>(
IDLIntegerConvMode::kDefault) == kNormalConversion,
"IDLIntegerConvMode::kDefault == kNormalConversion");
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 ea74c58..c74ba640 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
@@ -286,13 +286,7 @@
return StringType(wtf_string_);
}
- StringView ToStringView() const& {
- if (!v8_string_.IsEmpty()) [[likely]] {
- return ToBlinkStringView(isolate_, v8_string_, string_view_backing_store_,
- kExternalize);
- }
- return wtf_string_;
- }
+ StringView ToStringView() const&;
// Careful here, ordering some of the members here (mainly the isolate) may
// be important in the hot path. Having the isolate the second member showed
diff --git a/third_party/blink/renderer/core/css/css_style_declaration.cc b/third_party/blink/renderer/core/css/css_style_declaration.cc
index a5750d9b..b1fd1f8 100644
--- a/third_party/blink/renderer/core/css/css_style_declaration.cc
+++ b/third_party/blink/renderer/core/css/css_style_declaration.cc
@@ -210,8 +210,8 @@
// 829408).
ExceptionState exception_state(script_state->GetIsolate());
if (value->IsString()) {
- // NativeValueTraits::ToBlinkStringView() (called implicitly on conversion)
- // tries fairly hard to make an AtomicString out of the string,
+ // NativeValueTraitsStringAdapter::ToStringView() (called implicitly on
+ // conversion) tries fairly hard to make an AtomicString out of the string,
// on the basis that we'd probably like cheaper compares down the line.
// However, for our purposes, we never really use that; we mostly tokenize
// it or parse it in some other way. So if it's short enough, we try to
diff --git a/third_party/blink/renderer/platform/bindings/string_resource.h b/third_party/blink/renderer/platform/bindings/string_resource.h
index 8e9188f..cc2dfde 100644
--- a/third_party/blink/renderer/platform/bindings/string_resource.h
+++ b/third_party/blink/renderer/platform/bindings/string_resource.h
@@ -5,6 +5,8 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_BINDINGS_STRING_RESOURCE_H_
#define THIRD_PARTY_BLINK_RENDERER_PLATFORM_BINDINGS_STRING_RESOURCE_H_
+#include <type_traits>
+
#include "base/compiler_specific.h"
#include "base/dcheck_is_on.h"
#include "third_party/blink/renderer/platform/bindings/parkable_string.h"
@@ -22,6 +24,10 @@
USING_FAST_MALLOC(StringResourceBase);
public:
+ static StringResourceBase* GetExternalizedString(
+ v8::Isolate* isolate,
+ v8::Local<v8::String> v8_string);
+
explicit StringResourceBase(v8::Isolate* isolate, String string)
: plain_string_(std::move(string)) {
DCHECK(!plain_string_.IsNull());
@@ -304,6 +310,35 @@
}
};
+ALWAYS_INLINE StringResourceBase* StringResourceBase::GetExternalizedString(
+ v8::Isolate* isolate,
+ v8::Local<v8::String> v8_string) {
+ v8::String::Encoding encoding;
+ v8::String::ExternalStringResourceBase* resource =
+ v8_string->GetExternalStringResourceBase(isolate, &encoding);
+ if (resource) [[likely]] {
+ static_assert(std::is_base_of<v8::String::ExternalOneByteStringResource,
+ StringResource8Base>::value,
+ "");
+ static_assert(std::is_base_of<v8::String::ExternalStringResource,
+ StringResource16Base>::value,
+ "");
+ static_assert(
+ std::is_base_of<StringResourceBase, StringResource8Base>::value, "");
+ static_assert(
+ std::is_base_of<StringResourceBase, StringResource16Base>::value, "");
+ // Then StringResource{8,16}Base allows to go from one ancestry path to
+ // the other one. Even though it's empty, removing it causes UB, see
+ // crbug.com/909796.
+ if (encoding == v8::String::ONE_BYTE_ENCODING) {
+ return static_cast<StringResource8Base*>(resource);
+ }
+ return static_cast<StringResource16Base*>(resource);
+ }
+
+ return nullptr;
+}
+
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_PLATFORM_BINDINGS_STRING_RESOURCE_H_
diff --git a/third_party/blink/renderer/platform/bindings/to_blink_string.cc b/third_party/blink/renderer/platform/bindings/to_blink_string.cc
index 5e0db50d..6eeaa82 100644
--- a/third_party/blink/renderer/platform/bindings/to_blink_string.cc
+++ b/third_party/blink/renderer/platform/bindings/to_blink_string.cc
@@ -108,43 +108,6 @@
return mode == kExternalize && v8_string->CanMakeExternal(requested_encoding);
}
-// Retrieves the StringResourceBase from `v8_string`.
-//
-// Returns a nullptr if there was no previous externalization.
-ALWAYS_INLINE StringResourceBase* GetExternalizedString(
- v8::Isolate* isolate,
- v8::Local<v8::String> v8_string) {
- v8::String::Encoding encoding;
- v8::String::ExternalStringResourceBase* resource =
- v8_string->GetExternalStringResourceBase(isolate, &encoding);
- if (!!resource) [[likely]] {
- // Inheritance:
- // - V8 side: v8::String::ExternalStringResourceBase
- // -> v8::External{One,}ByteStringResource
- // - Both: StringResource{8,16}Base inherits from the matching v8 class.
- static_assert(std::is_base_of<v8::String::ExternalOneByteStringResource,
- StringResource8Base>::value,
- "");
- static_assert(std::is_base_of<v8::String::ExternalStringResource,
- StringResource16Base>::value,
- "");
- static_assert(
- std::is_base_of<StringResourceBase, StringResource8Base>::value, "");
- static_assert(
- std::is_base_of<StringResourceBase, StringResource16Base>::value, "");
- // Then StringResource{8,16}Base allows to go from one ancestry path to
- // the other one. Even though it's empty, removing it causes UB, see
- // crbug.com/909796.
- StringResourceBase* base;
- if (encoding == v8::String::ONE_BYTE_ENCODING)
- base = static_cast<StringResource8Base*>(resource);
- else
- base = static_cast<StringResource16Base*>(resource);
- return base;
- }
-
- return nullptr;
-}
// Converts a `v8_string` to a StringType optionally externalizing if
// `can_externalize` is true; sets `was_externalized` if on successful
@@ -207,7 +170,7 @@
// common case for all platforms with the one exception being super short
// strings on for platforms with v8 pointer compression.
StringResourceBase* string_resource =
- GetExternalizedString(isolate, v8_string);
+ StringResourceBase::GetExternalizedString(isolate, v8_string);
if (string_resource) {
return StringTraits<StringType>::FromStringResource(isolate,
string_resource);
@@ -237,111 +200,6 @@
v8::Local<v8::String>,
ExternalMode);
-StringView ToBlinkStringView(v8::Isolate* isolate,
- v8::Local<v8::String> v8_string,
- StringView::StackBackingStore& backing_store,
- ExternalMode mode) {
- // Be very careful in this code to ensure it is RVO friendly. Accidentally
- // breaking RVO will degrade some of the blink_perf benchmarks by a few
- // percent. This includes moving the StringTraits<>::FromStringResource() call
- // into GetExternalizedString() as it becomes impossible for the calling code
- // to satisfy all RVO constraints.
- StringResourceBase* string_resource =
- GetExternalizedString(isolate, v8_string);
- if (string_resource) {
- return StringTraits<AtomicString>::FromStringResource(isolate,
- string_resource)
- .Impl();
- }
-
- uint32_t length = v8_string->Length();
- if (!length) [[unlikely]] {
- return StringView(g_empty_atom);
- }
-
- // Note that this code path looks very similar to ToBlinkString(). The
- // critical difference in ToBlinkStringView(), if `can_externalize` is false,
- // there is no attempt to create either an AtomicString or an String. This
- // can very likely avoid a heap allocation and definitely avoids refcount
- // churn which can be significantly faster in some hot paths.
- const bool is_one_byte = v8_string->IsOneByte();
- bool can_externalize = CanExternalize(v8_string, mode, is_one_byte);
- if (can_externalize) [[likely]] {
- bool was_externalized;
- // An AtomicString is always used here for externalization. Using a String
- // would avoid the AtomicStringTable insert however it also means APIs
- // consuming the returned StringView must do O(l) operations on equality
- // checking.
- //
- // Given that externalization implies reuse of the string, taking the single
- // O(l) hit to insert into the AtomicStringTable ends up being faster in
- // most cases.
- //
- // If the caller explicitly wants a String, then using ToBlinkString<String>
- // is the better option.
- //
- // If the caller wants a disposable serialization where it knows the
- // v8::String is unlikely to be re-projected into Blink (seems rare?) then
- // calling this with kDoNotExternalize and relying on the
- // StringView::StackBackingStore yields the most efficient code.
- AtomicString blink_string = ConvertAndExternalizeString<AtomicString>(
- isolate, v8_string, can_externalize, is_one_byte, &was_externalized);
- if (was_externalized) {
- return StringView(blink_string.Impl());
- }
- }
-
- // The string has not been externalized. Serialize into `backing_store` and
- // return.
- //
- // Note on platforms with v8 pointer compression, this is the hot path
- // for short strings like "id" as those are never externalized whereas on
- // platforms without pointer compression GetExternalizedString() is the hot
- // path.
- //
- // This is particularly important when optimizing for blink_perf.bindings as
- // x64 vs ARM performance will have very different behavior; x64 has
- // pointer compression but ARM does not. Since a common string used in the
- // {get,set}-attribute benchmarks is "id", this means optimizations
- // that affect the microbenchmark in one architecture likely have no effect
- // (or even a negative effect due to different expectations in branch
- // prediction) in the other.
- //
- // When pointer compression is on, short strings always cause a
- // serialization to Blink and thus if there are 1000 runs of an API
- // asking to convert the same `v8_string` to a Blink string, each run will
- // behavior similarly.
- //
- // When pointer compression is off, the first run will externalize the string
- // going through this path, but subsequent runs will enter the
- // GetExternalizedString() path and be much faster as it is just extracting
- // a pointer.
- //
- // Confusingly, the ARM and x64 absolute numbers for the benchmarks look
- // similar (80-90 runs/s on a pixel2 and a Lenovo P920). This can give the
- // mistaken belief that they are related numbers even though they are
- // testing almost entirely completely different codepaths. When optimizing
- // this code, it is instructive to increase the test attribute name string
- // length. Using something like something like "abcd1234" will make all
- // platforms externalize and x64 will likely run much much faster (local
- // test sees 260 runs/s on a x64 P920).
- //
- // TODO(ajwong): Revisit if the length restriction on externalization makes
- // sense. It's odd that pointer compression changes externalization
- // behavior.
- if (is_one_byte) {
- base::span<LChar> lchar = backing_store.Realloc<LChar>(length);
- v8_string->WriteOneByteV2(isolate, 0, length, lchar.data());
- return StringView(lchar);
- }
-
- base::span<UChar> uchar = backing_store.Realloc<UChar>(length);
- static_assert(sizeof(UChar) == sizeof(uint16_t),
- "UChar isn't the same as uint16_t");
- v8_string->WriteV2(isolate, 0, length,
- reinterpret_cast<uint16_t*>(uchar.data()));
- return StringView(uchar);
-}
... (file diff truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page