From d86cbe9284c3176a05679345d9766e6869a7fc8e Mon Sep 17 00:00:00 2001 From: Samuel Attard Date: Sun, 19 Apr 2026 15:47:43 -0400 Subject: [PATCH] chore: cherry-pick 27 changes from chromium, v8, angle, skia, pdfium, libaom (#51137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- patches/angle/.patches | 1 + patches/angle/cherry-pick-b149a5c62d76.patch | 117 ++++ patches/chromium/.patches | 15 + .../chromium/cherry-pick-1b69067db7d2.patch | 103 +++ .../chromium/cherry-pick-23865499a86a.patch | 224 ++++++ .../chromium/cherry-pick-4002a66778d2.patch | 38 + .../chromium/cherry-pick-4073d491fb55.patch | 213 ++++++ .../chromium/cherry-pick-41bfbc009df8.patch | 60 ++ .../chromium/cherry-pick-847b11ad2fa3.patch | 203 ++++++ .../chromium/cherry-pick-8b08fb7c9dce.patch | 67 ++ .../chromium/cherry-pick-8c1ead5a699f.patch | 179 +++++ .../chromium/cherry-pick-a6357144e7bf.patch | 211 ++++++ .../chromium/cherry-pick-bb8d4c29dfdb.patch | 94 +++ .../chromium/cherry-pick-be87466afecb.patch | 224 ++++++ .../chromium/cherry-pick-c215f8e6f049.patch | 114 +++ .../chromium/cherry-pick-c81f01b469c4.patch | 47 ++ .../chromium/cherry-pick-d513cd2fe668.patch | 59 ++ .../chromium/cherry-pick-eeb3e031eb89.patch | 373 ++++++++++ patches/config.json | 6 +- patches/libaom/.patches | 4 + patches/libaom/cherry-pick-395efd18d8ef.patch | 27 + patches/libaom/cherry-pick-4369bd1258dc.patch | 24 + patches/libaom/cherry-pick-a047955845e5.patch | 187 +++++ patches/libaom/cherry-pick-c61e9586156f.patch | 37 + patches/pdfium/.patches | 2 + patches/pdfium/cherry-pick-bce2e6728279.patch | 36 + patches/pdfium/cherry-pick-ca8a943c247c.patch | 70 ++ patches/skia/.patches | 2 + patches/skia/cherry-pick-0566b2f5f0d1.patch | 651 ++++++++++++++++++ patches/skia/cherry-pick-3f9969421ad5.patch | 28 + patches/v8/.patches | 3 + patches/v8/cherry-pick-036e5e8f69be.patch | 65 ++ patches/v8/cherry-pick-07398289d921.patch | 172 +++++ patches/v8/cherry-pick-a068030f5179.patch | 194 ++++++ 34 files changed, 3849 insertions(+), 1 deletion(-) create mode 100644 patches/angle/.patches create mode 100644 patches/angle/cherry-pick-b149a5c62d76.patch create mode 100644 patches/chromium/cherry-pick-1b69067db7d2.patch create mode 100644 patches/chromium/cherry-pick-23865499a86a.patch create mode 100644 patches/chromium/cherry-pick-4002a66778d2.patch create mode 100644 patches/chromium/cherry-pick-4073d491fb55.patch create mode 100644 patches/chromium/cherry-pick-41bfbc009df8.patch create mode 100644 patches/chromium/cherry-pick-847b11ad2fa3.patch create mode 100644 patches/chromium/cherry-pick-8b08fb7c9dce.patch create mode 100644 patches/chromium/cherry-pick-8c1ead5a699f.patch create mode 100644 patches/chromium/cherry-pick-a6357144e7bf.patch create mode 100644 patches/chromium/cherry-pick-bb8d4c29dfdb.patch create mode 100644 patches/chromium/cherry-pick-be87466afecb.patch create mode 100644 patches/chromium/cherry-pick-c215f8e6f049.patch create mode 100644 patches/chromium/cherry-pick-c81f01b469c4.patch create mode 100644 patches/chromium/cherry-pick-d513cd2fe668.patch create mode 100644 patches/chromium/cherry-pick-eeb3e031eb89.patch create mode 100644 patches/libaom/.patches create mode 100644 patches/libaom/cherry-pick-395efd18d8ef.patch create mode 100644 patches/libaom/cherry-pick-4369bd1258dc.patch create mode 100644 patches/libaom/cherry-pick-a047955845e5.patch create mode 100644 patches/libaom/cherry-pick-c61e9586156f.patch create mode 100644 patches/pdfium/.patches create mode 100644 patches/pdfium/cherry-pick-bce2e6728279.patch create mode 100644 patches/pdfium/cherry-pick-ca8a943c247c.patch create mode 100644 patches/skia/.patches create mode 100644 patches/skia/cherry-pick-0566b2f5f0d1.patch create mode 100644 patches/skia/cherry-pick-3f9969421ad5.patch create mode 100644 patches/v8/cherry-pick-036e5e8f69be.patch create mode 100644 patches/v8/cherry-pick-07398289d921.patch create mode 100644 patches/v8/cherry-pick-a068030f5179.patch diff --git a/patches/angle/.patches b/patches/angle/.patches new file mode 100644 index 0000000000..5585e3b772 --- /dev/null +++ b/patches/angle/.patches @@ -0,0 +1 @@ +cherry-pick-b149a5c62d76.patch diff --git a/patches/angle/cherry-pick-b149a5c62d76.patch b/patches/angle/cherry-pick-b149a5c62d76.patch new file mode 100644 index 0000000000..28518e01ed --- /dev/null +++ b/patches/angle/cherry-pick-b149a5c62d76.patch @@ -0,0 +1,117 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Geoff Lang +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 +Commit-Queue: Geoff Lang + +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; + } + +- 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 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) + { diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 811c4a6a74..e51b184cbe 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -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 diff --git a/patches/chromium/cherry-pick-1b69067db7d2.patch b/patches/chromium/cherry-pick-1b69067db7d2.patch new file mode 100644 index 0000000000..08d38b0a10 --- /dev/null +++ b/patches/chromium/cherry-pick-1b69067db7d2.patch @@ -0,0 +1,103 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Vasilii Sukhanov +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 +Reviewed-by: Anna Tsvirchkova +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(); + password_manager_driver_->PreviewSuggestionById( + form->username_element_renderer_id, + form->password_element_renderer_id, + GetUsernameFromLabel(suggestion.labels[0][0].value), +- suggestion.GetPayload() +- .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()); diff --git a/patches/chromium/cherry-pick-23865499a86a.patch b/patches/chromium/cherry-pick-23865499a86a.patch new file mode 100644 index 0000000000..c496ca0d59 --- /dev/null +++ b/patches/chromium/cherry-pick-23865499a86a.patch @@ -0,0 +1,224 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Fergal Daly +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 +> Reviewed-by: Ming-Ying Chung +> Commit-Queue: Ming-Ying Chung +> 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 +Auto-Submit: Chrome Cherry Picker +Bot-Commit: Rubber Stamper +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 file_system_context) ++ : FileSystemAccessChangeSource(std::move(scope), ++ std::move(file_system_context)) {} ++ ~FakeChangeSource() override = default; ++ ++ // FileSystemAccessChangeSource: ++ void Initialize( ++ base::OnceCallback ++ 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 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 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 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", diff --git a/patches/chromium/cherry-pick-4002a66778d2.patch b/patches/chromium/cherry-pick-4002a66778d2.patch new file mode 100644 index 0000000000..d7d0d4ebe4 --- /dev/null +++ b/patches/chromium/cherry-pick-4002a66778d2.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Vasiliy Telezhnikov +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 +Reviewed-by: Joe Mason +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::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; + } + diff --git a/patches/chromium/cherry-pick-4073d491fb55.patch b/patches/chromium/cherry-pick-4073d491fb55.patch new file mode 100644 index 0000000000..4ef346e9d4 --- /dev/null +++ b/patches/chromium/cherry-pick-4073d491fb55.patch @@ -0,0 +1,213 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Matt Menke +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 +Commit-Queue: mmenke +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 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 diff --git a/patches/chromium/cherry-pick-41bfbc009df8.patch b/patches/chromium/cherry-pick-41bfbc009df8.patch new file mode 100644 index 0000000000..deef180b77 --- /dev/null +++ b/patches/chromium/cherry-pick-41bfbc009df8.patch @@ -0,0 +1,60 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tommy Steimel +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 +Commit-Queue: Tommy Steimel +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); + } diff --git a/patches/chromium/cherry-pick-847b11ad2fa3.patch b/patches/chromium/cherry-pick-847b11ad2fa3.patch new file mode 100644 index 0000000000..139eee6fa6 --- /dev/null +++ b/patches/chromium/cherry-pick-847b11ad2fa3.patch @@ -0,0 +1,203 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Joey Arhar +Date: Fri, 10 Apr 2026 12:19:11 -0700 +Subject: Fix crashes when restoring with + +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 +Commit-Queue: Joey Arhar +Reviewed-by: Dominic Farolino +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 with a 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 @@ ++ ++ ++
++ ++
++ ++ +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 @@ +- +- +- +- +- +- +- +- +- +- +- +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 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ diff --git a/patches/chromium/cherry-pick-8b08fb7c9dce.patch b/patches/chromium/cherry-pick-8b08fb7c9dce.patch new file mode 100644 index 0000000000..4eeaf5f0a7 --- /dev/null +++ b/patches/chromium/cherry-pick-8b08fb7c9dce.patch @@ -0,0 +1,67 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: p0-tato +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 +> Reviewed-by: Brian Sheedy +> Reviewed-by: Brandon Jones +> 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 +Bot-Commit: rubber-stamper@appspot.gserviceaccount.com +Commit-Queue: 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 + Jihun Brent Kim + Jihwan Marc Kim + Jihye Hyun ++Jihyeon Jeong + Jihyeon Lee + Jim Wu + Jin Yang +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 capability_configs; +- std::vector +- 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 ++ capability_config_ptrs; ++ for (auto& config : capability_configs) { ++ capability_config_ptrs.push_back(config.GetAsBaseHeader()); + } + + XrSpatialContextCreateInfoEXT create_info = { diff --git a/patches/chromium/cherry-pick-8c1ead5a699f.patch b/patches/chromium/cherry-pick-8c1ead5a699f.patch new file mode 100644 index 0000000000..a7947398ee --- /dev/null +++ b/patches/chromium/cherry-pick-8c1ead5a699f.patch @@ -0,0 +1,179 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Hiroki Nakagawa +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 +Commit-Queue: Hiroki Nakagawa +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, + std::unique_ptr 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, ++ std::unique_ptr handle); + + private: + // WebContentsObserver implementation: +@@ -310,8 +313,6 @@ class CONTENT_EXPORT PrerenderHostRegistry + void ScheduleToDeleteAbandonedHost( + std::unique_ptr prerender_host, + const PrerenderCancellationReason& cancellation_reason); +- void SchedulePendingDeletionPrerenderNewTabHandle( +- std::unique_ptr 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 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(), 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 +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 handle, ++ const PrerenderCancellationReason& reason); + + // Passes the ownership of `web_contents_` to the caller if it's available for + // new tab navigation with given params. diff --git a/patches/chromium/cherry-pick-a6357144e7bf.patch b/patches/chromium/cherry-pick-a6357144e7bf.patch new file mode 100644 index 0000000000..21a0a6e16b --- /dev/null +++ b/patches/chromium/cherry-pick-a6357144e7bf.patch @@ -0,0 +1,211 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lukasz Anforowicz +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 +Commit-Queue: Łukasz Anforowicz +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 ++ + #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> full_data_vec = ++ test::ReadFromFile(file_path.ToString()); ++ ASSERT_TRUE(full_data_vec); ++ base::span 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 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 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 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 already_started_frame_; + }; + diff --git a/patches/chromium/cherry-pick-bb8d4c29dfdb.patch b/patches/chromium/cherry-pick-bb8d4c29dfdb.patch new file mode 100644 index 0000000000..633cbf1a33 --- /dev/null +++ b/patches/chromium/cherry-pick-bb8d4c29dfdb.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Sunny Sachanandani +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 +> Auto-Submit: Sunny Sachanandani +> Commit-Queue: Sunny Sachanandani +> 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 +Auto-Submit: Sunny Sachanandani +Bot-Commit: Rubber Stamper +Commit-Queue: Sunny Sachanandani +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. diff --git a/patches/chromium/cherry-pick-be87466afecb.patch b/patches/chromium/cherry-pick-be87466afecb.patch new file mode 100644 index 0000000000..9f0c2ea532 --- /dev/null +++ b/patches/chromium/cherry-pick-be87466afecb.patch @@ -0,0 +1,224 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jonathan Ross +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 +Commit-Queue: Jonathan Ross +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 +- 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 ++ ++#include ++#include ++#include ++ ++#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 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 tokens; ++ base::Lock tokens_lock; ++ ++ std::atomic start_flag{false}; ++ std::atomic 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 diff --git a/patches/chromium/cherry-pick-c215f8e6f049.patch b/patches/chromium/cherry-pick-c215f8e6f049.patch new file mode 100644 index 0000000000..1fe1d686ea --- /dev/null +++ b/patches/chromium/cherry-pick-c215f8e6f049.patch @@ -0,0 +1,114 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Anders Hartvoll Ruud +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 +Reviewed-by: Morten Stenshorne +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 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>& + 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>& Unordered() const; + const HeapVector& 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 @@ ++ ++Crash Test: Layout subtree root becoming display:none ++ ++ ++
++
++
++
++
++
++
++
++
++
++
++
++
++
++ diff --git a/patches/chromium/cherry-pick-c81f01b469c4.patch b/patches/chromium/cherry-pick-c81f01b469c4.patch new file mode 100644 index 0000000000..f7e50f50c0 --- /dev/null +++ b/patches/chromium/cherry-pick-c81f01b469c4.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Benjamin Beaudry +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 +Commit-Queue: Kevin Babbitt +Auto-Submit: Benjamin Beaudry +Commit-Queue: Benjamin Beaudry +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(column_header_ids.size()); ++ *n_column_header_cells = static_cast(index); + return S_OK; + } + +@@ -4312,7 +4312,7 @@ IFACEMETHODIMP AXPlatformNodeWin::get_rowHeaderCells( + } + } + +- *n_row_header_cells = static_cast(row_header_ids.size()); ++ *n_row_header_cells = static_cast(index); + return S_OK; + } + diff --git a/patches/chromium/cherry-pick-d513cd2fe668.patch b/patches/chromium/cherry-pick-d513cd2fe668.patch new file mode 100644 index 0000000000..6670446527 --- /dev/null +++ b/patches/chromium/cherry-pick-d513cd2fe668.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kenichi Ishibashi +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 +Commit-Queue: Kenichi Ishibashi +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); diff --git a/patches/chromium/cherry-pick-eeb3e031eb89.patch b/patches/chromium/cherry-pick-eeb3e031eb89.patch new file mode 100644 index 0000000000..b4a484b9b2 --- /dev/null +++ b/patches/chromium/cherry-pick-eeb3e031eb89.patch @@ -0,0 +1,373 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Eugene Zemtsov +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 +> Commit-Queue: Eugene Zemtsov +> Reviewed-by: Dale Curtis +> 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 +Commit-Queue: Eugene Zemtsov +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 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 FrameBufferPool::AllocateAlphaPlaneForFrameBuffer( +- size_t min_size, +- void* fb_priv) { +- base::AutoLock lock(lock_); +- DCHECK(fb_priv); +- +- auto* frame_buffer = static_cast(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(&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 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( ++ 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_; + std::unique_ptr 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 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 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::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 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 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 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 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) + diff --git a/patches/config.json b/patches/config.json index 8d297c0fd3..a0cd7b57a5 100644 --- a/patches/config.json +++ b/patches/config.json @@ -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" } ] diff --git a/patches/libaom/.patches b/patches/libaom/.patches new file mode 100644 index 0000000000..05c4be2b46 --- /dev/null +++ b/patches/libaom/.patches @@ -0,0 +1,4 @@ +cherry-pick-4369bd1258dc.patch +cherry-pick-a047955845e5.patch +cherry-pick-c61e9586156f.patch +cherry-pick-395efd18d8ef.patch diff --git a/patches/libaom/cherry-pick-395efd18d8ef.patch b/patches/libaom/cherry-pick-395efd18d8ef.patch new file mode 100644 index 0000000000..9efc14a707 --- /dev/null +++ b/patches/libaom/cherry-pick-395efd18d8ef.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: James Zern +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]); diff --git a/patches/libaom/cherry-pick-4369bd1258dc.patch b/patches/libaom/cherry-pick-4369bd1258dc.patch new file mode 100644 index 0000000000..99007b65af --- /dev/null +++ b/patches/libaom/cherry-pick-4369bd1258dc.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: James Zern +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]); diff --git a/patches/libaom/cherry-pick-a047955845e5.patch b/patches/libaom/cherry-pick-a047955845e5.patch new file mode 100644 index 0000000000..af2b7204f2 --- /dev/null +++ b/patches/libaom/cherry-pick-a047955845e5.patch @@ -0,0 +1,187 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Marco Paniconi +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. diff --git a/patches/libaom/cherry-pick-c61e9586156f.patch b/patches/libaom/cherry-pick-c61e9586156f.patch new file mode 100644 index 0000000000..a723e4efd3 --- /dev/null +++ b/patches/libaom/cherry-pick-c61e9586156f.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Wan-Teh Chang +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); diff --git a/patches/pdfium/.patches b/patches/pdfium/.patches new file mode 100644 index 0000000000..615a9572d0 --- /dev/null +++ b/patches/pdfium/.patches @@ -0,0 +1,2 @@ +cherry-pick-ca8a943c247c.patch +cherry-pick-bce2e6728279.patch diff --git a/patches/pdfium/cherry-pick-bce2e6728279.patch b/patches/pdfium/cherry-pick-bce2e6728279.patch new file mode 100644 index 0000000000..e4a6ba33b7 --- /dev/null +++ b/patches/pdfium/cherry-pick-bce2e6728279.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Tom Sepez +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 +Commit-Queue: Tom Sepez + +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 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(); diff --git a/patches/pdfium/cherry-pick-ca8a943c247c.patch b/patches/pdfium/cherry-pick-ca8a943c247c.patch new file mode 100644 index 0000000000..52a0abbb78 --- /dev/null +++ b/patches/pdfium/cherry-pick-ca8a943c247c.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lei Zhang +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 +Commit-Queue: Lei Zhang + +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 ++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); diff --git a/patches/skia/.patches b/patches/skia/.patches new file mode 100644 index 0000000000..a9757f231e --- /dev/null +++ b/patches/skia/.patches @@ -0,0 +1,2 @@ +cherry-pick-0566b2f5f0d1.patch +cherry-pick-3f9969421ad5.patch diff --git a/patches/skia/cherry-pick-0566b2f5f0d1.patch b/patches/skia/cherry-pick-0566b2f5f0d1.patch new file mode 100644 index 0000000000..75716a1e65 --- /dev/null +++ b/patches/skia/cherry-pick-0566b2f5f0d1.patch @@ -0,0 +1,651 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Michael Ludwig +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 +Commit-Queue: Michael Ludwig + +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 + #include ++#include + #include + #include + +@@ -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::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 + #include ++#include + #include + + +@@ -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(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(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(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(keyBounds->left())); +- SkASSERT(SkTFitsIn(keyBounds->top())); +- SkASSERT(SkTFitsIn(keyBounds->right())); +- SkASSERT(SkTFitsIn(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(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(keyBounds->left())); ++ SkASSERT(SkTFitsIn(keyBounds->top())); ++ SkASSERT(SkTFitsIn(keyBounds->right())); ++ SkASSERT(SkTFitsIn(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 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& samplerData = samplerDesc.asSpan(); +- GraphiteResourceKey::Builder builder(&key, kType, samplerData.size()); ++ GraphiteResourceKey::Builder builder(&key, kType, SkTo(samplerData.size())); + + for (size_t i = 0; i < samplerData.size(); i++) { + builder[i] = samplerData[i]; +@@ -231,8 +231,8 @@ sk_sp 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(type) < (1u << 4)); + SkASSERT(static_cast(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(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::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(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(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& requestedDe + } + + GraphiteResourceKey key; +- GraphiteResourceKey::Builder builder(&key, kType, keyData.size()); ++ GraphiteResourceKey::Builder builder(&key, kType, SkTo(keyData.size())); + + for (int i = 0; i < keyData.size(); i++) { + builder[i] = keyData[i]; +@@ -548,7 +548,7 @@ sk_sp 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(key)); + } diff --git a/patches/skia/cherry-pick-3f9969421ad5.patch b/patches/skia/cherry-pick-3f9969421ad5.patch new file mode 100644 index 0000000000..c74b5759b0 --- /dev/null +++ b/patches/skia/cherry-pick-3f9969421ad5.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Kalvin Lee +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 +Commit-Queue: Michael Ludwig +Reviewed-by: Thomas Smith + +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 fSharedContext; + std::unique_ptr fResourceProvider; +- std::unique_ptr fQueueManager; + std::unique_ptr fMappedBufferManager; ++ std::unique_ptr fQueueManager; + std::unique_ptr fCPUContext; + + PersistentPipelineStorage* fPersistentPipelineStorage; diff --git a/patches/v8/.patches b/patches/v8/.patches index 9ffd5357d5..e0abe1bd7f 100644 --- a/patches/v8/.patches +++ b/patches/v8/.patches @@ -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 diff --git a/patches/v8/cherry-pick-036e5e8f69be.patch b/patches/v8/cherry-pick-036e5e8f69be.patch new file mode 100644 index 0000000000..295c5f8ca6 --- /dev/null +++ b/patches/v8/cherry-pick-036e5e8f69be.patch @@ -0,0 +1,65 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Nico Hartmann +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 +Reviewed-by: Darius Mercadier +Auto-Submit: Nico Hartmann +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(op); + } + +- + ContextAccess::ContextAccess(size_t depth, size_t index, bool immutable) +- : immutable_(immutable), +- depth_(static_cast(depth)), ++ : immutable_and_depth_(ImmutableField::encode(immutable) | ++ DepthField::encode(static_cast(depth))), + index_(static_cast(index)) { +- DCHECK(depth <= std::numeric_limits::max()); ++ CHECK_EQ(depth, DepthField::decode(immutable_and_depth_)); + DCHECK(index <= std::numeric_limits::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; ++ using DepthField = ImmutableField::Next; ++ + // 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_; + }; + diff --git a/patches/v8/cherry-pick-07398289d921.patch b/patches/v8/cherry-pick-07398289d921.patch new file mode 100644 index 0000000000..becab5d326 --- /dev/null +++ b/patches/v8/cherry-pick-07398289d921.patch @@ -0,0 +1,172 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Paolo Severini +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 +Reviewed-by: Daniel Lehmann +Commit-Queue: Matthias Liedtke +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(lhs).signature() != ++ static_cast(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); ++})(); diff --git a/patches/v8/cherry-pick-a068030f5179.patch b/patches/v8/cherry-pick-a068030f5179.patch new file mode 100644 index 0000000000..38c042c920 --- /dev/null +++ b/patches/v8/cherry-pick-a068030f5179.patch @@ -0,0 +1,194 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Victor Gomes +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ä +Commit-Queue: Victor Gomes +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()) return true; + ValueNode* nested_alloc = nested_value->Cast(); + 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()) return false; + InlinedAllocation* alloc = receiver->Cast(); ++ 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()->object(); + // TODO(victorgomes): Remove this once we track changes in the inlined + // allocated object. +- if (iterated_object->Cast()->IsEscaping()) { ++ if (IsEscaping(iterated_object->Cast())) { + 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");