CVE-2026-7926
Overview
Background
- Presentation API
- a Blink feature that lets a page discover and present to secondary displays, exposing
PresentationAvailabilityobjects whose promises resolve when screen availability changes. - `ScriptPromiseResolver`
- a Blink wrapper that resolves or rejects a JavaScript promise, and whose resolution runs JS microtasks synchronously, allowing re-entrancy back into Blink.
- `HeapVector<Member<T>>` / `WeakMember<T>`
- Oilpan garbage-collected containers; a
Memberkeeps its target alive while aWeakMemberdoes not, so iterating rawWeakMemberentries can touch a collected object. - Re-entrancy
- when resolving a promise synchronously invokes author JS that calls back into the same subsystem (e.g.
getAvailability()), mutating a collection that is still being iterated.
Root Cause Analysis
PresentationAvailability::RejectPendingPromises and ResolvePendingPromises iterated the member collection availability_resolvers_ directly and called resolver->Resolve(this) / RejectWithDOMException inside the loop. Resolving a promise drains microtasks, which can run author JS that re-enters the Presentation API (for example calling getAvailability() again), and that re-entrant path appends to or clears availability_resolvers_ while the loop still holds an iterator into it — classic iterate-while-mutate, invalidating the iterator and leaving the loop walking freed/poisoned backing store. The parallel path in presentation_availability_state.cc iterated listener->availabilities while calling RejectPendingPromises/ResolvePendingPromises on each entry, and only cleared the set after the loop, so a re-entrant RequestAvailability could mutate that set mid-iteration as well.
The fix swaps availability_resolvers_ into a local resolvers vector (leaving the member empty) before iterating, so any re-entrant mutation touches the now-empty member rather than the vector under iteration. The state-side fix first copies live entries into a local HeapVector<Member<PresentationAvailability>> (with a null check to drop stale WeakMembers and strong Member refs to keep objects alive), clears listener->availabilities, and only then resolves/rejects.
availability_resolvers_/listener->availabilities; the fix detaches the collection into a local copy (via swap / copy-then-clear) before any resolution runs, so re-entrancy can no longer corrupt the iteration.Attack Path
- Register availability listeners
A page calls the Presentation API so that one or more
ScriptPromiseResolvers accumulate in aPresentationAvailability’savailability_resolvers_and the object is tracked inlistener->availabilities. - Trigger resolution
A screen-availability change drives the state object to iterate
listener->availabilitiesand callResolvePendingPromises/RejectPendingPromises, which begins iteratingavailability_resolvers_. - Re-enter from JS
The promise resolution runs a
.thenhandler (modeled byReentrantResolverin the regression test) that calls back intoRequestAvailability/getAvailability(), mutatingavailability_resolvers_or re-adding the availability tolistener->availabilitieswhile the outer loop is still iterating. - Use freed/poisoned memory The outer loop continues over the now-reallocated or cleared backing store, dereferencing a stale element (use-after-free / use-after-poison) (inferred as to any further read/write primitive, which the diff does not demonstrate).
Impact Assessment
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
Audit Directions
- Iterate-while-resolveAudit any loop that calls
Resolve/Reject/RejectWithDOMException(or otherwise runs microtasks or author callbacks) while iterating a member container; detach viaswapor copy before the loop. - Clear-after-loop orderingFlag patterns that iterate a collection and only
clear()it afterward; re-entrant callbacks in the loop body can mutate it first, so clear or snapshot before dispatching. - Raw WeakMember iterationReview loops over
HeapVector<WeakMember<T>>/weak sets that dereference entries without a null check or without promoting to strongMember, since entries can be collected mid-iteration.
Patch
From 959627a8a6af4b0e26380aa129e0e40266003f93 Mon Sep 17 00:00:00 2001
From: mark a. foltz <mfoltz@chromium.org>
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 <muyaoxu@google.com>
Commit-Queue: Mark Foltz <mfoltz@chromium.org>
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]