CVE-2026-7926
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/modules/presentation/presentation_availability.cc |
modified | |
ifthird_party/blink/renderer/modules/presentation/presentation_availability_state.cc |
modified | |
forthird_party/blink/renderer/modules/presentation/presentation_availability_state.cc |
modified | |
ReentrantResolverthird_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc |
modified | |
ifthird_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc |
modified | |
ClosureOnResolvethird_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/presentation/presentation_availability.ccthird_party/blink/renderer/modules/presentation/presentation_availability_state.ccthird_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
Patch
From 959627a8a6af4b0e26380aa129e0e40266003f93 Mon Sep 17 00:00:00 2001 From: mark a. foltz <[email protected]> Date: Fri, 24 Apr 2026 15:33:13 -0700 Subject: [PATCH] [Presentation API] Fix Use-After-Free in PresentationAvailability This CL fixes a Use-After-Free/Use-After-Poison vulnerability caused by re-entrant modifications of collections during promise resolution. 1. In PresentationAvailability, we now swap availability_resolvers_ to a local vector before iterating. This prevents re-entrant calls (e.g., from JS calling getAvailability()) from modifying the collection being iterated. 2. In PresentationAvailabilityState, we now copy the availabilities set to a local vector using strong Member references and null checks. This ensures objects stay alive during iteration and handles WeakMember references safely. A regression test is added to verify that re-entrant RequestAvailability calls no longer trigger memory safety issues. Fixed: 456795077, 502249087 Change-Id: I34d722892b4d253b17d27612f0895c033dd46eaa Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793737 Reviewed-by: Muyao Xu <[email protected]> Commit-Queue: Mark Foltz <[email protected]> Cr-Commit-Position: refs/heads/main@{#1620515} --- diff --git a/third_party/blink/renderer/modules/presentation/presentation_availability.cc b/third_party/blink/renderer/modules/presentation/presentation_availability.cc index 582bc7e..13e5fb635 100644 --- a/third_party/blink/renderer/modules/presentation/presentation_availability.cc +++ b/third_party/blink/renderer/modules/presentation/presentation_availability.cc @@ -129,18 +129,20 @@ } void PresentationAvailability::RejectPendingPromises() { - for (auto& resolver : availability_resolvers_) { + HeapVector<Member<ScriptPromiseResolver<PresentationAvailability>>> resolvers; + resolvers.swap(availability_resolvers_); + for (auto& resolver : resolvers) { resolver->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, kNotSupportedErrorInfo); } - availability_resolvers_.clear(); } void PresentationAvailability::ResolvePendingPromises() { - for (auto& resolver : availability_resolvers_) { + HeapVector<Member<ScriptPromiseResolver<PresentationAvailability>>> resolvers; + resolvers.swap(availability_resolvers_); + for (auto& resolver : resolvers) { resolver->Resolve(this); } - availability_resolvers_.clear(); } void PresentationAvailability::Trace(Visitor* visitor) const { diff --git a/third_party/blink/renderer/modules/presentation/presentation_availability_state.cc b/third_party/blink/renderer/modules/presentation/presentation_availability_state.cc index eadd9ea..9786f71 100644 --- a/third_party/blink/renderer/modules/presentation/presentation_availability_state.cc +++ b/third_party/blink/renderer/modules/presentation/presentation_availability_state.cc @@ -103,17 +103,24 @@ observer->AvailabilityChanged(screen_availability); } - if (screen_availability == mojom::blink::ScreenAvailability::DISABLED) { - for (auto& availability_ptr : listener->availabilities) { - availability_ptr->RejectPendingPromises(); - } - } else { - for (auto& availability_ptr : listener->availabilities) { - availability_ptr->ResolvePendingPromises(); + HeapVector<Member<PresentationAvailability>> availabilities; + for (auto& availability_ptr : listener->availabilities) { + if (availability_ptr) { + availabilities.push_back(availability_ptr); } } listener->availabilities.clear(); + if (screen_availability == mojom::blink::ScreenAvailability::DISABLED) { + for (auto& availability_ptr : availabilities) { + availability_ptr->RejectPendingPromises(); + } + } else { + for (auto& availability_ptr : availabilities) { + availability_ptr->ResolvePendingPromises(); + } + } + for (const auto& availability_url : listener->urls) { MaybeStopListeningToURL(availability_url); } diff --git a/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc b/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc index 14da3c75..3718d50 100644 --- a/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc +++ b/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc @@ -38,6 +38,37 @@ const Vector<KURL> urls_; }; +// Helper class for ReentrantRequestAvailability test. +class ReentrantResolver final + : public ThenCallable<PresentationAvailability, ReentrantResolver> { + public: + ReentrantResolver(PresentationAvailabilityState* state, + PresentationAvailability* availability, + base::OnceClosure callback) + : state_(state), + availability_(availability), + callback_(std::move(callback)) {} + + void React(ScriptState*, PresentationAvailability*) { + // Simulate re-entrant RequestAvailability call. + state_->RequestAvailability(availability_); + if (callback_) { + std::move(callback_).Run(); + } + } + + void Trace(Visitor* visitor) const override { + visitor->Trace(state_); + visitor->Trace(availability_); + ThenCallable<PresentationAvailability, ReentrantResolver>::Trace(visitor); + } + + private: + Member<PresentationAvailabilityState> state_; + Member<PresentationAvailability> availability_; + base::OnceClosure callback_; +}; + // Helper classes for WaitForPromise{Fulfillment,Rejection}(). Provides a // function that invokes |callback| when a ScriptPromise is resolved/rejected. class ClosureOnResolve final @@ -638,4 +669,38 @@ ChangeURLState(url2_, ScreenAvailability::SOURCE_NOT_SUPPORTED); } +TEST_F(PresentationAvailabilityStateTest, ReentrantRequestAvailability) { + PresentationAvailabilityStateTestingContext context; + EXPECT_CALL(mock_presentation_service_, ListenForScreenAvailability(url1_)) + .Times(1); + + auto* resolver = + MakeGarbageCollected<ScriptPromiseResolver<PresentationAvailability>>( + context.GetScriptState(), context.GetExceptionContext()); + auto* availability = MakeGarbageCollected<PresentationAvailability>( + context.GetExecutionContext(), Vector<KURL>({url1_}), false); + availability->AddResolver(resolver); + state_->AddObserver(availability); + auto promise = resolver->Promise(); + + base::RunLoop run_loop; + promise.Then(context.GetScriptState(), + MakeGarbageCollected<ReentrantResolver>(state_, availability, + run_loop.QuitClosure())); + + TestRequestAvailability({ScreenAvailability::AVAILABLE}, availability); + + // Execute pending microtasks to trigger the re-entrant call. + context.GetScriptState() + ->GetContext() + ->GetMicrotaskQueue() + ->PerformCheckpoint(context.GetScriptState()->GetIsolate()); + run_loop.Run(); + + // The re-entrant RequestAvailability call added 'availability' back to + // listener->availabilities. + state_->UpdateAvailability(url1_, ScreenAvailability::AVAILABLE); + state_->RemoveObserver(availability); +} + } // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc b/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
index 14da3c75..3718d50 100644
--- a/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
+++ b/third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
@@ -38,6 +38,37 @@
const Vector<KURL> urls_;
};
+// Helper class for ReentrantRequestAvailability test.
+class ReentrantResolver final
+ : public ThenCallable<PresentationAvailability, ReentrantResolver> {
+ public:
+ ReentrantResolver(PresentationAvailabilityState* state,
+ PresentationAvailability* availability,
+ base::OnceClosure callback)
+ : state_(state),
+ availability_(availability),
+ callback_(std::move(callback)) {}
+
+ void React(ScriptState*, PresentationAvailability*) {
+ // Simulate re-entrant RequestAvailability call.
+ state_->RequestAvailability(availability_);
+ if (callback_) {
+ std::move(callback_).Run();
+ }
+ }
+
+ void Trace(Visitor* visitor) const override {
+ visitor->Trace(state_);
+ visitor->Trace(availability_);
+ ThenCallable<PresentationAvailability, ReentrantResolver>::Trace(visitor);
+ }
+
+ private:
+ Member<PresentationAvailabilityState> state_;
+ Member<PresentationAvailability> availability_;
+ base::OnceClosure callback_;
+};
+
// Helper classes for WaitForPromise{Fulfillment,Rejection}(). Provides a
// function that invokes |callback| when a ScriptPromise is resolved/rejected.
class ClosureOnResolve final
@@ -638,4 +669,38 @@
ChangeURLState(url2_, ScreenAvailability::SOURCE_NOT_SUPPORTED);
}
+TEST_F(PresentationAvailabilityStateTest, ReentrantRequestAvailability) {
+ PresentationAvailabilityStateTestingContext context;
+ EXPECT_CALL(mock_presentation_service_, ListenForScreenAvailability(url1_))
+ .Times(1);
+
+ auto* resolver =
+ MakeGarbageCollected<ScriptPromiseResolver<PresentationAvailability>>(
+ context.GetScriptState(), context.GetExceptionContext());
+ auto* availability = MakeGarbageCollected<PresentationAvailability>(
+ context.GetExecutionContext(), Vector<KURL>({url1_}), false);
+ availability->AddResolver(resolver);
+ state_->AddObserver(availability);
+ auto promise = resolver->Promise();
+
+ base::RunLoop run_loop;
+ promise.Then(context.GetScriptState(),
+ MakeGarbageCollected<ReentrantResolver>(state_, availability,
+ run_loop.QuitClosure()));
+
+ TestRequestAvailability({ScreenAvailability::AVAILABLE}, availability);
+
+ // Execute pending microtasks to trigger the re-entrant call.
+ context.GetScriptState()
+ ->GetContext()
+ ->GetMicrotaskQueue()
+ ->PerformCheckpoint(context.GetScriptState()->GetIsolate());
+ run_loop.Run();
+
+ // The re-entrant RequestAvailability call added 'availability' back to
+ // listener->availabilities.
+ state_->UpdateAvailability(url1_, ScreenAvailability::AVAILABLE);
+ state_->RemoveObserver(availability);
+}
+
} // namespace blink
Original Bug Report
UAF In PresentationAvailability::ResolvePendingPromises
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
Use-After-Free in
- chromium/blink/renderer/modules/presentation/presentation_availability.cc
void PresentationAvailability::ResolvePendingPromises() { for (auto& resolver : availability_resolvers_) { /************* [1] ****************/ resolver->Resolve(this); } availability_resolvers_.clear(); }
HeapVector<Member<ScriptPromiseResolver<PresentationAvailability>>> availability_resolvers_;
In PresentationAvailibility::ResolvePendingPromises,
the call resolver->Resolve(this) can trigger a synchronous javascript using a “then” getter on object prototype, The user javascript code can call AddResolver to add more resolvers into availability_resolvers_ heapVector, This mutates the heap vector backing store and reallocates , while the iterator is running on the stack. as A result the iterator points to stale memory a Use-After-Free.
void PresentationAvailability::AddResolver(
ScriptPromiseResolver<PresentationAvailability>* resolver) {
availability_resolvers_.push_back(resolver);
}
Other code in blink similar to the iterator resolvers first copy the resolvers on the stack and then run the for loop.
I will provide a poc as well as a patch to fix the bug.
To fix the bug here is the patch provided,
void PresentationAvailability::ResolvePendingPromises() {
HeapVector<Member<ScriptPromiseResolver<PresentationAvailability>>> resolvers;
resolvers.swap(availability_resolvers_);
for (auto& resolver : resolvers) {
resolver->Resolve(this);
}
availability_resolvers_.clear();
}
VULNERABILITY DETAILS Please provide a brief explanation of the security issue.
VERSION Chrome Version: HEAD Operating System: [Please indicate OS, version, and service pack level]
REPRODUCTION CASE Please include a demonstration of the security bug, such as an attached HTML or binary file that reproduces the bug when loaded in Chrome. PLEASE make the file as small as possible and remove any content not required to demonstrate the bug, or any personal or confidential information.
Please attach files directly, not in zip or other archive formats, and if you’ve created a demonstration site please also attach the files needed to reproduce the demonstration locally.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION Type of crash: [tab, browser, etc.] Crash State: [see link above: stack trace with symbols, registers, exception record] Client ID (if relevant): [see link above]
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: [anonymous]