chore: cherry-pick 27 changes from chromium, v8, angle, skia, pdfium, libaom (#51137)

* chore: cherry-pick b149a5c62d76 from angle

* chore: cherry-pick 4073d491fb55 from chromium

* chore: cherry-pick 0566b2f5f0d1 from skia

* chore: cherry-pick 8c1ead5a699f from chromium

* chore: cherry-pick 8b08fb7c9dce from chromium

* chore: cherry-pick be87466afecb from chromium

* chore: cherry-pick c215f8e6f049 from chromium

* chore: cherry-pick 036e5e8f69be from v8

* chore: cherry-pick a6357144e7bf from chromium

* chore: cherry-pick 3f9969421ad5 from skia

* chore: cherry-pick ca8a943c247c from pdfium

* chore: cherry-pick 07398289d921 from v8

* chore: cherry-pick 41bfbc009df8 from chromium

* chore: cherry-pick 4002a66778d2 from chromium

* chore: cherry-pick 23865499a86a from chromium

* chore: cherry-pick 7c11e1188705 from dawn

* chore: cherry-pick c81f01b469c4 from chromium

* chore: cherry-pick 1b69067db7d2 from chromium

* chore: cherry-pick d513cd2fe668 from chromium

* chore: cherry-pick bb8d4c29dfdb from chromium

* chore: cherry-pick 847b11ad2fa3 from chromium

* chore: cherry-pick bce2e6728279 from pdfium

* chore: cherry-pick eeb3e031eb89 from chromium

* chore: cherry-pick a068030f5179 from v8

* chore: cherry-pick 4 changes from libaom and add new patch dirs to config.json

* chore: update patches (e sync --3 resolved; drop dawn — no M146 upstream merge)

* chore: update patches

---------

Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
This commit is contained in:
Samuel Attard
2026-04-19 15:47:43 -04:00
committed by GitHub
parent 7f61587762
commit d86cbe9284
34 changed files with 3849 additions and 1 deletions

1
patches/angle/.patches Normal file
View File

@@ -0,0 +1 @@
cherry-pick-b149a5c62d76.patch

View File

@@ -0,0 +1,117 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Geoff Lang <geofflang@chromium.org>
Date: Fri, 27 Mar 2026 16:13:31 -0400
Subject: GL: Fix pack state for BlitGL::copySubTextureCPUReadback
copySubTextureCPUReadback does both ReadPixels and TexImage calls and
needs to make sure the client's pack states are not used. It does this
but in the wrong order causing an invalid pack state to be used for the
ReadPixels call.
Bug: chromium:490170083
Change-Id: I93dcabf52edd6e4e08f999aaa0d96d1fc325211a
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/7708753
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
Commit-Queue: Geoff Lang <geofflang@chromium.org>
diff --git a/src/libANGLE/renderer/gl/BlitGL.cpp b/src/libANGLE/renderer/gl/BlitGL.cpp
index 19780ad029525229cae6a9d07bf6e82dd72ee1aa..787405f204c3adf773b96e943c5f70e8e2aa7e5e 100644
--- a/src/libANGLE/renderer/gl/BlitGL.cpp
+++ b/src/libANGLE/renderer/gl/BlitGL.cpp
@@ -852,10 +852,10 @@ angle::Result BlitGL::copySubTextureCPUReadback(const gl::Context *context,
readFunction = angle::ReadColor<angle::R8G8B8A8, GLfloat>;
}
- gl::PixelUnpackState unpack;
- unpack.alignment = 1;
- ANGLE_TRY(mStateManager->setPixelUnpackState(context, unpack));
- ANGLE_TRY(mStateManager->setPixelUnpackBuffer(context, nullptr));
+ gl::PixelPackState pack;
+ pack.alignment = 1;
+ ANGLE_TRY(mStateManager->setPixelPackState(context, pack));
+ ANGLE_TRY(mStateManager->setPixelPackBuffer(context, nullptr));
ANGLE_GL_TRY(context, mFunctions->readPixels(readPixelsArea.x, readPixelsArea.y,
readPixelsArea.width, readPixelsArea.height,
readPixelsFormat, GL_UNSIGNED_BYTE, sourceMemory));
@@ -870,10 +870,10 @@ angle::Result BlitGL::copySubTextureCPUReadback(const gl::Context *context,
destInternalFormatInfo.format, destInternalFormatInfo.componentType, readPixelsArea.width,
readPixelsArea.height, 1, unpackFlipY, unpackPremultiplyAlpha, unpackUnmultiplyAlpha);
- gl::PixelPackState pack;
- pack.alignment = 1;
- ANGLE_TRY(mStateManager->setPixelPackState(context, pack));
- ANGLE_TRY(mStateManager->setPixelPackBuffer(context, nullptr));
+ gl::PixelUnpackState unpack;
+ unpack.alignment = 1;
+ ANGLE_TRY(mStateManager->setPixelUnpackState(context, unpack));
+ ANGLE_TRY(mStateManager->setPixelUnpackBuffer(context, nullptr));
nativegl::TexSubImageFormat texSubImageFormat =
nativegl::GetTexSubImageFormat(mFunctions, mFeatures, destFormat, destType);
diff --git a/src/tests/capture_replay_tests/capture_replay_expectations.txt b/src/tests/capture_replay_tests/capture_replay_expectations.txt
index 74e572d7a9ecb1503fdb46d273b77ab07375719f..201c64ab200def996ffd7d0f8f85a60446e3ba5b 100644
--- a/src/tests/capture_replay_tests/capture_replay_expectations.txt
+++ b/src/tests/capture_replay_tests/capture_replay_expectations.txt
@@ -80,6 +80,7 @@
42264831 : FramebufferTest_ES3.AttachmentsWithUnequalDimensions/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.ChangeAttachmentThenInvalidateAndDraw/* = SKIP_FOR_CAPTURE
42264831 : FramebufferTest_ES3.RenderAndInvalidateImmutableTextureWithBeyondMaxLevel/* = SKIP_FOR_CAPTURE
+490170083 : CopyTextureTestES3.SRGBWithPackParameters/* = SKIP_FOR_CAPTURE
# The following tests fail with forceRobustResourceInit
# They were accidentally passing until http://crrev/c/5588816
diff --git a/src/tests/gl_tests/CopyTextureTest.cpp b/src/tests/gl_tests/CopyTextureTest.cpp
index cd9bc970b7f646b3e9f037cca3d9e623f8f6afb3..44effc45e132819396c792fbcf947c320bbed0a4 100644
--- a/src/tests/gl_tests/CopyTextureTest.cpp
+++ b/src/tests/gl_tests/CopyTextureTest.cpp
@@ -1988,6 +1988,50 @@ TEST_P(CopyTextureTestDest, AlphaCopyWithRGB)
EXPECT_PIXEL_COLOR_EQ(0, 0, expectedPixels);
}
+// Regression test for TextureGL doing CPU readback when a PBO is bound
+TEST_P(CopyTextureTestES3, SRGBWithPackParameters)
+{
+ ANGLE_SKIP_TEST_IF(!checkExtensions());
+ ANGLE_SKIP_TEST_IF(!EnsureGLExtensionEnabled("GL_EXT_sRGB"));
+
+ GLColor originalPixels(50u, 100u, 150u, 155u);
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[1]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &originalPixels);
+ EXPECT_GL_NO_ERROR();
+
+ glBindTexture(GL_TEXTURE_2D, mTextures[0]);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA_EXT, 1, 1, 0, GL_SRGB_ALPHA_EXT, GL_UNSIGNED_BYTE,
+ nullptr);
+ EXPECT_GL_NO_ERROR();
+
+ GLFramebuffer dstFBO;
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFBO);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTextures[0],
+ 0);
+
+ // Should have no effect on the copy
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 100);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 100);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 100);
+ glPixelStorei(GL_PACK_ALIGNMENT, 8);
+ EXPECT_GL_NO_ERROR();
+
+ std::array<uint8_t, 100 * 100 * 4 * 2> bigPackBuffer = {0};
+ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, bigPackBuffer.data());
+
+ glCopySubTextureCHROMIUM(mTextures[1], 0, GL_TEXTURE_2D, mTextures[0], 0, 0, 0, 0, 0, 1, 1,
+ false, false, false);
+ EXPECT_GL_NO_ERROR();
+
+ glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
+ glPixelStorei(GL_PACK_SKIP_ROWS, 0);
+ glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ EXPECT_PIXEL_COLOR_EQ(0, 0, originalPixels);
+}
+
// Bug where TEXTURE_SWIZZLE_RGBA was not reset after the Luminance workaround. (crbug.com/1022080)
TEST_P(CopyTextureTestES3, LuminanceWorkaroundTextureSwizzleBug)
{

View File

@@ -155,3 +155,18 @@ cherry-pick-fc10b0d6304d.patch
cherry-pick-41c622eea273.patch
fix_initialize_com_on_desktopmedialistcapturethread_on_windows.patch
fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch
cherry-pick-4073d491fb55.patch
cherry-pick-8c1ead5a699f.patch
cherry-pick-8b08fb7c9dce.patch
cherry-pick-be87466afecb.patch
cherry-pick-c215f8e6f049.patch
cherry-pick-a6357144e7bf.patch
cherry-pick-41bfbc009df8.patch
cherry-pick-4002a66778d2.patch
cherry-pick-23865499a86a.patch
cherry-pick-c81f01b469c4.patch
cherry-pick-1b69067db7d2.patch
cherry-pick-d513cd2fe668.patch
cherry-pick-bb8d4c29dfdb.patch
cherry-pick-847b11ad2fa3.patch
cherry-pick-eeb3e031eb89.patch

View File

@@ -0,0 +1,103 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <vasilii@chromium.org>
Date: Wed, 8 Apr 2026 07:48:21 -0700
Subject: Fix cross-domain password leak via manual-fallback preview
In PasswordManualFallbackFlow::DidSelectSuggestion, when a user selects
a password suggestion, the browser process sends the cleartext password
to the renderer for previewing. If the suggestion is cross-domain, this
leak happens without consent or auth.
This CL fixes this by omitting the password in the preview message for
all the cases by sending the fake string.
Fixed: 498269651
Change-Id: Ic9546114c453f05de1030f05c7a9830b39d73038
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735152
Commit-Queue: Vasilii Sukhanov <vasilii@chromium.org>
Reviewed-by: Anna Tsvirchkova <atsvirchkova@google.com>
Cr-Commit-Position: refs/heads/main@{#1611490}
diff --git a/components/password_manager/core/browser/password_manual_fallback_flow.cc b/components/password_manager/core/browser/password_manual_fallback_flow.cc
index d65be8e82a2a8dd202d5eb1644ea3db9f59c18d4..14e18916fbac51665f9f94d99c8bc6ef8afbc112 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow.cc
@@ -211,12 +211,13 @@ void PasswordManualFallbackFlow::DidSelectSuggestion(
if (!form) {
return;
}
+ const auto payload =
+ suggestion.GetPayload<Suggestion::PasswordSuggestionDetails>();
password_manager_driver_->PreviewSuggestionById(
form->username_element_renderer_id,
form->password_element_renderer_id,
GetUsernameFromLabel(suggestion.labels[0][0].value),
- suggestion.GetPayload<Suggestion::PasswordSuggestionDetails>()
- .password);
+ std::u16string(payload.password.length(), '*'));
break;
}
case autofill::SuggestionType::kPasswordFieldByFieldFilling:
diff --git a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
index 866ca1f10b017f48a444742788f3965320647f7c..8789288a9630a921fe0cf79680cf41a54c619e38 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
@@ -656,7 +656,7 @@ TEST_F(PasswordManualFallbackFlowTest,
EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
form.password_element_renderer_id,
std::u16string(u"username"),
- std::u16string(u"password")));
+ std::u16string(u"********")));
Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
SuggestionType::kPasswordEntry, u"google.com",
CreateTestPasswordDetails());
@@ -667,6 +667,40 @@ TEST_F(PasswordManualFallbackFlowTest,
flow().DidSelectSuggestion(suggestion);
}
+// Test that password manual fallback suggestion is previewed without password
+// if the suggestion is cross-domain.
+TEST_F(PasswordManualFallbackFlowTest,
+ SelectFillFullFormSuggestion_CrossDomain_TriggeredOnAPasswordForm) {
+ InitializeFlow();
+ ProcessPasswordStoreUpdates();
+
+ PasswordForm form;
+ form.username_element_renderer_id = MakeFieldRendererId();
+ form.password_element_renderer_id = MakeFieldRendererId();
+ // Simulate that the field is/isn't classified as target filling password.
+ EXPECT_CALL(password_form_cache(),
+ GetPasswordForm(_, form.username_element_renderer_id))
+ .WillRepeatedly(Return(&form));
+
+ flow().RunFlow(form.username_element_renderer_id, gfx::RectF{},
+ TextDirection::LEFT_TO_RIGHT);
+
+ // Expect that the password is empty in the preview call.
+ EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
+ form.password_element_renderer_id,
+ std::u16string(u"username"),
+ std::u16string(u"********")));
+ Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
+ SuggestionType::kPasswordEntry, u"google.com",
+ Suggestion::PasswordSuggestionDetails(u"username", u"password",
+ "https://cross-domain.com/",
+ u"cross-domain.com",
+ /*is_cross_domain=*/true));
+ suggestion.labels = {{Suggestion::Text(u"username")}};
+ suggestion.acceptability = Suggestion::Acceptability::kAcceptable;
+ flow().DidSelectSuggestion(suggestion);
+}
+
// Test that only password field is previewed if the credential doesn't have
// a username saved for it.
TEST_F(PasswordManualFallbackFlowTest,
@@ -687,7 +721,7 @@ TEST_F(PasswordManualFallbackFlowTest,
EXPECT_CALL(driver(), PreviewSuggestionById(FieldRendererId(),
form.password_element_renderer_id,
std::u16string(),
- std::u16string(u"password")));
+ std::u16string(u"********")));
Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
SuggestionType::kPasswordEntry, u"google.com",
CreateTestPasswordDetails());

View File

