CVE-2026-11648
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/file_system_access/chrome_file_system_access_permission_context.cc |
modified | |
ifchrome/browser/renderer_context_menu/render_view_context_menu.cc |
modified | |
ifchrome/browser/ui/browser.cc |
modified | |
ifchrome/browser/ui/views/media_router/media_router_dialog_controller_views.cc |
modified | |
ifchrome/browser/ui/views/permissions/chooser_bubble_ui.cc |
modified | |
ifchrome/browser/ui/views/permissions/permission_prompt_bubble.cc |
modified | |
ifchrome/browser/webshare/share_service_impl.cc |
modified |
Files Changed
chrome/browser/file_system_access/chrome_file_system_access_permission_context.ccchrome/browser/renderer_context_menu/render_view_context_menu.ccchrome/browser/ui/browser.ccchrome/browser/ui/views/media_router/media_router_dialog_controller_views.ccchrome/browser/ui/views/permissions/chooser_bubble_ui.ccchrome/browser/ui/views/permissions/permission_prompt_bubble.ccchrome/browser/webshare/share_service_impl.cccontent/browser/file_system_access/file_system_access_manager_impl.cc
Patch
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
Regression Test / PoC
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));
Original Bug Report
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
- Build Chrome with ASAN
- Apply
patch.diff(RunLoop simulating::SetWindowPosnested message loop) - Launch:
chrome --no-sandbox poc.html - Click Start — a popup opens
- Click the popup body — it enters fullscreen, opener auto-resizes
- 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, ExitFullscreen → ExitFullscreenMode → ::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