Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chrome for iOS
DescriptionUse after free in Chrome for iOS
ComponentChrome for iOS
Sub-componentiOS
Bug ClassUAF
Tracker480153486
Fix commit93ab2329ea84 (chromium/src) +287/-1
CISA KEVNot listed
CreditedQadhafy Muhammad Tera
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
modified
if
ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm
modified
TEST_F
ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
modified
StubAppLauncherTabHelperDelegate
ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
modified
WebStateDelegateBrowserAgentTest
ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
modified

Files Changed

  • ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h
  • ios/chrome/browser/web/model/BUILD.gn
  • ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm
  • ios/chrome/browser/web/model/DEPS
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
  • ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
From 93ab2329ea8458baa9c9db6e6252ec1785e452fd Mon Sep 17 00:00:00 2001
From: Justin <[email protected]>
Date: Fri, 25 Sep 2026 09:01:00 -0700
Subject: [PATCH] ios: Drop window open and close requests on call prompt launch

Launching a call URL (e.g., facetime:, facetime-audio:, tel:) shows
an iOS system confirmation dialog over Chrome while keeping the scene
in ForegroundInactive. While AppLauncherTabHelper defers navigation
policy decisions in the WebState until the prompt resolves, requests
to open or close a window could still change the active tab behind
the system dialog.

Expose AppLauncherTabHelper::IsCallPromptLaunchPending() and drop
CreateNewWebState() and CloseWebState() requests while a call prompt
launch is pending in the active WebState. Also override
ShouldAllowResponse() in AppLauncherTabHelper to defer navigation
responses until pending app launches complete.

Bug: 480153486
Change-Id: I6ca9c6396efa72b44e72ecb9cfaee8fac7bfe125
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8446840
Commit-Queue: Justin Cohen <[email protected]>
Reviewed-by: Mike Dougherty <[email protected]>
Auto-Submit: Justin Cohen <[email protected]>
Reviewed-by: Gauthier Ambard <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1704791}
---
diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h
index 5581c76..de7ab552 100644
--- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h
+++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h
@@ -39,6 +39,12 @@
       id<AppLauncherTabHelperBrowserPresentationProvider>
           browser_presentation_provider);
 
+  // Returns true if an app launch for a call scheme that shows a system prompt
+  // (e.g. facetime:, facetime-audio:, tel:) is currently pending completion.
+  bool IsCallPromptLaunchPending() const {
+    return is_call_prompt_launch_pending_;
+  }
+
   // Requests to open the application with `url`.
   // The method checks if the application for `url` has been opened repeatedly
   // by the `source_page_url` page in a short time frame, in that case a prompt
@@ -57,6 +63,10 @@
       NSURLRequest* request,
       web::WebStatePolicyDecider::RequestInfo request_info,
       web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;
+  void ShouldAllowResponse(
+      NSURLResponse* response,
+      web::WebStatePolicyDecider::ResponseInfo response_info,
+      web::WebStatePolicyDecider::PolicyDecisionCallback callback) override;
 
  protected:
   // Constructor for AppLauncherTabHelper. `abuse_detector` provides policy for
@@ -143,6 +153,10 @@
   // calling `LaunchAppForTabHelper()` on the delegate.
   bool is_app_launch_request_pending_ = false;
 
+  // Whether there is a pending app launch request for a call URL scheme that
+  // displays a system prompt over Chrome.
+  bool is_call_prompt_launch_pending_ = false;
+
   // Stores callbacks which should be called once the ongoing app launch
   // completes. When `ShouldAllowRequest()` asks this tab helper for a policy
   // decision, the call to the policy decision callback might need to be delayed
diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
index d01f8bb7..834bd1d 100644
--- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
+++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.mm
@@ -178,6 +178,7 @@
     case ExternalAppLaunchPolicyAllow: {
       if (delegate_) {
         is_app_launch_request_pending_ = true;
+        is_call_prompt_launch_pending_ = UrlHasCallWithPromptScheme(url);
         delegate_->LaunchAppForTabHelper(
             this, url,
             base::BindOnce(&AppLauncherTabHelper::OnAppLaunchCompleted,
@@ -216,6 +217,7 @@
   }
 
   is_app_launch_request_pending_ = true;
+  is_call_prompt_launch_pending_ = UrlHasCallWithPromptScheme(url);
   delegate_->LaunchAppForTabHelper(
       this, url,
       base::BindOnce(&AppLauncherTabHelper::OnAppLaunchTried,
@@ -255,6 +257,7 @@
 
 void AppLauncherTabHelper::LaunchAppRequestCompleted() {
   is_app_launch_request_pending_ = false;
+  is_call_prompt_launch_pending_ = false;
   is_prompt_active_ = false;
 
   // Some of the callback may destruct `this`, so post the execution to remove
@@ -301,6 +304,19 @@
   }
 }
 
+void AppLauncherTabHelper::ShouldAllowResponse(
+    NSURLResponse* response,
+    web::WebStatePolicyDecider::ResponseInfo response_info,
+    web::WebStatePolicyDecider::PolicyDecisionCallback callback) {
+  if (is_app_launch_request_pending_ || is_prompt_active_) {
+    callbacks_waiting_for_app_launch_completion_.push_back(
+        base::BindOnce(std::move(callback),
+                       web::WebStatePolicyDecider::PolicyDecision::Allow()));
+    return;
+  }
+  std::move(callback).Run(web::WebStatePolicyDecider::PolicyDecision::Allow());
+}
+
 AppLauncherTabHelper::PolicyDecisionAndOptionalAppLaunchRequest
 AppLauncherTabHelper::GetPolicyDecisionAndOptionalAppLaunchRequest(
     NSURLRequest* request,
diff --git a/ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm b/ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm
index 73154f5a..b63a09f 100644
--- a/ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm
+++ b/ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm
@@ -15,6 +15,7 @@
 #import "components/content_settings/core/common/content_settings.h"
 #import "components/enterprise/client_certificates/ios/certificate_provisioning_service_ios.h"
 #import "components/enterprise/client_certificates/ios/client_identity_ios.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h"
 #import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
 #import "ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.h"
 #import "ios/chrome/browser/dialogs/ui_bundled/nsurl_protection_space_util.h"
@@ -93,6 +94,18 @@
   std::move(callback).Run(false);
 }
 
+// Returns whether window open or close requests should be dropped because a
+// system-prompted call launch (e.g. facetime:, tel:) is pending in
+// `active_web_state`.
+bool ShouldDropWindowRequests(web::WebState* active_web_state) {
+  if (!active_web_state) {
+    return false;
+  }
+  AppLauncherTabHelper* helper =
+      AppLauncherTabHelper::FromWebState(active_web_state);
+  return helper && helper->IsCallPromptLaunchPending();
+}
+
 // Returns true if a supervised user attempts to access the microphone or camera
 // content setting when a parent has explicitly set site settings controls to
 // block permissions.
@@ -255,6 +268,10 @@
     return nullptr;
   }
 
+  if (ShouldDropWindowRequests(web_state_list_->GetActiveWebState())) {
+    return nullptr;
+  }
+
   // Check if requested web state is a popup and block it if necessary.
   if (!initiated_by_user) {
     auto* helper = BlockedPopupTabHelper::FromWebState(source);
@@ -280,7 +297,8 @@
 
 void WebStateDelegateBrowserAgent::CloseWebState(web::WebState* source) {
   int index = web_state_list_->GetIndexOfWebState(source);
-  if (index != WebStateList::kInvalidIndex) {
+  if (index != WebStateList::kInvalidIndex &&
+      !ShouldDropWindowRequests(web_state_list_->GetActiveWebState())) {
     web_state_list_->CloseWebStateAt(index,
                                      WebStateList::ClosingReason::kUserAction);
   }
diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
index e2b1e14..fe41465 100644
--- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
+++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
@@ -15,6 +15,7 @@
 #import "base/memory/scoped_refptr.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/scoped_feature_list.h"
+#import "base/test/test_future.h"
 #import "base/time/default_clock.h"
 #import "components/policy/core/browser/url_list/policy_blocklist_service.h"
 #import "components/policy/policy_constants.h"
@@ -492,6 +493,90 @@
   EXPECT_TRUE(policy_decision.ShouldAllowNavigation());
 }
 
+// Test that IsCallPromptLaunchPending() returns true only while a call-prompt
+// URL launch (e.g. facetime-audio:) is pending completion.
+TEST_F(AppLauncherTabHelperTest, IsCallPromptLaunchPending) {
+  delegate_.SetShouldCompleteAppLaunchImmediately(false);
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+
+  // Non-call app schemes do not set IsCallPromptLaunchPending().
+  EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+  delegate_.CompleteAppLaunch();
+  delegate_.CompleteBackToApp();
+
+  // Call schemes (such as facetime-audio:) set IsCallPromptLaunchPending()
+  // until the scene becomes active again.
+  EXPECT_FALSE(TestShouldAllowRequest(@"facetime-audio://+12345551212",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  EXPECT_TRUE(tab_helper_->IsCallPromptLaunchPending());
+
+  delegate_.CompleteAppLaunch();
+  EXPECT_TRUE(tab_helper_->IsCallPromptLaunchPending());
+
+  delegate_.CompleteBackToApp();
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+}
+
+// Test that ShouldAllowResponse() allows navigation response immediately when
+// no app launch is pending.
+TEST_F(AppLauncherTabHelperTest, ShouldAllowResponseWhenNoAppLaunchPending) {
+  NSURL* url = [NSURL URLWithString:@"https://example.com"];
+  NSURLResponse* response = [[NSURLResponse alloc] initWithURL:url
+                                                      MIMEType:@"text/html"
+                                         expectedContentLength:0
+                                              textEncodingName:nil];
+  base::test::TestFuture<web::WebStatePolicyDecider::PolicyDecision> future;
+  tab_helper_->ShouldAllowResponse(
+      response,
+      web::WebStatePolicyDecider::ResponseInfo(/*for_main_frame=*/true),
+      future.GetCallback());
+  EXPECT_TRUE(future.IsReady());
+  EXPECT_TRUE(future.Get().ShouldAllowNavigation());
+}
+
+// Test that ShouldAllowResponse() waits for any pending app launch and scene
+// activation before allowing a navigation response to commit.
+TEST_F(AppLauncherTabHelperTest, ShouldAllowResponseWhileAppLaunchPending) {
+  delegate_.SetShouldCompleteAppLaunchImmediately(false);
+
+  EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  ASSERT_TRUE(delegate_.IsAppLaunchCompletionPending());
+
+  NSURL* url = [NSURL URLWithString:@"https://example.com"];
+  NSURLResponse* response = [[NSURLResponse alloc] initWithURL:url
+                                                      MIMEType:@"text/html"
+                                         expectedContentLength:0
+                                              textEncodingName:nil];
+  base::test::TestFuture<web::WebStatePolicyDecider::PolicyDecision> future;
+  tab_helper_->ShouldAllowResponse(
+      response,
+      web::WebStatePolicyDecider::ResponseInfo(/*for_main_frame=*/true),
+      future.GetCallback());
+  EXPECT_FALSE(future.IsReady());
+
+  delegate_.CompleteAppLaunch();
+  EXPECT_FALSE(future.IsReady());
+
+  delegate_.CompleteBackToApp();
+  ASSERT_TRUE(future.Wait());
+  EXPECT_TRUE(future.Get().ShouldAllowNavigation());
+}
+
 // Tests that a valid URL does not launch app when launch policy is to block.
 TEST_F(AppLauncherTabHelperTest, AbuseDetectorPolicyBlockedForValidUrl) {
   abuse_detector_.policy = ExternalAppLaunchPolicyBlock;
diff --git a/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm b/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
index bcff037a..69785fd 100644
--- a/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
+++ b/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
@@ -10,6 +10,10 @@
 #import "base/test/scoped_feature_list.h"
 #import "base/test/test_future.h"
 #import "components/content_settings/core/browser/host_content_settings_map.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_abuse_detector.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_browser_presentation_provider.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_delegate.h"
 #import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
 #import "ios/chrome/browser/enterprise/data_controls/model/data_controls_tab_helper.h"
 #import "ios/chrome/browser/overlays/model/public/overlay_request.h"
@@ -41,6 +45,55 @@
 const char kURL1[] = "https://www.some.url.com";
 const char kURL2[] = "https://www.some.url2.com";
 
+namespace {
+
+class StubAppLauncherTabHelperDelegate : public AppLauncherTabHelperDelegate {
+ public:
+  void CompleteAppLaunch() {
+    CHECK(app_launch_completion_);
+    CHECK(back_to_app_completion_);
+    std::move(app_launch_completion_).Run(/*success=*/true);
+    std::move(back_to_app_completion_).Run();
+  }
+
+  // Whether `LaunchAppForTabHelper()` was called and the launch has not been
+  // completed yet.
+  bool IsAppLaunchPending() const { return !!app_launch_completion_; }
+
+  // AppLauncherTabHelperDelegate:
+  void LaunchAppForTabHelper(
+      AppLauncherTabHelper* tab_helper,
+      const GURL& url,
+      base::OnceCallback<void(bool)> completion,
+      base::OnceCallback<void()> back_to_app_completion) override {
+    app_launch_completion_ = std::move(completion);
+    back_to_app_completion_ = std::move(back_to_app_completion);
+  }
+  void ShowAppLaunchAlert(AppLauncherTabHelper* tab_helper,
+                          AppLauncherAlertCause cause,
+                          base::OnceCallback<void(bool)> completion) override {
+    std::move(completion).Run(/*user_allowed=*/false);
+  }
+
+ private:
+  base::OnceCallback<void(bool)> app_launch_completion_;
+  base::OnceCallback<void()> back_to_app_completion_;
+};
+
+}  // namespace
+
+@interface WebStateDelegateTestAppLauncherPresentationProvider
+    : NSObject <AppLauncherTabHelperBrowserPresentationProvider>
+@end
+
+@implementation WebStateDelegateTestAppLauncherPresentationProvider
+
+- (BOOL)isBrowserPresentingUI {
+  return NO;
+}
+
+@end
+
 // Test fixture for WebStateDelegateTabHelper.
 class WebStateDelegateBrowserAgentTest : public PlatformTest {
  public:
@@ -49,6 +102,8 @@
     browser_ = std::make_unique<TestBrowser>(profile_.get());
     TabInsertionBrowserAgent::CreateForBrowser(browser_.get());
     WebStateDelegateBrowserAgent::CreateForBrowser(browser_.get());
+    app_launcher_presentation_provider_ =
+        [[WebStateDelegateTestAppLauncherPresentationProvider alloc] init];
   }
   ~WebStateDelegateBrowserAgentTest() override = default;
 
@@ -80,9 +135,24 @@
     return web_state_list->GetActiveWebState();
   }
 
+  AppLauncherTabHelper* AttachAppLauncherTabHelper(web::WebState* web_state) {
+    AppLauncherTabHelper::CreateForWebState(
+        web_state, [[AppLauncherAbuseDetector alloc] init],
+        /*incognito=*/false);
+    AppLauncherTabHelper* tab_helper =
+        AppLauncherTabHelper::FromWebState(web_state);
+    tab_helper->SetDelegate(&app_launcher_delegate_);
+    tab_helper->SetBrowserPresentationProvider(
+        app_launcher_presentation_provider_);
+    return tab_helper;
+  }
+
  protected:
   web::WebTaskEnvironment task_environment_;
   std::unique_ptr<TestProfileIOS> profile_;
+  StubAppLauncherTabHelperDelegate app_launcher_delegate_;
+  WebStateDelegateTestAppLauncherPresentationProvider*
+      app_launcher_presentation_provider_;
   std::unique_ptr<TestBrowser> browser_;
 };
 
@@ -116,6 +186,86 @@
   EXPECT_EQ(browser_->GetWebStateList()->count(), 1);
 }
 
+// Test that CreateNewWebState() and CloseWebState() are dropped (including
+// from background opener WebStates) while a call-prompt app launch (e.g.
+// facetime-audio:) is pending in the active WebState, and allowed again once
+// the launch resolves.
+TEST_F(WebStateDelegateBrowserAgentTest,
+       DropWindowRequestsDuringCallPromptLaunch) {
+  web::WebState* opener_web_state = InsertNewWebState(GURL(kURL1));
+  AttachAppLauncherTabHelper(opener_web_state);
+  web::WebState* active_web_state = InsertNewWebState(GURL(kURL1));
+  AppLauncherTabHelper* tab_helper =
+      AttachAppLauncherTabHelper(active_web_state);
+  active_web_state->WasShown();
+
+  tab_helper->RequestToLaunchApp(GURL("facetime-audio://+1234"), GURL(kURL1),
+                                 /*link_transition=*/true,
+                                 /*is_user_initiated=*/true,
+                                 /*user_tapped_recently=*/true);
+  ASSERT_TRUE(tab_helper->IsCallPromptLaunchPending());
+
+  // Window open and close requests from both the active WebState and a
+  // background opener WebState are dropped while the call prompt is pending.
+  EXPECT_EQ(nullptr, delegate()->CreateNewWebState(
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
index e2b1e14..fe41465 100644
--- a/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
+++ b/ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_unittest.mm
@@ -15,6 +15,7 @@
 #import "base/memory/scoped_refptr.h"
 #import "base/strings/sys_string_conversions.h"
 #import "base/test/scoped_feature_list.h"
+#import "base/test/test_future.h"
 #import "base/time/default_clock.h"
 #import "components/policy/core/browser/url_list/policy_blocklist_service.h"
 #import "components/policy/policy_constants.h"
@@ -492,6 +493,90 @@
   EXPECT_TRUE(policy_decision.ShouldAllowNavigation());
 }
 
+// Test that IsCallPromptLaunchPending() returns true only while a call-prompt
+// URL launch (e.g. facetime-audio:) is pending completion.
+TEST_F(AppLauncherTabHelperTest, IsCallPromptLaunchPending) {
+  delegate_.SetShouldCompleteAppLaunchImmediately(false);
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+
+  // Non-call app schemes do not set IsCallPromptLaunchPending().
+  EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+  delegate_.CompleteAppLaunch();
+  delegate_.CompleteBackToApp();
+
+  // Call schemes (such as facetime-audio:) set IsCallPromptLaunchPending()
+  // until the scene becomes active again.
+  EXPECT_FALSE(TestShouldAllowRequest(@"facetime-audio://+12345551212",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  EXPECT_TRUE(tab_helper_->IsCallPromptLaunchPending());
+
+  delegate_.CompleteAppLaunch();
+  EXPECT_TRUE(tab_helper_->IsCallPromptLaunchPending());
+
+  delegate_.CompleteBackToApp();
+  EXPECT_FALSE(tab_helper_->IsCallPromptLaunchPending());
+}
+
+// Test that ShouldAllowResponse() allows navigation response immediately when
+// no app launch is pending.
+TEST_F(AppLauncherTabHelperTest, ShouldAllowResponseWhenNoAppLaunchPending) {
+  NSURL* url = [NSURL URLWithString:@"https://example.com"];
+  NSURLResponse* response = [[NSURLResponse alloc] initWithURL:url
+                                                      MIMEType:@"text/html"
+                                         expectedContentLength:0
+                                              textEncodingName:nil];
+  base::test::TestFuture<web::WebStatePolicyDecider::PolicyDecision> future;
+  tab_helper_->ShouldAllowResponse(
+      response,
+      web::WebStatePolicyDecider::ResponseInfo(/*for_main_frame=*/true),
+      future.GetCallback());
+  EXPECT_TRUE(future.IsReady());
+  EXPECT_TRUE(future.Get().ShouldAllowNavigation());
+}
+
+// Test that ShouldAllowResponse() waits for any pending app launch and scene
+// activation before allowing a navigation response to commit.
+TEST_F(AppLauncherTabHelperTest, ShouldAllowResponseWhileAppLaunchPending) {
+  delegate_.SetShouldCompleteAppLaunchImmediately(false);
+
+  EXPECT_FALSE(TestShouldAllowRequest(@"valid://1234",
+                                      /*target_frame_is_main=*/true,
+                                      /*target_frame_is_cross_origin=*/false,
+                                      /*target_window_is_cross_origin=*/false,
+                                      /*is_user_initiated=*/true,
+                                      /*user_tapped_recently=*/true));
+  ASSERT_TRUE(delegate_.IsAppLaunchCompletionPending());
+
+  NSURL* url = [NSURL URLWithString:@"https://example.com"];
+  NSURLResponse* response = [[NSURLResponse alloc] initWithURL:url
+                                                      MIMEType:@"text/html"
+                                         expectedContentLength:0
+                                              textEncodingName:nil];
+  base::test::TestFuture<web::WebStatePolicyDecider::PolicyDecision> future;
+  tab_helper_->ShouldAllowResponse(
+      response,
+      web::WebStatePolicyDecider::ResponseInfo(/*for_main_frame=*/true),
+      future.GetCallback());
+  EXPECT_FALSE(future.IsReady());
+
+  delegate_.CompleteAppLaunch();
+  EXPECT_FALSE(future.IsReady());
+
+  delegate_.CompleteBackToApp();
+  ASSERT_TRUE(future.Wait());
+  EXPECT_TRUE(future.Get().ShouldAllowNavigation());
+}
+
 // Tests that a valid URL does not launch app when launch policy is to block.
 TEST_F(AppLauncherTabHelperTest, AbuseDetectorPolicyBlockedForValidUrl) {
   abuse_detector_.policy = ExternalAppLaunchPolicyBlock;
diff --git a/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm b/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
index bcff037a..69785fd 100644
--- a/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
+++ b/ios/chrome/browser/web/model/web_state_delegate_browser_agent_unittest.mm
@@ -10,6 +10,10 @@
 #import "base/test/scoped_feature_list.h"
 #import "base/test/test_future.h"
 #import "components/content_settings/core/browser/host_content_settings_map.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_abuse_detector.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_browser_presentation_provider.h"
+#import "ios/chrome/browser/app_launcher/model/app_launcher_tab_helper_delegate.h"
 #import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
 #import "ios/chrome/browser/enterprise/data_controls/model/data_controls_tab_helper.h"
 #import "ios/chrome/browser/overlays/model/public/overlay_request.h"
@@ -41,6 +45,55 @@
 const char kURL1[] = "https://www.some.url.com";
 const char kURL2[] = "https://www.some.url2.com";
 
+namespace {
+
+class StubAppLauncherTabHelperDelegate : public AppLauncherTabHelperDelegate {
+ public:
+  void CompleteAppLaunch() {
+    CHECK(app_launch_completion_);
+    CHECK(back_to_app_completion_);
+    std::move(app_launch_completion_).Run(/*success=*/true);
+    std::move(back_to_app_completion_).Run();
+  }
+
+  // Whether `LaunchAppForTabHelper()` was called and the launch has not been
+  // completed yet.
+  bool IsAppLaunchPending() const { return !!app_launch_completion_; }
+
+  // AppLauncherTabHelperDelegate:
+  void LaunchAppForTabHelper(
+      AppLauncherTabHelper* tab_helper,
+      const GURL& url,
+      base::OnceCallback<void(bool)> completion,
+      base::OnceCallback<void()> back_to_app_completion) override {
+    app_launch_completion_ = std::move(completion);
+    back_to_app_completion_ = std::move(back_to_app_completion);
+  }
+  void ShowAppLaunchAlert(AppLauncherTabHelper* tab_helper,
+                          AppLauncherAlertCause cause,
+                          base::OnceCallback<void(bool)> completion) override {
+    std::move(completion).Run(/*user_allowed=*/false);
+  }
+
+ private:
+  base::OnceCallback<void(bool)> app_launch_completion_;
+  base::OnceCallback<void()> back_to_app_completion_;
+};
+
+}  // namespace
+
+@interface WebStateDelegateTestAppLauncherPresentationProvider
+    : NSObject <AppLauncherTabHelperBrowserPresentationProvider>
+@end
+
+@implementation WebStateDelegateTestAppLauncherPresentationProvider
+
+- (BOOL)isBrowserPresentingUI {
+  return NO;
+}
+
+@end
+
 // Test fixture for WebStateDelegateTabHelper.
 class WebStateDelegateBrowserAgentTest : public PlatformTest {
  public:
@@ -49,6 +102,8 @@
     browser_ = std::make_unique<TestBrowser>(profile_.get());
     TabInsertionBrowserAgent::CreateForBrowser(browser_.get());
     WebStateDelegateBrowserAgent::CreateForBrowser(browser_.get());
+    app_launcher_presentation_provider_ =
+        [[WebStateDelegateTestAppLauncherPresentationProvider alloc] init];
   }
   ~WebStateDelegateBrowserAgentTest() override = default;
 
@@ -80,9 +135,24 @@
     return web_state_list->GetActiveWebState();
   }
 
+  AppLauncherTabHelper* AttachAppLauncherTabHelper(web::WebState* web_state) {
+    AppLauncherTabHelper::CreateForWebState(
+        web_state, [[AppLauncherAbuseDetector alloc] init],
+        /*incognito=*/false);
+    AppLauncherTabHelper* tab_helper =
+        AppLauncherTabHelper::FromWebState(web_state);
+    tab_helper->SetDelegate(&app_launcher_delegate_);
+    tab_helper->SetBrowserPresentationProvider(
+        app_launcher_presentation_provider_);
+    return tab_helper;
+  }
+
  protected:
   web::WebTaskEnvironment task_environment_;
   std::unique_ptr<TestProfileIOS> profile_;
+  StubAppLauncherTabHelperDelegate app_launcher_delegate_;
+  WebStateDelegateTestAppLauncherPresentationProvider*
+      app_launcher_presentation_provider_;
   std::unique_ptr<TestBrowser> browser_;
 };
 
@@ -116,6 +186,86 @@
   EXPECT_EQ(browser_->GetWebStateList()->count(), 1);
 }
 
+// Test that CreateNewWebState() and CloseWebState() are dropped (including
+// from background opener WebStates) while a call-prompt app launch (e.g.
+// facetime-audio:) is pending in the active WebState, and allowed again once
+// the launch resolves.
+TEST_F(WebStateDelegateBrowserAgentTest,
+       DropWindowRequestsDuringCallPromptLaunch) {
+  web::WebState* opener_web_state = InsertNewWebState(GURL(kURL1));
+  AttachAppLauncherTabHelper(opener_web_state);
+  web::WebState* active_web_state = InsertNewWebState(GURL(kURL1));
+  AppLauncherTabHelper* tab_helper =
+      AttachAppLauncherTabHelper(active_web_state);
+  active_web_state->WasShown();
+
+  tab_helper->RequestToLaunchApp(GURL("facetime-audio://+1234"), GURL(kURL1),
+                                 /*link_transition=*/true,
+                                 /*is_user_initiated=*/true,
+                                 /*user_tapped_recently=*/true);
+  ASSERT_TRUE(tab_helper->IsCallPromptLaunchPending());
+
+  // Window open and close requests from both the active WebState and a
+  // background opener WebState are dropped while the call prompt is pending.
+  EXPECT_EQ(nullptr, delegate()->CreateNewWebState(
+                         active_web_state, GURL(kURL2), GURL(kURL1), true));
+  EXPECT_EQ(nullptr, delegate()->CreateNewWebState(
+                         opener_web_state, GURL(kURL2), GURL(kURL1), true));
+  delegate()->CloseWebState(active_web_state);
+  delegate()->CloseWebState(opener_web_state);
+  EXPECT_EQ(browser_->GetWebStateList()->count(), 2);
+
+  // Once the call prompt launch completes, window requests succeed again.
+  app_launcher_delegate_.CompleteAppLaunch();
+  ASSERT_FALSE(tab_helper->IsCallPromptLaunchPending());
+  EXPECT_NE(nullptr, delegate()->CreateNewWebState(
+                         active_web_state, GURL(kURL2), GURL(kURL1), true));
+  EXPECT_EQ(browser_->GetWebStateList()->count(), 3);
+}
+
+// Test that window open and close requests are allowed while an app launch
+// that shows no iOS system prompt (e.g. calshow:) is pending. Chrome is
+// backgrounded for the whole round-trip for those schemes, so the renderer is
+// suspended and there is nothing to spoof; dropping the requests would instead
+// orphan tabs, since a page that launches an app and then closes itself gets
+// no second chance to call `window.close()`.
+// TODO(crbug.com/40166678): The test fails on device.
+#if TARGET_OS_SIMULATOR
+#define MAYBE_AllowWindowRequestsDuringNonPromptLaunch \
+  AllowWindowRequestsDuringNonPromptLaunch
+#else
+#define MAYBE_AllowWindowRequestsDuringNonPromptLaunch \
+  DISABLED_AllowWindowRequestsDuringNonPromptLaunch
+#endif
+TEST_F(WebStateDelegateBrowserAgentTest,
+       MAYBE_AllowWindowRequestsDuringNonPromptLaunch) {
+  WebStateList* web_state_list = browser_->GetWebStateList();
+  web::WebState* active_web_state = InsertNewWebState(GURL(kURL1));
+  AppLauncherTabHelper* tab_helper =
+      AttachAppLauncherTabHelper(active_web_state);
+  active_web_state->WasShown();
+
+  tab_helper->RequestToLaunchApp(GURL("calshow://1234"), GURL(kURL1),
+                                 /*link_transition=*/true,
+                                 /*is_user_initiated=*/true,
+                                 /*user_tapped_recently=*/true);
+  // The launch is under way, but it shows no system prompt.
+  ASSERT_TRUE(app_launcher_delegate_.IsAppLaunchPending());
+  ASSERT_FALSE(tab_helper->IsCallPromptLaunchPending());
+
+  EXPECT_NE(nullptr, delegate()->CreateNewWebState(
+                         active_web_state, GURL(kURL2), GURL(kURL1), true));
+  EXPECT_EQ(web_state_list->count(), 2);
+
+  // The DOM insertion above activated the new WebState. Re-activate the
+  // launching WebState so the close request below is evaluated against it.
+  web_state_list->ActivateWebStateAt(
+      web_state_list->GetIndexOfWebState(active_web_state));
+
+  delegate()->CloseWebState(active_web_state);
+  EXPECT_EQ(web_state_list->count(), 1);
+}
+
 // Test that CloseWebState() removed the web state from the web state list.
 TEST_F(WebStateDelegateBrowserAgentTest, CloseWebState) {
   web::WebState* web_state = InsertNewWebState(GURL(kURL1));
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.