High patch-now CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PresentationAPI
DescriptionUse after free in PresentationAPI
ComponentPresentationAPI
Sub-componentPresentation API
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (unverified)
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker502249087
Fix commit959627a8a6af (chromium/src) +85/-11
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedanonymous
Reported2026-04-14 (per vendor advisory)
Fix landed2026-04-24 (public git)
Disclosed2026-05-05
Patch gap11 days — fix public in source before the advisory

Background

Presentation API
a Blink feature that lets a page discover and present to secondary displays, exposing PresentationAvailability objects 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 Member keeps its target alive while a WeakMember does not, so iterating raw WeakMember entries 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.

Key insight
The code mutated the very container it was iterating, because promise resolution can synchronously re-enter and modify 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

  1. Register availability listeners A page calls the Presentation API so that one or more ScriptPromiseResolvers accumulate in a PresentationAvailability’s availability_resolvers_ and the object is tracked in listener->availabilities.
  2. Trigger resolution A screen-availability change drives the state object to iterate listener->availabilities and call ResolvePendingPromises/RejectPendingPromises, which begins iterating availability_resolvers_.
  3. Re-enter from JS The promise resolution runs a .then handler (modeled by ReentrantResolver in the regression test) that calls back into RequestAvailability/getAvailability(), mutating availability_resolvers_ or re-adding the availability to listener->availabilities while the outer loop is still iterating.
  4. 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

An attacker-controlled web page in the renderer process can, with no special privileges beyond running script and using the Presentation API, drive a use-after-free/use-after-poison during promise resolution. The advisory rates this high severity and describes it as a use after free in the Presentation API; it does not report in-the-wild exploitation. Escalation from the UAF to a read/write primitive or code execution is (inferred) and not shown by the patch.

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/presentation/presentation_availability.cc
modified
if
third_party/blink/renderer/modules/presentation/presentation_availability_state.cc
modified
for
third_party/blink/renderer/modules/presentation/presentation_availability_state.cc
modified
ReentrantResolver
third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
modified
if
third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
modified
ClosureOnResolve
third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
modified
TEST_F
third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/presentation/presentation_availability.cc
  • third_party/blink/renderer/modules/presentation/presentation_availability_state.cc
  • third_party/blink/renderer/modules/presentation/presentation_availability_state_test.cc

Audit Directions

  • Iterate-while-resolve
    Audit any loop that calls Resolve/Reject/RejectWithDOMException (or otherwise runs microtasks or author callbacks) while iterating a member container; detach via swap or copy before the loop.
  • Clear-after-loop ordering
    Flag 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 iteration
    Review loops over HeapVector<WeakMember<T>>/weak sets that dereference entries without a null check or without promoting to strong Member, since entries can be collected mid-iteration.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by na...@gmail.com

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

  1. chromium/blink/renderer/modules/presentation/presentation_availability.cc

void PresentationAvailability::ResolvePendingPromises() { for (auto& resolver : availability_resolvers_) { /************* [1] ****************/ resolver->Resolve(this); } availability_resolvers_.clear(); }

HeapVector&lt;Member&lt;ScriptPromiseResolver&lt;PresentationAvailability&gt;&gt;&gt; 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]

View on issue tracker