Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in FullScreen
DescriptionUse after free in FullScreen
ComponentFullScreen
Bug ClassUAF
Tracker506684534
Fix commit058c2d6ea49c (chromium/src) +118/-63
CISA KEVNot listed
CreditedMihnea Nicolau
Disclosed2026-06-08

Changed Functions

FunctionChangeNotes
if
chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
modified
if
chrome/browser/renderer_context_menu/render_view_context_menu.cc
modified
if
chrome/browser/ui/browser.cc
modified
if
chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc
modified
if
chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
modified
if
chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
modified
if
chrome/browser/webshare/share_service_impl.cc
modified

Files Changed

  • chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
  • chrome/browser/renderer_context_menu/render_view_context_menu.cc
  • chrome/browser/ui/browser.cc
  • chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc
  • chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
  • chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
  • chrome/browser/webshare/share_service_impl.cc
  • content/browser/file_system_access/file_system_access_manager_impl.cc
From 058c2d6ea49cf80034fe78cfdb88cf24be7435de Mon Sep 17 00:00:00 2001
From: Jordan Bayles <[email protected]>
Date: Thu, 07 May 2026 14:39:57 -0700
Subject: [PATCH] content: Prevent UAF in WebContentsImpl related to drop fullscreen

ForSecurityDropFullscreen can synchronously destroy the WebContentsImpl
object. This CL updates ForSecurityDropFullscreen() to return a
std::optional<base::ScopedClosureRunner> that is std::nullopt if `this`
was destroyed, forcing callers to handle it explicitly.

Affected methods:
- ShowCreatedWindow
- ViewSource
- EnumerateDirectory
- RunJavaScriptDialog
- RunBeforeUnloadConfirm
- RunFileChooser
- SetWindowRect
- DidCallFocus

Bug: 506684534
Change-Id: Ieb700f4d5f3785adc6360f8c6c2f637be769be5d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7818866
Commit-Queue: Jordan Bayles <[email protected]>
Reviewed-by: Avi Drissman <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1627248}
---

diff --git a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
index 05a3746..779aac2 100644
--- a/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
+++ b/chrome/browser/file_system_access/chrome_file_system_access_permission_context.cc
@@ -987,9 +987,13 @@
     }
 
     // Drop fullscreen mode so that the user sees the URL bar.
-    base::ScopedClosureRunner fullscreen_block =
-        web_contents->ForSecurityDropFullscreen(
-            /*display_id=*/display::kInvalidDisplayId);
+    auto blocker = web_contents->ForSecurityDropFullscreen(
+        /*display_id=*/display::kInvalidDisplayId);
+    if (!blocker) {
+      RunCallbackAndRecordPermissionRequestOutcome(
+          std::move(callback), PermissionRequestOutcome::kRequestAborted);
+      return;
+    }
 
     if (context_->IsEligibleToUpgradePermissionRequestToRestorePrompt(
             origin_, path_info_.path, handle_type_, user_action_, type_)) {
@@ -1001,7 +1005,7 @@
            origin_, request_data_list},
           base::BindOnce(&PermissionGrantImpl::OnRestorePermissionRequestResult,
                          this, std::move(callback)),
-          std::move(fullscreen_block));
+          std::move(*blocker));
       return;
     }
 
@@ -1019,7 +1023,7 @@
          {file_request_data}},
         base::BindOnce(&PermissionGrantImpl::OnPermissionRequestResult, this,
                        std::move(callback)),
-        std::move(fullscreen_block));
+        std::move(*blocker));
   }
 
   const url::Origin& origin() const {
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 2023197..e353113 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -3693,9 +3693,10 @@
       // so drop fullscreen when it is shown. https://crbug.com/40054574
       // TODO(avi): Do we need to attach the fullscreen block to the emoji
       // panel?
-      source_web_contents_
-          ->ForSecurityDropFullscreen(/*display_id=*/display::kInvalidDisplayId)
-          .RunAndReset();
+      if (!source_web_contents_->ForSecurityDropFullscreen(
+              /*display_id=*/display::kInvalidDisplayId)) {
+        return;
+      }
 
       Browser* browser = GetBrowser();
       if (browser) {
diff --git a/chrome/browser/ui/browser.cc b/chrome/browser/ui/browser.cc
index e7e0bd4..8ad020f 100644
--- a/chrome/browser/ui/browser.cc
+++ b/chrome/browser/ui/browser.cc
@@ -2660,17 +2660,17 @@
   permissions::PermissionRequestManager* permission_request_manager =
       permissions::PermissionRequestManager::FromWebContents(web_contents);
   if (permission_request_manager) {
-    // At this point, there will be UI presented, and running a dialog causes an
-    // exit to webpage-initiated fullscreen. http://crbug.com/41322524
-    base::ScopedClosureRunner fullscreen_block =
-        web_contents->ForSecurityDropFullscreen(
-            /*display_id=*/display::kInvalidDisplayId);
+    auto blocker = web_contents->ForSecurityDropFullscreen(
+        /*display_id=*/display::kInvalidDisplayId);
+    if (!blocker) {
+      return;
+    }
 
     permission_request_manager->AddRequest(
         requesting_frame,
         std::make_unique<
             custom_handlers::RegisterProtocolHandlerPermissionRequest>(
-            registry, handler, url, std::move(fullscreen_block)));
+            registry, handler, url, std::move(*blocker)));
   }
 }
 