@@ -0,0 +1,224 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fergal Daly <fergal@chromium.org>
Date: Sun, 12 Apr 2026 20:37:39 -0700
Subject: [M146] Fix UAF in FileSystemAccessChangeSource.
Original change's description:
> Fix UAF in FileSystemAccessChangeSource.
>
> `DidInitialize` calls any outstanding initialization callbacks but a
> callback can delete this. The code guards against this in its access
> of `initialization_callbacks_` but not `initialization_result_`.
>
> This fix keeps a copy of the result on the stack.
>
> This also adds a test which fails with ASAN before the fix is applied
> and passes after.
>
> The basic test code was written by Gemini.
>
> Fixed: 497880137
> Change-Id: I046831db23cb4b8e41964910e2aede9b1be0db7f
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728464
> Auto-Submit: Fergal Daly <fergal@chromium.org>
> Reviewed-by: Ming-Ying Chung <mych@chromium.org>
> Commit-Queue: Ming-Ying Chung <mych@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1610499}
(cherry picked from commit c0390bcd64ba1fd6594fbc9f6246a1649662d683)
Bug: 500247135,497880137
Change-Id: I046831db23cb4b8e41964910e2aede9b1be0db7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7754020
Commit-Queue: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Auto-Submit: Chrome Cherry Picker <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7680@{#3929}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/content/browser/file_system_access/file_system_access_change_source.cc b/content/browser/file_system_access/file_system_access_change_source.cc
index 566dc1ea40b43a54b33d70e82a20ff5695b57b5e..48bd867a9d3d140eaf515ea7bc1613231f7e79e9 100644
--- a/content/browser/file_system_access/file_system_access_change_source.cc
+++ b/content/browser/file_system_access/file_system_access_change_source.cc
@@ -71,13 +71,14 @@ void FileSystemAccessChangeSource::DidInitialize(
CHECK(!initialization_result_.has_value());
CHECK(!initialization_callbacks_.empty());
- initialization_result_ = std::move(result);
+ // The callbacks may cause |this| to be deleted, so we should only use
+ // stack-based objects below.
+ initialization_result_ = result->Clone();
- // Move the callbacks to the stack since they may cause |this| to be deleted.
auto initialization_callbacks = std::move(initialization_callbacks_);
initialization_callbacks_.clear();
for (auto& callback : initialization_callbacks) {
- std::move(callback).Run(initialization_result_->Clone());
+ std::move(callback).Run(result->Clone());
}
}
diff --git a/content/browser/file_system_access/file_system_access_change_source_unittest.cc b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..b0f15909bebda29fc2ec689a6d3b15d797dcc722
--- /dev/null
+++ b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
@@ -0,0 +1,146 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/file_system_access/file_system_access_change_source.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "content/browser/file_system_access/file_system_access_watch_scope.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "storage/browser/file_system/file_system_url.h"
+#include "storage/browser/quota/quota_manager_proxy.h"
+#include "storage/browser/test/test_file_system_context.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockRawChangeObserver
+ : public FileSystemAccessChangeSource::RawChangeObserver {
+ public:
+ MOCK_METHOD(void,
+ OnRawChange,
+ (const storage::FileSystemURL& changed_url,
+ bool error,
+ const FileSystemAccessChangeSource::ChangeInfo& change_info,
+ const FileSystemAccessWatchScope& scope),
+ (override));
+ MOCK_METHOD(void,
+ OnUsageChange,
+ (size_t old_usage,
+ size_t new_usage,
+ const FileSystemAccessWatchScope& scope),
+ (override));
+ MOCK_METHOD(void,
+ OnSourceBeingDestroyed,
+ (FileSystemAccessChangeSource * source),
+ (override));
+};
+
+class FakeChangeSource : public FileSystemAccessChangeSource {
+ public:
+ FakeChangeSource(
+ FileSystemAccessWatchScope scope,
+ scoped_refptr<storage::FileSystemContext> file_system_context)
+ : FileSystemAccessChangeSource(std::move(scope),
+ std::move(file_system_context)) {}
+ ~FakeChangeSource() override = default;
+
+ // FileSystemAccessChangeSource:
+ void Initialize(
+ base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)>
+ on_source_initialized) override {
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(on_source_initialized),
+ blink::mojom::FileSystemAccessError::New(
+ blink::mojom::FileSystemAccessStatus::kOk,
+ base::File::FILE_OK, "")));
+ }
+
+ void Signal(const storage::FileSystemURL& changed_url,
+ bool error = false,
+ ChangeInfo change_info = ChangeInfo()) {
+ NotifyOfChange(changed_url, error, change_info);
+ }
+};
+
+} // namespace
+
+class FileSystemAccessChangeSourceTest : public testing::Test {
+ public:
+ FileSystemAccessChangeSourceTest()
+ : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}
+
+ void SetUp() override {
+ ASSERT_TRUE(dir_.CreateUniqueTempDir());
+ file_system_context_ = storage::CreateFileSystemContextForTesting(
+ /*quota_manager_proxy=*/nullptr, dir_.GetPath());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ base::ScopedTempDir dir_;
+ scoped_refptr<storage::FileSystemContext> file_system_context_;
+};
+
+TEST_F(FileSystemAccessChangeSourceTest, CreateAndInitialize) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource source(scope, file_system_context_);
+
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+ source.EnsureInitialized(future.GetCallback());
+ EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+TEST_F(FileSystemAccessChangeSourceTest, NotifyOfChange) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource source(scope, file_system_context_);
+
+ MockRawChangeObserver observer;
+ source.AddObserver(&observer);
+
+ EXPECT_CALL(observer, OnRawChange(testing::Eq(file_url), testing::IsFalse(),
+ testing::_, testing::Eq(scope)));
+ source.Signal(file_url);
+
+ source.RemoveObserver(&observer);
+}
+
+// A callback passed to `EnsureInitialized` may result in `this` being
+// destroyed. This tests that `DidInitialize` (which calls the callbacks) is
+// robust to that situation. See https://crbug.com/497880137.
+TEST_F(FileSystemAccessChangeSourceTest, TestDestroyFromInitializeCallback) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource* source = new FakeChangeSource(scope, file_system_context_);
+
+ source->EnsureInitialized(base::BindOnce(
+ [](FakeChangeSource* source, blink::mojom::FileSystemAccessErrorPtr) {
+ delete source;
+ },
+ base::Unretained(source)));
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+ source->EnsureInitialized(future.GetCallback());
+ EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 07cbf495717714d71d977a8820e08050c3062526..f5d72a89c7229bf8e897c90660feca482ac82594 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2656,6 +2656,7 @@ test("content_unittests") {
"../browser/fenced_frame/redacted_fenced_frame_config_mojom_traits_unittest.cc",
"../browser/file_system/browser_file_system_helper_unittest.cc",
"../browser/file_system/file_system_operation_runner_unittest.cc",
+ "../browser/file_system_access/file_system_access_change_source_unittest.cc",
"../browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc",
"../browser/file_system_access/file_system_access_file_handle_impl_unittest.cc",
"../browser/file_system_access/file_system_access_file_modification_host_impl_unittest.cc",

View File

@@ -0,0 +1,38 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <vasilyt@chromium.org>
Date: Wed, 1 Apr 2026 11:19:52 -0700
Subject: Fix potential double free in deserializing CopyOutputResults
Skia always runs release proc of the SkBitmap::installPixels [1].
[1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/include/core/SkBitmap.h;drc=405f385dce2db578ff3b2301686d231ee8f0b042;l=586
Bug: 497846428
Change-Id: I4d3fd2e676fa7aa74e1022cbd2c9d9db8970a90c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718978
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Cr-Commit-Position: refs/heads/main@{#1608675}
diff --git a/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc b/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
index 59fbc85e257778d1743688716b314ed983f2f324..9d6bdd42622dffd90d39d6977bdec169baf98170 100644
--- a/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
+++ b/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
@@ -111,12 +111,14 @@ bool StructTraits<viz::mojom::BitmapInSharedMemoryDataView, SkBitmap>::Read(
return false;
}
- if (!sk_bitmap->installPixels(image_info, mapping_ptr->memory(),
+ // Skia guarantees that it will call release proc, so we pass release()'ed
+ // pointer into it.
+ void* bitmap_memory = mapping_ptr->memory();
+ if (!sk_bitmap->installPixels(image_info, bitmap_memory,
data.row_bytes(), &DeleteSharedMemoryMapping,
- mapping_ptr.get())) {
+ mapping_ptr.release())) {
return false;
}
- mapping_ptr.release();
return true;
}

View File

@@ -0,0 +1,213 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Matt Menke <mmenke@chromium.org>
Date: Fri, 27 Mar 2026 09:19:35 -0700
Subject: Make SpdySession::CreateStream() call DoDrainSession()
asynchronously.
Calling it synchronously would tear down all SpdyStreams immediately,
informing their consumers of the error. This could have side effects
that affect the caller trying to create the stream, so was unsafe.
This does introduce a state where a SpdySession is going away, but
neither DoDrainSession() nor StartGoingAway() was invoked. The
SpdySession never reached such a state before this CL, but this state
was used before - when there was a network change, we used to move
SpdySessions into such a state. This behavior was removed because we
ended up never actually closing those sockets, which could effectively
blackhole a destination. Since this CL posts a task to drain the
session, that shouldn't happen here.
The code is robust against extra DoDrainSession() calls, so it should
be fine if the session discovers through another path it should start
draining or otherwise going away,
Bug: 493628982
Change-Id: I23f1517b67fb55edd50d6e8fc8f1b4d8328e8ec5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7701714
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: mmenke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1606281}
diff --git a/net/socket/socket_test_util.cc b/net/socket/socket_test_util.cc
index 957ebf883fef96af8fea16cb883e8c663dce8b2b..eb811b8b5dc382cf1fbd4cc75a63c416d71f746b 100644
--- a/net/socket/socket_test_util.cc
+++ b/net/socket/socket_test_util.cc
@@ -1251,7 +1251,7 @@ void MockTCPClientSocket::Disconnect() {
bool MockTCPClientSocket::IsConnected() const {
if (!data_)
return false;
- return connected_ && !peer_closed_connection_;
+ return connected_ && !peer_closed_connection_ && !data_->silently_closed();
}
bool MockTCPClientSocket::IsConnectedAndIdle() const {
diff --git a/net/socket/socket_test_util.h b/net/socket/socket_test_util.h
index 204fcd3eb95a283783f508153137328e162bbde9..78ebf2140902f14e1f6506c6a1e3654e5fc8c4ff 100644
--- a/net/socket/socket_test_util.h
+++ b/net/socket/socket_test_util.h
@@ -447,6 +447,12 @@ class SocketDataProvider {
MockConnect connect_data() const { return connect_; }
void set_connect_data(const MockConnect& connect) { connect_ = connect; }
+ // Makes IsConnected() start returning false for any socket using `this`,
+ // without any read or write error. Useful for simulating cases where an
+ // IsConnected() call is the first time a socket is revealed to be closed.
+ void set_silently_closed() { silently_closed_ = true; }
+ bool silently_closed() const { return silently_closed_; }
+
private:
// Called to inform subclasses of initialization.
virtual void Reset() = 0;
@@ -459,6 +465,8 @@ class SocketDataProvider {
// This reflects the default state of TCPClientSockets.
bool no_delay_ = true;
+ bool silently_closed_ = false;
+
KeepAliveState keep_alive_state_ = KeepAliveState::kDefault;
int keep_alive_delay_ = 0;
diff --git a/net/spdy/spdy_session.cc b/net/spdy/spdy_session.cc
index 7465a28030a65eb4964e537134c60ac64ab54f25..9dfbb5dd63802add67931d2aa0b06aa62d6d7be1 100644
--- a/net/spdy/spdy_session.cc
+++ b/net/spdy/spdy_session.cc
@@ -1682,7 +1682,9 @@ int SpdySession::CreateStream(const SpdyStreamRequest& request,
UMA_HISTOGRAM_BOOLEAN("Net.SpdySession.CreateStreamWithSocketConnected",
socket_->IsConnected());
if (!socket_->IsConnected()) {
- DoDrainSession(
+ // Since there may be a consumer of the session on the stack, can't call
+ // DoDrainSession() synchronously, as it may result in reentrancy.
+ DoDrainSessionAsync(
ERR_CONNECTION_CLOSED,
"Tried to create SPDY stream for a closed socket connection.");
return ERR_CONNECTION_CLOSED;
@@ -2674,6 +2676,23 @@ void SpdySession::DoDrainSession(Error err,
MaybePostWriteLoop();
}
+void SpdySession::DoDrainSessionAsync(Error err,
+ std::string description,
+ bool force_send_go_away) {
+ // Make this unavailable to prevent consumers from pulling it from the session
+ // pool again, which could result in an infinite loop, or otherwise running
+ // into this error again rather than trying a new connection.
+ MakeUnavailable(err);
+
+ // This will close the socket and inform consumers asynchronously. If
+ // something happens before this task runs (like a read error), that should
+ // not cause issues, since DoDrainSession() does nothing if already draining.
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&SpdySession::DoDrainSession, weak_factory_.GetWeakPtr(),
+ err, std::move(description), force_send_go_away));
+}
+
void SpdySession::LogAbandonedStream(SpdyStream* stream, Error status) {
DCHECK(stream);
stream->LogStreamError(status, "Abandoned.");
diff --git a/net/spdy/spdy_session.h b/net/spdy/spdy_session.h
index b36f8fae637e49cb82141cf59d123a49c9c931ac..a5896d76fbd73c5dfd0abcae77d5dfaadbaa3d54 100644
--- a/net/spdy/spdy_session.h
+++ b/net/spdy/spdy_session.h
@@ -857,6 +857,15 @@ class NET_EXPORT SpdySession
const std::string& description,
bool force_send_go_away = false);
+ // Immediately marks a session as unavailable, to prevent reuse, and posts a
+ // task to call DoDrainSession (if the session is drained for some other
+ // reason in the meantime, that is fine). This should be used instead of
+ // DoDrainSession when there may be a consumer of the SpdySession on the
+ // stack, so as to avoid reentrancy.
+ void DoDrainSessionAsync(Error err,
+ std::string description,
+ bool force_send_go_away = false);
+
// Called right before closing a (possibly-inactive) stream for a
// reason other than being requested to by the stream.
void LogAbandonedStream(SpdyStream* stream, Error status);
diff --git a/net/spdy/spdy_session_unittest.cc b/net/spdy/spdy_session_unittest.cc
index 44c9ea47b39fe8c3efbe5e39f0b6d028fa73af97..6e808d05d16f144afdfdae229ec4c8f110d4fde0 100644
--- a/net/spdy/spdy_session_unittest.cc
+++ b/net/spdy/spdy_session_unittest.cc
@@ -976,10 +976,14 @@ TEST_F(SpdySessionTest, CreateStreamAfterGoAway) {
EXPECT_TRUE(session_->IsStreamActive(1));
SpdyStreamRequest stream_request;
+ // Note that `can_send_early` is needed to bypass confirming the handshake. If
+ // this regresses, may need to do what other tests to, and use
+ // CreateStreamSynchronously() to create an initial SpdyStream and set up the
+ // socket.
int rv = stream_request.StartRequest(
- SPDY_REQUEST_RESPONSE_STREAM, session_, test_url_, false, MEDIUM,
- SocketTag(), NetLogWithSource(), CompletionOnceCallback(),
- TRAFFIC_ANNOTATION_FOR_TESTS);
+ SPDY_REQUEST_RESPONSE_STREAM, session_, test_url_,
+ /*can_send_early=*/true, MEDIUM, SocketTag(), NetLogWithSource(),
+ CompletionOnceCallback(), TRAFFIC_ANNOTATION_FOR_TESTS);
EXPECT_THAT(rv, IsError(ERR_FAILED));
EXPECT_TRUE(session_);
@@ -2835,6 +2839,62 @@ TEST_F(SpdySessionTest, CancelTwoStalledCreateStream) {
EXPECT_EQ(0u, pending_create_stream_queue_size(LOWEST));
}
+// Check that SpdyStreamRequest::StartRequest() does not synchronously notify
+// live streams of their destruction when it notices the socket has been closed.
+// This can racily happen when a new request occurs before a read error from the
+// socket is processed. This synchronously informing other streams of their
+// destruction could result in modifying objects that are on the top of the
+// callstack due to shared state, which can lead to bugs.
+TEST_F(SpdySessionTest,
+ SpdyStreamRequestStartRequestAsynchronouslyNotifiesOtherStreams) {
+ StaticSocketDataProvider data;
+ session_deps_.socket_factory->AddSocketDataProvider(&data);
+ AddSSLSocketData();
+
+ CreateNetworkSession();
+ CreateSpdySession();
+
+ // Create a stream on the session, and set up a delegate to watch it.
+ base::WeakPtr<SpdyStream> spdy_stream =
+ CreateStreamSynchronously(SPDY_REQUEST_RESPONSE_STREAM, session_,
+ test_url_, MEDIUM, NetLogWithSource());
+ test::StreamDelegateDoNothing delegate(spdy_stream);
+ spdy_stream->SetDelegate(&delegate);
+
+ // Close the socket, without a read/write event, to simulate the
+ // StartRequest() being the first call to notice the socket is closed.
+ data.set_silently_closed();
+
+ // Start a StreamRequest request. Note that `can_send_early` must be true to
+ // avoid calling MockSSLClientSocket::ConfirmHandshake(), will cause the
+ // request not to check the state of the connection, while it waits for the
+ // SSL handshake to be confirmed (that handshake confirmation check will also
+ // cause the MockSSLClientSocket to CHECK, if it happens, as the socket is
+ // closed).
+ SpdyStreamRequest request;
+ int rv = request.StartRequest(
+ SPDY_REQUEST_RESPONSE_STREAM, session_, test_url_,
+ /*can_send_early=*/true, LOWEST, SocketTag(), NetLogWithSource(),
+ base::BindOnce([](int result) {
+ ADD_FAILURE()
+ << "Callback should not be invoked on synchronous completion";
+ }),
+ TRAFFIC_ANNOTATION_FOR_TESTS);
+ // The request should synchronously fail.
+ EXPECT_THAT(rv, IsError(ERR_CONNECTION_CLOSED));
+
+ // The session should be flagged as going away, and should no longer be
+ // available but should still exist.
+ EXPECT_TRUE(session_->IsGoingAway());
+ EXPECT_FALSE(HasSpdySession(spdy_session_pool_, key_));
+ // The first stream should not have been closed synchronously. Instead, a task
+ // should have been posted to close it.
+ EXPECT_FALSE(delegate.StreamIsClosed());
+
+ // Wait for the stream to be closed.
+ EXPECT_THAT(delegate.WaitForClose(), IsError(ERR_CONNECTION_CLOSED));
+}
+
// Test that SpdySession::DoReadLoop reads data from the socket
// without yielding. This test makes 32k - 1 bytes of data available
// on the socket for reading. It then verifies that it has read all

View File

@@ -0,0 +1,60 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Tommy Steimel <steimel@chromium.org>
Date: Tue, 31 Mar 2026 16:11:55 -0700
Subject: [Media Session] Don't assume there is still 1 normal player
There are some actions in MediaSessionImpl that are only available when
there is exactly 1 normal player, so when they're called, there's a
DCHECK that we do in fact have 1 normal player. However, since Mojo
calls are asynchronous, it's possible for one of these actions to be
legitimately called with 1 normal player, but by the time it runs there
are either 0 or 2+ normal players.
This CL changes these instances to no longer DCHECK that there is 1
normal player and instead just early return if there isn't.
Bug: 497412658
Change-Id: I0fdf3c6779c224db996091b2fd463bc3cb9464f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7719021
Reviewed-by: Benjamin Keen <bkeen@google.com>
Commit-Queue: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608166}
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index 542bd7ee9ab011b94d41517286334b75a94b47f8..24115ab4024a981334f1f9111d9017b37bf634b4 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -1290,7 +1290,6 @@ void MediaSessionImpl::EnterPictureInPicture() {
return;
}
- DCHECK_EQ(normal_players_.size(), 1u);
if (normal_players_.size() != 1u) {
// There should be one and only one player when we enter picture-in-picture.
return;
@@ -1355,13 +1354,23 @@ void MediaSessionImpl::Raise() {
}
void MediaSessionImpl::SetMute(bool mute) {
- DCHECK_EQ(normal_players_.size(), 1u);
+ // The SetMute action should only be available when there is one normal
+ // player, though due to the asynchronous nature of mojo, we may no longer
+ // have 1 normal player. In that case, just return.
+ if (normal_players_.size() != 1u) {
+ return;
+ }
normal_players_.begin()->first.observer->OnSetMute(
normal_players_.begin()->first.player_id, mute);
}
void MediaSessionImpl::RequestMediaRemoting() {
- DCHECK_EQ(normal_players_.size(), 1u);
+ // The RequestMediaRemoting action should only be available when there is one
+ // normal player, though due to the asynchronous nature of mojo, we may no
+ // longer have 1 normal player. In that case, just return.
+ if (normal_players_.size() != 1u) {
+ return;
+ }
normal_players_.begin()->first.observer->OnRequestMediaRemoting(
normal_players_.begin()->first.player_id);
}

View File

@@ -0,0 +1,203 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Joey Arhar <jarhar@chromium.org>
Date: Fri, 10 Apr 2026 12:19:11 -0700
Subject: Fix crashes when restoring <selectedcontent> with <input>
When restoring form control state, the DOM could be modified to add or
remove more listed elements to the form if a select element is being
restored which has a selectedcontent element.
Fixed: 499384399
Change-Id: I18f69c30ae25396c53625f7a3172626b79de3ae3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7732030
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1613032}
diff --git a/third_party/blink/renderer/core/html/forms/form_controller.cc b/third_party/blink/renderer/core/html/forms/form_controller.cc
index cb0e37de57188c17dcdec899573b4a6c6396932d..214eb921095c52c02e4a16b4783897953dff2a79 100644
--- a/third_party/blink/renderer/core/html/forms/form_controller.cc
+++ b/third_party/blink/renderer/core/html/forms/form_controller.cc
@@ -492,8 +492,10 @@ void FormController::RestoreControlStateIn(HTMLFormElement& form) {
if (!document_->HasFinishedParsing())
return;
EventQueueScope scope;
- const ListedElement::List& elements = form.ListedElements();
- for (const auto& control : elements) {
+ // Make a copy of the list because the DOM could be modified during
+ // restoration of a <select> with a <selectedcontent> element.
+ ListedElement::List elements_copy(form.ListedElements());
+ for (const auto& control : elements_copy) {
if (!control->ClassSupportsStateRestore())
continue;
if (OwnerFormForState(*control) != &form)
@@ -550,7 +552,11 @@ void FormController::RestoreAllControlsInDocumentOrder() {
return;
HeapHashSet<Member<HTMLFormElement>> finished_forms;
EventQueueScope scope;
- for (auto& control : document_state_->GetControlList()) {
+ // Make a copy of the list because the DOM could be modified during
+ // restoration of a <select> with a <selectedcontent> element.
+ DocumentState::ControlList control_list_copy(
+ document_state_->GetControlList());
+ for (auto& control : control_list_copy) {
auto* owner = OwnerFormForState(*control);
if (!owner)
RestoreControlStateFor(*control);
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/resources/selectedcontent-input.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/resources/selectedcontent-input.html
new file mode 100644
index 0000000000000000000000000000000000000000..847f42ac304835c2049cf434a4dec68814ad533c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/resources/selectedcontent-input.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<style>
+select,::picker(select) {
+ appearance: base-select;
+}
+</style>
+<form action="blank.html">
+ <select>
+ <button>
+ <selectedcontent></selectedcontent>
+ </button>
+ <option id=one>one</option>
+ <option id=two>two</option>
+ </select>
+</form>
+
+<script>
+window.createInput = () => {
+ const selectedcontent = document.querySelector('selectedcontent');
+ const input = document.createElement('input');
+ window.input = input;
+ input.name = 'input';
+ selectedcontent.innerHTML = '';
+ selectedcontent.appendChild(input);
+};
+window.createInput();
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.html
deleted file mode 100644
index da5fe450abbae0d19826021f114cc6388f97bc57..0000000000000000000000000000000000000000
--- a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<link rel=author href="mailto:jarhar@chromium.org">
-<link rel=help href="https://github.com/whatwg/html/issues/9799">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="/resources/testdriver.js"></script>
-<script src="/resources/testdriver-vendor.js"></script>
-
-<iframe src="resources/selectedcontent-restore-iframe.html"></iframe>
-
-<script>
-const iframe = document.querySelector('iframe');
-promise_test(async () => {
- await new Promise(resolve => iframe.onload = resolve);
- await test_driver.bless();
-
- iframe.contentDocument.querySelector('select').value = 'two';
- assert_equals(iframe.contentDocument.querySelector('select').value, 'two',
- 'Assining two to select.value should work.');
- iframe.contentDocument.querySelector('form').submit();
- await new Promise(resolve => iframe.onload = resolve);
-
- await test_driver.bless();
- iframe.contentWindow.history.back();
- await new Promise(resolve => iframe.onload = resolve);
- await new Promise(requestAnimationFrame);
- await new Promise(requestAnimationFrame);
-
- assert_equals(iframe.contentDocument.querySelector('select').value, 'two',
- 'The selects value should be restored after navigating back.');
- assert_equals(iframe.contentDocument.querySelector('selectedcontent').innerHTML,
- iframe.contentDocument.querySelector('option[value=two]').innerHTML,
- 'selectedcontent.innerHTML should match the selected <option>');
-}, '<selectedcontent> should be up to date after form restoration.');
-</script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.optional.html b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.optional.html
new file mode 100644
index 0000000000000000000000000000000000000000..1d0064659cd9df06d6267261bf0b39b3fb29aeef
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/forms/the-select-element/customizable-select/selectedcontent-restore.optional.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://github.com/whatwg/html/issues/9799">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<!-- This test is marked optional because form control restoration is not explicitly specified. -->
+
+<iframe id=iframe1 src="resources/selectedcontent-restore-iframe.html"></iframe>
+<iframe id=iframe2 src="resources/selectedcontent-input.html"></iframe>
+
+<script>
+const iframe1 = document.getElementById('iframe1');
+const iframe2 = document.getElementById('iframe2');
+const iframe1load = new Promise(resolve => iframe1.onload = resolve);
+const iframe2load = new Promise(resolve => iframe2.onload = resolve);
+
+promise_test(async () => {
+ await iframe1load;
+ await test_driver.bless();
+
+ iframe1.contentDocument.querySelector('select').value = 'two';
+ assert_equals(iframe1.contentDocument.querySelector('select').value, 'two',
+ 'Assigning two to select.value should work.');
+ iframe1.contentDocument.querySelector('form').submit();
+ await new Promise(resolve => iframe1.onload = resolve);
+
+ await test_driver.bless();
+ iframe1.contentWindow.history.back();
+ // Form controls are restored immediately after the load event is fired, so
+ // one rAF is added after awaiting the load event. See
+ // LocalDOMWindow::DispatchLoadAndPageshowEvents.
+ await new Promise(resolve => iframe1.onload = resolve);
+ await new Promise(requestAnimationFrame);
+
+ assert_equals(iframe1.contentDocument.querySelector('select').value, 'two',
+ 'The selects value should be restored after navigating back.');
+ assert_equals(iframe1.contentDocument.querySelector('selectedcontent').innerHTML,
+ iframe1.contentDocument.querySelector('option[value=two]').innerHTML,
+ 'selectedcontent.innerHTML should match the selected <option>');
+}, '<selectedcontent> should be up to date after form restoration.');
+
+promise_test(async () => {
+ await iframe2load;
+ await test_driver.bless();
+
+ iframe2.contentDocument.querySelector('select').value = 'two';
+ iframe2.contentWindow.createInput();
+ iframe2.contentDocument.querySelector('input').value = 'value';
+ iframe2.contentDocument.querySelector('form').submit();
+ await new Promise(resolve => iframe2.onload = resolve);
+
+ await test_driver.bless();
+ iframe2.contentWindow.history.back();
+ // Form controls are restored immediately after the load event is fired, so
+ // one rAF is added after awaiting the load event. See
+ // LocalDOMWindow::DispatchLoadAndPageshowEvents.
+ await new Promise(resolve => iframe2.onload = resolve);
+ await new Promise(requestAnimationFrame);
+
+ // A crash would happen here because the form restoration code would iterate
+ // over all of the form controls and remove an input element to restore during
+ // restoration of the selectedcontent element, then try to restore the
+ // disconnected input.
+
+ assert_equals(iframe2.contentDocument.querySelector('select').value, 'two',
+ 'The selects value should be restored after navigating back.');
+ assert_equals(iframe2.contentDocument.querySelector('selectedcontent').innerHTML,
+ iframe2.contentDocument.getElementById('two').innerHTML,
+ 'selectedcontent.innerHTML should match the selected <option>');
+ assert_equals(iframe2.contentWindow.input.value, '',
+ 'The text inputs value should not be restored because it was removed before restoring.');
+}, '<input> inside <selectedcontent> should be restored after form submission.');
+</script>

View File

@@ -0,0 +1,67 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: p0-tato <smartphonewithbear@gmail.com>
Date: Tue, 14 Apr 2026 13:14:30 -0700
Subject: [M146] Fix dangling pointers in OpenXrSpatialFrameworkManager
Original change's description:
> Fix dangling pointers in OpenXrSpatialFrameworkManager
>
> Pointers to vector elements were collected during emplace_back,
> which invalidates them on reallocation. Split into two loops
> and reserve the correct capacity.
>
> Bug: 497724498
> Change-Id: I204534bc1bd1522fe03db86f03c2c3e0d285631c
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735242
> Commit-Queue: Brian Sheedy <bsheedy@chromium.org>
> Reviewed-by: Brian Sheedy <bsheedy@chromium.org>
> Reviewed-by: Brandon Jones <bajones@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1613990}
(cherry picked from commit b173791bf4026a6bb43124f7c5f46cfa4539c014)
Bug: 502440265,497724498
Change-Id: I204534bc1bd1522fe03db86f03c2c3e0d285631c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759844
Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7680@{#3944}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/AUTHORS b/AUTHORS
index 7cc777b399ab46f88b6b1809bf6fd0cb22170694..505480b09c1d41b1facf4e2b165bad86b1815127 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -729,6 +729,7 @@ Jihoon Chung <jihoon@gmail.com>
Jihun Brent Kim <devgrapher@gmail.com>
Jihwan Marc Kim <bluewhale.marc@gmail.com>
Jihye Hyun <jijinny26@gmail.com>
+Jihyeon Jeong <smartphonewithbear@gmail.com>
Jihyeon Lee <wlgus7464@gmail.com>
Jim Wu <lofoz.tw@gmail.com>
Jin Yang <jin.a.yang@intel.com>
diff --git a/device/vr/openxr/openxr_spatial_framework_manager.cc b/device/vr/openxr/openxr_spatial_framework_manager.cc
index 520f25230c427bf775333910530d1ad841f3ad71..5c93d694aa5a2259c683f1d521611046293195a2 100644
--- a/device/vr/openxr/openxr_spatial_framework_manager.cc
+++ b/device/vr/openxr/openxr_spatial_framework_manager.cc
@@ -71,12 +71,15 @@ OpenXrSpatialFrameworkManager::OpenXrSpatialFrameworkManager(
// to help abstract some of the details of creating the child structs, even
// though at present we only have a configuration base.
std::vector<OpenXrSpatialCapabilityConfigurationBase> capability_configs;
- std::vector<XrSpatialCapabilityConfigurationBaseHeaderEXT*>
- capability_config_ptrs;
+ capability_configs.reserve(capability_configuration.size());
for (auto& [capability, components] : capability_configuration) {
capability_configs.emplace_back(capability, components);
- capability_config_ptrs.push_back(
- capability_configs.back().GetAsBaseHeader());
+ }
+
+ std::vector<XrSpatialCapabilityConfigurationBaseHeaderEXT*>
+ capability_config_ptrs;
+ for (auto& config : capability_configs) {
+ capability_config_ptrs.push_back(config.GetAsBaseHeader());
}
XrSpatialContextCreateInfoEXT create_info = {

View File

@@ -0,0 +1,179 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Hiroki Nakagawa <nhiroki@chromium.org>
Date: Sun, 29 Mar 2026 18:12:59 -0700
Subject: Prerender: Update PrerenderNewTabHandle destruction
Updates PrerenderNewTabHandle::CancelPrerendering to be a static method
CancelPrerenderingAndDestroy that takes ownership of the handle.
Defers the destruction of PrerenderNewTabHandle using DeleteSoon() to
ensure the owned WebContentsImpl is not destroyed synchronously during
processing or iteration of handles.
Adds a browser test NewTabPrerenderCancellationByBrowsingDataRemover to
verify safe destruction.
Bug: 497053588
Change-Id: I97944ce9a5d1df486adb3e2438a8b381ee9f298e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7710193
Reviewed-by: Huanpo Lin <robertlin@chromium.org>
Commit-Queue: Hiroki Nakagawa <nhiroki@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1606854}
diff --git a/content/browser/preloading/prerender/prerender_browsertest.cc b/content/browser/preloading/prerender/prerender_browsertest.cc
index 0b8550aad9a12563f870cd07f35338e4484ca5d0..3b18b20c19366dd124d75f544243d9bcc919b9dc 100644
--- a/content/browser/preloading/prerender/prerender_browsertest.cc
+++ b/content/browser/preloading/prerender/prerender_browsertest.cc
@@ -3903,6 +3903,39 @@ IN_PROC_BROWSER_TEST_F(PrerenderTargetHintBrowserTest,
PrerenderFinalStatus::kTabClosedWithoutUserGesture);
}
+// Tests that trigger cancellation via BrowsingDataRemover (e.g.,
+// Clear-Site-Data) is handled safely without causing Use-After-Free due to
+// synchronous destruction of the WebContentsImpl during iteration.
+IN_PROC_BROWSER_TEST_F(PrerenderTargetHintBrowserTest,
+ NewTabPrerenderCancellationByBrowsingDataRemover) {
+ const GURL initial_url = GetUrl("/empty.html");
+ const GURL prerendering_url = GetUrl("/empty.html?prerender");
+
+ // Navigate to an initial page.
+ ASSERT_TRUE(NavigateToURL(shell(), initial_url));
+
+ // Start prerendering.
+ PrerenderHostId host_id = prerender_helper()->AddPrerender(
+ prerendering_url, /*eagerness=*/std::nullopt, "_blank");
+ auto* prerender_web_contents =
+ test::PrerenderTestHelper::GetPrerenderWebContents(host_id);
+ WebContentsDestroyedWatcher wc_destroyed_watcher(prerender_web_contents);
+
+ // Trigger browsing data removal which will call CancelHostsByOriginFilter.
+ BrowsingDataRemover* remover =
+ web_contents_impl()->GetBrowserContext()->GetBrowsingDataRemover();
+ BrowsingDataRemoverCompletionObserver completion_observer(remover);
+ remover->RemoveAndReply(base::Time::Min(), base::Time::Max(),
+ BrowsingDataRemover::DATA_TYPE_CACHE,
+ BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
+ &completion_observer);
+ completion_observer.BlockUntilCompletion();
+
+ // WebContents created for the new-tab trigger will be destroyed safely.
+ wc_destroyed_watcher.Wait();
+ EXPECT_FALSE(prerender_helper()->HasNewTabHandle(host_id));
+}
+
// Tests that prerendering is cancelled if a network request for the
// navigation results in an empty response with 404 status.
IN_PROC_BROWSER_TEST_P(PrerenderTargetAgnosticBrowserTest,
diff --git a/content/browser/preloading/prerender/prerender_host_registry.cc b/content/browser/preloading/prerender/prerender_host_registry.cc
index 0cdaa3f62b31cf4abe7f568e480c3c5d354516f2..26e71de66d6e3ad2ace9e2bf1ad4fa7e416cae0a 100644
--- a/content/browser/preloading/prerender/prerender_host_registry.cc
+++ b/content/browser/preloading/prerender/prerender_host_registry.cc
@@ -1133,14 +1133,8 @@ bool PrerenderHostRegistry::CancelNewTabHostInternal(
prerender_new_tab_handle_by_id_.erase(iter);
NotifyCancel(handle->prerender_host_id(), reason);
- if (reason.final_status() == PrerenderFinalStatus::kSpeculationRuleRemoved) {
- auto& new_tab_registry = handle->GetPrerenderHostRegistry();
- new_tab_registry.SchedulePendingDeletionPrerenderNewTabHandle(
- std::move(handle));
- new_tab_registry.CancelHost(prerender_host_id, reason);
- } else {
- handle->CancelPrerendering(reason);
- }
+ PrerenderNewTabHandle::CancelPrerenderingAndDestroy(std::move(handle),
+ reason);
return true;
}
@@ -1829,6 +1823,7 @@ void PrerenderHostRegistry::DeletePendingDeletionHosts(
}
void PrerenderHostRegistry::SchedulePendingDeletionPrerenderNewTabHandle(
+ base::PassKey<PrerenderNewTabHandle>,
std::unique_ptr<PrerenderNewTabHandle> handle) {
CHECK(!pending_deletion_new_tab_prerender_handle_);
pending_deletion_new_tab_prerender_handle_ = std::move(handle);
diff --git a/content/browser/preloading/prerender/prerender_host_registry.h b/content/browser/preloading/prerender/prerender_host_registry.h
index 7f84a1edac3fdbdd3904940a7cff453d71bcd866..d3d41e1c42605d5b317b28fc75102beece44643c 100644
--- a/content/browser/preloading/prerender/prerender_host_registry.h
+++ b/content/browser/preloading/prerender/prerender_host_registry.h
@@ -288,6 +288,9 @@ class CONTENT_EXPORT PrerenderHostRegistry
PrerenderHostId GetPrerenderHostIdForNavigation(
NavigationRequest* navigation_request);
+ void SchedulePendingDeletionPrerenderNewTabHandle(
+ base::PassKey<PrerenderNewTabHandle>,
+ std::unique_ptr<PrerenderNewTabHandle> handle);
private:
// WebContentsObserver implementation:
@@ -310,8 +313,6 @@ class CONTENT_EXPORT PrerenderHostRegistry
void ScheduleToDeleteAbandonedHost(
std::unique_ptr<PrerenderHost> prerender_host,
const PrerenderCancellationReason& cancellation_reason);
- void SchedulePendingDeletionPrerenderNewTabHandle(
- std::unique_ptr<PrerenderNewTabHandle> handle);
void DeleteAbandonedHosts();
diff --git a/content/browser/preloading/prerender/prerender_new_tab_handle.cc b/content/browser/preloading/prerender/prerender_new_tab_handle.cc
index 5c4bc5e6ff6a0fdb6911db60f286899a36f4266b..99b5bd9648649db43a70e091e9837bf75ac87a89 100644
--- a/content/browser/preloading/prerender/prerender_new_tab_handle.cc
+++ b/content/browser/preloading/prerender/prerender_new_tab_handle.cc
@@ -10,6 +10,7 @@
#include "content/browser/preloading/preloading_data_impl.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
+#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame.mojom.h"
#include "content/public/browser/web_contents_delegate.h"
@@ -103,9 +104,29 @@ PrerenderHostId PrerenderNewTabHandle::StartPrerendering(
return prerender_host_id_;
}
-void PrerenderNewTabHandle::CancelPrerendering(
+// static
+void PrerenderNewTabHandle::CancelPrerenderingAndDestroy(
+ std::unique_ptr<PrerenderNewTabHandle> handle,
const PrerenderCancellationReason& reason) {
- GetPrerenderHostRegistry().CancelHost(prerender_host_id_, reason);
+ auto& registry = handle->GetPrerenderHostRegistry();
+ PrerenderHostId host_id = handle->prerender_host_id();
+
+ if (reason.final_status() == PrerenderFinalStatus::kSpeculationRuleRemoved) {
+ // Defer destruction of the handle until the pagehide event is fired in a
+ // prerendered page in a new tab. The event is fired only when prerendering
+ // is intentionally cancelled by an initiator page (i.e., Speculation rule
+ // is removed).
+ registry.SchedulePendingDeletionPrerenderNewTabHandle(
+ base::PassKey<PrerenderNewTabHandle>(), std::move(handle));
+ } else {
+ // Defer destruction of the handle to avoid synchronous destruction of the
+ // owned WebContentsImpl. This prevents Use-After-Free if this is called
+ // while iterating over a snapshot of raw pointers to all WebContents (e.g.,
+ // in BrowsingDataRemoverImpl::RemoveImpl).
+ base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(
+ FROM_HERE, std::move(handle));
+ }
+ registry.CancelHost(host_id, reason);
}
std::unique_ptr<WebContentsImpl>
diff --git a/content/browser/preloading/prerender/prerender_new_tab_handle.h b/content/browser/preloading/prerender/prerender_new_tab_handle.h
index eae10a2834f66463919b81e8fe02d8e115edef0a..bfb969ca9894b2edddb92efcee73793f38bcaf48 100644
--- a/content/browser/preloading/prerender/prerender_new_tab_handle.h
+++ b/content/browser/preloading/prerender/prerender_new_tab_handle.h
@@ -49,8 +49,10 @@ class PrerenderNewTabHandle {
const PreloadingPredictor& enacting_predictor,
PreloadingConfidence confidence);
- // Cancels prerendering started in `web_contents_`.
- void CancelPrerendering(const PrerenderCancellationReason& reason);
+ // Cancels prerendering and schedules the destruction of the handle.
+ static void CancelPrerenderingAndDestroy(
+ std::unique_ptr<PrerenderNewTabHandle> handle,
+ const PrerenderCancellationReason& reason);
// Passes the ownership of `web_contents_` to the caller if it's available for
// new tab navigation with given params.

View File

@@ -0,0 +1,211 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Lukasz Anforowicz <lukasza@chromium.org>
Date: Wed, 1 Apr 2026 15:44:32 -0700
Subject: [rust png] Invalidate `already_started_frame_` after clearing frames.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This CL ensures that `already_started_frame_` doesn't become stale
after:
* Re-allocating the memory buffer backing a frame.
* Failing a call to `startIncrementalDecode`
The regression tests in this CL have been mostly created by Gemini CLI
and then reviewed and cleaned up by the author.
Fixed: 496282147
Change-Id: I92349cc2a0d7b9d1d401ab7256cb941a4d8383d1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7703000
Reviewed-by: Daniel Cheng <dcheng@chromium.org>
Commit-Queue: Łukasz Anforowicz <lukasza@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608827}
diff --git a/third_party/blink/renderer/platform/graphics/image_frame_generator_test.cc b/third_party/blink/renderer/platform/graphics/image_frame_generator_test.cc
index 8a8536968c9b0c08ce0962ac9c6b551b8a76e0ba..0bda0dd5b6ed0edd62322b74fbaaa10cdf926475 100644
--- a/third_party/blink/renderer/platform/graphics/image_frame_generator_test.cc
+++ b/third_party/blink/renderer/platform/graphics/image_frame_generator_test.cc
@@ -26,6 +26,7 @@
#include "third_party/blink/renderer/platform/graphics/image_frame_generator.h"
#include <memory>
+
#include "base/features.h"
#include "base/location.h"
#include "base/test/metrics/histogram_tester.h"
@@ -39,8 +40,10 @@
#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
+#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
@@ -413,4 +416,65 @@ TEST_F(ImageFrameGeneratorTest, clearMultiFrameDecoder) {
EXPECT_EQ(kNotFound, requested_clear_except_frame_);
}
+// This is a regression test for https://crbug.com/496282147.
+//
+// This is a more realistic, product-like, almost-end-to-end version of the
+// `AnimatedPNGTests.ClearingPartiallyDecodedFrame` unit test.
+TEST_F(ImageFrameGeneratorTest, ClearingPartiallyDecodedFrame) {
+ StringBuilder file_path;
+ file_path.Append(test::BlinkWebTestsDir());
+ file_path.Append(
+ "/images/resources/png-animated-three-independent-frames.png");
+ std::optional<Vector<char>> full_data_vec =
+ test::ReadFromFile(file_path.ToString());
+ ASSERT_TRUE(full_data_vec);
+ base::span<const uint8_t> full_data = base::as_byte_span(*full_data_vec);
+ SkISize size(50, 50);
+
+ // Can't reuse `generator_` from `SetUp`, because it sets `is_multi_frame` to
+ // `false`. Can't use `SetFrameCount`, because this test needs to use a real
+ // `SkiaImageDecoderBase` decoder, rather than `UseMockImageDecoderFactory`.
+ constexpr bool kIsMultiframe = true;
+ const Vector<SkISize> kSupportedSizes = {};
+ generator_ =
+ ImageFrameGenerator::Create(size, kIsMultiframe, ColorBehavior::kTag,
+ cc::AuxImage::kDefault, kSupportedSizes);
+
+ // Partially decode frame 1.
+ //
+ // `fcTL` chunk starts at offset 180. `fdAT` at 218.
+ // Let's provide 240 bytes - in the middle of `fdAT` chunk.
+ //
+ // After this step `SkiaImageDecoderBase::already_started_frame_` is `1`.
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(size.width(), size.height());
+ cc::PaintImage::GeneratorClientId client_id =
+ cc::PaintImage::GetNextGeneratorClientId();
+ auto partial_data = SharedBuffer::Create(full_data.first(240u));
+ auto segment_reader = SegmentReader::CreateFromSharedBuffer(partial_data);
+ bool success = generator_->DecodeAndScale(segment_reader.get(),
+ /*all_data_received=*/false, 1,
+ bitmap.pixmap(), client_id);
+ EXPECT_TRUE(success);
+
+ // Decode an out-of-bounds frame to clear the cache and transitively call
+ // `ImageFrame::ClearPixelData`.
+ success = generator_->DecodeAndScale(segment_reader.get(),
+ /*all_data_received=*/false, 1000,
+ bitmap.pixmap(), client_id);
+ EXPECT_FALSE(success);
+
+ // Resume decoding of frame 1. Despite starting with
+ // `SkiaImageDecoderBase::already_started_frame_` set to `1` this operation
+ // needs to call `SkCodec::startIncrementalDecode` because the old buffer has
+ // been freed in the previous step.
+ auto full_shared_buffer = SharedBuffer::Create(full_data);
+ auto full_segment_reader =
+ SegmentReader::CreateFromSharedBuffer(full_shared_buffer);
+ success = generator_->DecodeAndScale(full_segment_reader.get(),
+ /*all_data_received=*/true, 1,
+ bitmap.pixmap(), client_id);
+ EXPECT_TRUE(success);
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/platform/image-decoders/png/png_image_decoder_test.cc b/third_party/blink/renderer/platform/image-decoders/png/png_image_decoder_test.cc
index ae4343652b2bd536a70846a73d7c35befb6584e6..b47a2c1da2156440a5dfa6b6f601c9dd8dc5db47 100644
--- a/third_party/blink/renderer/platform/image-decoders/png/png_image_decoder_test.cc
+++ b/third_party/blink/renderer/platform/image-decoders/png/png_image_decoder_test.cc
@@ -879,6 +879,53 @@ TEST(AnimatedPNGTests, IncrementalDecodeOfDifferentFrame) {
EXPECT_EQ(frame1->GetStatus(), ImageFrame::kFrameComplete);
}
+// This is a regression test for https://crbug.com/496282147.
+//
+// This test uses `blink::ImageDecoder` and `blink::ImageFrame` APIs in a way
+// that doesn't necessarily reflect how they would actually be used in the
+// product (e.g. calling `ClearPixelData` and/or calling `Append` instead of
+// `SetData`). This nevertheless seems like a valid test, because:
+//
+// * Supporting all usage patterns allowed by the public APIs (and the type
+// system) seems more robust then 1) adding extra requirements on the caller
+// of these APIs (such as never clearing a partially decoded frame), and/or 2)
+// discovering the callers that may violate such requirements.
+// * A separate `ImageFrameGeneratorTest.ClearingPartiallyDecodedFrame` test
+// shows how a similr usage pattern is indeed reachable via web-exposed APIs.
+TEST(AnimatedPNGTests, ClearingPartiallyDecodedFrame) {
+ Vector<char> full_data = ReadFile(
+ "/images/resources/"
+ "png-animated-idat-part-of-animation.png");
+ ASSERT_FALSE(full_data.empty());
+ auto decoder = CreatePNGDecoder();
+
+ // Provide only enough data for the first frame to be partial.
+ const size_t kPartialDataSize = 160;
+ scoped_refptr<SharedBuffer> data =
+ SharedBuffer::Create(base::span(full_data).first(kPartialDataSize));
+ decoder->SetData(data.get(), false);
+
+ // Partially decode frame 0.
+ ImageFrame* frame0 = decoder->DecodeFrameBufferAtIndex(0);
+ ASSERT_TRUE(frame0);
+ EXPECT_EQ(frame0->GetStatus(), ImageFrame::kFramePartial);
+
+ // Manually clear frame 0 pixel data.
+ frame0->ClearPixelData();
+
+ // Provide more data by appending to the same `SharedBuffer`.
+ // This avoids clobbering the decoder state with a new `SetData` call.
+ data->Append(base::span(full_data).subspan(kPartialDataSize));
+
+ // Try to decode frame 0 again. This verifies that
+ // `SkCodec::startIncrementalDecode` has been called to reinitialize decoding
+ // state - avoiding writing to the memory buffer that has been freed by
+ // `ClearPixelData` above.
+ frame0 = decoder->DecodeFrameBufferAtIndex(0);
+ ASSERT_TRUE(frame0);
+ EXPECT_EQ(frame0->GetStatus(), ImageFrame::kFrameComplete);
+}
+
// Verify that a malformatted PNG, where the IEND appears before any frame data
// (IDAT), invalidates the decoder.
TEST(AnimatedPNGTests, VerifyIENDBeforeIDATInvalidatesDecoder) {
diff --git a/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.cc b/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.cc
index a6766b5d51ddf64eab40ebe838e0a5daaef44dba..f4a8c849986d41d36baffa11d66b3d9bdc9bdc96 100644
--- a/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.cc
+++ b/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.cc
@@ -301,6 +301,11 @@ void SkiaImageDecoderBase::Decode(wtf_size_t index) {
UpdateAggressivePurging(current_frame_index);
if (frame.GetStatus() == ImageFrame::kFrameEmpty) {
+ // `AllocatePixelData` (or `TakeBitmapDataIfWritable` / `CopyBitmapData`)
+ // calls mean that we can't reuse old buffer pointers that may have been
+ // stashed in `SkCodec` by previous `startIncrementalDecode` calls.
+ already_started_frame_.reset();
+
wtf_size_t required_previous_frame_index =
frame.RequiredPreviousFrameIndex();
if (required_previous_frame_index == kNotFound) {
@@ -404,6 +409,7 @@ void SkiaImageDecoderBase::Decode(wtf_size_t index) {
options.fPriorFrame = prior_frame_;
options.fZeroInitialized = SkCodec::kNo_ZeroInitialized;
+ already_started_frame_.reset();
SkCodec::Result start_incremental_decode_result =
codec_->startIncrementalDecode(image_info, frame.Bitmap().getPixels(),
frame.Bitmap().rowBytes(), &options);
diff --git a/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.h b/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.h
index ab2c197704a522261ffa40171ca31f03c0cdbf1c..86726262a31b09c602de4697fa5cc8e2bd51fd7e 100644
--- a/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.h
+++ b/third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.h
@@ -98,8 +98,9 @@ class PLATFORM_EXPORT SkiaImageDecoderBase : public ImageDecoder {
const wtf_size_t reading_offset_ = 0;
// Number of a frame for which calling `SkCodec::incrementalDecode` is okay.
- // Set after calling `SkCodec::startIncrementalDecode` and reset after
- // `SkCodec::incrementalDecode` succeeds or encounters a non-resumable error.
+ // Set after calling `SkCodec::startIncrementalDecode` and reset after either
+ // 1) the memory buffer of that frame was reset or 2) the frame was completely
+ // decoded (successfully, or with a non-resumable error).
std::optional<wtf_size_t> already_started_frame_;
};

View File

@@ -0,0 +1,94 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Fri, 10 Apr 2026 23:37:43 -0700
Subject: [M146] [gpu] Fix OOB write due to unvalidated get_offset
Original change's description:
> [gpu] Fix OOB write due to unvalidated get_offset
>
> A compromised GPU process can provide an invalid get_offset to the
> CommandBufferHelper (e.g., via shared memory). This offset is used to
> calculate available space and could lead to out-of-bounds writes in the
> Browser process if not validated.
>
> This change adds a bounds check in
> CommandBufferHelper::UpdateCachedState to ensure that the cached
> get_offset is within the valid range [0, total_entry_count_]. If an
> invalid offset is detected, it forces a context loss, frees the ring
> buffer, and marks the helper as unusable, preventing further operations.
>
> Bug: 498782145
> Test: CommandBufferHelperTest.*
> Change-Id: I8c64e546ecdc90a5a22d15e57ff762a86a6a6964
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7739951
> Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
> Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
> Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1611853}
(cherry picked from commit dc5e20c4c055d6952854a566d520211c6d505f74)
Bug: 498782145
Fixed: 500956607
Change-Id: Ia726612e0a930ee79460fbd7d795afa4d94e2a7b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7745786
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Cr-Commit-Position: refs/branch-heads/7680@{#3919}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/gpu/command_buffer/client/cmd_buffer_helper.cc b/gpu/command_buffer/client/cmd_buffer_helper.cc
index ccda45b133c6a9f2ee60ccc8900bd4a4ce328394..5aea0c81b29b3507099f399c374f3cb372a3100e 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper.cc
@@ -158,6 +158,17 @@ void CommandBufferHelper::UpdateCachedState(const CommandBuffer::State& state) {
service_on_old_buffer_ =
(state.set_get_buffer_count != set_get_buffer_count_);
cached_get_offset_ = service_on_old_buffer_ ? 0 : state.get_offset;
+
+ if (!service_on_old_buffer_ &&
+ (cached_get_offset_ < 0 || cached_get_offset_ > total_entry_count_)) {
+ command_buffer_->ForceLostContext(error::kGuilty);
+ FreeRingBuffer();
+ usable_ = false;
+ context_lost_ = true;
+ cached_get_offset_ = 0; // Safe fallback
+ return;
+ }
+
cached_last_token_read_ = state.token;
// Don't transition from a lost context to a working context.
context_lost_ |= error::IsError(state.error);
diff --git a/gpu/command_buffer/client/cmd_buffer_helper_test.cc b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
index 1b9254d318ae770ca980d2fed1399a69438afa10..009a87e8bf7a3475f63cd51206868dec187f5e06 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper_test.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
@@ -67,6 +67,8 @@ class CommandBufferHelperTest : public testing::Test {
return helper_->immediate_entry_count_;
}
+ int32_t TotalEntryCount() const { return helper_->total_entry_count_; }
+
// Adds a command to the buffer through the helper, while adding it as an
// expected call on the API mock.
void AddCommandWithExpect(error::Error _return,
@@ -655,6 +657,17 @@ TEST_F(CommandBufferHelperTest, IsContextLost) {
EXPECT_TRUE(helper_->IsContextLost());
}
+TEST_F(CommandBufferHelperTest, TestInvalidGetOffset) {
+ EXPECT_FALSE(helper_->IsContextLost());
+ EXPECT_TRUE(helper_->usable());
+
+ command_buffer_->SetGetOffsetForTest(TotalEntryCount() + 1);
+ helper_->RefreshCachedToken(); // calls UpdateCachedState internally.
+
+ EXPECT_TRUE(helper_->IsContextLost());
+ EXPECT_FALSE(helper_->usable());
+}
+
// Checks helper's 'flush generation' updates.
TEST_F(CommandBufferHelperTest, TestFlushGeneration) {
// Explicit flushing only.

View File

@@ -0,0 +1,224 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
Date: Wed, 8 Apr 2026 17:15:45 -0700
Subject: gl: Make DCOMPSurfaceRegistry thread-safe
DCOMPSurfaceRegistry is accessed from both the GPU IO thread (via
GpuServiceImpl) and the GPU main scheduler thread (via DCOMPTexture).
The underlying base::flat_map is not thread-safe, leading to potential
container corruption and crashes (UAF, BOf) during concurrent access.
This CL adds a base::Lock to protect all accesses to the map and
includes a new multi-threaded stress test to verify the fix.
Bug: 493315759
Change-Id: Ibb7ef5e602f222410fde06a61fb3f5e571e7a70f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737061
Reviewed-by: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611867}
diff --git a/ui/gl/BUILD.gn b/ui/gl/BUILD.gn
index 3584b693370b5199456608a26ceb763f6e9c3446..1cb66199a0b8adf2035a05fecc411c67180f7e80 100644
--- a/ui/gl/BUILD.gn
+++ b/ui/gl/BUILD.gn
@@ -552,6 +552,7 @@ test("gl_unittests") {
if (is_win) {
sources += [
"dcomp_presenter_unittest.cc",
+ "dcomp_surface_registry_unittest.cc",
"delegated_ink_point_renderer_gpu_unittest.cc",
"gl_fence_win_unittest.cc",
"hdr_metadata_helper_win_unittest.cc",
diff --git a/ui/gl/dcomp_surface_registry.cc b/ui/gl/dcomp_surface_registry.cc
index 352cc298b9ea97361ae2a7d668b7d7e9eb455cd5..410f76f8980438abae32b6c89e7083ae48cf1699 100644
--- a/ui/gl/dcomp_surface_registry.cc
+++ b/ui/gl/dcomp_surface_registry.cc
@@ -3,8 +3,11 @@
// found in the LICENSE file.
#include "ui/gl/dcomp_surface_registry.h"
+
+#include "base/check.h"
#include "base/logging.h"
#include "base/no_destructor.h"
+#include "base/synchronization/lock.h"
namespace gl {
@@ -20,8 +23,11 @@ base::UnguessableToken DCOMPSurfaceRegistry::RegisterDCOMPSurfaceHandle(
base::win::ScopedHandle surface) {
DVLOG(1) << __func__;
base::UnguessableToken token = base::UnguessableToken::Create();
- DCHECK(surface_handle_map_.find(token) == surface_handle_map_.end());
- surface_handle_map_[token] = std::move(surface);
+ {
+ base::AutoLock lock(lock_);
+ DCHECK(surface_handle_map_.find(token) == surface_handle_map_.end());
+ surface_handle_map_[token] = std::move(surface);
+ }
DVLOG(1) << __func__ << ": Surface handle registered with token " << token;
return token;
}
@@ -29,12 +35,14 @@ base::UnguessableToken DCOMPSurfaceRegistry::RegisterDCOMPSurfaceHandle(
void DCOMPSurfaceRegistry::UnregisterDCOMPSurfaceHandle(
const base::UnguessableToken& token) {
DVLOG(1) << __func__;
+ base::AutoLock lock(lock_);
surface_handle_map_.erase(token);
}
base::win::ScopedHandle DCOMPSurfaceRegistry::TakeDCOMPSurfaceHandle(
const base::UnguessableToken& token) {
DVLOG(1) << __func__;
+ base::AutoLock lock(lock_);
auto surface_iter = surface_handle_map_.find(token);
if (surface_iter != surface_handle_map_.end()) {
// Take ownership.
diff --git a/ui/gl/dcomp_surface_registry.h b/ui/gl/dcomp_surface_registry.h
index 803a3cc6398f0777504063118920998869086d7f..7cd9fdbfe8669bc97d4b664fdb29573ec2ea26de 100644
--- a/ui/gl/dcomp_surface_registry.h
+++ b/ui/gl/dcomp_surface_registry.h
@@ -7,6 +7,7 @@
#include "base/containers/flat_map.h"
#include "base/no_destructor.h"
+#include "base/synchronization/lock.h"
#include "base/unguessable_token.h"
#include "base/win/scoped_handle.h"
#include "ui/gl/gl_export.h"
@@ -44,7 +45,9 @@ class GL_EXPORT DCOMPSurfaceRegistry {
~DCOMPSurfaceRegistry();
base::flat_map<base::UnguessableToken, base::win::ScopedHandle>
- surface_handle_map_;
+ surface_handle_map_ GUARDED_BY(lock_);
+
+ base::Lock lock_;
};
} // namespace gl
diff --git a/ui/gl/dcomp_surface_registry_unittest.cc b/ui/gl/dcomp_surface_registry_unittest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..595e2388e9f50df33214359ecef0c135d94610b8
--- /dev/null
+++ b/ui/gl/dcomp_surface_registry_unittest.cc
@@ -0,0 +1,118 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/gl/dcomp_surface_registry.h"
+
+#include <windows.h>
+
+#include <atomic>
+#include <thread>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/unguessable_token.h"
+#include "base/win/scoped_handle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gl {
+
+namespace {
+
+class DCOMPSurfaceRegistryTest : public testing::Test {
+ public:
+ void SetUp() override { registry_ = DCOMPSurfaceRegistry::GetInstance(); }
+
+ protected:
+ raw_ptr<DCOMPSurfaceRegistry> registry_;
+};
+
+} // namespace
+
+// Stress test for concurrent access to DCOMPSurfaceRegistry using the
+// barrier pattern to ensure TSAN consistently catches data races.
+//
+// Without proper synchronization (e.g., base::Lock), this test would likely
+// fail in the following ways:
+// 1. Memory Corruption (UAF/HeapBOf): base::flat_map uses a contiguous
+// std::vector. If one thread triggers a reallocation during an insertion
+// while another thread is searching or erasing, the latter will hold an
+// invalidated iterator or pointer.
+// 2. Container Inconsistency: Concurrent insertions and erasures can leave
+// the map in an unsorted or corrupted state, leading to failed lookups
+// for valid tokens.
+// 3. Sanitizer Triggers: ASan would detect container-overflow or
+// heap-use-after-free, and TSan would flag a data race.
+TEST_F(DCOMPSurfaceRegistryTest, ConcurrentRegisterAndTake) {
+ const int kOpsPerThread = 100;
+
+ std::vector<base::UnguessableToken> tokens;
+ base::Lock tokens_lock;
+
+ std::atomic<bool> start_flag{false};
+ std::atomic<int> threads_ready{0};
+
+ auto register_worker = [&]() {
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ for (int i = 0; i < kOpsPerThread; ++i) {
+ base::win::ScopedHandle handle(
+ ::CreateEvent(nullptr, FALSE, FALSE, nullptr));
+ base::UnguessableToken token =
+ registry_->RegisterDCOMPSurfaceHandle(std::move(handle));
+ {
+ base::AutoLock lock(tokens_lock);
+ tokens.push_back(token);
+ }
+ }
+ };
+
+ auto take_worker = [&]() {
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ int taken = 0;
+ while (taken < kOpsPerThread) {
+ base::UnguessableToken token;
+ {
+ base::AutoLock lock(tokens_lock);
+ if (!tokens.empty()) {
+ token = tokens.back();
+ tokens.pop_back();
+ }
+ }
+ if (!token.is_empty()) {
+ base::win::ScopedHandle handle =
+ registry_->TakeDCOMPSurfaceHandle(token);
+ taken++;
+ } else {
+ std::this_thread::yield();
+ }
+ }
+ };
+
+ // With the barrier pattern, two threads are sufficient to trigger
+ // the race condition for TSAN.
+ std::thread t1(register_worker);
+ std::thread t2(take_worker);
+
+ // Wait until both threads are ready at the starting line.
+ while (threads_ready.load(std::memory_order_relaxed) < 2) {
+ std::this_thread::yield();
+ }
+
+ // Signal the staring flag to allow both threads to race from the initialized
+ // state.
+ start_flag.store(true, std::memory_order_release);
+
+ t1.join();
+ t2.join();
+}
+
+} // namespace gl

View File

@@ -0,0 +1,114 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Anders Hartvoll Ruud <andruud@chromium.org>
Date: Tue, 17 Mar 2026 18:43:46 -0700
Subject: Iterate on copy of layout subtree roots during LFV::PerformLayout()
During iteration of LocalFrameView::layout_subtree_root_list_
in PerformLayout(), we can do interleaved style and layout tree
building due to e.g. container queries. Such layout tree rebuilds
can destroy the LayoutObjects being subtree roots,
and call LocalFrameView::ClearLayoutSubtreeRoot() in the process,
modifying layout_subtree_root_list_ during the iteration.
To fix this, iterate on a copy of the layout subtree roots instead.
Note that even though we do clear layout_subtree_root_list_ immediately
after iteration, we can not just std::move the list to a local,
since we need to discover (and skip) the roots that were removed
during previous iterations.
Fixed: 491994185
Change-Id: I729e3df6e938533467ff4d45e66c666fe27a83c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7669842
Commit-Queue: Anders Hartvoll Ruud <andruud@chromium.org>
Reviewed-by: Morten Stenshorne <mstensho@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1600948}
diff --git a/third_party/blink/renderer/core/frame/local_frame_view.cc b/third_party/blink/renderer/core/frame/local_frame_view.cc
index ee76e9f10babe692b891c54a50ae8ee8552ab1c9..4f8ba8fc6b77ea2442ce9c1bddf79efbdf0c58c4 100644
--- a/third_party/blink/renderer/core/frame/local_frame_view.cc
+++ b/third_party/blink/renderer/core/frame/local_frame_view.cc
@@ -750,9 +750,17 @@ void LocalFrameView::PerformLayout() {
++add_result.stored_value->value;
}
}
- for (auto& root : layout_subtree_root_list_.Ordered()) {
- bool should_rebuild_fragments = false;
+ HeapVector<LayoutObjectWithDepth> ordered_roots =
+ layout_subtree_root_list_.Ordered();
+ for (LayoutObjectWithDepth& root : ordered_roots) {
LayoutObject& root_layout_object = *root;
+ if (!layout_subtree_root_list_.Contains(root_layout_object)) {
+ // A previous iteration removed the entry from the list.
+ // This can happen when interleaved style recalc sets the element
+ // associated the layout subtree root to display:none.
+ continue;
+ }
+ bool should_rebuild_fragments = false;
LayoutBox* container_box = root->ContainingNGBox();
if (container_box) {
auto it = fragment_tree_spines.find(container_box);
diff --git a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
index f4df5f1bcd9ba21368cf7f178970add45112c56a..36d51800f4b48ff2a98abd09e1eb7f988ca998a3 100644
--- a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
+++ b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.cc
@@ -93,6 +93,10 @@ unsigned LayoutObjectWithDepth::DetermineDepth(LayoutObject* object) {
return depth;
}
+bool DepthOrderedLayoutObjectList::Contains(LayoutObject& object) const {
+ return Unordered().Contains(&object);
+}
+
const HeapHashSet<Member<LayoutObject>>&
DepthOrderedLayoutObjectList::Unordered() const {
return data_->objects();
diff --git a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
index c22ce8c5aba5019c38144206b98f1bd4f9d96683..94d953cc07eb1b7e9338d29b802ea7857bbf28be 100644
--- a/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
+++ b/third_party/blink/renderer/core/layout/depth_ordered_layout_object_list.h
@@ -59,6 +59,7 @@ class DepthOrderedLayoutObjectList {
int size() const;
CORE_EXPORT bool IsEmpty() const;
+ bool Contains(LayoutObject&) const;
const HeapHashSet<Member<LayoutObject>>& Unordered() const;
const HeapVector<LayoutObjectWithDepth>& Ordered();
diff --git a/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
new file mode 100644
index 0000000000000000000000000000000000000000..cf27c57818d234e079ab485513636961a054080f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-conditional/container-queries/crashtests/chrome-bug-491994185-crash.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Crash Test: Layout subtree root becoming display:none</title>
+<link rel="help" href="https://issues.chromium.org/issues/491994185">
+<style>
+ #container {
+ container-type: inline-size;
+ }
+ @container (min-width: 300px) {
+ #victim { display: none; }
+ }
+</style>
+<div style="contain:strict;">
+ <div id="herring"></div>
+</div>
+<div>
+ <div style="contain:strict; width:400px;">
+ <div id="containerResizer" style="width:10px;">
+ <div id="container">
+ <div id="victim" style="contain:strict;">
+ <div id="innerElm"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<script>
+ document.body.offsetTop;
+ innerElm.style.height = '40px';
+ herring.style.height = '60px';
+ containerResizer.style.width = '400px';
+ document.body.offsetTop;
+</script>

View File

@@ -0,0 +1,47 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
Date: Tue, 31 Mar 2026 10:03:10 -0700
Subject: [a11y] Fix uninitialized memory in
get_columnHeaderCells/get_rowHeaderCells
Both get_columnHeaderCells and get_rowHeaderCells allocate a COM array
via CoTaskMemAlloc, but report the requested array size
(column_header_ids.size()) to the caller instead of the number of
successfully populated elements (index). If a node lookup fails
mid-iteration, the trailing array slots contain uninitialized memory
that the COM marshaller will treat as valid IUnknown* pointers.
This CL fixes both methods to return the actual populated count
(index), matching the pattern already used by get_targets.
Fixed: 40833630
Change-Id: I596745388199d61eef8261fe0ae6e1d3e773f240
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7717598
Reviewed-by: Kevin Babbitt <kbabbitt@microsoft.com>
Commit-Queue: Kevin Babbitt <kbabbitt@microsoft.com>
Auto-Submit: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
Commit-Queue: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1607938}
diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc
index 773834c969ff28b0038d27af2c0351120dba8fc2..f374a0f2e202f2f6eccbab230211f593748974c8 100644
--- a/ui/accessibility/platform/ax_platform_node_win.cc
+++ b/ui/accessibility/platform/ax_platform_node_win.cc
@@ -4258,7 +4258,7 @@ IFACEMETHODIMP AXPlatformNodeWin::get_columnHeaderCells(
}
}
- *n_column_header_cells = static_cast<LONG>(column_header_ids.size());
+ *n_column_header_cells = static_cast<LONG>(index);
return S_OK;
}
@@ -4312,7 +4312,7 @@ IFACEMETHODIMP AXPlatformNodeWin::get_rowHeaderCells(
}
}
- *n_row_header_cells = static_cast<LONG>(row_header_ids.size());
+ *n_row_header_cells = static_cast<LONG>(index);
return S_OK;
}

View File

@@ -0,0 +1,59 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <bashi@chromium.org>
Date: Fri, 10 Apr 2026 17:14:24 -0700
Subject: [CORS] Block forbidden methods for no-cors requests
Previously, forbidden methods like TRACE and TRACK were allowed when
the request mode was no-cors, and only CONNECT was unconditionally
blocked.
This CL updates CorsURLLoaderFactory::IsValidRequest to block all
forbidden methods regardless of the request mode. The unit test is
also updated to reflect this new restriction.
Bug: 498765210
Change-Id: Ie451a3c2b8fa7aafdebade8b3ba517be3ce255f8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7743444
Reviewed-by: mmenke <mmenke@chromium.org>
Commit-Queue: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1613186}
diff --git a/services/network/cors/cors_url_loader_factory.cc b/services/network/cors/cors_url_loader_factory.cc
index cbc36ef7c5703ebb09fd8c86e5674aea59d8569a..08220f19e3f27500357506eff870ab9126f7bb5f 100644
--- a/services/network/cors/cors_url_loader_factory.cc
+++ b/services/network/cors/cors_url_loader_factory.cc
@@ -908,13 +908,8 @@ bool CorsURLLoaderFactory::IsValidRequest(
return false;
}
- // Don't allow forbidden methods for any requests except RequestMode::kNoCors.
- // Don't allow CONNECT method for any request.
- if ((request.mode != mojom::RequestMode::kNoCors &&
- cors::IsForbiddenMethod(request.method)) ||
- (request.mode == mojom::RequestMode::kNoCors &&
- base::EqualsCaseInsensitiveASCII(
- request.method, net::HttpRequestHeaders::kConnectMethod))) {
+ // Don't allow forbidden methods.
+ if (cors::IsForbiddenMethod(request.method)) {
mojo::ReportBadMessage("CorsURLLoaderFactory: Forbidden method");
return false;
}
diff --git a/services/network/cors/cors_url_loader_unittest.cc b/services/network/cors/cors_url_loader_unittest.cc
index 403f3b4ee828b768d3c6e844bb28208b909ed072..27834acc57d05616bf527af290aa8bd202ee52d9 100644
--- a/services/network/cors/cors_url_loader_unittest.cc
+++ b/services/network/cors/cors_url_loader_unittest.cc
@@ -109,11 +109,10 @@ TEST_F(CorsURLLoaderTest, ForbiddenMethods) {
std::string forbidden_method;
bool expect_allowed_for_no_cors;
} kTestCases[] = {
- // CONNECT is never allowed, while TRACE and TRACK are allowed only with
- // RequestMode::kNoCors.
+ // CONNECT, TRACE and TRACK are not allowed for any mode.
{"CONNECT", false},
- {"TRACE", true},
- {"TRACK", true},
+ {"TRACE", false},
+ {"TRACK", false},
};
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.forbidden_method);

View File

@@ -0,0 +1,373 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Mon, 13 Apr 2026 22:52:33 -0700
Subject: [M146] media: Zero-copy VP9 alpha decoding in VpxVideoDecoder
Original change's description:
> media: Zero-copy VP9 alpha decoding in VpxVideoDecoder
>
> Configures the VP9 alpha decoder to use `memory_pool_` for external
> frame buffers, eliminating the need for `libyuv::CopyPlane`.
>
> The `VideoFrame` now wraps the alpha data directly from the pool using
> a second destruction observer. `AllocateAlphaPlaneForFrameBuffer` and
> `alpha_data` tracking are removed from `FrameBufferPool`.
>
> Bug: 500066234
> Change-Id: I6e7cf13bcc8a5a1759acfd51961859c4c57fcbf2
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737984
> Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
> Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
> Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1611919}
(cherry picked from commit fc79e8cc2dfcc8f7ec8ee9cf0acf0993f32aec27)
Bug: 501314839,500066234
Change-Id: I6e7cf13bcc8a5a1759acfd51961859c4c57fcbf2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7757063
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Cr-Commit-Position: refs/branch-heads/7680@{#3937}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/media/base/frame_buffer_pool.cc b/media/base/frame_buffer_pool.cc
index e90f07036baab4056398c93a03f8751bbfaa5d69..e2aa3a9243e3ce45b5087853eb2bd7d7dae7acfe 100644
--- a/media/base/frame_buffer_pool.cc
+++ b/media/base/frame_buffer_pool.cc
@@ -56,7 +56,6 @@ struct FrameBufferPool::FrameBuffer {
// Not using std::vector<uint8_t> as resize() calls take a really long time
// for large buffers.
BytesArray data;
- BytesArray alpha_data;
bool held_by_library = false;
// Needs to be a counter since a frame buffer might be used multiple times.
int held_by_frame = 0;
@@ -148,31 +147,6 @@ void FrameBufferPool::ReleaseFrameBuffer(void* fb_priv) {
}
}
-base::span<uint8_t> FrameBufferPool::AllocateAlphaPlaneForFrameBuffer(
- size_t min_size,
- void* fb_priv) {
- base::AutoLock lock(lock_);
- DCHECK(fb_priv);
-
- auto* frame_buffer = static_cast<FrameBuffer*>(fb_priv);
- DCHECK(IsUsedLocked(frame_buffer));
- if (frame_buffer->alpha_data.size() < min_size) {
- // Free the existing |alpha_data| first so that the memory can be reused,
- // if possible. Note that the new array is purposely not initialized.
- frame_buffer->alpha_data = {};
- uint8_t* data = nullptr;
- if (force_allocation_error_ ||
- !base::UncheckedMalloc(min_size, reinterpret_cast<void**>(&data)) ||
- !data) {
- return {};
- }
- // SAFETY: We have just allocated `min_size` of memory for `data`.
- frame_buffer->alpha_data =
- UNSAFE_BUFFERS(BytesArray::FromOwningPointer(data, min_size));
- }
- return frame_buffer->alpha_data;
-}
-
base::OnceClosure FrameBufferPool::CreateFrameCallback(void* fb_priv) {
base::AutoLock lock(lock_);
@@ -210,10 +184,9 @@ bool FrameBufferPool::OnMemoryDump(
size_t bytes_reserved = 0;
for (const auto& frame_buffer : frame_buffers_) {
if (IsUsedLocked(frame_buffer.get())) {
- bytes_used += frame_buffer->data.size() + frame_buffer->alpha_data.size();
+ bytes_used += frame_buffer->data.size();
}
- bytes_reserved +=
- frame_buffer->data.size() + frame_buffer->alpha_data.size();
+ bytes_reserved += frame_buffer->data.size();
}
memory_dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
diff --git a/media/base/frame_buffer_pool.h b/media/base/frame_buffer_pool.h
index ac839b8e8bfa00d2fea203be5248a56f04cecc71..2ccb01676b0e8e1e3ca1b3cb60f2883538f2f13c 100644
--- a/media/base/frame_buffer_pool.h
+++ b/media/base/frame_buffer_pool.h
@@ -48,11 +48,6 @@ class MEDIA_EXPORT FrameBufferPool
// Called when a frame buffer allocation is no longer needed.
void ReleaseFrameBuffer(void* fb_priv);
- // Allocates (or reuses) room for an alpha plane on a given frame buffer.
- // |fb_priv| must be a value previously returned by GetFrameBuffer().
- base::span<uint8_t> AllocateAlphaPlaneForFrameBuffer(size_t min_size,
- void* fb_priv);
-
// Generates a "no_longer_needed" closure that holds a reference to this pool;
// |fb_priv| must be a value previously returned by GetFrameBuffer(). The
// callback may be called on any thread.
diff --git a/media/base/frame_buffer_pool_unittest.cc b/media/base/frame_buffer_pool_unittest.cc
index a5b7bff2b8af3d2f9a531e894ec28e31e7823ac0..4cfdb1520cc18548fd91b2cca8b03a0124de944f 100644
--- a/media/base/frame_buffer_pool_unittest.cc
+++ b/media/base/frame_buffer_pool_unittest.cc
@@ -32,12 +32,6 @@ TEST(FrameBufferPool, BasicFunctionality) {
EXPECT_NE(buf1.data(), buf2.data());
std::ranges::fill(buf2, 0);
- auto alpha = pool->AllocateAlphaPlaneForFrameBuffer(kBufferSize, priv1);
- ASSERT_FALSE(alpha.empty());
- EXPECT_NE(alpha.data(), buf1.data());
- EXPECT_NE(alpha.data(), buf2.data());
- std::ranges::fill(alpha, 0);
-
EXPECT_EQ(2u, pool->get_pool_size_for_testing());
// Frames are not released immediately, so this should still show two frames.
@@ -52,7 +46,6 @@ TEST(FrameBufferPool, BasicFunctionality) {
EXPECT_EQ(1u, pool->get_pool_size_for_testing());
std::ranges::fill(buf1, 0);
- std::ranges::fill(alpha, 0);
// This will release all memory since we're in the shutdown state.
std::move(frame_release_cb).Run();
diff --git a/media/filters/vpx_video_decoder.cc b/media/filters/vpx_video_decoder.cc
index 0be38f7ee110a0084854c571784e9dd3c8144f51..32cd3c423f4f01aa4cbe21ae71bf149f26a1deee 100644
--- a/media/filters/vpx_video_decoder.cc
+++ b/media/filters/vpx_video_decoder.cc
@@ -269,7 +269,21 @@ bool VpxVideoDecoder::ConfigureDecoder(const VideoDecoderConfig& config) {
DCHECK(!vpx_codec_alpha_);
vpx_codec_alpha_ = InitializeVpxContext(config);
- return !!vpx_codec_alpha_;
+ if (!vpx_codec_alpha_) {
+ return false;
+ }
+
+ if (config.codec() == VideoCodec::kVP9) {
+ if (vpx_codec_set_frame_buffer_functions(
+ vpx_codec_alpha_.get(), &GetVP9FrameBuffer, &ReleaseVP9FrameBuffer,
+ memory_pool_.get())) {
+ DLOG(ERROR) << "Failed to configure external buffers for alpha. "
+ << vpx_codec_error(vpx_codec_alpha_.get());
+ return false;
+ }
+ }
+
+ return true;
}
void VpxVideoDecoder::CloseDecoder() {
@@ -576,20 +590,13 @@ bool VpxVideoDecoder::CopyVpxImageToVideoFrame(
if (memory_pool_) {
DCHECK_EQ(VideoCodec::kVP9, config_.codec());
if (vpx_image_alpha) {
+ CHECK_GT(vpx_image_alpha->stride[VPX_PLANE_Y], 0);
size_t alpha_plane_size =
vpx_image_alpha->stride[VPX_PLANE_Y] * vpx_image_alpha->d_h;
- auto alpha_plane = memory_pool_->AllocateAlphaPlaneForFrameBuffer(
- alpha_plane_size, vpx_image->fb_priv);
- if (alpha_plane.empty()) {
- error_status_ = DecoderStatus::Codes::kOutOfMemory;
- // In case of OOM, abort copy.
- return false;
- }
- libyuv::CopyPlane(vpx_image_alpha->planes[VPX_PLANE_Y],
- vpx_image_alpha->stride[VPX_PLANE_Y],
- alpha_plane.data(),
- vpx_image_alpha->stride[VPX_PLANE_Y],
- vpx_image_alpha->d_w, vpx_image_alpha->d_h);
+ // SAFETY: libvpx guarantees that the Y plane has at least `stride * d_h`
+ // bytes available.
+ auto alpha_plane = UNSAFE_BUFFERS(base::span<uint8_t>(
+ vpx_image_alpha->planes[VPX_PLANE_Y], alpha_plane_size));
*video_frame = VideoFrame::WrapExternalYuvaData(
codec_format, coded_size, gfx::Rect(visible_size), natural_size,
vpx_image->stride[VPX_PLANE_Y], vpx_image->stride[VPX_PLANE_U],
@@ -605,8 +612,14 @@ bool VpxVideoDecoder::CopyVpxImageToVideoFrame(
if (!(*video_frame))
return false;
- video_frame->get()->AddDestructionObserver(
- memory_pool_->CreateFrameCallback(vpx_image->fb_priv));
+ (*video_frame)
+ ->AddDestructionObserver(
+ memory_pool_->CreateFrameCallback(vpx_image->fb_priv));
+ if (vpx_image_alpha) {
+ (*video_frame)
+ ->AddDestructionObserver(
+ memory_pool_->CreateFrameCallback(vpx_image_alpha->fb_priv));
+ }
return true;
}
diff --git a/media/filters/vpx_video_decoder.h b/media/filters/vpx_video_decoder.h
index 7bcba319954ed43175e42c2dc1b991c5b6129138..2ab3767680ee408215bf2debb6f85c033f45af68 100644
--- a/media/filters/vpx_video_decoder.h
+++ b/media/filters/vpx_video_decoder.h
@@ -104,8 +104,8 @@ class MEDIA_EXPORT VpxVideoDecoder : public OffloadableVideoDecoder {
std::unique_ptr<vpx_codec_ctx> vpx_codec_;
std::unique_ptr<vpx_codec_ctx> vpx_codec_alpha_;
- // |memory_pool_| is a single-threaded memory pool used for VP9 decoding
- // with no alpha. |frame_pool_| is used for all other cases.
+ // |memory_pool_| is a thread-safe memory pool used for zero-copy VP9 decoding
+ // (both with and without alpha). |frame_pool_| is used for VP8.
scoped_refptr<FrameBufferPool> memory_pool_;
VideoFramePool frame_pool_;
diff --git a/media/filters/vpx_video_decoder_unittest.cc b/media/filters/vpx_video_decoder_unittest.cc
index c7f6d13bd825425230b63d87c13466e49f3c3c59..5203645bc8ec89dd93827fc0cbebb92e803faac1 100644
--- a/media/filters/vpx_video_decoder_unittest.cc
+++ b/media/filters/vpx_video_decoder_unittest.cc
@@ -176,6 +176,28 @@ class VpxVideoDecoderTest : public testing::Test {
output_frames_.push_back(std::move(frame));
}
+ // Extracts the compressed video data from the AVPacket and also checks for
+ // side data containing an alpha channel. If found, it copies the alpha data
+ // into the DecoderBuffer's side data. This is necessary because FFmpeg
+ // demuxes alpha channel data as side data associated with the video packet.
+ static scoped_refptr<DecoderBuffer> CreateBufferWithAlphaFromPacket(
+ const AVPacket* packet) {
+ auto buffer = DecoderBuffer::CopyFrom(AVPacketData(*packet));
+ size_t side_data_size = 0;
+ uint8_t* side_data_ptr = av_packet_get_side_data(
+ packet, AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, &side_data_size);
+ if (side_data_size > 8) {
+ // SAFETY: The best we can do here is trust the size reported by ffmpeg.
+ auto side_data =
+ UNSAFE_BUFFERS(base::span(side_data_ptr, side_data_size));
+ if (base::U64FromBigEndian(side_data.first<8u>()) == 1) {
+ buffer->WritableSideData().alpha_data =
+ base::HeapArray<uint8_t>::CopiedFrom(side_data.subspan(8u));
+ }
+ }
+ return buffer;
+ }
+
MOCK_METHOD1(DecodeDone, void(DecoderStatus));
base::test::TaskEnvironment task_env_;
@@ -293,6 +315,68 @@ TEST_F(VpxVideoDecoderTest, SimpleFrameReuse) {
EXPECT_EQ(old_y_data, output_frames_.back()->data(VideoFrame::Plane::kY));
}
+TEST_F(VpxVideoDecoderTest, SimpleAlphaFrameReuse) {
+ VideoDecoderConfig config = TestVideoConfig::Normal(VideoCodec::kVP9);
+ config.Initialize(
+ config.codec(), config.profile(),
+ VideoDecoderConfig::AlphaMode::kHasAlpha, config.color_space_info(),
+ config.video_transformation(), config.coded_size(), config.visible_rect(),
+ config.natural_size(), config.extra_data(), config.encryption_scheme());
+ InitializeWithConfig(config);
+ scoped_refptr<DecoderBuffer> alpha_frame = ReadTestDataFile("bear-vp9a.webm");
+
+ // Read frames from the webm file.
+ InMemoryUrlProtocol protocol(*alpha_frame, false);
+ FFmpegGlue glue(&protocol);
+ ASSERT_TRUE(glue.OpenContext());
+
+ auto packet = ScopedAVPacket::Allocate();
+
+ // Decode first frame
+ ASSERT_GE(av_read_frame(glue.format_context(), packet.get()), 0);
+ auto buffer = CreateBufferWithAlphaFromPacket(packet.get());
+ Decode(buffer);
+ av_packet_unref(packet.get());
+
+ ASSERT_EQ(1u, output_frames_.size());
+ scoped_refptr<VideoFrame> frame = std::move(output_frames_.front());
+ EXPECT_EQ(PIXEL_FORMAT_I420A, frame->format());
+ const uint8_t* old_y_data = frame->data(VideoFrame::Plane::kY);
+ const uint8_t* old_a_data = frame->data(VideoFrame::Plane::kA);
+ output_frames_.pop_back();
+
+ // Clear frame reference to return the frame to the pool.
+ frame = nullptr;
+
+ // Decode second frame.
+ Decode(buffer);
+ const uint8_t* mid_y_data =
+ output_frames_.front()->data(VideoFrame::Plane::kY);
+ const uint8_t* mid_a_data =
+ output_frames_.front()->data(VideoFrame::Plane::kA);
+ output_frames_.clear();
+
+ // Issuing another decode should reuse buffers from the pool.
+ Decode(buffer);
+
+ ASSERT_EQ(1u, output_frames_.size());
+ const uint8_t* new_y_data =
+ output_frames_.back()->data(VideoFrame::Plane::kY);
+ const uint8_t* new_a_data =
+ output_frames_.back()->data(VideoFrame::Plane::kA);
+
+ // The pool is shared, so buffers might be reused in a different order (e.g. Y
+ // might get the buffer previously used for A). Because libvpx allocates the
+ // new frame before releasing the old reference frame, we need to check across
+ // all previously allocated buffers.
+ bool reused_y = new_y_data == old_y_data || new_y_data == old_a_data ||
+ new_y_data == mid_y_data || new_y_data == mid_a_data;
+ bool reused_a = new_a_data == old_y_data || new_a_data == old_a_data ||
+ new_a_data == mid_y_data || new_a_data == mid_a_data;
+ EXPECT_TRUE(reused_y);
+ EXPECT_TRUE(reused_a);
+}
+
TEST_F(VpxVideoDecoderTest, SimpleFormatChange) {
scoped_refptr<DecoderBuffer> large_frame =
ReadTestDataFile("vp9-I-frame-1280x720");
@@ -312,9 +396,41 @@ TEST_F(VpxVideoDecoderTest, FrameValidAfterPoolDestruction) {
// Write to the Y plane. The memory tools should detect a
// use-after-free if the storage was actually removed by pool destruction.
- memset(output_frames_.front()->writable_data(VideoFrame::Plane::kY), 0xff,
- output_frames_.front()->rows(VideoFrame::Plane::kY) *
- output_frames_.front()->stride(VideoFrame::Plane::kY));
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kY), 0xff);
+}
+
+TEST_F(VpxVideoDecoderTest, AlphaFrameValidAfterPoolDestruction) {
+ VideoDecoderConfig config = TestVideoConfig::Normal(VideoCodec::kVP9);
+ config.Initialize(
+ config.codec(), config.profile(),
+ VideoDecoderConfig::AlphaMode::kHasAlpha, config.color_space_info(),
+ config.video_transformation(), config.coded_size(), config.visible_rect(),
+ config.natural_size(), config.extra_data(), config.encryption_scheme());
+ InitializeWithConfig(config);
+ scoped_refptr<DecoderBuffer> alpha_frame = ReadTestDataFile("bear-vp9a.webm");
+
+ InMemoryUrlProtocol protocol(*alpha_frame, false);
+ FFmpegGlue glue(&protocol);
+ ASSERT_TRUE(glue.OpenContext());
+
+ auto packet = ScopedAVPacket::Allocate();
+ ASSERT_GE(av_read_frame(glue.format_context(), packet.get()), 0);
+ auto buffer = CreateBufferWithAlphaFromPacket(packet.get());
+ Decode(std::move(buffer));
+ av_packet_unref(packet.get());
+
+ ASSERT_EQ(1u, output_frames_.size());
+ EXPECT_EQ(PIXEL_FORMAT_I420A, output_frames_.front()->format());
+
+ Destroy();
+
+ // Write to the Y and A planes. The memory tools should detect a
+ // use-after-free if the storage was actually removed by pool destruction.
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kY), 0xff);
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kA), 0xff);
}
// The test stream uses profile 2, which needs high bit depth support in libvpx.
@@ -362,8 +478,7 @@ TEST_F(VpxVideoDecoderTest, MemoryPoolAllowsMultipleDisplay) {
Destroy();
// ASAN will be very unhappy with this line if the above is incorrect.
- memset(last_frame->writable_data(VideoFrame::Plane::kY), 0,
- last_frame->row_bytes(VideoFrame::Plane::kY));
+ std::ranges::fill(last_frame->writable_span(VideoFrame::Plane::kY), 0);
}
#endif // !defined(LIBVPX_NO_HIGH_BIT_DEPTH) && !defined(ARCH_CPU_ARM_FAMILY)

View File

@@ -12,5 +12,9 @@
{ "patch_dir": "src/electron/patches/ReactiveObjC", "repo": "src/third_party/squirrel.mac/vendor/ReactiveObjC" },
{ "patch_dir": "src/electron/patches/webrtc", "repo": "src/third_party/webrtc" },
{ "patch_dir": "src/electron/patches/reclient-configs", "repo": "src/third_party/engflow-reclient-configs" },
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" }
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" },
{ "patch_dir": "src/electron/patches/angle", "repo": "src/third_party/angle" },
{ "patch_dir": "src/electron/patches/skia", "repo": "src/third_party/skia" },
{ "patch_dir": "src/electron/patches/pdfium", "repo": "src/third_party/pdfium" },
{ "patch_dir": "src/electron/patches/libaom", "repo": "src/third_party/libaom/source/libaom" }
]

4
patches/libaom/.patches Normal file
View File

@@ -0,0 +1,4 @@
cherry-pick-4369bd1258dc.patch
cherry-pick-a047955845e5.patch
cherry-pick-c61e9586156f.patch
cherry-pick-395efd18d8ef.patch

View File

@@ -0,0 +1,27 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: James Zern <jzern@google.com>
Date: Wed, 1 Apr 2026 20:56:24 -0700
Subject: av1_nonrd_pick_inter_mode_sb: normalize ref frame check
Prefer `search_state.use_ref_frame_mask[]` over `cpi->ref_frame_flags`.
These are equivalent and checking the former is more consistent with the
rest of the function. This is a follow up to:
4369bd1258 av1_nonrd_pick_inter_mode_sb: add missing ref_frame_flags check
Bug: 495477995, 495996858
Change-Id: Ie4bd1f4c80c4182add35c7a9c1977c15ce97d3bd
(cherry picked from commit 395efd18d8ef31d8452a0336e848c02072feffe7)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index 29539b18566c37d0e81975f54429f699907f619e..54d497af619cb7f59f3797c87dd3208c2b2448da 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -3448,7 +3448,7 @@ void av1_nonrd_pick_inter_mode_sb(AV1_COMP *cpi, TileDataEnc *tile_data,
!x->force_zeromv_skip_for_blk &&
x->content_state_sb.source_sad_nonrd != kZeroSad &&
x->source_variance == 0 && bsize < cm->seq_params->sb_size &&
- (cpi->ref_frame_flags & AOM_LAST_FLAG) &&
+ search_state.use_ref_frame_mask[LAST_FRAME] &&
search_state.yv12_mb[LAST_FRAME][0].width == cm->width &&
search_state.yv12_mb[LAST_FRAME][0].height == cm->height) {
set_block_source_sad(cpi, x, bsize, &search_state.yv12_mb[LAST_FRAME][0]);

View File

@@ -0,0 +1,24 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: James Zern <jzern@google.com>
Date: Fri, 27 Mar 2026 10:56:13 -0700
Subject: av1_nonrd_pick_inter_mode_sb: add missing ref_frame_flags check
Before calling `set_block_source_sad()` ensure `LAST_FRAME` is
available. Fixes a crash that may present as a use after free (UAF).
Bug: 495477995, 495996858
Change-Id: I61452ce412fb9071c3370b4350ed8878013a8355
(cherry picked from commit 4369bd1258dc99fa759916d9aba6509cdda9d877)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index f2010062323b0ff4a1236ef63516d9b2d8f3007a..0f2a1c780a56a51f69bba8893fea9d9ad98b85a3 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -3440,6 +3440,7 @@ void av1_nonrd_pick_inter_mode_sb(AV1_COMP *cpi, TileDataEnc *tile_data,
!x->force_zeromv_skip_for_blk &&
x->content_state_sb.source_sad_nonrd != kZeroSad &&
x->source_variance == 0 && bsize < cm->seq_params->sb_size &&
+ (cpi->ref_frame_flags & AOM_LAST_FLAG) &&
search_state.yv12_mb[LAST_FRAME][0].width == cm->width &&
search_state.yv12_mb[LAST_FRAME][0].height == cm->height) {
set_block_source_sad(cpi, x, bsize, &search_state.yv12_mb[LAST_FRAME][0]);

View File

@@ -0,0 +1,187 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Marco Paniconi <marpan@google.com>
Date: Sun, 29 Mar 2026 20:27:20 -0700
Subject: Set force_mv_inter_layer earlier in skip_inter_mode
For nonrd_pickmode: move the setting of
force_mv_inter_layer earlier in the
skip_inter_mode_nonrd(), to make sure it always
get set (in case of false return in that function).
Thie prevents the usage of a scaled_ref in pickmode
(combined_motion search) when it has actually not been
set/scaled in av1_scale_references (before encoding).
Fixes a crash for use after free (UAF), reported
in the issues below.
Added svc unittest to generate the issue. Also added
assert check for scaled_ref in combined_motion_search.
Bug: 495477995, 495996858
Change-Id: I578d19156d97a50546edc9422bc3581566f1236e
(cherry picked from commit a047955845e50e43786d51cdefcfc9e87804ed61)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index 0f2a1c780a56a51f69bba8893fea9d9ad98b85a3..942b8ab23a2d448877c8801940fee4d0baae9aef 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -192,7 +192,7 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int *rate_mv, int64_t best_rd_sofar,
int use_base_mv) {
MACROBLOCKD *xd = &x->e_mbd;
- const AV1_COMMON *cm = &cpi->common;
+ AV1_COMMON *cm = &cpi->common;
const SPEED_FEATURES *sf = &cpi->sf;
MB_MODE_INFO *mi = xd->mi[0];
int step_param = (sf->rt_sf.fullpel_search_step_param)
@@ -207,6 +207,14 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int cost_list[5];
int search_subpel = 1;
+ if (av1_is_scaled(get_ref_scale_factors(cm, ref))) {
+ const YV12_BUFFER_CONFIG *scaled_ref = av1_get_scaled_ref_frame(cpi, ref);
+ (void)scaled_ref;
+ assert(scaled_ref != NULL);
+ assert(scaled_ref->y_crop_width == cm->width &&
+ scaled_ref->y_crop_height == cm->height);
+ }
+
start_mv = get_fullmv_from_mv(&ref_mv);
if (!use_base_mv)
@@ -2490,6 +2498,23 @@ static AOM_FORCE_INLINE bool skip_inter_mode_nonrd(
(*this_mode != GLOBALMV || *ref_frame != LAST_FRAME))
return true;
+ *force_mv_inter_layer = 0;
+ if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
+ ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
+ (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
+ (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
+ // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
+ // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
+ // Skip newmv and filter search.
+ *force_mv_inter_layer = 1;
+ if (*this_mode == NEWMV) {
+ search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
+ } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
+ svc_mv.as_int) {
+ return true;
+ }
+ }
+
// If the segment reference frame feature is enabled then do nothing if the
// current ref frame is not allowed.
if (segfeature_active(seg, segment_id, SEG_LVL_REF_FRAME)) {
@@ -2565,23 +2590,6 @@ static AOM_FORCE_INLINE bool skip_inter_mode_nonrd(
return true;
}
- *force_mv_inter_layer = 0;
- if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
- ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
- (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
- (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
- // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
- // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
- // Skip newmv and filter search.
- *force_mv_inter_layer = 1;
- if (*this_mode == NEWMV) {
- search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
- } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
- svc_mv.as_int) {
- return true;
- }
- }
-
// For screen content: skip mode testing based on source_sad.
if (cpi->oxcf.tune_cfg.content == AOM_CONTENT_SCREEN &&
!x->force_zeromv_skip_for_blk) {
diff --git a/test/svc_datarate_test.cc b/test/svc_datarate_test.cc
index 0df678212acb0519aa4420ae57186840e12c682c..2f68ba7a214932b284a6eacbe1a9b5b474b6c659 100644
--- a/test/svc_datarate_test.cc
+++ b/test/svc_datarate_test.cc
@@ -247,6 +247,7 @@ class DatarateTestSVC
external_resize_pattern_ = 0;
dynamic_tl_ = false;
dynamic_scale_factors_ = false;
+ disable_last_ref_ = false;
}
void PreEncodeFrameHook(::libaom_test::VideoSource *video,
@@ -302,7 +303,7 @@ class DatarateTestSVC
spatial_layer_id, multi_ref_, comp_pred_,
(video->frame() % cfg_.kf_max_dist) == 0, dynamic_enable_disable_mode_,
rps_mode_, rps_recovery_frame_, simulcast_mode_, use_last_as_scaled_,
- use_last_as_scaled_single_ref_);
+ use_last_as_scaled_single_ref_, disable_last_ref_);
if (intra_only_ == 1 && frame_sync_ > 0) {
// Set an Intra-only frame on SL0 at frame_sync_.
// In order to allow decoding to start on SL0 in mid-sequence we need to
@@ -964,7 +965,7 @@ class DatarateTestSVC
int multi_ref, int comp_pred, int is_key_frame,
int dynamic_enable_disable_mode, int rps_mode, int rps_recovery_frame,
int simulcast_mode, bool use_last_as_scaled,
- bool use_last_as_scaled_single_ref) {
+ bool use_last_as_scaled_single_ref, bool disable_last_ref) {
int lag_index = 0;
int base_count = frame_cnt >> 2;
layer_id->spatial_layer_id = spatial_layer;
@@ -1164,6 +1165,11 @@ class DatarateTestSVC
if (dynamic_enable_disable_mode == 1 &&
layer_id->spatial_layer_id == number_spatial_layers_ - 1)
ref_frame_config->reference[0] = 0;
+ // Always disable LAST reference under this flag. use GOLDEN reference.
+ if (disable_last_ref) {
+ ref_frame_config->reference[0] = 0;
+ ref_frame_config->reference[3] = 1;
+ }
return layer_flags;
}
@@ -1508,6 +1514,23 @@ class DatarateTestSVC
CheckDatarate(0.80, 1.60);
}
+ virtual void BasicRateTargetingSVC1TL2SLDisableLASTTest() {
+ SetUpCbr();
+ cfg_.g_error_resilient = 0;
+
+ ::libaom_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352,
+ 288, 30, 1, 0, 300);
+ const int bitrate_array[2] = { 300, 600 };
+ cfg_.rc_target_bitrate = bitrate_array[GET_PARAM(4)];
+ ResetModel();
+ disable_last_ref_ = true;
+ screen_mode_ = true;
+ ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ((int)GetMismatchFrames(), 0);
+#endif
+ }
+
virtual void BasicRateTargetingSVC3TL3SLIntraStartDecodeBaseMidSeq() {
SetUpCbr();
cfg_.rc_max_quantizer = 56;
@@ -2380,6 +2403,7 @@ class DatarateTestSVC
int external_resize_pattern_;
bool dynamic_tl_;
bool dynamic_scale_factors_;
+ bool disable_last_ref_;
};
// Check basic rate targeting for CBR, for 3 temporal layers, 1 spatial.
@@ -2458,6 +2482,12 @@ TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SL) {
BasicRateTargetingSVC1TL2SLTest();
}
+// Check basic rate targeting for CBR, for 2 spatial layers, 1 temporal.
+// Disable the usage of LAST referenc frame.
+TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SLDisableLAST) {
+ BasicRateTargetingSVC1TL2SLDisableLASTTest();
+}
+
// Check basic rate targeting for CBR, for 3 spatial layers, 3 temporal,
// with Intra-only frame inserted in the stream. Verify that we can start
// decoding the SL0 stream at the intra_only frame in mid-sequence.

View File

@@ -0,0 +1,37 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Wan-Teh Chang <wtc@google.com>
Date: Wed, 1 Apr 2026 18:57:32 -0700
Subject: Change cm back to const in combined_motion_search
The local variable cm in combined_motion_search() was changed to a
non-const pointer so that it could be passed to get_ref_scale_factors().
There is a get_ref_scale_factors_const() function for this purpose.
A follow-up to commit a047955.
Bug: 495477995, 495996858
Change-Id: Ic8b66f8060247a3487a7740fe5383c6e5455fa10
(cherry picked from commit c61e9586156f0023ad31e8a6abb0dfdcfd820927)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index 942b8ab23a2d448877c8801940fee4d0baae9aef..29539b18566c37d0e81975f54429f699907f619e 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -192,7 +192,7 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int *rate_mv, int64_t best_rd_sofar,
int use_base_mv) {
MACROBLOCKD *xd = &x->e_mbd;
- AV1_COMMON *cm = &cpi->common;
+ const AV1_COMMON *cm = &cpi->common;
const SPEED_FEATURES *sf = &cpi->sf;
MB_MODE_INFO *mi = xd->mi[0];
int step_param = (sf->rt_sf.fullpel_search_step_param)
@@ -207,7 +207,7 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int cost_list[5];
int search_subpel = 1;
- if (av1_is_scaled(get_ref_scale_factors(cm, ref))) {
+ if (av1_is_scaled(get_ref_scale_factors_const(cm, ref))) {
const YV12_BUFFER_CONFIG *scaled_ref = av1_get_scaled_ref_frame(cpi, ref);
(void)scaled_ref;
assert(scaled_ref != NULL);

2
patches/pdfium/.patches Normal file
View File

@@ -0,0 +1,2 @@
cherry-pick-ca8a943c247c.patch
cherry-pick-bce2e6728279.patch

View File

@@ -0,0 +1,36 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 7 Apr 2026 15:50:30 -0700
Subject: Use safe arithmetic in CFX_PSRenderer::DrawDIBits()
Hardening suggestion from the AI bot.
Bug: 500036290
Change-Id: Ie521629d06ba944f610b941a8c9e9505fa29aea7
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145731
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
diff --git a/core/fxge/win32/cfx_psrenderer.cpp b/core/fxge/win32/cfx_psrenderer.cpp
index b38f1a2b7c3271769e609763be2e183f2890ebb3..b8710e50ed01233b2aefbf1760e26e05964b315e 100644
--- a/core/fxge/win32/cfx_psrenderer.cpp
+++ b/core/fxge/win32/cfx_psrenderer.cpp
@@ -620,8 +620,16 @@ bool CFX_PSRenderer::DrawDIBits(RetainPtr<const CFX_DIBBase> bitmap,
encoder_iface_->pJpegEncodeFunc(bitmap, &output_buf, &output_size)) {
filter = "/DCTDecode filter ";
} else {
- int src_pitch = width * bytes_per_pixel;
- output_size = height * src_pitch;
+ FX_SAFE_UINT32 safe_pitch = bytes_per_pixel;
+ safe_pitch *= width;
+ FX_SAFE_UINT32 safe_output_size = safe_pitch;
+ safe_output_size *= height;
+ if (!safe_output_size.IsValid()) {
+ WriteString("\nQ\n");
+ return false;
+ }
+ uint32_t src_pitch = safe_pitch.ValueOrDie();
+ output_size = safe_output_size.ValueOrDie();
output_buf = FX_Alloc(uint8_t, output_size);
for (int row = 0; row < height; row++) {
const uint8_t* src_scan = bitmap->GetScanline(row).data();

View File

@@ -0,0 +1,70 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Fri, 27 Mar 2026 14:52:16 -0700
Subject: Patch an overflow in libtiff
Apply fix [1] from upstream, which is not in the most recent versioned
release.
[1] https://gitlab.com/libtiff/libtiff/-/commit/0f726d9
Bug: 496907110
Change-Id: Ic8665879ebdd4445f473e9a1e156cfc42c294d51
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145550
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/third_party/libtiff/0034-tiff-jpeg-overflow.patch b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
new file mode 100644
index 0000000000000000000000000000000000000000..ba6086a38adfa0bd7726affda0f11381e04501e5
--- /dev/null
+++ b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
@@ -0,0 +1,25 @@
+commit 0f726d9477a11e15eb67ca349c03907f6cfb82a9
+Author: Mikhail Khachaiants <mkhachaiants@gmail.com>
+Date: Mon Dec 1 22:26:34 2025 +0200
+
+ tif_jpeg: reject mismatched JPEG data precision to avoid write overflow
+
+ Ensure TIFF BitsPerSample matches both BITS_IN_JSAMPLE and the JPEG
+ header data_precision for JPEG-compressed images. This prevents
+ under-sized scanline buffers that can lead to write buffer overflows
+ in jdcolor.c/null_convert when decoding malformed inputs.
+
+diff --git a/libtiff/tif_jpeg.c b/libtiff/tif_jpeg.c
+index aba5f99b..4d6370b5 100644
+--- a/libtiff/tif_jpeg.c
++++ b/libtiff/tif_jpeg.c
+@@ -1282,7 +1282,8 @@ int TIFFJPEGIsFullStripRequired(TIFF *tif)
+ sp->cinfo.d.data_precision = td->td_bitspersample;
+ sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
+ #else
+- if (sp->cinfo.d.data_precision != td->td_bitspersample)
++ if (td->td_bitspersample != BITS_IN_JSAMPLE ||
++ sp->cinfo.d.data_precision != td->td_bitspersample)
+ {
+ TIFFErrorExtR(tif, module, "Improper JPEG data precision");
+ return (0);
diff --git a/third_party/libtiff/README.pdfium b/third_party/libtiff/README.pdfium
index 9953e767853bcd30683cc24d0d1839c916659185..e3f352d747007641b5d0bd2256a5dbc8af7c20af 100644
--- a/third_party/libtiff/README.pdfium
+++ b/third_party/libtiff/README.pdfium
@@ -19,3 +19,4 @@ Local Modifications:
0028-nstrips-OOM.patch: return error for excess number of tiles/strips.
0031-safe_size_ingtStripContig.patch: return error if the size to read overflow from int32.
0033-avail-out-overflow.patch: signed comparison in PixarLogDecode().
+0034-tiff-jpeg-overflow.patch: reject mismatched JPEG data precision.
diff --git a/third_party/libtiff/tif_jpeg.c b/third_party/libtiff/tif_jpeg.c
index 5281457d936a0dfa5f877c6a7efff6a65066f520..a9764f073db04d6e593e421105d0f59efbfbbeb2 100644
--- a/third_party/libtiff/tif_jpeg.c
+++ b/third_party/libtiff/tif_jpeg.c
@@ -1287,7 +1287,8 @@ int TIFFJPEGIsFullStripRequired(TIFF *tif)
sp->cinfo.d.data_precision = td->td_bitspersample;
sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
#else
- if (sp->cinfo.d.data_precision != td->td_bitspersample)
+ if (td->td_bitspersample != BITS_IN_JSAMPLE ||
+ sp->cinfo.d.data_precision != td->td_bitspersample)
{
TIFFErrorExtR(tif, module, "Improper JPEG data precision");
return (0);

2
patches/skia/.patches Normal file
View File

@@ -0,0 +1,2 @@
cherry-pick-0566b2f5f0d1.patch
cherry-pick-3f9969421ad5.patch

View File

@@ -0,0 +1,651 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Wed, 1 Apr 2026 09:48:48 -0400
Subject: Use 16-bit size for ResourceKeys
Internally, ResourceKey required the size to fit into a uint16_t so this
makes that explicit in the public API. It also changes how the size is
stored to instead record the num32DataCount directly and then convert to
bytes as needed, whereas previously it was requiring that the actual
byte count fit into a uint16_t. This gives a bit more head room.
Call sites to the ResourceKey builders are updated to now have the
responsibility of checking that their size can fit into a uint16_t. For
the most part, these were fixed or trivially small variable key sizes.
The two exceptions were Ganesh's style key (with dashes) and its
inherited key system for shapes with applied styles and path effects.
They now have reasonable limits to prevent the keys from growing bigger
than about 1kb.
Bug: b/495700484
Change-Id: I6ac4f17628b9a2e1a777c473b74e6d1f5c68b27d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1199497
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/gpu/ResourceKey.h b/src/gpu/ResourceKey.h
index f8dee7983036a95d2f5fd7404553916b5c616e83..19851a67653669058361570c615d9be45dc5153a 100644
--- a/src/gpu/ResourceKey.h
+++ b/src/gpu/ResourceKey.h
@@ -19,6 +19,7 @@
#include <cstdint>
#include <cstring>
+#include <limits>
#include <new>
#include <utility>
@@ -77,14 +78,10 @@ public:
}
protected:
- Builder(ResourceKey* key, uint32_t domain, int data32Count) : fKey(key) {
- size_t count = SkToSizeT(data32Count);
+ Builder(ResourceKey* key, uint16_t domain, uint16_t data32Count) : fKey(key) {
SkASSERT(domain != kInvalidDomain);
- key->fKey.reset(kMetaDataCnt + count);
- size_t size = (count + kMetaDataCnt) * sizeof(uint32_t);
- SkASSERT(SkToU16(size) == size);
- SkASSERT(SkToU16(domain) == domain);
- key->fKey[kDomainAndSize_MetaDataIdx] = SkToU32(domain | (size << 16));
+ key->fKey.reset(kMetaDataCnt + data32Count);
+ key->fKey[kDomainAndSize_MetaDataIdx] = domain | (data32Count << 16);
}
private:
@@ -92,7 +89,7 @@ public:
};
protected:
- static const uint32_t kInvalidDomain = 0;
+ static const uint16_t kInvalidDomain = 0;
ResourceKey() { this->reset(); }
@@ -118,10 +115,10 @@ protected:
return *this;
}
- uint32_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
+ uint16_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
/** size of the key data, excluding meta-data (hash, domain, etc). */
- size_t dataSize() const { return this->size() - 4 * kMetaDataCnt; }
+ size_t dataSize() const { return (fKey[kDomainAndSize_MetaDataIdx] >> 16) * sizeof(uint32_t); }
/** ptr to the key data, excluding meta-data (hash, domain, etc). */
const uint32_t* data() const {
@@ -149,14 +146,17 @@ protected:
private:
enum MetaDataIdx {
kHash_MetaDataIdx,
- // The key domain and size are packed into a single uint32_t.
+ // The key domain and size are packed into a single uint32_t. The stored size is in units
+ // of uint32_t and does not include the metadata, i.e. it stores the data32Count provided
+ // to the original key builder.
kDomainAndSize_MetaDataIdx,
kLastMetaDataIdx = kDomainAndSize_MetaDataIdx
};
static const uint32_t kMetaDataCnt = kLastMetaDataIdx + 1;
- size_t internalSize() const { return fKey[kDomainAndSize_MetaDataIdx] >> 16; }
+ // Total size in bytes, including metadata
+ size_t internalSize() const { return this->dataSize() + sizeof(uint32_t) * kMetaDataCnt; }
void validate() const {
SkASSERT(this->isValid());
@@ -197,7 +197,7 @@ private:
class ScratchKey : public ResourceKey {
public:
/** Uniquely identifies the type of resource that is cached as scratch. */
- typedef uint32_t ResourceType;
+ typedef uint16_t ResourceType;
/** Generate a unique ResourceType. */
static ResourceType GenerateResourceType();
@@ -219,7 +219,7 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(ScratchKey* key, ResourceType type, int data32Count)
+ Builder(ScratchKey* key, ResourceType type, uint16_t data32Count)
: ResourceKey::Builder(key, type, data32Count) {}
};
};
@@ -240,7 +240,7 @@ public:
*/
class UniqueKey : public ResourceKey {
public:
- typedef uint32_t Domain;
+ typedef uint16_t Domain;
/** Generate a Domain for unique keys. */
static Domain GenerateDomain();
@@ -279,17 +279,17 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(UniqueKey* key, Domain type, int data32Count, const char* tag = nullptr)
+ Builder(UniqueKey* key, Domain type, uint16_t data32Count, const char* tag = nullptr)
: ResourceKey::Builder(key, type, data32Count) {
key->fTag = tag;
}
/** Used to build a key that wraps another key and adds additional data. */
- Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, int extraData32Cnt,
+ Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, uint16_t extraData32Cnt,
const char* tag = nullptr)
: ResourceKey::Builder(key,
domain,
- Data32CntForInnerKey(innerKey) + extraData32Cnt) {
+ Data32CntForInnerKey(innerKey, extraData32Cnt)) {
SkASSERT(&innerKey != key);
// add the inner key to the end of the key so that op[] can be indexed normally.
uint32_t* innerKeyData = &this->operator[](extraData32Cnt);
@@ -300,9 +300,15 @@ public:
}
private:
- static int Data32CntForInnerKey(const UniqueKey& innerKey) {
- // key data + domain
- return SkToInt((innerKey.dataSize() >> 2) + 1);
+ static uint16_t Data32CntForInnerKey(const UniqueKey& innerKey, uint16_t extraData32Cnt) {
+ // key data + domain + extraData32Cnt needs to fit into a uint16_t. This key builder is
+ // only used in Ganesh for wrapping textures
+ uint16_t innerData32Cnt = innerKey.dataSize() >> 2;
+ // The Builder API doesn't have a way to return a failure, so if this is somehow
+ // exceeded, then we have no way to recover.
+ SkASSERT_RELEASE((uint32_t) extraData32Cnt + (uint32_t) innerData32Cnt + 1 <=
+ (uint32_t) std::numeric_limits<uint16_t>::max());
+ return innerData32Cnt + extraData32Cnt + 1;
}
};
diff --git a/src/gpu/ganesh/GrStyle.cpp b/src/gpu/ganesh/GrStyle.cpp
index 5d7bc9c1d971bbcf3df0fa720f660f23dfdcbab5..d1bdcf5a117b61f3a8ac3010d6d3cc3702f82ced 100644
--- a/src/gpu/ganesh/GrStyle.cpp
+++ b/src/gpu/ganesh/GrStyle.cpp
@@ -18,8 +18,18 @@
int GrStyle::KeySize(const GrStyle &style, Apply apply, uint32_t flags) {
static_assert(sizeof(uint32_t) == sizeof(SkScalar));
+
+ // We embed the dash interval pattern into the key, and the key size must fit within 16-bits.
+ // However, we put a more conservative upper limit on the dashes because we don't want to keep
+ // key memory locked up in caches during pathological cases.
+ static constexpr int kDashIntervalKeyLimit = 512;
+
int size = 0;
if (style.isDashed()) {
+ if (style.dashIntervalCnt() > kDashIntervalKeyLimit) {
+ return -1; // Disable caching for pathologically large dash patterns
+ }
+
// One scalar for scale, one for dash phase, and one for each dash value.
size += 2 + style.dashIntervalCnt();
} else if (style.pathEffect()) {
diff --git a/src/gpu/ganesh/GrStyle.h b/src/gpu/ganesh/GrStyle.h
index 41b0ce9db13e57db63cd1949dc87b9c20023fcc6..252b975e1e66dd654326449f3c9cbc317b8a2fda 100644
--- a/src/gpu/ganesh/GrStyle.h
+++ b/src/gpu/ganesh/GrStyle.h
@@ -74,6 +74,8 @@ public:
* into a key. This occurs when there is a path effect that is not a dash. The key can
* either reflect just the path effect (if one) or the path effect and the strokerec. Note
* that a simple fill has a zero sized key.
+ *
+ * If a positive value is returned, it will fit in a uint16_t.
*/
static int KeySize(const GrStyle&, Apply, uint32_t flags = 0);
diff --git a/src/gpu/ganesh/geometry/GrStyledShape.cpp b/src/gpu/ganesh/geometry/GrStyledShape.cpp
index 3c2b942aa6c614a6312e6695309cb9fc8dd6f5d5..6aa4daa3f8d76ab0dfe062dfb2f4b1503a8a5e1f 100644
--- a/src/gpu/ganesh/geometry/GrStyledShape.cpp
+++ b/src/gpu/ganesh/geometry/GrStyledShape.cpp
@@ -19,6 +19,7 @@
#include <algorithm>
#include <cstring>
+#include <limits>
#include <utility>
@@ -141,12 +142,12 @@ static void write_path_key_from_data(const SkPath& path, uint32_t* origKey) {
SkASSERT(key - origKey == path_key_from_data_size(path));
}
-int GrStyledShape::unstyledKeySize() const {
+uint16_t GrStyledShape::unstyledKeySize() const {
if (fInheritedKey.count()) {
- return fInheritedKey.count();
+ return SkTo<uint16_t>(fInheritedKey.count());
}
- int count = 1; // Every key has the state flags from the GrShape
+ uint16_t count = 1; // Every key has the state flags from the GrShape
switch(fShape.type()) {
case GrShape::Type::kPoint:
static_assert(0 == sizeof(SkPoint) % sizeof(uint32_t));
@@ -170,11 +171,13 @@ int GrStyledShape::unstyledKeySize() const {
break;
case GrShape::Type::kPath: {
if (0 == fGenID) {
- return -1; // volatile, so won't be keyed
+ return 0; // volatile, so won't be keyed
}
+ // When >= 0, `dataKeySize` is a reasonably small number bounded by
+ // kMaxKeyFromDataVerbCnt since point count is derived from verb count.
int dataKeySize = path_key_from_data_size(fShape.path());
if (dataKeySize >= 0) {
- count += dataKeySize;
+ count += SkTo<uint16_t>(dataKeySize);
} else {
count++; // Just adds the gen ID.
}
@@ -251,6 +254,7 @@ void GrStyledShape::writeUnstyledKey(uint32_t* key) const {
void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply apply,
SkScalar scale) {
+ static constexpr int kInheritedKeyLimit = 1024;
SkASSERT(!fInheritedKey.count());
// If the output shape turns out to be simple, then we will just use its geometric key
if (fShape.isPath()) {
@@ -264,7 +268,7 @@ void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply
bool useParentGeoKey = !parentCnt;
if (useParentGeoKey) {
parentCnt = parent.unstyledKeySize();
- if (parentCnt < 0) {
+ if (!parentCnt) {
// The parent's geometry has no key so we will have no key.
fGenID = 0;
return;
@@ -283,7 +287,12 @@ void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply
// we try to get a key for the shape.
fGenID = 0;
return;
+ } else if (parentCnt + styleCnt > kInheritedKeyLimit) {
+ // Prevent chained path effects and styles from growing the key too large
+ fGenID = 0;
+ return;
}
+
fInheritedKey.reset(parentCnt + styleCnt);
if (useParentGeoKey) {
// This will be the geo key.
diff --git a/src/gpu/ganesh/geometry/GrStyledShape.h b/src/gpu/ganesh/geometry/GrStyledShape.h
index 97db583a5cf8aa8c7fe8c0e054ef622ca6945744..ed4345355478121dfedb1a894e159b80c4fca304 100644
--- a/src/gpu/ganesh/geometry/GrStyledShape.h
+++ b/src/gpu/ganesh/geometry/GrStyledShape.h
@@ -252,11 +252,11 @@ public:
/**
* Gets the size of the key for the shape represented by this GrStyledShape (ignoring its
- * styling). A negative value is returned if the shape has no key (shouldn't be cached).
+ * styling). A zero value is returned if the shape has no key (shouldn't be cached).
*/
- int unstyledKeySize() const;
+ uint16_t unstyledKeySize() const;
- bool hasUnstyledKey() const { return this->unstyledKeySize() >= 0; }
+ bool hasUnstyledKey() const { return this->unstyledKeySize() > 0; }
/**
* Writes unstyledKeySize() bytes into the provided pointer. Assumes that there is enough
diff --git a/src/gpu/ganesh/image/GrImageUtils.cpp b/src/gpu/ganesh/image/GrImageUtils.cpp
index a88ff15c0a59f1a5f417aa37f243385503a3dfd9..8fd9ea6a55e4a77c793c7af2361aab7e06c63f08 100644
--- a/src/gpu/ganesh/image/GrImageUtils.cpp
+++ b/src/gpu/ganesh/image/GrImageUtils.cpp
@@ -683,8 +683,7 @@ GrSurfaceProxyView FindOrMakeCachedMipmappedView(GrRecordingContext* rContext,
SkASSERT(baseKey.isValid());
skgpu::UniqueKey mipmappedKey;
static const skgpu::UniqueKey::Domain kMipmappedDomain = skgpu::UniqueKey::GenerateDomain();
- { // No extra values beyond the domain are required. Must name the var to please
- // clang-tidy.
+ { // No extra values beyond the domain are required. Must name the var to please clang-tidy.
skgpu::UniqueKey::Builder b(&mipmappedKey, baseKey, kMipmappedDomain, 0);
}
SkASSERT(mipmappedKey.isValid());
diff --git a/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp b/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
index 124e51842eafbf7d3b010ca3232731d549515a9b..1bf3038847223c8167fa60c852c23216d186c5cc 100644
--- a/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
+++ b/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
@@ -282,8 +282,7 @@ private:
bool inverseFill = shape.inverseFilled();
static constexpr int kClipBoundsCnt = sizeof(devClipBounds) / sizeof(uint32_t);
- int shapeKeyDataCnt = shape.unstyledKeySize();
- SkASSERT(shapeKeyDataCnt >= 0);
+ uint16_t shapeKeyDataCnt = shape.unstyledKeySize();
skgpu::UniqueKey::Builder builder(key, kDomain, shapeKeyDataCnt + kClipBoundsCnt, "Path");
shape.writeUnstyledKey(&builder[0]);
// For inverse fills, the tessellation is dependent on clip bounds.
diff --git a/src/gpu/graphite/GraphiteResourceKey.h b/src/gpu/graphite/GraphiteResourceKey.h
index 12e72d1b24f45ac5885a125cb3d52cb648a55402..d52f0099a722aaf2a4b655abf6bf8c0ea26dcf57 100644
--- a/src/gpu/graphite/GraphiteResourceKey.h
+++ b/src/gpu/graphite/GraphiteResourceKey.h
@@ -46,7 +46,7 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(GraphiteResourceKey* key, ResourceType type, int data32Count)
+ Builder(GraphiteResourceKey* key, ResourceType type, uint16_t data32Count)
: ResourceKey::Builder(key, type, data32Count) {}
};
};
diff --git a/src/gpu/graphite/RasterPathUtils.cpp b/src/gpu/graphite/RasterPathUtils.cpp
index 1d8b5563e41bf8e3f515438c666760b228c3947c..8557d35bc4803cf71224457d103c3a9ce874012c 100644
--- a/src/gpu/graphite/RasterPathUtils.cpp
+++ b/src/gpu/graphite/RasterPathUtils.cpp
@@ -140,7 +140,7 @@ skgpu::UniqueKey GeneratePathMaskKey(const Shape& shape,
skgpu::UniqueKey maskKey;
{
static const skgpu::UniqueKey::Domain kDomain = skgpu::UniqueKey::GenerateDomain();
- int styleKeySize = 7;
+ uint16_t styleKeySize = 7;
if (!strokeRec.isHairlineStyle() && !strokeRec.isFillStyle()) {
// Add space for width and miter if needed
styleKeySize += 2;
@@ -185,66 +185,56 @@ skgpu::UniqueKey GenerateClipMaskKey(uint32_t stackRecordID,
skgpu::UniqueKey maskKey;
// if the element list is too large we just use the stackRecordID
if (elementsForMask->size() <= kMaxShapeCountForKey) {
- constexpr int kXformKeySize = 5;
- int keySize = 0;
- bool canCreateKey = true;
- // Iterate through to get key size and see if we can create a key at all
+ static constexpr int kXformKeySize = 5;
+ uint16_t keySize = includeBounds ? 2 : 0;
+ // Iterate through to get key size; given kMaxShapeCountForKey and Shape's own key size
+ // limitations, this should always fit safely within a 16-bit number
for (int i = 0; i < elementsForMask->size(); ++i) {
- int shapeKeySize = (*elementsForMask)[i]->fShape.keySize();
- if (shapeKeySize < 0) {
- canCreateKey = false;
- break;
- }
- keySize += kXformKeySize + shapeKeySize;
+ keySize += kXformKeySize + (*elementsForMask)[i]->fShape.keySize();
}
- if (canCreateKey) {
- if (includeBounds) {
- keySize += 2;
- }
- skgpu::UniqueKey::Builder builder(&maskKey, kDomain, keySize,
- "Clip Path Mask");
- int elementKeyIndex = 0;
- Rect unclippedBounds = Rect::InfiniteInverted();
- for (int i = 0; i < elementsForMask->size(); ++i) {
- const ClipStack::Element* element = (*elementsForMask)[i];
-
- // Add transform key and get packed fractional translation bits
- uint32_t fracBits = add_transform_key(&builder,
- elementKeyIndex,
- element->fLocalToDevice);
- uint32_t opBits = static_cast<uint32_t>(element->fOp);
- builder[elementKeyIndex + 4] = fracBits | (opBits << 16);
-
- const Shape& shape = element->fShape;
- shape.writeKey(&builder[elementKeyIndex + kXformKeySize],
- /*includeInverted=*/true);
-
- elementKeyIndex += kXformKeySize + shape.keySize();
-
- Rect transformedBounds = element->fLocalToDevice.mapRect(element->fShape.bounds());
- unclippedBounds.join(transformedBounds);
- }
-
- // The keyBounds are the maskDeviceBounds relative to the full transformed mask. We use
- // this to ensure we capture the situation where the maskDeviceBounds are equal in two
- // cases but actually enclose different regions of the full mask due to an integer
- // translation (which is not captured in the key) in the element transforms.
- *keyBounds = maskDeviceBounds.makeOffset(-unclippedBounds.left(),
- -unclippedBounds.top());
-
- if (includeBounds) {
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->left()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->top()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->right()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->bottom()));
-
- builder[elementKeyIndex] = keyBounds->left() | (keyBounds->top() << 16);
- builder[elementKeyIndex+1] = keyBounds->right() | (keyBounds->bottom() << 16);
- }
-
- *usesPathKey = true;
- return maskKey;
+
+ skgpu::UniqueKey::Builder builder(&maskKey, kDomain, keySize, "Clip Path Mask");
+ int elementKeyIndex = 0;
+ Rect unclippedBounds = Rect::InfiniteInverted();
+ for (int i = 0; i < elementsForMask->size(); ++i) {
+ const ClipStack::Element* element = (*elementsForMask)[i];
+
+ // Add transform key and get packed fractional translation bits
+ uint32_t fracBits = add_transform_key(&builder,
+ elementKeyIndex,
+ element->fLocalToDevice);
+ uint32_t opBits = static_cast<uint32_t>(element->fOp);
+ builder[elementKeyIndex + 4] = fracBits | (opBits << 16);
+
+ const Shape& shape = element->fShape;
+ shape.writeKey(&builder[elementKeyIndex + kXformKeySize],
+ /*includeInverted=*/true);
+
+ elementKeyIndex += kXformKeySize + shape.keySize();
+
+ Rect transformedBounds = element->fLocalToDevice.mapRect(element->fShape.bounds());
+ unclippedBounds.join(transformedBounds);
+ }
+
+ // The keyBounds are the maskDeviceBounds relative to the full transformed mask. We use
+ // this to ensure we capture the situation where the maskDeviceBounds are equal in two
+ // cases but actually enclose different regions of the full mask due to an integer
+ // translation (which is not captured in the key) in the element transforms.
+ *keyBounds = maskDeviceBounds.makeOffset(-unclippedBounds.left(),
+ -unclippedBounds.top());
+
+ if (includeBounds) {
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->left()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->top()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->right()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->bottom()));
+
+ builder[elementKeyIndex] = keyBounds->left() | (keyBounds->top() << 16);
+ builder[elementKeyIndex+1] = keyBounds->right() | (keyBounds->bottom() << 16);
}
+
+ *usesPathKey = true;
+ return maskKey;
}
// Either we have too many elements or at least one shape can't create a key
diff --git a/src/gpu/graphite/ResourceProvider.cpp b/src/gpu/graphite/ResourceProvider.cpp
index cd67a8af6fe60c2239dc6cfc7adaa8604ef3743a..80ce839d942b96f6ba7eb0456881a986accfe145 100644
--- a/src/gpu/graphite/ResourceProvider.cpp
+++ b/src/gpu/graphite/ResourceProvider.cpp
@@ -177,7 +177,7 @@ sk_sp<Sampler> ResourceProvider::findOrCreateCompatibleSampler(const SamplerDesc
// immutable sampler details into the SamplerDesc, so there is no need to delegate to Caps
// to create a specific key.
const SkSpan<const uint32_t>& samplerData = samplerDesc.asSpan();
- GraphiteResourceKey::Builder builder(&key, kType, samplerData.size());
+ GraphiteResourceKey::Builder builder(&key, kType, SkTo<uint16_t>(samplerData.size()));
for (size_t i = 0; i < samplerData.size(); i++) {
builder[i] = samplerData[i];
@@ -231,8 +231,8 @@ sk_sp<Buffer> ResourceProvider::findOrCreateBuffer(
// For the key we need ((sizeof(size_t) + (sizeof(uint32_t) - 1)) / (sizeof(uint32_t))
// uint32_t's for the size and one uint32_t for the rest.
static_assert(sizeof(uint32_t) == 4);
- static const int kSizeKeyNum32DataCnt = (sizeof(size_t) + 3) / 4;
- static const int kKeyNum32DataCnt = kSizeKeyNum32DataCnt + 1;
+ static const uint16_t kSizeKeyNum32DataCnt = (sizeof(size_t) + 3) / 4;
+ static const uint16_t kKeyNum32DataCnt = kSizeKeyNum32DataCnt + 1;
SkASSERT(static_cast<uint32_t>(type) < (1u << 4));
SkASSERT(static_cast<uint32_t>(accessPattern) < (1u << 2));
diff --git a/src/gpu/graphite/dawn/DawnCaps.cpp b/src/gpu/graphite/dawn/DawnCaps.cpp
index 3717790e1413401c0fbf12ff4cebd31132153ffd..1c281e3e7fd0299fb14d7fa8c763690bf68bae6b 100644
--- a/src/gpu/graphite/dawn/DawnCaps.cpp
+++ b/src/gpu/graphite/dawn/DawnCaps.cpp
@@ -1016,7 +1016,7 @@ uint32_t DawnCaps::getRenderPassDescKeyForPipeline(const RenderPassDesc& renderP
loadResolveAttachmentKey;
}
-static constexpr int kDawnGraphicsPipelineKeyData32Count = 4;
+static constexpr uint16_t kDawnGraphicsPipelineKeyData32Count = 4;
UniqueKey DawnCaps::makeGraphicsPipelineKey(const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc) const {
@@ -1234,7 +1234,7 @@ void DawnCaps::buildKeyForTexture(SkISize dimensions,
SkASSERT(static_cast<uint32_t>(dawnInfo.fUsage) < (1u << 28)); // usage is remaining 28 bits
// We need two uint32_ts for dimensions, 1 for format, and 1 for the rest of the key;
- int num32DataCnt = 2 + 1 + 1;
+ uint16_t num32DataCnt = 2 + 1 + 1;
bool hasYcbcrInfo = false;
#if !defined(__EMSCRIPTEN__)
// If we are using ycbcr texture/sampling, more key information is needed.
diff --git a/src/gpu/graphite/geom/AnalyticBlurMask.cpp b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
index 97f38ba054f66249d70c739cea0318f4d3e30203..5a118bf8b1792be4400a5a43db2d936366157fd0 100644
--- a/src/gpu/graphite/geom/AnalyticBlurMask.cpp
+++ b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
@@ -375,7 +375,7 @@ std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder,
static const UniqueKey::Domain kRRectBlurDomain = UniqueKey::GenerateDomain();
UniqueKey key;
{
- static constexpr int kKeySize = sizeof(DerivedParams) / sizeof(uint32_t);
+ static constexpr uint16_t kKeySize = sizeof(DerivedParams) / sizeof(uint32_t);
static_assert(SkIsAlign4(sizeof(DerivedParams)));
// TODO: We should discretize the sigma to perceptibly meaningful changes to the table,
// as well as the underlying the round rect geometry.
diff --git a/src/gpu/graphite/geom/Shape.cpp b/src/gpu/graphite/geom/Shape.cpp
index 29898fb00507aa64317ffd78354f36a18ca0a7b0..2465dcfb9fc92b678e67520e7b0e104d8514c147 100644
--- a/src/gpu/graphite/geom/Shape.cpp
+++ b/src/gpu/graphite/geom/Shape.cpp
@@ -183,8 +183,8 @@ void write_path_key_from_data(const SkPath& path, uint32_t* origKey) {
}
} // anonymous namespace
-int Shape::keySize() const {
- int count = 1; // Every key has the state flags from the Shape
+uint16_t Shape::keySize() const {
+ uint16_t count = 1; // Every key has the state flags from the Shape
switch(this->type()) {
case Type::kLine:
static_assert(0 == sizeof(skvx::float4) % sizeof(uint32_t));
@@ -207,7 +207,7 @@ int Shape::keySize() const {
if (!this->path().isEmpty()) {
int dataKeySize = path_key_from_data_size(this->path());
if (dataKeySize >= 0) {
- count += dataKeySize;
+ count += SkTo<uint16_t>(dataKeySize);
} else {
count++; // Just adds the gen ID.
}
diff --git a/src/gpu/graphite/geom/Shape.h b/src/gpu/graphite/geom/Shape.h
index 8c02945b7090779385a82aa4a8d257dad758e331..30dd8fffb797aee0c8a85093f5d5cdb97ef1a0fa 100644
--- a/src/gpu/graphite/geom/Shape.h
+++ b/src/gpu/graphite/geom/Shape.h
@@ -184,7 +184,7 @@ public:
/**
* Gets the size of the key for the shape represented by this Shape.
*/
- int keySize() const;
+ uint16_t keySize() const;
/**
* Writes keySize() bytes into the provided pointer. Assumes that there is enough
diff --git a/src/gpu/graphite/mtl/MtlCaps.mm b/src/gpu/graphite/mtl/MtlCaps.mm
index 7816ca699def160c1aa56afb28463d3c9d77926f..e5939af8fef877c297e98195e111860c88b0e876 100644
--- a/src/gpu/graphite/mtl/MtlCaps.mm
+++ b/src/gpu/graphite/mtl/MtlCaps.mm
@@ -949,7 +949,7 @@ MTLPixelFormat format_from_compression(SkTextureCompressionType compression) {
return {formatInfo.fColorTypeInfos.get(), formatInfo.fColorTypeInfoCount};
}
-static constexpr int kMtlGraphicsPipelineKeyData32Count = 4;
+static constexpr uint16_t kMtlGraphicsPipelineKeyData32Count = 4;
UniqueKey MtlCaps::makeGraphicsPipelineKey(const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc) const {
@@ -1193,7 +1193,7 @@ MTLPixelFormat format_from_compression(SkTextureCompressionType compression) {
SkASSERT(static_cast<uint32_t>(isFBOnly) < (1u << 1));
// We need two uint32_ts for dimensions, 2 for format, and 1 for the rest of the key;
- static int kNum32DataCnt = 2 + 2 + 1;
+ static uint16_t kNum32DataCnt = 2 + 2 + 1;
GraphiteResourceKey::Builder builder(key, type, kNum32DataCnt);
diff --git a/src/gpu/graphite/vk/VulkanCaps.cpp b/src/gpu/graphite/vk/VulkanCaps.cpp
index 799d90b03c54cee89b24281e25c46813adef5046..f5ae0b882af66050b3e90c121675ab1b3a4ec870 100644
--- a/src/gpu/graphite/vk/VulkanCaps.cpp
+++ b/src/gpu/graphite/vk/VulkanCaps.cpp
@@ -2087,7 +2087,7 @@ bool VulkanCaps::msaaTextureRenderToSingleSampledSupport(const TextureInfo& info
// 4 uint32s for the render step id, paint id, compatible render pass description, and write
// swizzle.
-static constexpr int kPipelineKeyData32Count = 4;
+static constexpr uint16_t kPipelineKeyData32Count = 4;
static constexpr int kPipelineKeyRenderStepIDIndex = 0;
static constexpr int kPipelineKeyPaintParamsIDIndex = 1;
@@ -2173,15 +2173,15 @@ void VulkanCaps::buildKeyForTexture(SkISize dimensions,
SkASSERT(vkInfo.fAspectMask < (1u << 11)); // aspectMask is bits 8 - 19
// We need two uint32_ts for dimensions and 3 for miscellaneous information.
- static constexpr int kNum32DimensionDataCnt = 2;
- static constexpr int kNum32MiscDataCnt = 3;
+ static constexpr uint16_t kNum32DimensionDataCnt = 2;
+ static constexpr uint16_t kNum32MiscDataCnt = 3;
// Non-YCbCr formats need 1 int for format.
// YCbCr conversion needs 1 int for non-format flags, and a 64-bit format (external or regular).
- static constexpr int kNum32FormatDataCntNoYcbcr = 1;
- static constexpr int kNum32FormatDataCntYcbcr = 3;
+ static constexpr uint16_t kNum32FormatDataCntNoYcbcr = 1;
+ static constexpr uint16_t kNum32FormatDataCntYcbcr = 3;
const VulkanYcbcrConversionInfo& ycbcrInfo = vkInfo.fYcbcrConversionInfo;
- const int num32DataCnt =
+ const uint16_t num32DataCnt =
kNum32DimensionDataCnt + kNum32MiscDataCnt +
(ycbcrInfo.isValid() ? kNum32FormatDataCntYcbcr : kNum32FormatDataCntNoYcbcr);
diff --git a/src/gpu/graphite/vk/VulkanResourceProvider.cpp b/src/gpu/graphite/vk/VulkanResourceProvider.cpp
index bb2f400250c569b73120f857133cafcca51c1c77..f66c6d0d2cf8c8bb4b34c7479445185c3a3c7cf4 100644
--- a/src/gpu/graphite/vk/VulkanResourceProvider.cpp
+++ b/src/gpu/graphite/vk/VulkanResourceProvider.cpp
@@ -218,7 +218,7 @@ GraphiteResourceKey build_desc_set_key(const SkSpan<DescriptorData>& requestedDe
}
GraphiteResourceKey key;
- GraphiteResourceKey::Builder builder(&key, kType, keyData.size());
+ GraphiteResourceKey::Builder builder(&key, kType, SkTo<uint16_t>(keyData.size()));
for (int i = 0; i < keyData.size(); i++) {
builder[i] = keyData[i];
@@ -548,7 +548,7 @@ sk_sp<VulkanYcbcrConversion> VulkanResourceProvider::findOrCreateCompatibleYcbcr
GraphiteResourceKey key;
{
static const ResourceType kType = GraphiteResourceKey::GenerateResourceType();
- static constexpr int kKeySize = 3;
+ static constexpr uint16_t kKeySize = 3;
GraphiteResourceKey::Builder builder(&key, kType, kKeySize);
ImmutableSamplerInfo packedInfo = VulkanYcbcrConversion::ToImmutableSamplerInfo(ycbcrInfo);
diff --git a/src/utils/SkShadowUtils.cpp b/src/utils/SkShadowUtils.cpp
index 68da13ca9b048cd0d1dc9b62090c17793a11b3a1..9c3ef544ffda6ba72c2972f020ac2a06232484b6 100644
--- a/src/utils/SkShadowUtils.cpp
+++ b/src/utils/SkShadowUtils.cpp
@@ -358,7 +358,10 @@ public:
const SkMatrix& viewMatrix() const { return *fViewMatrix; }
#if defined(SK_GANESH)
/** Negative means the vertices should not be cached for this path. */
- int keyBytes() const { return fShapeForKey.unstyledKeySize() * sizeof(uint32_t); }
+ int keyBytes() const {
+ return fShapeForKey.hasUnstyledKey() ? fShapeForKey.unstyledKeySize() * sizeof(uint32_t)
+ : -1;
+ }
void writeKey(void* key) const {
fShapeForKey.writeUnstyledKey(reinterpret_cast<uint32_t*>(key));
}

View File

@@ -0,0 +1,28 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Kalvin Lee <kdlee@chromium.org>
Date: Fri, 27 Mar 2026 16:37:30 +0900
Subject: Terracotta-Phase-1: Reorder member destruction
This is a speculative patch. Please see the bug for details.
Bug: b/496393742
Change-Id: Ib574a0086f92abda83715b36a0d1e7a99e9edd67
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1196676
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Thomas Smith <thomsmit@google.com>
diff --git a/include/gpu/graphite/Context.h b/include/gpu/graphite/Context.h
index d3a8ce563713d5a6866b4efa8dcdfa959c92ff09..9397220c2662798c69008343b36e925ad6e4a768 100644
--- a/include/gpu/graphite/Context.h
+++ b/include/gpu/graphite/Context.h
@@ -403,8 +403,8 @@ private:
sk_sp<SharedContext> fSharedContext;
std::unique_ptr<ResourceProvider> fResourceProvider;
- std::unique_ptr<QueueManager> fQueueManager;
std::unique_ptr<ClientMappedBufferManager> fMappedBufferManager;
+ std::unique_ptr<QueueManager> fQueueManager;
std::unique_ptr<const skcpu::ContextImpl> fCPUContext;
PersistentPipelineStorage* fPersistentPipelineStorage;

View File

@@ -2,3 +2,6 @@ chore_allow_customizing_microtask_policy_per_context.patch
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
cherry-pick-b54c7841e2cd.patch
cherry-pick-ba0258ba9609.patch
cherry-pick-036e5e8f69be.patch
cherry-pick-07398289d921.patch
cherry-pick-a068030f5179.patch

View File

@@ -0,0 +1,65 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Nico Hartmann <nicohartmann@chromium.org>
Date: Wed, 25 Mar 2026 12:11:21 +0100
Subject: [turbofan] Grow ContextAccess' depth field to 31 bits
Fixed: 495273999
Change-Id: I1ce294051aad3f413744386223976cd9c8b24bca
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7698216
Commit-Queue: Nico Hartmann <nicohartmann@chromium.org>
Reviewed-by: Darius Mercadier <dmercadier@chromium.org>
Auto-Submit: Nico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106031}
diff --git a/src/compiler/js-operator.cc b/src/compiler/js-operator.cc
index a598cbc13d4558a6623bf182b07f9c5368bab65d..46a4346720861c9262f8529d1afd02cdd015690c 100644
--- a/src/compiler/js-operator.cc
+++ b/src/compiler/js-operator.cc
@@ -152,16 +152,14 @@ const CallRuntimeParameters& CallRuntimeParametersOf(const Operator* op) {
return OpParameter<CallRuntimeParameters>(op);
}
-
ContextAccess::ContextAccess(size_t depth, size_t index, bool immutable)
- : immutable_(immutable),
- depth_(static_cast<uint16_t>(depth)),
+ : immutable_and_depth_(ImmutableField::encode(immutable) |
+ DepthField::encode(static_cast<uint32_t>(depth))),
index_(static_cast<uint32_t>(index)) {
- DCHECK(depth <= std::numeric_limits<uint16_t>::max());
+ CHECK_EQ(depth, DepthField::decode(immutable_and_depth_));
DCHECK(index <= std::numeric_limits<uint32_t>::max());
}
-
bool operator==(ContextAccess const& lhs, ContextAccess const& rhs) {
return lhs.depth() == rhs.depth() && lhs.index() == rhs.index() &&
lhs.immutable() == rhs.immutable();
diff --git a/src/compiler/js-operator.h b/src/compiler/js-operator.h
index ac3b4c0534ce6a328cf212f8d603e521ca97eaef..a2c4fa47aecab627199d55087da07f041666857e 100644
--- a/src/compiler/js-operator.h
+++ b/src/compiler/js-operator.h
@@ -354,15 +354,19 @@ class ContextAccess final {
public:
ContextAccess(size_t depth, size_t index, bool immutable);
- size_t depth() const { return depth_; }
+ size_t depth() const { return DepthField::decode(immutable_and_depth_); }
size_t index() const { return index_; }
- bool immutable() const { return immutable_; }
+ bool immutable() const {
+ return ImmutableField::decode(immutable_and_depth_);
+ }
private:
+ using ImmutableField = base::BitField<bool, 0, 1>;
+ using DepthField = ImmutableField::Next<uint32_t, 31>;
+
// For space reasons, we keep this tightly packed, otherwise we could just use
// a simple int/int/bool POD.
- const bool immutable_;
- const uint16_t depth_;
+ uint32_t immutable_and_depth_;
const uint32_t index_;
};

View File

@@ -0,0 +1,172 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Paolo Severini <paolosev@microsoft.com>
Date: Tue, 31 Mar 2026 13:05:12 +0200
Subject: [compiler] Fix FrameStateFunctionInfo comparison for JS-to-Wasm
frames
The equality operator for FrameStateFunctionInfo did not compare the
wasm signature field in JSToWasmFrameStateFunctionInfo. This could
cause CSE to incorrectly merge FrameState nodes with different wasm
signatures, leading to the deoptimizer using the wrong return type
when materializing a JS-to-Wasm builtin continuation frame.
Bug: 497404188
Change-Id: I671cda5784089dd9875d90c5f48e8580cb5fa697
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7709449
Reviewed-by: Matthias Liedtke <mliedtke@chromium.org>
Reviewed-by: Daniel Lehmann <dlehmann@chromium.org>
Commit-Queue: Matthias Liedtke <mliedtke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106175}
diff --git a/src/compiler/frame-states.cc b/src/compiler/frame-states.cc
index 7c15107243dd233cf3a4f4fa0b4d61557201ce7e..5312f07b5feb7c356e907d43d3bbc6bd35525e90 100644
--- a/src/compiler/frame-states.cc
+++ b/src/compiler/frame-states.cc
@@ -37,11 +37,12 @@ std::ostream& operator<<(std::ostream& os, OutputFrameStateCombine const& sc) {
bool operator==(FrameStateFunctionInfo const& lhs,
FrameStateFunctionInfo const& rhs) {
#if V8_HOST_ARCH_X64
-// If this static_assert fails, then you've probably added a new field to
-// FrameStateFunctionInfo. Make sure to take it into account in this equality
-// function, and update the static_assert.
+// If these static_asserts fail, then you've probably added a new field to
+// FrameStateFunctionInfo or JSToWasmFrameStateFunctionInfo. Make sure to
+// take it into account in this function, and update the static_assert.
#if V8_ENABLE_WEBASSEMBLY
static_assert(sizeof(FrameStateFunctionInfo) == 40);
+ static_assert(sizeof(JSToWasmFrameStateFunctionInfo) == 48);
#else
static_assert(sizeof(FrameStateFunctionInfo) == 32);
#endif
@@ -52,6 +53,18 @@ bool operator==(FrameStateFunctionInfo const& lhs,
lhs.wasm_function_index() != rhs.wasm_function_index()) {
return false;
}
+
+ // JSToWasmFrameStateFunctionInfo has an additional signature_ field.
+ // Two frame states with different wasm signatures must not compare equal,
+ // otherwise CSE/GVN can merge them and the deoptimizer will use the wrong
+ // signature to materialize the continuation frame.
+ if (lhs.type() == FrameStateType::kJSToWasmBuiltinContinuation &&
+ rhs.type() == FrameStateType::kJSToWasmBuiltinContinuation) {
+ if (static_cast<const JSToWasmFrameStateFunctionInfo&>(lhs).signature() !=
+ static_cast<const JSToWasmFrameStateFunctionInfo&>(rhs).signature()) {
+ return false;
+ }
+ }
#endif
return lhs.type() == rhs.type() &&
diff --git a/src/compiler/frame-states.h b/src/compiler/frame-states.h
index 1faaa599bd7d83ad372eec823ebc553a4ec0745a..e17798a1a8e3a127aa6aa7bb2466d2efda02e75a 100644
--- a/src/compiler/frame-states.h
+++ b/src/compiler/frame-states.h
@@ -108,6 +108,10 @@ class FrameStateFunctionInfo {
bytecode_array_(bytecode_array) {
}
+ // Prevent slicing when copying through base-class references.
+ FrameStateFunctionInfo(const FrameStateFunctionInfo&) = delete;
+ FrameStateFunctionInfo& operator=(const FrameStateFunctionInfo&) = delete;
+
int local_count() const { return local_count_; }
uint16_t parameter_count() const { return parameter_count_; }
uint16_t parameter_count_without_receiver() const {
diff --git a/test/mjsunit/regress/wasm/regress-497404188.js b/test/mjsunit/regress/wasm/regress-497404188.js
new file mode 100644
index 0000000000000000000000000000000000000000..54992bd37cdeeb81d9d542462c075e8601dcec83
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-497404188.js
@@ -0,0 +1,92 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax --turbofan --no-maglev
+
+// Regression test for a type confusion in the deoptimizer caused by missing
+// signature comparison in FrameStateFunctionInfo::operator==.
+// Two wasm functions with different return types (externref vs i64) but
+// identical parameter signatures can have their JSToWasmBuiltinContinuation
+// frame states merged by CSE. On lazy deopt, the deoptimizer then uses the
+// wrong signature to materialize the result, reading a tagged reference as
+// an untagged i64 or vice versa.
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+function makeInstance(callback) {
+ const builder = new WasmModuleBuilder();
+ const callback_index = builder.addImport('env', 'callback', kSig_v_v);
+
+ // Mutable Wasm globals to hold the return values for the two functions. The
+ // callback can modify these to trigger deopt at the right moment and test
+ // that the correct type is materialized after deopt.
+ const g_ref =
+ builder.addGlobal(kWasmExternRef, true, false).exportAs('g_ref');
+ const g_i64 = builder.addGlobal(kWasmI64, true, false).exportAs('g_i64');
+
+ // Returns an externref global after calling the callback (which may trigger
+ // deopt).
+ builder.addFunction('return_ref', kSig_r_v)
+ .addBody([
+ kExprCallFunction, callback_index,
+ kExprGlobalGet, g_ref.index,
+ ])
+ .exportFunc();
+
+ // Returns an i64 global after calling the callback (which may trigger deopt).
+ builder.addFunction('return_i64', kSig_l_v)
+ .addBody([
+ kExprCallFunction, callback_index,
+ kExprGlobalGet, g_i64.index,
+ ])
+ .exportFunc();
+
+ return builder.instantiate({env: {callback}}).exports;
+}
+
+(function testNoTypeConfusionOnLazyDeopt() {
+ let arm_deopt = false;
+
+ function ProtoForI64() {}
+ function ProtoForRef() {}
+
+ const exports_ = makeInstance(() => {
+ // Trigger deopt by changing the prototype after optimization.
+ if (arm_deopt) {
+ ProtoForRef.prototype.deopt_marker = 1;
+ }
+ });
+
+ // Install wasm getters with different return types on different prototypes.
+ Object.defineProperty(
+ ProtoForI64.prototype, 'x', {get: exports_.return_i64});
+ Object.defineProperty(
+ ProtoForRef.prototype, 'x', {get: exports_.return_ref});
+
+ function foo(o) {
+ return o.x;
+ }
+
+ const obj_i64 = new ProtoForI64();
+ const obj_ref = new ProtoForRef();
+
+ const sentinel = {tag: 'sentinel'};
+ exports_.g_ref.value = sentinel;
+ exports_.g_i64.value = 42n;
+
+ // Train the function with both receiver types.
+ %PrepareFunctionForOptimization(foo);
+ for (let i = 0; i < 20; ++i) {
+ foo(obj_i64);
+ foo(obj_ref);
+ }
+
+ // Optimize and run once without deopt.
+ %OptimizeFunctionOnNextCall(foo);
+ assertEquals(42n, foo(obj_i64));
+
+ arm_deopt = true;
+ const result = foo(obj_ref);
+ assertEquals(sentinel, result);
+})();

View File

@@ -0,0 +1,194 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Thu, 26 Mar 2026 10:39:16 +0100
Subject: [maglev] Use transitive IsEscaping check for inlined allocations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Check if a virtual object is escaping transitively.
Rename non-transitive function to HasEscapeUses instead.
Fixed: 495751197
Change-Id: I1df519079515b522974708f5419a81b5cbee7ade
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7702700
Reviewed-by: Marja Hölttä <marja@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106064}
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8ec21a522c 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4646,19 +4646,20 @@ void MaglevGraphBuilder::BuildInitializeStore_TrustedPointer(
CHECK(!result.IsDoneWithAbort());
}
-namespace {
-bool IsEscaping(Graph* graph, InlinedAllocation* alloc) {
- if (alloc->IsEscaping()) return true;
- auto it = graph->allocations_elide_map().find(alloc);
- if (it == graph->allocations_elide_map().end()) return false;
+bool MaglevGraphBuilder::IsEscaping(InlinedAllocation* alloc) {
+ if (alloc->HasEscapingUses()) return true;
+ auto it = graph_->allocations_elide_map().find(alloc);
+ if (it == graph_->allocations_elide_map().end()) return false;
for (InlinedAllocation* inner_alloc : it->second) {
- if (IsEscaping(graph, inner_alloc)) {
+ if (IsEscaping(inner_alloc)) {
return true;
}
}
return false;
}
+namespace {
+
bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
for (VirtualObject* vo : vos) {
if (vo->allocation() == alloc) continue;
@@ -4668,7 +4669,7 @@ bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
if (!nested_value->Is<InlinedAllocation>()) return true;
ValueNode* nested_alloc = nested_value->Cast<InlinedAllocation>();
if (nested_alloc == alloc) {
- if (vo->allocation()->IsEscaping() ||
+ if (vo->allocation()->HasEscapingUses() ||
!VerifyIsNotEscaping(vos, vo->allocation())) {
escaped = true;
}
@@ -4687,6 +4688,7 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
if (!v8_flags.maglev_object_tracking) return false;
if (!receiver->Is<InlinedAllocation>()) return false;
InlinedAllocation* alloc = receiver->Cast<InlinedAllocation>();
+ if (IsEscaping(alloc)) return false;
if (mode == TrackObjectMode::kStore) {
// If we have two objects A and B, such that A points to B (it contains B in
// one of its field), we cannot change B without also changing A, even if
@@ -4695,7 +4697,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
graph_->allocations_elide_map().end()) {
return false;
}
- if (alloc->IsEscaping()) return false;
// Ensure object is escaped if we are within a try-catch block. This is
// crucial because a deoptimization point inside the catch handler could
// re-materialize objects differently, depending on whether the throw
@@ -4704,9 +4705,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
// the try-block started, but for now, err on the side of caution and
// always escape.
if (IsInsideTryBlock()) return false;
- } else {
- DCHECK_EQ(mode, TrackObjectMode::kLoad);
- if (IsEscaping(graph_, alloc)) return false;
}
// We don't support loop phis inside VirtualObjects, so any access inside a
// loop should escape the object, except for objects that were created since
@@ -9195,7 +9193,7 @@ MaybeReduceResult MaglevGraphBuilder::TryReduceArrayIteratorPrototypeNext(
VirtualObject* array = iterated_object->Cast<InlinedAllocation>()->object();
// TODO(victorgomes): Remove this once we track changes in the inlined
// allocated object.
- if (iterated_object->Cast<InlinedAllocation>()->IsEscaping()) {
+ if (IsEscaping(iterated_object->Cast<InlinedAllocation>())) {
FAIL("allocation is escaping, map could have been changed");
}
// TODO(victorgomes): This effectively disable the optimization for `for-of`
@@ -12315,7 +12313,7 @@ MaglevGraphBuilder::TryGetNonEscapingArgumentsObject(ValueNode* value) {
}
// TODO(victorgomes): We can probably loosen the IsNotEscaping requirement if
// we keep track of the arguments object changes so far.
- if (alloc->IsEscaping()) return {};
+ if (IsEscaping(alloc)) return {};
VirtualObject* object = alloc->object();
if (!object->has_static_map()) return {};
// TODO(victorgomes): Support simple JSArray forwarding.
diff --git a/src/maglev/maglev-graph-builder.h b/src/maglev/maglev-graph-builder.h
index 6b162e22b09b008a470612b5689323749891e5fd..1bf3400df6da6d15c959d74a722c6ecfa08b3ac1 100644
--- a/src/maglev/maglev-graph-builder.h
+++ b/src/maglev/maglev-graph-builder.h
@@ -1447,6 +1447,7 @@ class MaglevGraphBuilder {
ValueNode* object, ValueNode* callable,
compiler::FeedbackSource feedback_source);
+ bool IsEscaping(InlinedAllocation* allocation);
VirtualObject* GetObjectFromAllocation(InlinedAllocation* allocation);
VirtualObject* GetModifiableObjectFromAllocation(
InlinedAllocation* allocation);
diff --git a/src/maglev/maglev-ir.h b/src/maglev/maglev-ir.h
index d6282a00ebb327be9ed431e764fbe6a92abf72d5..d224c28bf639e952c8b7eaff1f28ce650cf1f33e 100644
--- a/src/maglev/maglev-ir.h
+++ b/src/maglev/maglev-ir.h
@@ -6119,7 +6119,7 @@ class InlinedAllocation : public FixedInputValueNodeT<1, InlinedAllocation> {
DCHECK(!HasBeenAnalysed());
non_escaping_use_count_ += n;
}
- bool IsEscaping() const {
+ bool HasEscapingUses() const {
DCHECK(!HasBeenAnalysed());
return use_count_ > non_escaping_use_count_;
}
diff --git a/src/maglev/maglev-post-hoc-optimizations-processors.h b/src/maglev/maglev-post-hoc-optimizations-processors.h
index a7740283358d90383f366d29fea9360f1fd8661b..715cef1c67f9fdca830530fd438b3b6334922413 100644
--- a/src/maglev/maglev-post-hoc-optimizations-processors.h
+++ b/src/maglev/maglev-post-hoc-optimizations-processors.h
@@ -382,7 +382,7 @@ class AnyUseMarkingProcessor {
auto* alloc = it.first;
if (alloc->HasBeenAnalysed()) continue;
// Check if all its uses are non escaping.
- if (alloc->IsEscaping()) {
+ if (alloc->HasEscapingUses()) {
// Escape this allocation and all its dependencies.
EscapeAllocation(graph, alloc, it.second);
} else {
diff --git a/test/mjsunit/maglev/regress-495751197.js b/test/mjsunit/maglev/regress-495751197.js
new file mode 100644
index 0000000000000000000000000000000000000000..b6e5baefea2a5d626bff5716f23a2ceadab01bad
--- /dev/null
+++ b/test/mjsunit/maglev/regress-495751197.js
@@ -0,0 +1,47 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --allow-natives-syntax
+
+let do_transition = false;
+let marker = {x: 1.337};
+
+Object.defineProperty(Object.prototype, '_leak', {
+ get() {
+ if (do_transition) {
+ this[1][0] = marker;
+ }
+ return this;
+ },
+ configurable: true
+});
+
+function target(iter) {
+ return iter.next().value;
+}
+
+function inner() {
+ var leaked;
+ // Forces WithContext creation. _leak triggers the runtime call.
+ with (arguments) { leaked = _leak; }
+ return target.apply(null, arguments);
+}
+
+function outer() {
+ let a = [1.1, 2.2, 3.3];
+ let iter = a.values();
+ return inner(iter, a);
+}
+
+%PrepareFunctionForOptimization(target);
+%PrepareFunctionForOptimization(inner);
+%PrepareFunctionForOptimization(outer);
+
+outer(); outer(); outer();
+
+%OptimizeMaglevOnNextCall(outer);
+
+do_transition = true;
+let r = outer();
+assertFalse(typeof r === "number");