CVE-2026-87494
Overview
Background
- WinRT `PropertiesChanged` event
- a Windows Runtime notification (registered here via
add_PropertiesChangedoncaption_statics2_) that fires when OS closed-caption styling changes, and which the system may deliver on an arbitrary thread. - `NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated()`
- the call that walks the
NativeThemeobserver list and pushes a caption-style update to registeredNativeThemeObservers, whose data lifecycle is owned by the sequence that registered the listener. - `base::SequencedTaskRunner`
- a handle to a specific task sequence;
HasCurrentDefault()/GetCurrentDefault()capture the current sequence so work can later bePostTasked back to it rather than run on whatever thread is executing. - Sequence affinity
- the invariant that a given object (here the
NativeThemeobserver list and its observers) is only touched from one sequence; violating it produces data races and use-after-free.
Root Cause Analysis
Before the patch, the IEventHandler<IInspectable*> callback registered with add_PropertiesChanged invoked NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated() directly inside the handler body. Because a WinRT PropertiesChanged event “may be delivered on an arbitrary thread” (as the added comment states), this meant the NativeTheme observer list — and the observers it notifies — could be walked and dereferenced from a thread other than the one that owns that data’s lifecycle, violating the sequence-affinity invariant. When the notification runs on the wrong thread concurrently with the owning sequence tearing down or mutating those observer objects, the handler operates on freed or racing state, which is the use-after-free reported for the Browser process.
The fix captures the registering sequence’s runner via base::SequencedTaskRunner::GetCurrentDefault() (guarded by HasCurrentDefault()) into task_runner, and when present bounces the notification back with task_runner->PostTask(FROM_HERE, base::BindOnce([]{ ... NotifyOnCaptionStyleUpdated(); })) so the observer walk always runs on the sequence that registered the listener. Only when no sequenced runner is available does it fall back to the original inline call, preserving prior behavior in that case. The shipped regression test EventOnOtherThreadNotifiesOnRegisteringSequence confirms the intent by firing the event from a separate base::Thread and asserting the notification lands on the original (main) thread.
NotifyOnCaptionStyleUpdated) directly on the arbitrary thread that delivered the WinRT event, instead of on the sequence that owns the observer data; the fix corrects it by capturing that sequence’s task_runner at registration time and PostTask-ing the notification back to it.Attack Path
- Listener is registered on the Browser's owning sequence
Chrome calls the listener-registration path (enclosing function name not shown in the hunk header; the commit message references
RegisterClosedCaptionPropertiesChangedListener(inferred)), invokingcaption_statics2_->add_PropertiesChangedwith a callback that, pre-patch, callsNotifyOnCaptionStyleUpdated()inline. - OS delivers the caption-change event on a foreign thread
a closed-caption styling change causes the WinRT
PropertiesChangedevent to fire and be dispatched on an arbitrary system thread, not the registering sequence, as the added comment and the regression test’s separatebase::Threaddemonstrate. - Cross-thread observer walk races the owning sequence
the callback runs
NativeTheme::GetInstanceForWeb()->NotifyOnCaptionStyleUpdated()on that foreign thread while the owning sequence may be concurrently destroying or mutating theNativeThemeobserver list, so the walk dereferences freed/racing observer state (the use-after-free).(inferred)— the exact freed object and the timing window are not shown in the diff, which only adds the thread bounce. - Escalation
controlled memory corruption of freed observer objects could be steered toward further compromise of the Browser process.
(inferred)— no read/write primitive, sink, or code-execution step is demonstrated by the patch.
Impact Assessment
NativeTheme observer list. The precondition is that the WinRT PropertiesChanged event be delivered on a thread other than the registering sequence (the normal system behavior for these events) while that sequence is mutating or destroying observers. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory classifies this only as “Use after free in Browser” and does not state exploited-in-the-wild status or a severity rating in the provided metadata.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/native_theme/native_theme_win.cc |
modified |
Files Changed
ui/native_theme/native_theme_win_unittest.ccui/native_theme/native_theme_win.cc
Audit Directions
- WinRT/COM event handlers that touch sequence-affine stateaudit every
add_*/Microsoft::WRL::Callbackhandler whose body directly touches singletons or observer lists (likeNativeTheme::GetInstanceForWeb()), since these events may arrive on arbitrary threads and mustPostTaskback to the owning sequence. - OS-callback lambdas invoking observer notificationslook for callbacks that call
Notify*/observer-walk methods inline without capturing and hopping to aSequencedTaskRunner, the exact pattern fixed here. - `HasCurrentDefault` fallback pathsreview handlers registered where no sequenced runner exists (the
elsebranch here still runs inline), to confirm those code paths cannot be reached on a foreign thread and are genuinely safe.
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.