diff --git a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc
index f7aa2cc..e6a5839c 100644
--- a/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc
+++ b/chrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc
@@ -78,8 +78,12 @@
     FullscreenController* fullscreen_controller =
         exclusive_access_manager->fullscreen_controller();
     if (fullscreen_controller->IsTabFullscreen()) {
-      fullscreen_blocker_ =
+      auto blocker =
           initiator()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
+      if (!blocker) {
+        return;
+      }
+      fullscreen_blocker_ = std::move(*blocker);
     }
   }
 
diff --git a/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc b/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
index 06bebdf..30a9f227 100644
--- a/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
+++ b/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
@@ -158,8 +158,11 @@
   // Drop fullscreen mode for the current webcontent so that the user sees the
   // URL.
   if (fullscreen_controller->IsTabFullscreen()) {
-    fullscreen_blocker_ =
+    auto blocker =
         contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
+    if (blocker) {
+      fullscreen_blocker_ = std::move(*blocker);
+    }
   }
 }
 
diff --git a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
index bf1ddf2..f176aab 100644
--- a/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
+++ b/chrome/browser/ui/views/permissions/permission_prompt_bubble.cc
@@ -46,8 +46,12 @@
                                                     ->fullscreen_controller();
   CHECK(fullscreen_controller);
   if (fullscreen_controller->IsTabFullscreen()) {
-    fullscreen_blocker_ =
+    auto blocker =
         web_contents()->ForSecurityDropFullscreen(display::kInvalidDisplayId);
+    if (!blocker) {
+      return;
+    }
+    fullscreen_blocker_ = std::move(*blocker);
   }
 
   raw_ptr<PermissionPromptBubbleBaseView> prompt_bubble =
diff --git a/chrome/browser/webshare/share_service_impl.cc b/chrome/browser/webshare/share_service_impl.cc
index 158d69f6..8804fd9 100644
--- a/chrome/browser/webshare/share_service_impl.cc
+++ b/chrome/browser/webshare/share_service_impl.cc
@@ -305,11 +305,12 @@
          blink::mojom::ShareError result) { std::move(callback).Run(result); },
       std::move(sharing_service_operation), std::move(callback)));
 #elif BUILDFLAG(IS_WIN)
-  // Drop fullscreen mode so the Share UI can be easily clicked away from,
-  // without clicking back into the web contents
-  base::ScopedClosureRunner fullscreen_block =
-      web_contents->ForSecurityDropFullscreen(
-          /*display_id=*/display::kInvalidDisplayId);
+  auto blocker = web_contents->ForSecurityDropFullscreen(
+      /*display_id=*/display::kInvalidDisplayId);
+  if (!blocker) {
+    std::move(callback).Run(blink::mojom::ShareError::PERMISSION_DENIED);
+    return;
+  }
 
   auto share_operation = std::make_unique<webshare::ShareOperation>(
       title, text, share_url, web_contents);
@@ -323,7 +324,7 @@
             fullscreen_block.RunAndReset();
             std::move(callback).Run(result);
           },
-          std::move(share_operation), std::move(fullscreen_block),
+          std::move(share_operation), std::move(*blocker),
           std::move(callback)));
 #else
   NOTREACHED();
diff --git a/content/browser/file_system_access/file_system_access_manager_impl.cc b/content/browser/file_system_access/file_system_access_manager_impl.cc
index 12dabda..d027b6ad 100644
--- a/content/browser/file_system_access/file_system_access_manager_impl.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/web_contents/web_contents_impl_browsertest.cc b/content/browser/web_contents/web_contents_impl_browsertest.cc
index fccff6d..a4749d3 100644
--- a/content/browser/web_contents/web_contents_impl_browsertest.cc
+++ b/content/browser/web_contents/web_contents_impl_browsertest.cc
@@ -4018,9 +4018,9 @@
 
   // While the |fullscreen_block| is in scope, fullscreen should fail with an
   // error.
