CVE-2026-87494
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/native_theme/native_theme_win.cc |
modified |
Files Changed
ui/native_theme/native_theme_win.ccui/native_theme/native_theme_win_unittest.cc
Patch
From 61d783000461a95d107ef90e6213f77d90fbc97e Mon Sep 17 00:00:00 2001
From: Ari Chivukula <arichiv@chromium.org>
Date: Fri, 31 Jul 2026 12:09:57 -0700
Subject: [PATCH] Reland "[win] PropertiesChanged listener must use correct thread"
This reverts commit 15d710d92728f6ea8c56f966a2e6a97d8ec38180.
Reason for revert: Adding HasCurrentDefault first to unbreak unittests
Original change's description:
> Revert "[win] PropertiesChanged listener must use correct thread"
>
> This reverts commit f24ddfe94420b50854c20a68018b4276c1112492.
>
> Reason for revert:
> LUCI Bisection has identified this change as the cause of a test failure. See the analysis: https://ci.chromium.org/ui/p/chromium/bisection/test-analysis/b/5920246637527040
>
> Sample build with failed test: https://ci.chromium.org/b/8674831598765593553
> Affected test(s):
> [://chrome/test\:browser_tests!gtest::AXTreeDistillerTest#DistillsWebPage/simple_page](https://ci.chromium.org/ui/test/chromium/:%2F%2Fchrome%2Ftest%5C:browser_tests%21gtest::AXTreeDistillerTest%23DistillsWebPage%2Fsimple_page?q=VHash%3A553c1560028db846)
> [://chrome/test\:browser_tests!gtest::ChromeContentSettingsAgentDelegateBrowserTest#PluginsTemporarilyAllowed](https://ci.chromium.org/ui/test/chromium/:%2F%2Fchrome%2Ftest%5C:browser_tests%21gtest::ChromeContentSettingsAgentDelegateBrowserTest%23PluginsTemporarilyAllowed?q=VHash%3A553c1560028db846)
> [://chrome/test\:browser_tests!gtest::ChromeRenderFrameObserverTest#LoadTimesAndCsiDefinedUnconditionally](https://ci.chromium.org/ui/test/chromium/:%2F%2Fchrome%2Ftest%5C:browser_tests%21gtest::ChromeRenderFrameObserverTest%23LoadTimesAndCsiDefinedUnconditionally?q=VHash%3A553c1560028db846)
> [://chrome/test\:browser_tests!gtest::PasswordAutofillAgentTest#AcceptingSuggestionDoesntRewriteUsername](https://ci.chromium.org/ui/test/chromium/:%2F%2Fchrome%2Ftest%5C:browser_tests%21gtest::PasswordAutofillAgentTest%23AcceptingSuggestionDoesntRewriteUsername?q=VHash%3A553c1560028db846)
> [://chrome/test\:browser_tests!gtest::PasswordAutofillAgentTest#PreviewField](https://ci.chromium.org/ui/test/chromium/:%2F%2Fchrome%2Ftest%5C:browser_tests%21gtest::PasswordAutofillAgentTest%23PreviewField?q=VHash%3A553c1560028db846)
> and 59 more ...
>
> If this is a false positive, please report it at http://b.corp.google.com/createIssue?component=1199205&description=Analysis%3A+https%3A%2F%2Fci.chromium.org%2Fui%2Fp%2Fchromium%2Fbisection%2Ftest-analysis%2Fb%2F5920246637527040&format=PLAIN&priority=P3&title=Wrongly+blamed+https%3A%2F%2Fchromium-review.googlesource.com%2Fc%2Fchromium%2Fsrc%2F%2B%2F8163303&type=BUG
>
> Original change's description:
> > [win] PropertiesChanged listener must use correct thread
> >
> > RegisterClosedCaptionPropertiesChangedListener must resolve changes on
> > the thread that manages the data lifecycle of the relevant data.
> >
> > Fixed: 520161438
> > Change-Id: I57f3f04159214d3ed4cb7c35dca58366cdf37d44
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8163303
> > Auto-Submit: Ari Chivukula <arichiv@chromium.org>
> > Reviewed-by: Elly <ellyjones@chromium.org>
> > Commit-Queue: Elly <ellyjones@chromium.org>
> > Cr-Commit-Position: refs/heads/main@{#1671131}
> >
>
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Change-Id: Iec9b72d42906d9f87816a68d85157d55f070964d
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8175977
> Commit-Queue: Shunya Shishido <sisidovski@chromium.org>
> Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
> Owners-Override: Shunya Shishido <sisidovski@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1671582}
Fixed: 520161438
Change-Id: I0a7ae2daf9b8db1827207d7b90c6ce14c1c3c393
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8182567
Reviewed-by: ccameron chromium <ccameron@chromium.org>
Auto-Submit: Ari Chivukula <arichiv@chromium.org>
Commit-Queue: Ari Chivukula <arichiv@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1672006}
---
diff --git a/ui/native_theme/native_theme_win.cc b/ui/native_theme/native_theme_win.cc
index e1229fc0..e380266 100644
--- a/ui/native_theme/native_theme_win.cc
+++ b/ui/native_theme/native_theme_win.cc
@@ -23,11 +23,14 @@
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/cstring_view.h"
+#include "base/task/sequenced_task_runner.h"
#include "base/win/core_winrt_util.h"
#include "base/win/hstring_reference.h"
#include "base/win/scoped_gdi_object.h"
@@ -1133,10 +1136,24 @@
}
EventRegistrationToken token;
+ scoped_refptr<base::SequencedTaskRunner> task_runner;
+ if (base::SequencedTaskRunner::HasCurrentDefault()) {
+ task_runner = base::SequencedTaskRunner::GetCurrentDefault();
+ }
hr = caption_statics2_->add_PropertiesChanged(
Microsoft::WRL::Callback<ABI::Windows::Foundation::IEventHandler<
- IInspectable*>>([](IInspectable*, IInspectable*) -> HRESULT {
- NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated();
+ IInspectable*>>([task_runner](IInspectable*,
+ IInspectable*) -> HRESULT {
+ // The event may be delivered on an arbitrary thread; bounce the
+ // notification back to the sequence that registered the listener.
+ if (task_runner) {
+ task_runner->PostTask(
+ FROM_HERE, base::BindOnce([] {
+ NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated();
+ }));
+ } else {
+ NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated();
+ }
return S_OK;
}).Get(),
&token);
diff --git a/ui/native_theme/native_theme_win_unittest.cc b/ui/native_theme/native_theme_win_unittest.cc
index 0caf26a..33c887a5 100644
--- a/ui/native_theme/native_theme_win_unittest.cc
+++ b/ui/native_theme/native_theme_win_unittest.cc
@@ -13,7 +13,9 @@
#include <wrl/implements.h>
#include "base/scoped_observation.h"
+#include "base/test/bind.h"
#include "base/test/task_environment.h"
+#include "base/threading/thread.h"
#include "base/win/scoped_com_initializer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -181,6 +183,7 @@
// Simulate a caption style change from the OS.
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ base::RunLoop().RunUntilIdle();
}
// Verifies that multiple PropertiesChanged events each trigger
@@ -199,6 +202,43 @@
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ base::RunLoop().RunUntilIdle();
+}
+
+// Verifies that when the WinRT PropertiesChanged event is delivered on a
+// different thread, OnCaptionStyleUpdated is still dispatched to observers on
+// the sequence that registered the listener.
+TEST_F(NativeThemeWinCaptionTest,
+ EventOnOtherThreadNotifiesOnRegisteringSequence) {
+ TestNativeThemeWin theme;
+ ASSERT_TRUE(fake_statics().has_handler());
+
+ const base::PlatformThreadId main_thread_id =
+ base::PlatformThread::CurrentId();
+ base::PlatformThreadId notify_thread_id;
+
+ MockCaptionObserver observer;
+ EXPECT_CALL(observer, OnCaptionStyleUpdated()).WillOnce([&] {
+ notify_thread_id = base::PlatformThread::CurrentId();
+ });
+ base::ScopedObservation<NativeTheme, NativeThemeObserver> observation(
+ &observer);
+ observation.Observe(NativeTheme::GetInstanceForWeb());
+
+ // Invoke the registered handler from a different thread, as the system may
+ // do for WinRT events.
+ base::Thread other_thread("CaptionEventThread");
+ ASSERT_TRUE(other_thread.Start());
+ base::RunLoop run_loop;
+ other_thread.task_runner()->PostTaskAndReply(
+ FROM_HERE, base::BindLambdaForTesting([&] {
+ EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ }),
+ run_loop.QuitClosure());
+ run_loop.Run();
+ other_thread.Stop();
+
+ EXPECT_EQ(main_thread_id, notify_thread_id);
}
} // namespace
Regression Test / PoC
diff --git a/ui/native_theme/native_theme_win_unittest.cc b/ui/native_theme/native_theme_win_unittest.cc
index 0caf26a..33c887a5 100644
--- a/ui/native_theme/native_theme_win_unittest.cc
+++ b/ui/native_theme/native_theme_win_unittest.cc
@@ -13,7 +13,9 @@
#include <wrl/implements.h>
#include "base/scoped_observation.h"
+#include "base/test/bind.h"
#include "base/test/task_environment.h"
+#include "base/threading/thread.h"
#include "base/win/scoped_com_initializer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -181,6 +183,7 @@
// Simulate a caption style change from the OS.
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ base::RunLoop().RunUntilIdle();
}
// Verifies that multiple PropertiesChanged events each trigger
@@ -199,6 +202,43 @@
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ base::RunLoop().RunUntilIdle();
+}
+
+// Verifies that when the WinRT PropertiesChanged event is delivered on a
+// different thread, OnCaptionStyleUpdated is still dispatched to observers on
+// the sequence that registered the listener.
+TEST_F(NativeThemeWinCaptionTest,
+ EventOnOtherThreadNotifiesOnRegisteringSequence) {
+ TestNativeThemeWin theme;
+ ASSERT_TRUE(fake_statics().has_handler());
+
+ const base::PlatformThreadId main_thread_id =
+ base::PlatformThread::CurrentId();
+ base::PlatformThreadId notify_thread_id;
+
+ MockCaptionObserver observer;
+ EXPECT_CALL(observer, OnCaptionStyleUpdated()).WillOnce([&] {
+ notify_thread_id = base::PlatformThread::CurrentId();
+ });
+ base::ScopedObservation<NativeTheme, NativeThemeObserver> observation(
+ &observer);
+ observation.Observe(NativeTheme::GetInstanceForWeb());
+
+ // Invoke the registered handler from a different thread, as the system may
+ // do for WinRT events.
+ base::Thread other_thread("CaptionEventThread");
+ ASSERT_TRUE(other_thread.Start());
+ base::RunLoop run_loop;
+ other_thread.task_runner()->PostTaskAndReply(
+ FROM_HERE, base::BindLambdaForTesting([&] {
+ EXPECT_HRESULT_SUCCEEDED(fake_statics().SimulatePropertiesChanged());
+ }),
+ run_loop.QuitClosure());
+ run_loop.Run();
+ other_thread.Stop();
+
+ EXPECT_EQ(main_thread_id, notify_thread_id);
}
} // namespace
Original Bug Report
Potential Use-After-Free in NativeThemeWin via off-thread WinRT PropertiesChanged callback
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in the Windows browser process of Chromium due to a thread-safety issue with the WinRT ClosedCaptionProperties.PropertiesChanged event handler. The event callback runs on a system Multi-Threaded Apartment (MTA) thread and calls NotifyOnCaptionStyleUpdated directly without dispatching back to the UI thread. This creates a race condition with the UI thread mutating the non-thread-safe base::ObserverList, potentially leading to memory corruption or browser process code execution.
Affected files:
ui/native_theme/native_theme_win.ccui/native_theme/native_theme.cc
Estimated timestamp from git blame: 2026-04-22
Description
A potential Use-After-Free (UAF) vulnerability exists in the Windows browser process of Chromium. The issue lies in NativeThemeWin::RegisterClosedCaptionPropertiesChangedListener() (ui/native_theme/native_theme_win.cc:1119-1149), which registers a WinRT event handler whose body runs directly on the calling thread of the WinRT event source—typically a system MTA threadpool thread—and immediately calls NotifyOnCaptionStyleUpdated() on the Web theme instance:
// ui/native_theme/native_theme_win.cc:1135-1142
EventRegistrationToken token;
hr = caption_statics2_->add_PropertiesChanged(
Microsoft::WRL::Callback<ABI::Windows::Foundation::IEventHandler<
IInspectable*>>([](IInspectable*, IInspectable*) -> HRESULT {
NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated();
return S_OK;
}).Get(),
&token);
In a production release build, the sequence checker inside NotifyOnCaptionStyleUpdated() is compiled out:
// ui/native_theme/native_theme.cc:293-300
void NativeTheme::NotifyOnCaptionStyleUpdated() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); // Stripped in release builds
native_theme_observers_.Notify(&NativeThemeObserver::OnCaptionStyleUpdated);
}
The member native_theme_observers_ is a non-thread-safe base::ObserverList<> (ui/native_theme/native_theme.h:598-602), backed by a std::vector of observers (base/observer_list.h:510). The class documentation explicitly states:
> “ObserverList is not thread-compatible. Iterating on the same ObserverList simultaneously in different threads is not safe.”
Because this list is mutated on the browser’s UI thread (e.g., when a page rapidly attaches/detaches iframe frames or opens/closes sub-windows, driving WebContentsImpl construction and destruction which registers/unregisters observers), an MTA thread iterating the list via the WinRT callback concurrently with a UI thread reallocating the underlying std::vector triggers a Use-After-Free (UAF).
Potential Attack/Trigger Path
Note: These are potential, suggested steps to trigger the race condition, as our tooling agent doesn’t yet have the ability to run code.
- User Interaction: The user is social-engineered to alter accessibility options (e.g., Settings → Accessibility → Captions) while visiting a malicious page.
- Page-Driven Observer Churn: The page continuously opens and closes frames or sub-windows, forcing the browser process UI thread to register/deregister
WebContentsImplinstances to/from theNativeTheme::GetInstanceForWeb()observer list. This results in frequent memory reallocations of the underlyingstd::vector. - MTA Thread Notification: The Windows OS raises the
PropertiesChangedevent, running the registered callback on an MTA thread. - Race Condition and UAF: The MTA thread processes the callback and indexes into the freed/reallocated observer list buffer. It reads a corrupted
CheckedObserverAdapterand performs a virtual call toOnCaptionStyleUpdated()on an attacker-influenced pointer, potentially leading to control flow hijacking / Remote Code Execution (RCE) in the browser process.
Suggested Fix
To fix this thread-safety issue, the WinRT callback must hop back to the correct sequence using a task runner before performing any observer notification. This can be securely achieved by wrapping the callback using base::BindPostTask or a sequence task runner.
For instance, capture the current SequencedTaskRunner and post a task:
auto task_runner = base::SequencedTaskRunner::GetCurrentDefault();
hr = caption_statics2_->add_PropertiesChanged(
Microsoft::WRL::Callback<ABI::Windows::Foundation::IEventHandler<
IInspectable*>>([task_runner](IInspectable*, IInspectable*) -> HRESULT {
task_runner->PostTask(
FROM_HERE,
base::BindOnce([]() {
NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated();
}));
return S_OK;
}).Get(),
&token);
Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.