-  base::ScopedClosureRunner fullscreen_block =
-      web_contents->ForSecurityDropFullscreen(
-          /*display_id=*/display::kInvalidDisplayId);
+  auto blocker = web_contents->ForSecurityDropFullscreen(
+      /*display_id=*/display::kInvalidDisplayId);
+  ASSERT_TRUE(blocker.has_value());
 
   EXPECT_TRUE(ExecJs(main_frame, "document.body.requestFullscreen();",
                      EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
Loading diff…

Original Bug Report

reported by [email protected]

Use-After-Free in WebContentsImpl::SetWindowRect

Use-After-Free in WebContentsImpl::SetWindowRect via ForSecurityDropFullscreen

Summary

WebContentsImpl::SetWindowRect() calls ForSecurityDropFullscreen() which internally calls ExitFullscreen() on other WebContents. On Windows, this can spin a nested message loop via ::SetWindowPos. During this nested loop, the calling WebContents can be destroyed (e.g., via window.close() from a popup’s fullscreenchange handler). The subsequent call to delegate_->SetContentsBounds(this, bounds) then reads from the freed WebContentsImpl object.

This is a variant of my earlier report, Bug 505045913 (UAF in RenderFrameHostImpl::ExitFullscreen). That fix added a WeakPtr guard in ExitFullscreen, but the same nested message loop race exists in SetWindowRect via ForSecurityDropFullscreen and was not addressed.

Version

Chrome 149.0.7805.0, Windows x64, ASAN build (is_asan=true is_debug=false symbol_level=1)

Steps to Reproduce

  1. Build Chrome with ASAN
  2. Apply patch.diff (RunLoop simulating ::SetWindowPos nested message loop)
  3. Launch: chrome --no-sandbox poc.html
  4. Click Start — a popup opens
  5. Click the popup body — it enters fullscreen, opener auto-resizes
  6. ASAN reports heap-use-after-free in SetWindowRect

Root Cause

content/browser/web_contents/web_contents_impl.cc:

void WebContentsImpl::SetWindowRect(const gfx::Rect& new_bounds) {
  // ...
  ForSecurityDropFullscreen(display_id).RunAndReset();  // [1]
  delegate_->SetContentsBounds(this, bounds);            // [2]
}

[1] ForSecurityDropFullscreen iterates fullscreen WebContents and calls ExitFullscreen() on each. On Windows, ExitFullscreenExitFullscreenMode::SetWindowPos spins a nested message loop. During this loop, Mojo IPCs are processed — including RequestClose from the popup’s fullscreenchange handler calling opener.close() — destroying this WebContents.

[2] delegate_->SetContentsBounds(this, bounds) reads this->delegate_ from the freed 5592-byte WebContentsImpl allocation.

The ExitFullscreen fix (Bug 505045913) added a WeakPtr guard in RenderFrameHostImpl::ExitFullscreen(), but SetWindowRect reaches the same nested message loop via ForSecurityDropFullscreen without any guard.

ASAN Trace

==17584==ERROR: AddressSanitizer: heap-use-after-free on address 0x1218f580c210
READ of size 8 at 0x1218f580c210 thread T0
    #0 in content::WebContentsImpl::SetWindowRect web_contents_impl.cc:9737
    #1 in content::RenderFrameHostImpl::SetWindowRect render_frame_host_impl.cc:7809

freed by thread T0 here:
    #0 in operator delete
    #1 in content::WebContentsImpl::~WebContentsImpl web_contents_impl.cc:1370
    #7 in content::WebContentsImpl::Close web_contents_impl.cc:9699
    #8 in content::RenderFrameHostImpl::ClosePageIgnoringUnloadEvents render_frame_host_impl.cc:7769

MiraclePtr Status: NOT PROTECTED
This crash is still exploitable with MiraclePtr.

Patch Explanation

patch.diff adds a base::RunLoop pump between ForSecurityDropFullscreen() and SetContentsBounds(). This simulates the natural race from the ::SetWindowPos nested message loop on Windows, allowing the pending RequestClose Mojo IPC to execute and destroy the WebContents.

Suggested Fix

fix.diff adds a base::WeakPtr guard after ForSecurityDropFullscreen(), matching the pattern from the ExitFullscreen fix (Bug 505045913):

base::WeakPtr<WebContentsImpl> weak_this = weak_factory_.GetWeakPtr();
ForSecurityDropFullscreen(display_id).RunAndReset();
if (!weak_this) {
  return;
}
delegate_->SetContentsBounds(this, bounds);

Reporter Credit: Mihnea Nicolau

View on issue tracker