diff --git a/DEPS b/DEPS
index a341c87c..f98f90b4 100644
--- a/DEPS
+++ b/DEPS
@@ -304,15 +304,15 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '91a14b75ee56bdba22d97d8573e7878ce17bfa8c',
+  'skia_revision': '8c2e58a736090658f5e51104f0e21e6336fac47a',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': '7ab98454e908c42f4055901d90db0801c119e4d1',
+  'v8_revision': '3a6551d622111e5cc6ce3c6328c0226bc99ab6e5',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ANGLE
   # and whatever else without interference from each other.
-  'angle_revision': 'df202660de19da8e96078a4d35ba90131aadbce6',
+  'angle_revision': '8d2f5e0ed46244ead609a14bc4a7798ea67dfb72',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -383,7 +383,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling devtools-frontend
   # and whatever else without interference from each other.
-  'devtools_frontend_revision': '11fa1c32d0fad40398f283b6c3fa985497768fb0',
+  'devtools_frontend_revision': 'd1aa06917bd906ec743e27fe47c5c7ec4bd9732b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libprotobuf-mutator
   # and whatever else without interference from each other.
@@ -419,7 +419,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'dawn_revision': 'ffe356d558bce8b912890d8080dc7be94c0b0c2d',
+  'dawn_revision': 'ad98166fe71e1c5dcbc6981e2c7082960000b112',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -770,7 +770,7 @@
 
   'src/clank': {
     'url': 'https://chrome-internal.googlesource.com/clank/internal/apps.git' + '@' +
-    '4d7ad3ac8f5d7ff0b45a2b8b19ad08add2968ce9',
+    '934e146d0f42ad72b9a19bf5f7923c83b4de4a86',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -959,7 +959,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'z1Ltgk5lzJuoEqOSTMM3F1QuAXbGFIEN6SQ0jX4qHd0C',
+          'version': 'amhjv4Hj48NMXw5Ntbp8u4o8TrRCD7nJxJsdEt10Cb0C',
       },
     ],
     'condition': 'checkout_android',
@@ -1210,7 +1210,7 @@
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
 
   'src/third_party/devtools-frontend-internal': {
-      'url': 'https://chrome-internal.googlesource.com/devtools/devtools-internal.git' + '@' + 'f998e8e412a064d152566fe3f6d59407dc1400dc',
+      'url': 'https://chrome-internal.googlesource.com/devtools/devtools-internal.git' + '@' + 'e016065f54348d1ca6848cb82e68bc743c8e076d',
     'condition': 'checkout_src_internal',
   },
 
@@ -1695,7 +1695,7 @@
       'packages': [
           {
               'package': 'chromium/third_party/r8',
-              'version': 'XBkol4a9AeFOtX-9cvNPpKCbqj9UAqZuoVvFop6AX9wC',
+              'version': 'PhbyY6VT6R_nwiYKM8E7SZ5EGYxvBqKo_aQtlY-wrg4C',
           },
       ],
       'condition': 'checkout_android',
@@ -1793,14 +1793,14 @@
       'packages': [
           {
               'package': 'chromium/third_party/turbine',
-              'version': 'uQFvRkwygckj0pmxUx9_4WqWm-VdcDxs2o1t3xyEDjYC',
+              'version': 't0TeGgk2CZr3B0HtEqBb60BSRwVPRJm9066izhJwzz0C',
           },
       ],
       'condition': 'checkout_android',
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@d7dfd14fa0d0bc1411976c7800a9076bab44524a',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@fbcb93d162f3756c2f4cdeb384fb02c37ab5a5e1',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + 'ebe84bec02c041d28f902da0214bf442743fc907',
@@ -1910,7 +1910,7 @@
     Var('chromium_git') + '/v8/v8.git' + '@' +  Var('v8_revision'),
 
   'src-internal': {
-    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@e606cde9a929d27e4ed1183e0f85ae8f2a594023',
+    'url': 'https://chrome-internal.googlesource.com/chrome/src-internal.git@2ab4c3e237edb91a9daa62f6b065ac8825ce880f',
     'condition': 'checkout_src_internal',
   },
 
@@ -1940,7 +1940,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/help_app/app',
-        'version': 'NLzCnLDN3V07zNPL3gpctuBdEfh-a75rVj0mNSk2lDgC',
+        'version': 'oIDm8HJ9PM6AMC5q9VI_i3qncs1iQ2leHxvRXcR7MEIC',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -1962,7 +1962,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/projector_app/app',
-        'version': '6LasY8XCK9rPkq-aAP6z5wyz8QbUJcoNls1jR1fmYYAC',
+        'version': 'm581vq4D8SsE0_bMxv_mFM3-o6wz1lnPYk030fF2LN0C',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
diff --git a/android_webview/browser/metrics/aw_component_metrics_provider_delegate.cc b/android_webview/browser/metrics/aw_component_metrics_provider_delegate.cc
index 252c532..3c59dca 100644
--- a/android_webview/browser/metrics/aw_component_metrics_provider_delegate.cc
+++ b/android_webview/browser/metrics/aw_component_metrics_provider_delegate.cc
@@ -46,12 +46,11 @@
   absl::optional<AppPackageNameLoggingRule> record =
       client_->GetCachedAppPackageNameLoggingRule();
   if (record.has_value()) {
-    components.push_back(
-        ComponentInfo(kWebViewAppsPackageNamesAllowlistComponentId, "",
-                      std::u16string(), record.value().GetVersion()));
+    components.emplace_back(kWebViewAppsPackageNamesAllowlistComponentId, "",
+                            std::u16string(), record.value().GetVersion(), "");
   }
 
   return components;
 }
 
-}  // namespace android_webview
\ No newline at end of file
+}  // namespace android_webview
diff --git a/ash/components/arc/net/arc_net_host_impl.cc b/ash/components/arc/net/arc_net_host_impl.cc
index 36d87cc..86d7d68 100644
--- a/ash/components/arc/net/arc_net_host_impl.cc
+++ b/ash/components/arc/net/arc_net_host_impl.cc
@@ -351,7 +351,7 @@
   if (shill_dict) {
     for (const auto* property :
          {shill::kStaticIPConfigProperty, shill::kSavedIPConfigProperty}) {
-      AddIpConfiguration(mojo.get(), shill_dict->FindKey(property));
+      AddIpConfiguration(mojo.get(), shill_dict->GetDict().Find(property));
     }
   }
 
diff --git a/ash/display/cursor_window_controller.cc b/ash/display/cursor_window_controller.cc
index 688d5f37..69ddf3b2 100644
--- a/ash/display/cursor_window_controller.cc
+++ b/ash/display/cursor_window_controller.cc
@@ -38,9 +38,10 @@
 #include "ui/gfx/image/image_skia.h"
 #include "ui/gfx/image/image_skia_operations.h"
 #include "ui/views/widget/widget.h"
-#include "ui/wm/core/cursors_aura.h"
+#include "ui/wm/core/cursor_util.h"
 
 namespace ash {
+
 namespace {
 
 const int kMinLargeCursorSize = 25;
diff --git a/ash/system/privacy_hub/camera_privacy_switch_controller.cc b/ash/system/privacy_hub/camera_privacy_switch_controller.cc
index 03a101b..b5710f4e 100644
--- a/ash/system/privacy_hub/camera_privacy_switch_controller.cc
+++ b/ash/system/privacy_hub/camera_privacy_switch_controller.cc
@@ -50,6 +50,10 @@
   }
 }
 
+PrivacyHubNotificationController* GetPrivacyHubNotificationController() {
+  return Shell::Get()->system_notification_controller()->privacy_hub();
+}
+
 }  // namespace
 
 CameraPrivacySwitchController::CameraPrivacySwitchController()
@@ -124,18 +128,12 @@
 
   if (pref_val == CameraSWPrivacySwitchSetting::kDisabled) {
     camera_used_while_deactivated_ = true;
-    Shell::Get()
-        ->system_notification_controller()
-        ->privacy_hub()
-        ->ShowSensorDisabledNotification(
-            PrivacyHubNotificationController::Sensor::kCamera);
+    GetPrivacyHubNotificationController()->ShowSensorDisabledNotification(
+        PrivacyHubNotificationController::Sensor::kCamera);
   } else {
     camera_used_while_deactivated_ = false;
-    Shell::Get()
-        ->system_notification_controller()
-        ->privacy_hub()
-        ->RemoveSensorDisabledNotification(
-            PrivacyHubNotificationController::Sensor::kCamera);
+    GetPrivacyHubNotificationController()->RemoveSensorDisabledNotification(
+        PrivacyHubNotificationController::Sensor::kCamera);
   }
 }
 
@@ -220,28 +218,22 @@
     active_applications_using_camera_count_--;
   }
 
-  // Notification should pop up when an application starts using the camera but
-  // the camera is disabled by the software switch.
-  if (application_added &&
-      GetUserSwitchPreference() == CameraSWPrivacySwitchSetting::kDisabled) {
-    camera_used_while_deactivated_ = true;
-    Shell::Get()
-        ->system_notification_controller()
-        ->privacy_hub()
-        ->ShowSensorDisabledNotification(
-            PrivacyHubNotificationController::Sensor::kCamera);
+  if (GetUserSwitchPreference() != CameraSWPrivacySwitchSetting::kDisabled) {
+    return;
   }
 
-  // Remove existing software switch notification when no application is using
-  // the camera anymore.
   if (active_applications_using_camera_count_ == 0 &&
       camera_used_while_deactivated_) {
     camera_used_while_deactivated_ = false;
-    Shell::Get()
-        ->system_notification_controller()
-        ->privacy_hub()
-        ->RemoveSensorDisabledNotification(
-            PrivacyHubNotificationController::Sensor::kCamera);
+    GetPrivacyHubNotificationController()->RemoveSensorDisabledNotification(
+        PrivacyHubNotificationController::Sensor::kCamera);
+  } else if (application_added) {
+    camera_used_while_deactivated_ = true;
+    GetPrivacyHubNotificationController()->ShowSensorDisabledNotification(
+        PrivacyHubNotificationController::Sensor::kCamera);
+  } else {
+    GetPrivacyHubNotificationController()->UpdateSensorDisabledNotification(
+        PrivacyHubNotificationController::Sensor::kCamera);
   }
 }
 
diff --git a/ash/system/privacy_hub/camera_privacy_switch_controller_unittest.cc b/ash/system/privacy_hub/camera_privacy_switch_controller_unittest.cc
index a715043..869cc3f2 100644
--- a/ash/system/privacy_hub/camera_privacy_switch_controller_unittest.cc
+++ b/ash/system/privacy_hub/camera_privacy_switch_controller_unittest.cc
@@ -65,6 +65,14 @@
     apps_accessing_camera_.insert(apps_accessing_camera_.begin(), app_name);
   }
 
+  void CloseAppAccessingCamera(const std::u16string& app_name) {
+    auto it = std::find(apps_accessing_camera_.begin(),
+                        apps_accessing_camera_.end(), app_name);
+    if (it != apps_accessing_camera_.end()) {
+      apps_accessing_camera_.erase(it);
+    }
+  }
+
  private:
   std::vector<std::u16string> apps_accessing_camera_;
 };
@@ -133,6 +141,12 @@
 
   void LaunchAppAccessingCamera(const std::u16string& app_name) {
     delegate_.LaunchAppAccessingCamera(app_name);
+    controller_->ActiveApplicationsChanged(/*application_added=*/true);
+  }
+
+  void CloseAppAccessingCamera(const std::u16string& app_name) {
+    delegate_.CloseAppAccessingCamera(app_name);
+    controller_->ActiveApplicationsChanged(/*application_added=*/false);
   }
 
   void WaitUntilNotificationRemoved(const std::string& notification_id) {
@@ -547,58 +561,63 @@
 
 // Tests if the camera software switch notification contains proper text.
 TEST_F(PrivacyHubCameraControllerTests, NotificationText) {
-  SetUserPref(true);
-
-  // The notification should not be in the message center initially.
-  EXPECT_FALSE(FindNotificationById(kPrivacyHubCameraOffNotificationId));
-
-  // This fakes launching an application with name "app_1_name".
-  LaunchAppAccessingCamera(u"app_1_name");
-  controller_->ActiveApplicationsChanged(/*application_added=*/true);
-
   // Disabling camera using the software switch.
   SetUserPref(false);
+  EXPECT_FALSE(FindNotificationById(kPrivacyHubCameraOffNotificationId));
 
-  // Notification should pop up. The notification body should contain the app
-  // name "app_1_name".
+  // Launch app1 that's accessing camera, a notification should be displayed
+  // with the application name in the notification body.
+  const std::u16string app1 = u"app1";
+  LaunchAppAccessingCamera(app1);
+
   message_center::Notification* notification =
       FindNotificationById(kPrivacyHubCameraOffNotificationId);
-  EXPECT_TRUE(notification);
+  ASSERT_TRUE(notification);
   EXPECT_EQ(
       l10n_util::GetStringUTF16(IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_TITLE),
       notification->title());
   EXPECT_EQ(
       l10n_util::GetStringFUTF16(
           IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME,
-          u"app_1_name"),
+          app1),
       notification->message());
 
-  // This fakes launching another application with name "app_2_name".
-  LaunchAppAccessingCamera(u"app_2_name");
-  controller_->ActiveApplicationsChanged(/*application_added=*/true);
+  // Launch app2 that's also accessing camera, a notification should be
+  // displayed again with both of the application names in the notification
+  // body.
+  const std::u16string app2 = u"app2";
+  LaunchAppAccessingCamera(app2);
 
-  // A new notification should pop up. The notification body should contain both
-  // the application names in order of most recently launched first.
   notification = FindNotificationById(kPrivacyHubCameraOffNotificationId);
-  EXPECT_TRUE(notification);
+  ASSERT_TRUE(notification);
   EXPECT_EQ(
       l10n_util::GetStringFUTF16(
           IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
-          u"app_2_name", u"app_1_name"),
+          app2, app1),
       notification->message());
 
-  // This fakes launching another application with name "app_3_name".
-  LaunchAppAccessingCamera(u"app_3_name");
-  controller_->ActiveApplicationsChanged(/*application_added=*/true);
+  // Launch app3 that's also accessing camera, a notification should be
+  // displayed again with generic text.
+  const std::u16string app3 = u"app3";
+  LaunchAppAccessingCamera(app3);
 
-  // A new notification should pop up. The notification body should not contain
-  // any application name as there are more than 2 applications attempting to
-  // access camera.
   notification = FindNotificationById(kPrivacyHubCameraOffNotificationId);
-  EXPECT_TRUE(notification);
+  ASSERT_TRUE(notification);
   EXPECT_EQ(l10n_util::GetStringUTF16(
                 IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE),
             notification->message());
+
+  // Close one of the applications. The notification should be updated to
+  // contain the name of the two remaining applications.
+  CloseAppAccessingCamera(app2);
+
+  notification = FindNotificationById(kPrivacyHubCameraOffNotificationId);
+  ASSERT_TRUE(notification);
+  EXPECT_EQ(
+      l10n_util::GetStringFUTF16(
+          IDS_PRIVACY_HUB_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
+          app3, app1),
+      notification->message());
 }
 
 TEST_F(PrivacyHubCameraControllerTests, MetricCollection) {
diff --git a/ash/system/privacy_hub/microphone_privacy_switch_controller.cc b/ash/system/privacy_hub/microphone_privacy_switch_controller.cc
index 56ec790..8b15b4f 100644
--- a/ash/system/privacy_hub/microphone_privacy_switch_controller.cc
+++ b/ash/system/privacy_hub/microphone_privacy_switch_controller.cc
@@ -135,10 +135,13 @@
   const bool stream_count_increased = input_stream_count > input_stream_count_;
   input_stream_count_ = input_stream_count;
 
-  if (stream_count_increased) {
-    SetMicrophoneNotificationVisible(input_stream_count_ && mic_mute_on_);
-  } else if (!input_stream_count_) {
+  if (!input_stream_count_) {
     SetMicrophoneNotificationVisible(false);
+  } else if (stream_count_increased) {
+    SetMicrophoneNotificationVisible(input_stream_count_ && mic_mute_on_);
+  } else if (mic_mute_on_) {
+    // Microphone is muted and stream count has decreased.
+    UpdateMicrophoneNotification();
   }
 }
 
@@ -188,4 +191,17 @@
   }
 }
 
+void MicrophonePrivacySwitchController::UpdateMicrophoneNotification() {
+  if (mic_muted_by_mute_switch_) {
+    mute_switch_notification_.Update();
+    return;
+  }
+
+  Shell::Get()
+      ->system_notification_controller()
+      ->privacy_hub()
+      ->UpdateSensorDisabledNotification(
+          PrivacyHubNotificationController::Sensor::kMicrophone);
+}
+
 }  // namespace ash
diff --git a/ash/system/privacy_hub/microphone_privacy_switch_controller.h b/ash/system/privacy_hub/microphone_privacy_switch_controller.h
index 05cbf91..cddc9c0 100644
--- a/ash/system/privacy_hub/microphone_privacy_switch_controller.h
+++ b/ash/system/privacy_hub/microphone_privacy_switch_controller.h
@@ -54,6 +54,10 @@
   // notification should be shown.
   void SetMicrophoneNotificationVisible(bool visible);
 
+  // Silently updates the message of the exiting microphone mute notification
+  // with the appropriate application names(s).
+  void UpdateMicrophoneNotification();
+
   size_t input_stream_count_ = 0;
   bool mic_mute_on_ = false;
   bool mic_muted_by_mute_switch_ = false;
diff --git a/ash/system/privacy_hub/microphone_privacy_switch_controller_unittest.cc b/ash/system/privacy_hub/microphone_privacy_switch_controller_unittest.cc
index e86d295e..f615e18 100644
--- a/ash/system/privacy_hub/microphone_privacy_switch_controller_unittest.cc
+++ b/ash/system/privacy_hub/microphone_privacy_switch_controller_unittest.cc
@@ -57,6 +57,14 @@
     }
   }
 
+  void CloseAppAccessingMicrophone(const std::u16string& app_name) {
+    auto it = std::find(apps_accessing_microphone_.begin(),
+                        apps_accessing_microphone_.end(), app_name);
+    if (it != apps_accessing_microphone_.end()) {
+      apps_accessing_microphone_.erase(it);
+    }
+  }
+
  private:
   std::vector<std::u16string> apps_accessing_microphone_;
 };
@@ -173,6 +181,10 @@
     delegate_.LaunchAppAccessingMicrophone(app_name);
   }
 
+  void CloseApp(const std::u16string& app_name) {
+    delegate_.CloseAppAccessingMicrophone(app_name);
+  }
+
   const base::HistogramTester& histogram_tester() const {
     return histogram_tester_;
   }
@@ -638,4 +650,74 @@
             GetNotification()->title());
 }
 
+TEST_F(PrivacyHubMicrophoneControllerTest, NotificationUpdatedWhenAppClosed) {
+  // No notification initially.
+  EXPECT_FALSE(GetNotification());
+
+  // Mute the mic using sw switch, still no notification.
+  MuteMicrophone();
+  EXPECT_FALSE(GetNotification());
+
+  // Launch app1 that's accessing the mic, a notification should be displayed
+  // with the application name in the notification body.
+  const std::u16string app1 = u"app1";
+  LaunchApp(app1);
+  SetNumberOfActiveInputStreams(1);
+  message_center::Notification* notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(
+      l10n_util::GetStringFUTF16(
+          IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME, app1),
+      notification_ptr->message());
+
+  // Launch app2 that's also accessing the mic, the microphone mute notification
+  // should be displayed again with both the application names in the
+  // notification body.
+  const std::u16string app2 = u"app2";
+  LaunchApp(app2);
+  SetNumberOfActiveInputStreams(2);
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(l10n_util::GetStringFUTF16(
+                IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
+                app2, app1),
+            notification_ptr->message());
+
+  // Close one of the applications. The notification message should be updated
+  // to only contain the name of the other application.
+  CloseApp(app1);
+  SetNumberOfActiveInputStreams(1);
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(
+      l10n_util::GetStringFUTF16(
+          IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME, app2),
+      notification_ptr->message());
+
+  // Test the HW switch notification case.
+  // HW switch is turned ON.
+  SetMicrophoneMuteSwitchState(/*muted=*/true);
+
+  // Launch the closed app (app1) again.
+  LaunchApp(app1);
+  SetNumberOfActiveInputStreams(2);
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(l10n_util::GetStringFUTF16(
+                IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
+                app1, app2),
+            notification_ptr->message());
+
+  // Closing one of the applications should remove the name of that application
+  // from the hw switch notification message.
+  CloseApp(app2);
+  SetNumberOfActiveInputStreams(1);
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(
+      l10n_util::GetStringFUTF16(
+          IDS_MICROPHONE_MUTED_NOTIFICATION_MESSAGE_WITH_ONE_APP_NAME, app1),
+      notification_ptr->message());
+}
+
 }  // namespace ash
diff --git a/ash/system/privacy_hub/privacy_hub_notification.cc b/ash/system/privacy_hub/privacy_hub_notification.cc
index 8c7c80ca..d9608fd 100644
--- a/ash/system/privacy_hub/privacy_hub_notification.cc
+++ b/ash/system/privacy_hub/privacy_hub_notification.cc
@@ -91,13 +91,8 @@
     remove_timer_.Stop();
     RemoveNotification(id_);
   }
-  const std::vector<std::u16string> apps = GetAppsAccessingSensors();
 
-  if (const size_t num_apps = apps.size(); num_apps < message_ids_.size()) {
-    builder_.SetMessageWithArgs(message_ids_.at(num_apps), apps);
-  } else {
-    builder_.SetMessageId(message_ids_.at(0));
-  }
+  SetNotificationMessage();
 
   message_center::MessageCenter::Get()->AddNotification(builder_.BuildPtr());
   last_time_shown_ = last_time_shown_.value_or(base::Time::Now());
@@ -120,6 +115,14 @@
   last_time_shown_.reset();
 }
 
+void PrivacyHubNotification::Update() {
+  if (message_center::MessageCenter::Get()->FindNotificationById(id_)) {
+    SetNotificationMessage();
+    message_center::MessageCenter::Get()->UpdateNotification(
+        id_, builder_.BuildPtr());
+  }
+}
+
 std::vector<std::u16string> PrivacyHubNotification::GetAppsAccessingSensors()
     const {
   std::vector<std::u16string> app_names;
@@ -143,4 +146,14 @@
   return app_names;
 }
 
+void PrivacyHubNotification::SetNotificationMessage() {
+  const std::vector<std::u16string> apps = GetAppsAccessingSensors();
+
+  if (const size_t num_apps = apps.size(); num_apps < message_ids_.size()) {
+    builder_.SetMessageWithArgs(message_ids_.at(num_apps), apps);
+  } else {
+    builder_.SetMessageId(message_ids_.at(0));
+  }
+}
+
 }  // namespace ash
diff --git a/ash/system/privacy_hub/privacy_hub_notification.h b/ash/system/privacy_hub/privacy_hub_notification.h
index b92d2f5..172ba23 100644
--- a/ash/system/privacy_hub/privacy_hub_notification.h
+++ b/ash/system/privacy_hub/privacy_hub_notification.h
@@ -87,6 +87,12 @@
   // remaining time and then hidden.
   void Hide();
 
+  // Silently updates the notification when needed, for example, when an
+  // application stops accessing a sensor and the name of that application needs
+  // to be removed from the notification without letting the notification pop up
+  // again.
+  void Update();
+
   // Get the underlying `SystemNotificationBuilder` to do modifications beyond
   // what this wrapper allows you to do. If you change the ID of the message
   // `Show()` and `Hide()` are not going to work reliably.
@@ -97,6 +103,10 @@
   // `message_ids_.size()` elements will be returned.
   std::vector<std::u16string> GetAppsAccessingSensors() const;
 
+  // Sets the notification message depending on the list of apps accessing the
+  // `sensors_for_apps_`.
+  void SetNotificationMessage();
+
   std::string id_;
   SystemNotificationBuilder builder_;
   MessageIds message_ids_;
diff --git a/ash/system/privacy_hub/privacy_hub_notification_controller.cc b/ash/system/privacy_hub/privacy_hub_notification_controller.cc
index d379e02..67993cb 100644
--- a/ash/system/privacy_hub/privacy_hub_notification_controller.cc
+++ b/ash/system/privacy_hub/privacy_hub_notification_controller.cc
@@ -110,6 +110,12 @@
   ShowAllActiveNotifications(sensor);
 }
 
+void PrivacyHubNotificationController::UpdateSensorDisabledNotification(
+    const Sensor sensor) {
+  sw_notifications_.at(sensor)->Update();
+  combined_notification_->Update();
+}
+
 void PrivacyHubNotificationController::OpenPrivacyHubSettingsPage() {
   privacy_hub_metrics::LogPrivacyHubOpenedFromNotification();
   Shell::Get()->system_tray_model()->client()->ShowPrivacyHubSettings();
diff --git a/ash/system/privacy_hub/privacy_hub_notification_controller.h b/ash/system/privacy_hub/privacy_hub_notification_controller.h
index 6d209d2..85cd53f 100644
--- a/ash/system/privacy_hub/privacy_hub_notification_controller.h
+++ b/ash/system/privacy_hub/privacy_hub_notification_controller.h
@@ -44,6 +44,10 @@
   // should be removed from the notification center and popups.
   void RemoveSensorDisabledNotification(Sensor sensor);
 
+  // Called by any sensor system when a notification for `sensor` should be
+  // updated, for example, when an application stops accessing `sensor`.
+  void UpdateSensorDisabledNotification(Sensor sensor);
+
   static constexpr const char kCombinedNotificationId[] =
       "ash.system.privacy_hub.enable_microphone_and_camera";
 
diff --git a/ash/system/privacy_hub/privacy_hub_notification_unittest.cc b/ash/system/privacy_hub/privacy_hub_notification_unittest.cc
index 7f0474d..002ba11 100644
--- a/ash/system/privacy_hub/privacy_hub_notification_unittest.cc
+++ b/ash/system/privacy_hub/privacy_hub_notification_unittest.cc
@@ -26,7 +26,16 @@
     return apps_;
   }
 
-  void LaunchApp(const std::u16string& app_name) { apps_.push_back(app_name); }
+  void LaunchApp(const std::u16string& app_name) {
+    apps_.insert(apps_.begin(), app_name);
+  }
+
+  void CloseApp(const std::u16string& app_name) {
+    auto it = std::find(apps_.begin(), apps_.end(), app_name);
+    if (it != apps_.end()) {
+      apps_.erase(it);
+    }
+  }
 
  private:
   std::vector<std::u16string> apps_;
@@ -55,6 +64,43 @@
   base::RunLoop run_loop_;
 };
 
+// A waiter class, once `Wait()` is invoked, waits until a pop up of the
+// notification with id `kNotificationId` is closed.
+class NotificationPopupWaiter : public message_center::MessageCenterObserver {
+ public:
+  NotificationPopupWaiter() {
+    message_center::MessageCenter::Get()->AddObserver(this);
+  }
+  ~NotificationPopupWaiter() override {
+    message_center::MessageCenter::Get()->RemoveObserver(this);
+  }
+  NotificationPopupWaiter& operator=(const NotificationPopupWaiter&) = delete;
+  NotificationPopupWaiter(const NotificationPopupWaiter&) = delete;
+
+  void Wait() { run_loop_.Run(); }
+
+  // message_center::MessageCenterObserver:
+  void OnNotificationPopupShown(const std::string& notification_id,
+                                bool mark_notification_as_read) override {
+    if (notification_id == kNotificationId) {
+      run_loop_.Quit();
+    }
+  }
+
+ private:
+  base::RunLoop run_loop_;
+};
+
+message_center::Notification* GetNotification() {
+  return message_center::MessageCenter::Get()->FindNotificationById(
+      kNotificationId);
+}
+
+message_center::Notification* GetPopupNotification() {
+  return message_center::MessageCenter::Get()->FindPopupNotificationById(
+      kNotificationId);
+}
+
 }  // namespace
 
 class PrivacyHubNotificationTest : public AshTestBase {
@@ -133,35 +179,52 @@
 }
 
 TEST_F(PrivacyHubNotificationTest, ShowAndHide) {
-  EXPECT_FALSE(message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId));
+  EXPECT_FALSE(GetNotification());
 
   notification().Show();
 
-  EXPECT_TRUE(message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId));
+  EXPECT_TRUE(GetNotification());
 
   notification().Hide();
 
-  EXPECT_TRUE(message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId));
+  EXPECT_TRUE(GetNotification());
 
   RemoveNotificationWaiter waiter;
 
   waiter.Wait();
 
-  EXPECT_FALSE(message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId));
+  EXPECT_FALSE(GetNotification());
+}
+
+TEST_F(PrivacyHubNotificationTest, UpdateNotification) {
+  // No notification initially.
+  EXPECT_FALSE(GetNotification());
+  EXPECT_FALSE(GetPopupNotification());
+
+  notification().Show();
+  // The notification should pop up.
+  EXPECT_TRUE(GetPopupNotification());
+
+  // Wait until pop up of the notification is closed.
+  NotificationPopupWaiter waiter;
+  waiter.Wait();
+  // The notification pop up should close by now. But the notification should
+  // stay in the message center.
+  EXPECT_TRUE(GetNotification());
+  EXPECT_FALSE(GetPopupNotification());
+
+  notification().Update();
+  // The update should be silent. The notification should not pop up but stay in
+  // the message center.
+  EXPECT_TRUE(GetNotification());
+  EXPECT_FALSE(GetPopupNotification());
 }
 
 TEST_F(PrivacyHubNotificationTest, WithApps) {
   // No apps -> generic notification text.
   notification().Show();
 
-  message_center::Notification* notification_ptr =
-      message_center::MessageCenter::Get()->FindNotificationById(
-          kNotificationId);
-
+  message_center::Notification* notification_ptr = GetNotification();
   ASSERT_TRUE(notification_ptr);
   EXPECT_EQ(
       notification_ptr->message(),
@@ -171,10 +234,10 @@
   // Launch a single app -> notification with message for one app.
   const std::u16string app1 = u"test1";
   sensor_delegate().LaunchApp(app1);
-
   notification().Show();
-  notification_ptr = message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId);
+
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
   EXPECT_EQ(
       notification_ptr->message(),
       l10n_util::GetStringFUTF16(
@@ -184,24 +247,38 @@
   // Launch a second app -> notification with message for two apps.
   const std::u16string app2 = u"test2";
   sensor_delegate().LaunchApp(app2);
-
   notification().Show();
-  notification_ptr = message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId);
-  EXPECT_TRUE(base::Contains(notification_ptr->message(), app1));
-  EXPECT_TRUE(base::Contains(notification_ptr->message(), app2));
+
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(
+      l10n_util::GetStringFUTF16(
+          IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
+          app2, app1),
+      notification_ptr->message());
 
   // More than two apps -> generic notification text.
   const std::u16string app3 = u"test3";
   sensor_delegate().LaunchApp(app3);
-
   notification().Show();
-  notification_ptr = message_center::MessageCenter::Get()->FindNotificationById(
-      kNotificationId);
+
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
+  EXPECT_EQ(l10n_util::GetStringUTF16(
+                IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE),
+            notification_ptr->message());
+
+  // Close one of the applications -> notification with message for two apps.
+  sensor_delegate().CloseApp(app2);
+  notification().Update();
+
+  notification_ptr = GetNotification();
+  ASSERT_TRUE(notification_ptr);
   EXPECT_EQ(
-      notification_ptr->message(),
-      l10n_util::GetStringUTF16(
-          IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE));
+      l10n_util::GetStringFUTF16(
+          IDS_PRIVACY_HUB_MICROPHONE_AND_CAMERA_OFF_NOTIFICATION_MESSAGE_WITH_TWO_APP_NAMES,
+          app3, app1),
+      notification_ptr->message());
 }
 
 }  // namespace ash
diff --git a/base/allocator/partition_allocator/partition_alloc_config.h b/base/allocator/partition_allocator/partition_alloc_config.h
index a8cce36..4abf110 100644
--- a/base/allocator/partition_allocator/partition_alloc_config.h
+++ b/base/allocator/partition_allocator/partition_alloc_config.h
@@ -118,6 +118,10 @@
 // cause significant jank.
 #define PA_CONFIG_STARSCAN_ENABLE_STARSCAN_ON_RECLAIM() 0
 
+// Double free detection comes with expensive cmpxchg (with the loop around it).
+// We currently disable it to improve the runtime.
+#define PA_CONFIG_STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED() 0
+
 // POSIX is not only UNIX, e.g. macOS and other OSes. We do use Linux-specific
 // features such as futex(2).
 #define PA_CONFIG_HAS_LINUX_KERNEL() \
diff --git a/base/allocator/partition_allocator/starscan/pcscan.h b/base/allocator/partition_allocator/starscan/pcscan.h
index 70c5e84..ec6d4df 100644
--- a/base/allocator/partition_allocator/starscan/pcscan.h
+++ b/base/allocator/partition_allocator/starscan/pcscan.h
@@ -10,16 +10,12 @@
 #include "base/allocator/partition_allocator/page_allocator.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/compiler_specific.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/component_export.h"
+#include "base/allocator/partition_allocator/partition_alloc_config.h"
 #include "base/allocator/partition_allocator/partition_alloc_forward.h"
 #include "base/allocator/partition_allocator/partition_direct_map_extent.h"
 #include "base/allocator/partition_allocator/partition_page.h"
 #include "base/allocator/partition_allocator/starscan/pcscan_scheduling.h"
 #include "base/allocator/partition_allocator/tagging.h"
-
-// Double free detection comes with expensive cmpxchg (with the loop around it).
-// We currently disable it to improve the runtime.
-#define PA_STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED 0
-
 namespace partition_alloc {
 
 class StatsReporter;
@@ -253,12 +249,12 @@
   // the clearing to avoid racing with *Scan Sweeper.
   [[maybe_unused]] const bool succeeded =
       state_bitmap->Quarantine(slot_start, instance.epoch());
-#if PA_STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED
+#if PA_CONFIG(STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED)
   if (PA_UNLIKELY(!succeeded))
     DoubleFreeAttempt();
 #else
   // The compiler is able to optimize cmpxchg to a lock-prefixed and.
-#endif
+#endif  // PA_CONFIG(STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED)
 
   const bool is_limit_reached = instance.scheduler_.AccountFreed(slot_size);
   if (PA_UNLIKELY(is_limit_reached)) {
diff --git a/base/allocator/partition_allocator/starscan/pcscan_internal.cc b/base/allocator/partition_allocator/starscan/pcscan_internal.cc
index 16db316c..52cd56e4 100644
--- a/base/allocator/partition_allocator/starscan/pcscan_internal.cc
+++ b/base/allocator/partition_allocator/starscan/pcscan_internal.cc
@@ -1068,7 +1068,7 @@
       [this, &stat, should_discard](uintptr_t super_page) {
         auto* root = ThreadSafePartitionRoot::FromFirstSuperPage(super_page);
 
-#if PA_STARSCAN_BATCHED_FREE
+#if PA_CONFIG(STARSCAN_BATCHED_FREE)
         SweepSuperPageWithBatchedFree(root, super_page, pcscan_epoch_, stat);
         (void)should_discard;
 #else
@@ -1077,7 +1077,7 @@
                                                    pcscan_epoch_, stat);
         else
           SweepSuperPage(root, super_page, pcscan_epoch_, stat);
-#endif
+#endif  // PA_CONFIG(STARSCAN_BATCHED_FREE)
       });
 
   stats_.IncreaseSweptSize(stat.swept_bytes);
diff --git a/base/allocator/partition_allocator/starscan/pcscan_unittest.cc b/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
index f1d06543..94176a9 100644
--- a/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
+++ b/base/allocator/partition_allocator/starscan/pcscan_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/allocator/partition_allocator/partition_alloc_base/compiler_specific.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/cpu.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/logging.h"
+#include "base/allocator/partition_allocator/partition_alloc_config.h"
 #include "base/allocator/partition_allocator/partition_alloc_constants.h"
 #include "base/allocator/partition_allocator/partition_root.h"
 #include "base/allocator/partition_allocator/starscan/stack/stack.h"
@@ -577,14 +578,14 @@
 
 // Death tests misbehave on Android, http://crbug.com/643760.
 #if defined(GTEST_HAS_DEATH_TEST) && !BUILDFLAG(IS_ANDROID)
-#if PA_STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED
+#if PA_CONFIG(STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED)
 TEST_F(PartitionAllocPCScanTest, DoubleFree) {
   auto* list = List<1>::Create(root());
   List<1>::Destroy(root(), list);
   EXPECT_DEATH(List<1>::Destroy(root(), list), "");
 }
-#endif
-#endif
+#endif  // PA_CONFIG(STARSCAN_EAGER_DOUBLE_FREE_DETECTION_ENABLED)
+#endif  // defined(GTEST_HAS_DEATH_TEST) && !BUILDFLAG(IS_ANDROID)
 
 namespace {
 template <typename SourceList, typename ValueList>
diff --git a/build/android/gyp/proguard.py b/build/android/gyp/proguard.py
index f259374a..7c4e0dd6 100755
--- a/build/android/gyp/proguard.py
+++ b/build/android/gyp/proguard.py
@@ -254,9 +254,6 @@
         # Restricts horizontal class merging to apply only to classes that
         # share a .java file (nested classes). https://crbug.com/1363709
         '-Dcom.android.tools.r8.enableSameFilePolicy=1',
-        # Enables API modelling for all classes that need it. Breaks reflection
-        # on SDK versions that we no longer support. http://b/259076765
-        '-Dcom.android.tools.r8.stubNonThrowableClasses=1',
     ]
     if options.dump_inputs:
       cmd += ['-Dcom.android.tools.r8.dumpinputtofile=r8inputs.zip']
diff --git a/build/config/compiler/BUILD.gn b/build/config/compiler/BUILD.gn
index 8651ba1b..93fdee7 100644
--- a/build/config/compiler/BUILD.gn
+++ b/build/config/compiler/BUILD.gn
@@ -2429,11 +2429,12 @@
 
     # The gcc-based nacl compilers don't support -fdebug-compilation-dir (see
     # elsewhere in this file), so they can't have build-dir-independent output.
+    # Moreover pnacl does not support newer flags such as -fdebug-prefix-map
     # Disable symbols for nacl object files to get deterministic,
-    # build-directory-independent output. pnacl and nacl-clang do support that
-    # flag, so we can use use -g1 for pnacl and nacl-clang compiles.
-    # gcc nacl is is_nacl && !is_clang, pnacl and nacl-clang are && is_clang.
-    if ((!is_nacl || is_clang) && current_os != "zos") {
+    # build-directory-independent output.
+    # Keeping -g2 for saigo as it's the only toolchain whose artifacts that are
+    # part of chromium release (other nacl toolchains are used only for tests).
+    if ((!is_nacl || is_nacl_saigo) && current_os != "zos") {
       cflags += [ "-g2" ]
     }
 
@@ -2560,11 +2561,12 @@
 
     # The gcc-based nacl compilers don't support -fdebug-compilation-dir (see
     # elsewhere in this file), so they can't have build-dir-independent output.
+    # Moreover pnacl does not support newer flags such as -fdebug-prefix-map
     # Disable symbols for nacl object files to get deterministic,
-    # build-directory-independent output. pnacl and nacl-clang do support that
-    # flag, so we can use use -g1 for pnacl and nacl-clang compiles.
-    # gcc nacl is is_nacl && !is_clang, pnacl and nacl-clang are && is_clang.
-    if (!is_nacl || is_clang) {
+    # build-directory-independent output.
+    # Keeping -g1 for saigo as it's the only toolchain whose artifacts that are
+    # part of chromium release (other nacl toolchains are used only for tests).
+    if (!is_nacl || is_nacl_saigo) {
       cflags += [ "-g1" ]
     }
 
diff --git a/build/fuchsia/linux_internal.sdk.sha1 b/build/fuchsia/linux_internal.sdk.sha1
index 7825042..2611d2d 100644
--- a/build/fuchsia/linux_internal.sdk.sha1
+++ b/build/fuchsia/linux_internal.sdk.sha1
@@ -1 +1 @@
-11.20230122.3.1
+11.20230123.1.1
diff --git a/build_overrides/partition_alloc.gni b/build_overrides/partition_alloc.gni
index e1fa5fe..aa6433b 100644
--- a/build_overrides/partition_alloc.gni
+++ b/build_overrides/partition_alloc.gni
@@ -80,5 +80,8 @@
 enable_mte_checked_ptr_support_default = false
 
 put_ref_count_in_previous_slot_default = true
-enable_backup_ref_ptr_slow_checks_default = false
+
+# TODO(keishi): Slow check is enabled temporarily for a few canary releases to
+# check for bugs.
+enable_backup_ref_ptr_slow_checks_default = true
 enable_dangling_raw_ptr_checks_default = false
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/ImmersiveModeController.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/ImmersiveModeController.java
index 2670537..9ec0efc 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/ImmersiveModeController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/features/ImmersiveModeController.java
@@ -148,6 +148,8 @@
         }
     }
 
+    // BEHAVIOR_SHOW_BARS_BY_SWIPE is deprecated.
+    @SuppressWarnings("WrongConstant")
     private void updateImmersiveFlagsOnAndroidNot11() {
         Window window = mActivity.getWindow();
         View decor = window.getDecorView();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/page_info/PageInfoAboutThisSiteController.java b/chrome/android/java/src/org/chromium/chrome/browser/page_info/PageInfoAboutThisSiteController.java
index 587365b..5ce34fd2 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/page_info/PageInfoAboutThisSiteController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/page_info/PageInfoAboutThisSiteController.java
@@ -36,6 +36,7 @@
 import org.chromium.content_public.browser.LoadUrlParams;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.ui.LayoutInflaterUtils;
+import org.chromium.ui.base.PageTransition;
 import org.chromium.url.GURL;
 
 /**
@@ -157,7 +158,7 @@
 
     private void openInNewTab(String url) {
         new TabDelegate(/*incognito=*/false)
-                .createNewTab(new LoadUrlParams(url), TabLaunchType.FROM_CHROME_UI,
+                .createNewTab(new LoadUrlParams(url, PageTransition.LINK), TabLaunchType.FROM_LINK,
                         TabUtils.fromWebContents(mWebContents));
     }
 
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 788b46b6..0b824f6 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -6098,10 +6098,10 @@
           Google Password Manager <ph name="SEPARATOR">•</ph> <ph name="ACCOUNT">$1<ex>user@gmail.com</ex></ph>
         </message>
         <message name="IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2" desc="The description of the Google Password Manager's save password prompt when the user is signed in.">
-          To your Google Account, for <ph name="ACCOUNT">$1<ex>user@gmail.com</ex></ph>
+          To your Google Account, <ph name="ACCOUNT">$1<ex>user@gmail.com</ex></ph>
         </message>
         <message name="IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2" desc="The description of the Google Password Manager's save password prompt when the user is signed in.">
-          In your Google Account, for <ph name="ACCOUNT">$1<ex>user@gmail.com</ex></ph>
+          In your Google Account, <ph name="ACCOUNT">$1<ex>user@gmail.com</ex></ph>
         </message>
         <message name="IDS_PASSWORD_MANAGER_SAVE_UPDATE_PASSWORD_SIGNED_OUT_MESSAGE_DESCRIPTION_V1" desc="The description of the Google Password Manager's save password prompt when the user is signed out and passwords are only stored locally.">
           Only on this device
diff --git a/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1 b/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
index 2e2d4e4..72ce985 100644
--- a/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_SAVE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
@@ -1 +1 @@
-8287d85ba36e624ada15c29e4cd5a26258cc92cb
\ No newline at end of file
+5df6ac81b86877357db99c0a23babdc093d1eab0
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1 b/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
index c679fe1..1a9f903 100644
--- a/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_PASSWORD_MANAGER_UPDATE_PASSWORD_SIGNED_IN_MESSAGE_DESCRIPTION_V2.png.sha1
@@ -1 +1 @@
-9f302d188c61200852a74ffbc2a2564299326dde
\ No newline at end of file
+960d14f917a018b8db54978545977944a49cd3c4
\ No newline at end of file
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index d40bb28..5c21877 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3193,17 +3193,20 @@
     kPrivacySandboxSettings4ForceShowConsentForTesting[] = {
         {privacy_sandbox::
              kPrivacySandboxSettings4ForceShowConsentForTestingName,
-         "true"}};
+         "true"},
+        {privacy_sandbox::kPrivacySandboxSettings4ConsentRequiredName, "true"}};
 const FeatureEntry::FeatureParam
     kPrivacySandboxSettings4ForceShowROWNoticeForTesting[] = {
         {privacy_sandbox::
              kPrivacySandboxSettings4ForceShowNoticeRowForTestingName,
-         "true"}};
+         "true"},
+        {privacy_sandbox::kPrivacySandboxSettings4NoticeRequiredName, "true"}};
 const FeatureEntry::FeatureParam
     kPrivacySandboxSettings4ForceShowEEANoticeForTesting[] = {
         {privacy_sandbox::
              kPrivacySandboxSettings4ForceShowNoticeEeaForTestingName,
-         "true"}};
+         "true"},
+        {privacy_sandbox::kPrivacySandboxSettings4ConsentRequiredName, "true"}};
 
 const FeatureEntry::FeatureVariation kPrivacySandboxSettings4Variations[] = {
     {"Sample Data", kPrivacySandboxSettings4ShowSampleDataForTesting,
diff --git a/chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h b/chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h
index 8c0f1b9..0530784 100644
--- a/chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h
+++ b/chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h
@@ -5,6 +5,7 @@
 #define CHROME_BROWSER_APPS_APP_SERVICE_PROMISE_APPS_PROMISE_APP_REGISTRY_CACHE_H_
 
 #include <map>
+#include <memory>
 
 namespace apps {
 
diff --git a/chrome/browser/apps/guest_view/web_view_interactive_browsertest.cc b/chrome/browser/apps/guest_view/web_view_interactive_browsertest.cc
index 1cbab2cf..374b554 100644
--- a/chrome/browser/apps/guest_view/web_view_interactive_browsertest.cc
+++ b/chrome/browser/apps/guest_view/web_view_interactive_browsertest.cc
@@ -10,6 +10,7 @@
 #include "base/functional/bind.h"
 #include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/run_loop.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
@@ -437,7 +438,10 @@
     }
 
     size_t initial_widget_count_ = 0;
-    content::RenderWidgetHost* last_render_widget_host_ = nullptr;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #constexpr-ctor-field-initializer
+    RAW_PTR_EXCLUSION content::RenderWidgetHost* last_render_widget_host_ =
+        nullptr;
     std::unique_ptr<base::RunLoop> run_loop_;
   };
 
diff --git a/chrome/browser/apps/platform_apps/app_browsertest.cc b/chrome/browser/apps/platform_apps/app_browsertest.cc
index 43f6c39c..238cbdc 100644
--- a/chrome/browser/apps/platform_apps/app_browsertest.cc
+++ b/chrome/browser/apps/platform_apps/app_browsertest.cc
@@ -16,6 +16,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/run_loop.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/threading/thread_restrictions.h"
@@ -186,7 +187,9 @@
  private:
   uint32_t total_page_count_ = 1;
   uint32_t rendered_page_count_ = 0;
-  base::RunLoop* run_loop_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION base::RunLoop* run_loop_ = nullptr;
   gfx::Size dialog_size_;
 };
 
diff --git a/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.cc b/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.cc
index f226d2bc..637b053c 100644
--- a/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.cc
+++ b/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.cc
@@ -7,10 +7,8 @@
 #include <utility>
 
 #include "base/logging.h"
-#include "base/path_service.h"
 #include "chrome/browser/ash/app_mode/arc/arc_kiosk_app_manager.h"
 #include "chrome/browser/browser_process.h"
-#include "chrome/common/chrome_paths.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/scoped_user_pref_update.h"
 #include "content/public/browser/browser_thread.h"
@@ -44,7 +42,13 @@
   PrefService* local_state = g_browser_process->local_state();
   const base::Value::Dict& dict = local_state->GetDict(dictionary_name());
 
-  return LoadFromDictionary(dict);
+  if (!LoadFromDictionary(dict)) {
+    return false;
+  }
+
+  DecodeIcon(base::BindOnce(&ArcKioskAppData::OnIconLoadDone,
+                            weak_ptr_factory_.GetWeakPtr()));
+  return true;
 }
 
 void ArcKioskAppData::SetCache(const std::string& name,
@@ -65,16 +69,16 @@
   SaveToDictionary(dict_update);
 }
 
-void ArcKioskAppData::OnIconLoadSuccess(const gfx::ImageSkia& icon) {
+void ArcKioskAppData::OnIconLoadDone(absl::optional<gfx::ImageSkia> icon) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   kiosk_app_icon_loader_.reset();
-  icon_ = icon;
-}
 
-void ArcKioskAppData::OnIconLoadFailure() {
-  kiosk_app_icon_loader_.reset();
-  LOG(ERROR) << "Icon Load Failure";
-  // Do nothing
+  if (!icon.has_value()) {
+    LOG(ERROR) << "Icon Load Failure";
+    return;
+  }
+
+  icon_ = icon.value();
 }
 
 }  // namespace ash
diff --git a/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.h b/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.h
index 499b4fa..ec3dfbd9 100644
--- a/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.h
+++ b/chrome/browser/ash/app_mode/arc/arc_kiosk_app_data.h
@@ -39,15 +39,15 @@
   // Sets the cached data.
   void SetCache(const std::string& name, const gfx::ImageSkia& icon);
 
-  // Callbacks for KioskAppIconLoader.
-  void OnIconLoadSuccess(const gfx::ImageSkia& icon) override;
-  void OnIconLoadFailure() override;
-
  private:
+  void OnIconLoadDone(absl::optional<gfx::ImageSkia> icon);
+
   // Not cached, always provided in ctor.
   const std::string package_name_;
   const std::string activity_;
   const std::string intent_;
+
+  base::WeakPtrFactory<ArcKioskAppData> weak_ptr_factory_{this};
 };
 
 }  // namespace ash
diff --git a/chrome/browser/ash/app_mode/kiosk_app_data.cc b/chrome/browser/ash/app_mode/kiosk_app_data.cc
index b041889..cb70aad 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_data.cc
+++ b/chrome/browser/ash/app_mode/kiosk_app_data.cc
@@ -160,8 +160,9 @@
   void NotifyFinishedOnUIThread() {
     DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
-    if (client_)
+    if (client_) {
       client_->OnCrxLoadFinished(this);
+    }
   }
 
   base::WeakPtr<KioskAppData> client_;
@@ -204,8 +205,9 @@
   ~WebstoreDataParser() override = default;
 
   void ReportFailure() {
-    if (client_)
+    if (client_) {
       client_->OnWebstoreParseFailure();
+    }
 
     delete this;
   }
@@ -235,8 +237,9 @@
       required_platform_version = temp->GetString();
     }
 
-    if (client_)
+    if (client_) {
       client_->OnWebstoreParseSuccess(icon, required_platform_version);
+    }
     delete this;
   }
   void OnWebstoreParseFailure(const std::string& id,
@@ -274,8 +277,9 @@
 void KioskAppData::Load() {
   SetStatus(Status::kLoading);
 
-  if (LoadFromCache())
+  if (LoadFromCache()) {
     return;
+  }
 
   StartFetch();
 }
@@ -305,8 +309,9 @@
 }
 
 void KioskAppData::SetCachedCrx(const base::FilePath& crx_file) {
-  if (crx_file_ == crx_file)
+  if (crx_file_ == crx_file) {
     return;
+  }
 
   crx_file_ = crx_file;
   LoadFromCrx();
@@ -346,13 +351,15 @@
     status = Status::kLoaded;
   }
 
-  if (status_ == status)
+  if (status_ == status) {
     return;
+  }
 
   status_ = status;
 
-  if (!delegate_)
+  if (!delegate_) {
     return;
+  }
 
   switch (status_) {
     case Status::kInit:
@@ -375,8 +382,12 @@
   PrefService* local_state = g_browser_process->local_state();
   const base::Value::Dict& dict = local_state->GetDict(dictionary_name());
 
-  if (!LoadFromDictionary(dict))
+  if (!LoadFromDictionary(dict)) {
     return false;
+  }
+
+  DecodeIcon(base::BindOnce(&KioskAppData::OnIconLoadDone,
+                            weak_factory_.GetWeakPtr()));
 
   const std::string app_key = std::string(kKeyApps) + '.' + app_id();
   const std::string required_platform_version_key =
@@ -384,8 +395,9 @@
 
   const std::string* maybe_required_platform_version =
       dict.FindStringByDottedPath(required_platform_version_key);
-  if (!maybe_required_platform_version)
+  if (!maybe_required_platform_version) {
     return false;
+  }
 
   required_platform_version_ = *maybe_required_platform_version;
   return true;
@@ -400,8 +412,9 @@
   icon_.MakeThreadSafe();
 
   base::FilePath cache_dir;
-  if (delegate_)
+  if (delegate_) {
     delegate_->GetKioskAppIconCacheDir(&cache_dir);
+  }
 
   SaveIcon(icon, cache_dir);
 
@@ -430,17 +443,17 @@
   SetStatus(Status::kLoaded);
 }
 
-void KioskAppData::OnIconLoadSuccess(const gfx::ImageSkia& icon) {
+void KioskAppData::OnIconLoadDone(absl::optional<gfx::ImageSkia> icon) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   kiosk_app_icon_loader_.reset();
-  icon_ = icon;
-  SetStatus(Status::kLoaded);
-}
+  if (!icon.has_value()) {
+    // Re-fetch data from web store when failed to load cached data.
+    StartFetch();
+    return;
+  }
 
-void KioskAppData::OnIconLoadFailure() {
-  kiosk_app_icon_loader_.reset();
-  // Re-fetch data from web store when failed to load cached data.
-  StartFetch();
+  icon_ = icon.value();
+  SetStatus(Status::kLoaded);
 }
 
 // static
@@ -499,15 +512,19 @@
   webstore_fetcher_.reset();
 
   std::string manifest;
-  if (!CheckResponseKeyValue(*id, webstore_data, kManifestKey, &manifest))
+  if (!CheckResponseKeyValue(*id, webstore_data, kManifestKey, &manifest)) {
     return;
+  }
 
-  if (!CheckResponseKeyValue(*id, webstore_data, kLocalizedNameKey, &name_))
+  if (!CheckResponseKeyValue(*id, webstore_data, kLocalizedNameKey, &name_)) {
     return;
+  }
 
   std::string icon_url_string;
-  if (!CheckResponseKeyValue(*id, webstore_data, kIconUrlKey, &icon_url_string))
+  if (!CheckResponseKeyValue(*id, webstore_data, kIconUrlKey,
+                             &icon_url_string)) {
     return;
+  }
 
   GURL icon_url =
       extension_urls::GetWebstoreLaunchURL().Resolve(icon_url_string);
@@ -547,8 +564,9 @@
 }
 
 void KioskAppData::LoadFromCrx() {
-  if (crx_file_.empty())
+  if (crx_file_.empty()) {
     return;
+  }
 
   scoped_refptr<CrxLoader> crx_loader(
       new CrxLoader(weak_factory_.GetWeakPtr(), crx_file_));
@@ -558,24 +576,27 @@
 void KioskAppData::OnCrxLoadFinished(const CrxLoader* crx_loader) {
   DCHECK(crx_loader);
 
-  if (crx_loader->crx_file() != crx_file_)
+  if (crx_loader->crx_file() != crx_file_) {
     return;
+  }
 
   if (!crx_loader->success()) {
     LOG(ERROR) << "Failed to load cached extension data for app_id="
                << app_id();
-    // If after unpacking the cached extension we received an error, schedule a
-    // redownload upon next session start(kiosk or login).
-    if (delegate_)
+    // If after unpacking the cached extension we received an error, schedule
+    // a redownload upon next session start(kiosk or login).
+    if (delegate_) {
       delegate_->OnExternalCacheDamaged(app_id());
+    }
 
     SetStatus(Status::kInit);
     return;
   }
 
   SkBitmap icon = crx_loader->icon();
-  if (icon.empty())
+  if (icon.empty()) {
     icon = *extensions::util::GetDefaultAppIcon().bitmap();
+  }
   SetCache(crx_loader->name(), icon, crx_loader->required_platform_version());
 
   SetStatus(Status::kLoaded);
diff --git a/chrome/browser/ash/app_mode/kiosk_app_data.h b/chrome/browser/ash/app_mode/kiosk_app_data.h
index d6caa559..0f43ecf 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_data.h
+++ b/chrome/browser/ash/app_mode/kiosk_app_data.h
@@ -21,7 +21,7 @@
 namespace extensions {
 class Extension;
 class WebstoreDataFetcher;
-}
+}  // namespace extensions
 
 namespace gfx {
 class Image;
@@ -31,7 +31,7 @@
 namespace mojom {
 class URLLoaderFactory;
 }
-}
+}  // namespace network
 
 namespace ash {
 
@@ -90,10 +90,6 @@
       const GURL& update_url,
       const std::string& required_platform_version);
 
-  // Callbacks for KioskAppIconLoader.
-  void OnIconLoadSuccess(const gfx::ImageSkia& icon) override;
-  void OnIconLoadFailure() override;
-
   // Tests do not always fake app data download.
   // This allows to ignore download errors.
   static void SetIgnoreKioskAppDataLoadFailuresForTesting(bool value);
@@ -148,6 +144,8 @@
 
   void OnCrxLoadFinished(const CrxLoader* crx_loader);
 
+  void OnIconLoadDone(absl::optional<gfx::ImageSkia> icon);
+
   KioskAppDataDelegate* delegate_;  // not owned.
   Status status_;
 
diff --git a/chrome/browser/ash/app_mode/kiosk_app_data_base.cc b/chrome/browser/ash/app_mode/kiosk_app_data_base.cc
index bd3da593..15030135 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_data_base.cc
+++ b/chrome/browser/ash/app_mode/kiosk_app_data_base.cc
@@ -12,6 +12,7 @@
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
 #include "base/threading/scoped_blocking_call.h"
+#include "chrome/browser/ash/app_mode/kiosk_app_icon_loader.h"
 #include "chrome/browser/browser_process.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/scoped_user_pref_update.h"
@@ -56,8 +57,9 @@
   if (delimiter_position != base::StringPiece::npos) {
     current_dictionary =
         dict.FindDictByDottedPath(current_path.substr(0, delimiter_position));
-    if (!current_dictionary)
+    if (!current_dictionary) {
       return;
+    }
     current_path = current_path.substr(delimiter_position + 1);
   }
   current_dictionary->Remove(current_path);
@@ -95,8 +97,7 @@
   dict_update->SetByDottedPath(icon_path_key, icon_path_.value());
 }
 
-bool KioskAppDataBase::LoadFromDictionary(const base::Value::Dict& dict,
-                                          bool lazy_icon_load) {
+bool KioskAppDataBase::LoadFromDictionary(const base::Value::Dict& dict) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   const std::string app_key =
       std::string(KioskAppDataBase::kKeyApps) + '.' + app_id_;
@@ -105,8 +106,9 @@
 
   // If there is no title stored, do not stop, sometimes only icon is cached.
   const std::string* maybe_name = dict.FindStringByDottedPath(name_key);
-  if (maybe_name)
+  if (maybe_name) {
     name_ = *maybe_name;
+  }
 
   const std::string* icon_path_string =
       dict.FindStringByDottedPath(icon_path_key);
@@ -116,16 +118,13 @@
 
   icon_path_ = base::FilePath(*icon_path_string);
 
-  if (!lazy_icon_load) {
-    DecodeIcon();
-  }
-
   return true;
 }
 
-void KioskAppDataBase::DecodeIcon() {
+void KioskAppDataBase::DecodeIcon(KioskAppIconLoader::ResultCallback callback) {
   DLOG_IF(ERROR, icon_path_.empty()) << "Icon path is empty";
-  kiosk_app_icon_loader_ = std::make_unique<KioskAppIconLoader>(this);
+  kiosk_app_icon_loader_ =
+      std::make_unique<KioskAppIconLoader>(std::move(callback));
   kiosk_app_icon_loader_->Start(icon_path_);
 }
 
diff --git a/chrome/browser/ash/app_mode/kiosk_app_data_base.h b/chrome/browser/ash/app_mode/kiosk_app_data_base.h
index 2993683d..4949b670 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_data_base.h
+++ b/chrome/browser/ash/app_mode/kiosk_app_data_base.h
@@ -12,6 +12,7 @@
 #include "chrome/browser/ash/app_mode/kiosk_app_icon_loader.h"
 #include "components/account_id/account_id.h"
 #include "components/prefs/scoped_user_pref_update.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/gfx/image/image_skia.h"
 
 namespace base {
@@ -20,8 +21,15 @@
 
 namespace ash {
 
-class KioskAppDataBase : public KioskAppIconLoader::Delegate {
+class KioskAppDataBase {
  public:
+  KioskAppDataBase(const std::string& dictionary_name,
+                   const std::string& app_id,
+                   const AccountId& account_id);
+  KioskAppDataBase(const KioskAppDataBase&) = delete;
+  KioskAppDataBase& operator=(const KioskAppDataBase&) = delete;
+  virtual ~KioskAppDataBase();
+
   // Dictionary key for apps.
   static const char kKeyApps[];
 
@@ -31,35 +39,22 @@
   const std::string& name() const { return name_; }
   const gfx::ImageSkia& icon() const { return icon_; }
 
-  // Callbacks for KioskAppIconLoader.
-  void OnIconLoadSuccess(const gfx::ImageSkia& icon) override = 0;
-  void OnIconLoadFailure() override = 0;
-
   // Clears locally cached data.
   void ClearCache();
 
  protected:
-  KioskAppDataBase(const std::string& dictionary_name,
-                   const std::string& app_id,
-                   const AccountId& account_id);
-  KioskAppDataBase(const KioskAppDataBase&) = delete;
-  KioskAppDataBase& operator=(const KioskAppDataBase&) = delete;
-  ~KioskAppDataBase() override;
-
   // Helper to save name and icon to provided dictionary.
   void SaveToDictionary(ScopedDictPrefUpdate& dict_update);
 
   // Helper to save icon to provided dictionary.
   void SaveIconToDictionary(ScopedDictPrefUpdate& dict_update);
 
-  // Helper to load name and icon from provided dictionary.
-  // if |lazy_icon_load| is set to true, the icon will not be updated, only
-  // icon_path_.
-  bool LoadFromDictionary(const base::Value::Dict& dict,
-                          bool lazy_icon_load = false);
+  // Helper to load name and icon_path from provided dictionary.
+  // This method does not load the icon from disk.
+  bool LoadFromDictionary(const base::Value::Dict& dict);
 
   // Starts loading the icon from |icon_path_|;
-  void DecodeIcon();
+  void DecodeIcon(KioskAppIconLoader::ResultCallback callback);
 
   // Helper to cache |icon| to |cache_dir|.
   void SaveIcon(const SkBitmap& icon, const base::FilePath& cache_dir);
diff --git a/chrome/browser/ash/app_mode/kiosk_app_icon_loader.cc b/chrome/browser/ash/app_mode/kiosk_app_icon_loader.cc
index e483ea1..5057f4e9 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_icon_loader.cc
+++ b/chrome/browser/ash/app_mode/kiosk_app_icon_loader.cc
@@ -70,8 +70,8 @@
   ImageDecoder::Start(image_request, std::move(data));
 }
 
-KioskAppIconLoader::KioskAppIconLoader(Delegate* delegate)
-    : delegate_(delegate) {}
+KioskAppIconLoader::KioskAppIconLoader(ResultCallback callback)
+    : callback_(std::move(callback)) {}
 
 KioskAppIconLoader::~KioskAppIconLoader() = default;
 
@@ -91,11 +91,7 @@
     absl::optional<gfx::ImageSkia> result) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
 
-  if (result.has_value()) {
-    delegate_->OnIconLoadSuccess(result.value());
-  } else {
-    delegate_->OnIconLoadFailure();
-  }
+  std::move(callback_).Run(result);
 }
 
 }  // namespace ash
diff --git a/chrome/browser/ash/app_mode/kiosk_app_icon_loader.h b/chrome/browser/ash/app_mode/kiosk_app_icon_loader.h
index 52b0ac3..2c6d99f 100644
--- a/chrome/browser/ash/app_mode/kiosk_app_icon_loader.h
+++ b/chrome/browser/ash/app_mode/kiosk_app_icon_loader.h
@@ -5,10 +5,8 @@
 #ifndef CHROME_BROWSER_ASH_APP_MODE_KIOSK_APP_ICON_LOADER_H_
 #define CHROME_BROWSER_ASH_APP_MODE_KIOSK_APP_ICON_LOADER_H_
 
-#include "base/functional/callback_forward.h"
-#include "base/memory/ref_counted_memory.h"
+#include "base/functional/callback.h"
 #include "base/memory/weak_ptr.h"
-#include "base/task/sequenced_task_runner.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/gfx/image/image_skia.h"
 
@@ -21,19 +19,10 @@
 // Loads locally stored icon data and decodes it.
 class KioskAppIconLoader {
  public:
-  class Delegate {
-   public:
-    virtual void OnIconLoadSuccess(const gfx::ImageSkia& icon) = 0;
-    virtual void OnIconLoadFailure() = 0;
-
-   protected:
-    virtual ~Delegate() = default;
-  };
-
   using ResultCallback =
-      base::OnceCallback<void(absl::optional<gfx::ImageSkia> result)>;
+      base::OnceCallback<void(absl::optional<gfx::ImageSkia>)>;
 
-  explicit KioskAppIconLoader(Delegate* delegate);
+  explicit KioskAppIconLoader(ResultCallback delegate);
   KioskAppIconLoader(const KioskAppIconLoader&) = delete;
   KioskAppIconLoader& operator=(const KioskAppIconLoader&) = delete;
   ~KioskAppIconLoader();
@@ -43,10 +32,7 @@
  private:
   void OnImageDecodingFinished(absl::optional<gfx::ImageSkia> result);
 
-  // Delegate always lives longer than this class as it's owned by delegate.
-  Delegate* const delegate_;
-
-  gfx::ImageSkia icon_;
+  ResultCallback callback_;
 
   base::WeakPtrFactory<KioskAppIconLoader> weak_factory_{this};
 };
diff --git a/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.cc b/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.cc
index 713c75f..31efdcc 100644
--- a/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.cc
+++ b/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.cc
@@ -91,8 +91,9 @@
         base::BindOnce(
             [](base::WeakPtr<WebKioskAppData> client,
                std::unique_ptr<std::string> response_body) {
-              if (!client)
+              if (!client) {
                 return;
+              }
               client->icon_fetcher_->OnSimpleLoaderComplete(
                   std::move(response_body));
             },
@@ -113,8 +114,9 @@
  private:
   // ImageDecoder::ImageRequest:
   void OnImageDecoded(const SkBitmap& decoded_image) override {
-    if (!client_)
+    if (!client_) {
       return;
+    }
 
     // Icons have to be square shaped.
     if (decoded_image.width() != decoded_image.height()) {
@@ -168,8 +170,9 @@
   PrefService* local_state = g_browser_process->local_state();
   const base::Value::Dict& dict = local_state->GetDict(dictionary_name());
 
-  if (!LoadFromDictionary(dict, /* lazy_icon_load= */ true))
+  if (!LoadFromDictionary(dict)) {
     return false;
+  }
 
   if (LoadLaunchUrlFromDictionary(dict)) {
     SetStatus(Status::kInstalled);
@@ -178,27 +181,32 @@
 
   // If the icon was previously downloaded using a different url and the app has
   // not been installed earlier, do not use that icon.
-  if (GetLastIconUrl(dict) != icon_url_)
+  if (GetLastIconUrl(dict) != icon_url_) {
     return false;
+  }
 
   // Wait while icon is loaded.
-  if (status_ == Status::kInit)
+  if (status_ == Status::kInit) {
     SetStatus(Status::kLoading);
+  }
   return true;
 }
 
 void WebKioskAppData::LoadIcon() {
-  if (!icon_.isNull())
-    return;
-
-  // Decode the icon if one is already cached.
-  if (status_ != Status::kInit) {
-    DecodeIcon();
+  if (!icon_.isNull()) {
     return;
   }
 
-  if (!icon_url_.is_valid())
+  // Decode the icon if one is already cached.
+  if (status_ != Status::kInit) {
+    DecodeIcon(base::BindOnce(&WebKioskAppData::OnIconLoadDone,
+                              weak_ptr_factory_.GetWeakPtr()));
     return;
+  }
+
+  if (!icon_url_.is_valid()) {
+    return;
+  }
 
   DCHECK(!icon_fetcher_);
 
@@ -225,8 +233,9 @@
   name_ = title;
 
   base::FilePath cache_dir;
-  if (delegate_)
+  if (delegate_) {
     delegate_->GetKioskAppIconCacheDir(&cache_dir);
+  }
 
   auto it = icon_bitmaps.any.find(kWebKioskIconSize);
   if (it != icon_bitmaps.any.end()) {
@@ -260,8 +269,9 @@
     std::move(on_loaded_closure_for_testing_).Run();
   }
 
-  if (delegate_ && notify)
+  if (delegate_ && notify) {
     delegate_->OnKioskAppDataChanged(app_id());
+  }
 }
 
 bool WebKioskAppData::LoadLaunchUrlFromDictionary(
@@ -273,8 +283,9 @@
           ->FindDict(app_id())
           ->FindString(kKeyLaunchUrl);
 
-  if (!launch_url_string)
+  if (!launch_url_string) {
     return false;
+  }
 
   launch_url_ = GURL(*launch_url_string);
   return true;
@@ -300,8 +311,9 @@
   }
 
   base::FilePath cache_dir;
-  if (delegate_)
+  if (delegate_) {
     delegate_->GetKioskAppIconCacheDir(&cache_dir);
+  }
 
   SaveIcon(icon, cache_dir);
 
@@ -316,20 +328,22 @@
   SetStatus(Status::kLoaded);
 }
 
-void WebKioskAppData::OnIconLoadSuccess(const gfx::ImageSkia& icon) {
+void WebKioskAppData::OnIconLoadDone(absl::optional<gfx::ImageSkia> icon) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   kiosk_app_icon_loader_.reset();
-  icon_ = icon;
-  if (status_ != Status::kInstalled)
-    SetStatus(Status::kLoaded);
-  else
-    SetStatus(Status::kInstalled);  // To notify menu controller.
-}
 
-void WebKioskAppData::OnIconLoadFailure() {
-  kiosk_app_icon_loader_.reset();
-  LOG(ERROR) << "Icon Load Failure";
-  SetStatus(Status::kLoaded, /*notify=*/false);
+  if (!icon.has_value()) {
+    LOG(ERROR) << "Icon Load Failure";
+    SetStatus(Status::kLoaded, /*notify=*/false);
+    return;
+  }
+
+  icon_ = icon.value();
+  if (status_ != Status::kInstalled) {
+    SetStatus(Status::kLoaded);
+  } else {
+    SetStatus(Status::kInstalled);  // To notify menu controller.
+  }
 }
 
 }  // namespace ash
diff --git a/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h b/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h
index 92bc103..12f877f 100644
--- a/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h
+++ b/chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h
@@ -12,6 +12,7 @@
 #include "chrome/browser/ash/app_mode/kiosk_app_data_base.h"
 #include "chrome/browser/web_applications/web_app_install_info.h"
 #include "components/account_id/account_id.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #include "ui/gfx/image/image_skia.h"
 #include "url/gurl.h"
 
@@ -52,10 +53,6 @@
   // Get a proper URL to launch according to the app status.
   GURL GetLaunchableUrl() const;
 
-  // KioskAppDataBase overrides:
-  void OnIconLoadSuccess(const gfx::ImageSkia& icon) override;
-  void OnIconLoadFailure() override;
-
   // Updates |status_|. Based on |notify|, we will notify |delegate_| about data
   // update.
   void SetStatus(Status status, bool notify = true);
@@ -75,6 +72,7 @@
  private:
   class IconFetcher;
   void OnDidDownloadIcon(const SkBitmap& icon);
+  void OnIconLoadDone(absl::optional<gfx::ImageSkia> icon);
 
   bool LoadLaunchUrlFromDictionary(const base::Value::Dict& dict);
 
diff --git a/chrome/browser/ash/crosapi/browser_loader_unittest.cc b/chrome/browser/ash/crosapi/browser_loader_unittest.cc
index 034d6cafa22..5e9882b0 100644
--- a/chrome/browser/ash/crosapi/browser_loader_unittest.cc
+++ b/chrome/browser/ash/crosapi/browser_loader_unittest.cc
@@ -146,7 +146,7 @@
   EXPECT_CALL(mock_component_update_service_, GetComponents())
       .WillOnce(Return(std::vector<component_updater::ComponentInfo>{
           {kLacrosComponentId, "", lacros_component_name,
-           base::Version("1.0.0")}}));
+           base::Version("1.0.0"), ""}}));
 
   // Use stateful when a rootfs lacros-chrome version is invalid.
   bool callback_called = false;
@@ -176,7 +176,7 @@
   EXPECT_CALL(mock_component_update_service_, GetComponents())
       .WillOnce(Return(std::vector<component_updater::ComponentInfo>{
           {kLacrosComponentId, "", lacros_component_name,
-           base::Version("1.0.0")}}));
+           base::Version("1.0.0"), ""}}));
 
   bool callback_called = false;
   fake_upstart_client_.set_start_job_cb(base::BindRepeating(
@@ -206,7 +206,7 @@
   EXPECT_CALL(mock_component_update_service_, GetComponents())
       .WillOnce(Return(std::vector<component_updater::ComponentInfo>{
           {kLacrosComponentId, "", lacros_component_name,
-           base::Version("3.0.0")}}));
+           base::Version("3.0.0"), ""}}));
 
   bool callback_called = false;
   fake_upstart_client_.set_start_job_cb(base::BindRepeating(
diff --git a/chrome/browser/ash/crosapi/browser_version_service_ash_unittest.cc b/chrome/browser/ash/crosapi/browser_version_service_ash_unittest.cc
index 5b98314..f62b20d 100644
--- a/chrome/browser/ash/crosapi/browser_version_service_ash_unittest.cc
+++ b/chrome/browser/ash/crosapi/browser_version_service_ash_unittest.cc
@@ -77,7 +77,7 @@
   sample_components.emplace_back(
       sample_browser_component_id, "",
       base::UTF8ToUTF16(browser_util::kLacrosDogfoodDevInfo.name),
-      base::Version(sample_browser_version_str));
+      base::Version(sample_browser_version_str), "");
   ON_CALL(mock_component_update_service, GetComponents())
       .WillByDefault(Return(sample_components));
 
@@ -110,7 +110,7 @@
   sample_components.emplace_back(
       sample_browser_component_id, "",
       base::UTF8ToUTF16(browser_util::kLacrosDogfoodDevInfo.name),
-      base::Version(sample_browser_version_str));
+      base::Version(sample_browser_version_str), "");
   ON_CALL(mock_component_update_service, GetComponents())
       .WillByDefault(Return(sample_components));
 
diff --git a/chrome/browser/ash/crostini/crostini_reporting_util_unittest.cc b/chrome/browser/ash/crostini/crostini_reporting_util_unittest.cc
index 967930cc..4f47214 100644
--- a/chrome/browser/ash/crostini/crostini_reporting_util_unittest.cc
+++ b/chrome/browser/ash/crostini/crostini_reporting_util_unittest.cc
@@ -48,7 +48,7 @@
   test_clock_.SetNow(time);
 
   const auto component_info = component_updater::ComponentInfo(
-      "id2", "fingerprint2", u"cros-termina", base::Version("1.33.7"));
+      "id2", "fingerprint2", u"cros-termina", base::Version("1.33.7"), "");
   EXPECT_CALL(update_service_, GetComponents())
       .Times(1)
       .WillOnce(Return(
@@ -155,11 +155,11 @@
   Mock::VerifyAndClearExpectations(update_service);
 
   const auto component_info_1 = component_updater::ComponentInfo(
-      "id1", "fingerprint1", u"name1", base::Version("1.0"));
+      "id1", "fingerprint1", u"name1", base::Version("1.0"), "");
   const auto component_info_2 = component_updater::ComponentInfo(
-      "id2", "fingerprint2", u"cros-termina", base::Version("1.33.7"));
+      "id2", "fingerprint2", u"cros-termina", base::Version("1.33.7"), "");
   const auto component_info_3 = component_updater::ComponentInfo(
-      "id3", "fingerprint3", u"name1", base::Version("1.0"));
+      "id3", "fingerprint3", u"name1", base::Version("1.0"), "");
   EXPECT_CALL(*update_service, GetComponents())
       .Times(1)
       .WillOnce(Return(std::vector<component_updater::ComponentInfo>(
diff --git a/chrome/browser/ash/login/saml/saml_browsertest.cc b/chrome/browser/ash/login/saml/saml_browsertest.cc
index 2fabd1d..493cb73 100644
--- a/chrome/browser/ash/login/saml/saml_browsertest.cc
+++ b/chrome/browser/ash/login/saml/saml_browsertest.cc
@@ -34,6 +34,7 @@
 #include "chrome/browser/ash/login/test/device_state_mixin.h"
 #include "chrome/browser/ash/login/test/embedded_policy_test_server_mixin.h"
 #include "chrome/browser/ash/login/test/enrollment_ui_mixin.h"
+#include "chrome/browser/ash/login/test/feature_parameter_interface.h"
 #include "chrome/browser/ash/login/test/js_checker.h"
 #include "chrome/browser/ash/login/test/login_manager_mixin.h"
 #include "chrome/browser/ash/login/test/oobe_base_test.h"
@@ -2140,30 +2141,13 @@
   ASSERT_FALSE(fake_saml_idp()->IsLastChallengeResponseExists());
 }
 
-class SAMLDeviceTrustTest
-    : public SAMLDeviceAttestationTest,
-      public testing::WithParamInterface<std::tuple<bool, bool>> {
+// Similar to SamlTestWithFeatures, this class runs for all possible scenarios
+// of the features kDeviceTrustConnectorEnabled &
+// kLoginScreenDeviceTrustConnectorEnabled.
+class SAMLDeviceTrustTest : public SAMLDeviceAttestationTest,
+                            public FeatureAsParameterInterface<2> {
  public:
   SAMLDeviceTrustTest() {
-    std::vector<base::test::FeatureRef> enabled_features;
-    std::vector<base::test::FeatureRef> disabled_features;
-    if (std::get<0>(GetParam())) {
-      enabled_features.push_back(
-          enterprise_connectors::kDeviceTrustConnectorEnabled);
-    } else {
-      disabled_features.push_back(
-          enterprise_connectors::kDeviceTrustConnectorEnabled);
-    }
-
-    if (std::get<1>(GetParam())) {
-      enabled_features.push_back(
-          features::kLoginScreenDeviceTrustConnectorEnabled);
-    } else {
-      disabled_features.push_back(
-          features::kLoginScreenDeviceTrustConnectorEnabled);
-    }
-
-    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
     device_state_.SetState(
         DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED);
   }
@@ -2185,11 +2169,13 @@
   }
 
   bool login_screen_device_trust_enabled() {
-    return std::get<0>(GetParam()) && std::get<1>(GetParam());
+    return IsFeatureEnabledInThisTestCase(
+               enterprise_connectors::kDeviceTrustConnectorEnabled) &&
+           IsFeatureEnabledInThisTestCase(
+               features::kLoginScreenDeviceTrustConnectorEnabled);
   }
 
  private:
-  base::test::ScopedFeatureList scoped_feature_list_;
   base::HistogramTester histogram_tester_;
 };
 
@@ -2337,14 +2323,19 @@
 
 INSTANTIATE_TEST_SUITE_P(All, SamlTestWithFeatures, ::testing::Bool());
 
+const auto kDeviceTrustFeatureVariations =
+    FeatureAsParameterInterface<2>::Generator(
+        {&enterprise_connectors::kDeviceTrustConnectorEnabled,
+         &features::kLoginScreenDeviceTrustConnectorEnabled});
+
 INSTANTIATE_TEST_SUITE_P(All,
                          SAMLDeviceTrustTest,
-                         ::testing::Combine(::testing::Bool(),
-                                            ::testing::Bool()));
+                         testing::ValuesIn(kDeviceTrustFeatureVariations),
+                         FeatureAsParameterInterface<2>::ParamInfoToString);
 
 INSTANTIATE_TEST_SUITE_P(All,
                          SAMLDeviceTrustEnrolledTest,
-                         ::testing::Combine(::testing::Bool(),
-                                            ::testing::Bool()));
+                         testing::ValuesIn(kDeviceTrustFeatureVariations),
+                         FeatureAsParameterInterface<2>::ParamInfoToString);
 
 }  // namespace ash
diff --git a/chrome/browser/bluetooth/web_bluetooth_browsertest.cc b/chrome/browser/bluetooth/web_bluetooth_browsertest.cc
index d123c91..64f61d4 100644
--- a/chrome/browser/bluetooth/web_bluetooth_browsertest.cc
+++ b/chrome/browser/bluetooth/web_bluetooth_browsertest.cc
@@ -198,7 +198,7 @@
     auto fake_notify_session =
         std::make_unique<testing::NiceMock<MockBluetoothGattNotifySession>>(
             GetWeakPtr());
-    active_notify_sessions_.insert(fake_notify_session.get());
+    active_notify_sessions_.insert(fake_notify_session->unique_id());
 
     if (deferred_read_callback_) {
       // A new value as a result of calling readValue().
@@ -224,7 +224,7 @@
     EXPECT_TRUE(IsNotifying());
   }
 
-  void StopNotifySession(BluetoothGattNotifySession* session,
+  void StopNotifySession(BluetoothGattNotifySession::Id session,
                          base::OnceClosure callback) override {
     EXPECT_TRUE(base::Contains(active_notify_sessions_, session));
     std::move(callback).Run();
@@ -251,7 +251,7 @@
   ValueCallback deferred_read_callback_;
   bool defer_read_until_notification_start_ = false;
   bool emit_value_change_at_notification_start_ = false;
-  std::set<BluetoothGattNotifySession*> active_notify_sessions_;
+  std::set<BluetoothGattNotifySession::Id> active_notify_sessions_;
 };
 
 class FakeBluetoothGattConnection
diff --git a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
index 5ff8a56..ff6a34d 100644
--- a/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
+++ b/chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_unittest.cc
@@ -19,6 +19,7 @@
 #include "base/json/values_util.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/raw_ref.h"
 #include "base/memory/weak_ptr.h"
 #include "base/run_loop.h"
@@ -421,7 +422,9 @@
 
  private:
   // TestingProfile owns the history service; we shouldn't delete it.
-  history::HistoryService* history_service_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #constexpr-ctor-field-initializer
+  RAW_PTR_EXCLUSION history::HistoryService* history_service_ = nullptr;
 };
 
 class RemoveFaviconTester {
diff --git a/chrome/browser/devtools/protocol/form_devtools_issues_browsertest.cc b/chrome/browser/devtools/protocol/form_devtools_issues_browsertest.cc
new file mode 100644
index 0000000..6307799
--- /dev/null
+++ b/chrome/browser/devtools/protocol/form_devtools_issues_browsertest.cc
@@ -0,0 +1,83 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <tuple>
+
+#include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/test/browser_test.h"
+#include "content/public/test/content_browser_test_utils.h"
+
+namespace content {
+
+namespace {
+class AutofillFormDevtoolsProtocolTest : public DevToolsProtocolTestBase {
+ public:
+  AutofillFormDevtoolsProtocolTest() = default;
+
+  void NavigateToFormPageAndEnableAudits() {
+    GURL test_url =
+        GetTestUrl("autofill", "autofill_form_devtools_issues_test.html");
+    EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), test_url));
+    EXPECT_TRUE(content::WaitForLoadStop(web_contents()));
+
+    Attach();
+    SendCommandSync("Audits.enable");
+  }
+
+  base::Value::Dict WaitForGenericIssueAdded(const std::string& error_type) {
+    auto matcher = [](const std::string& error_type,
+                      const base::Value::Dict& params) {
+      const std::string* maybe_error_type = params.FindStringByDottedPath(
+          "issue.details.genericIssueDetails.errorType");
+      return maybe_error_type && *maybe_error_type == error_type;
+    };
+
+    base::Value::Dict notification = WaitForMatchingNotification(
+        "Audits.issueAdded", base::BindRepeating(matcher, error_type));
+
+    EXPECT_EQ(*notification.FindStringByDottedPath("issue.code"),
+              "GenericIssue");
+
+    return notification;
+  }
+};
+}  // namespace
+
+IN_PROC_BROWSER_TEST_F(AutofillFormDevtoolsProtocolTest,
+                       FormHasLabelAssociatedToNameAttribute) {
+  NavigateToFormPageAndEnableAudits();
+  base::Value::Dict notification =
+      WaitForGenericIssueAdded("FormLabelForNameError");
+  EXPECT_TRUE(notification
+                  .FindIntByDottedPath(
+                      "issue.details.genericIssueDetails.violatingNodeId")
+                  .has_value());
+}
+
+IN_PROC_BROWSER_TEST_F(AutofillFormDevtoolsProtocolTest,
+                       FormHasInputsWithDuplicateId) {
+  NavigateToFormPageAndEnableAudits();
+  base::Value::Dict notification =
+      WaitForGenericIssueAdded("FormDuplicateIdForInputError");
+  EXPECT_TRUE(notification
+                  .FindIntByDottedPath(
+                      "issue.details.genericIssueDetails.violatingNodeId")
+                  .has_value());
+}
+
+IN_PROC_BROWSER_TEST_F(AutofillFormDevtoolsProtocolTest,
+                       FormHasInputWithNoLabels) {
+  NavigateToFormPageAndEnableAudits();
+  base::Value::Dict notification =
+      WaitForGenericIssueAdded("FormInputWithNoLabelError");
+  EXPECT_TRUE(notification
+                  .FindIntByDottedPath(
+                      "issue.details.genericIssueDetails.violatingNodeId")
+                  .has_value());
+}
+
+}  // namespace content
diff --git a/chrome/browser/download/bubble/download_display_controller_unittest.cc b/chrome/browser/download/bubble/download_display_controller_unittest.cc
index a8569472..aeb6452 100644
--- a/chrome/browser/download/bubble/download_display_controller_unittest.cc
+++ b/chrome/browser/download/bubble/download_display_controller_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/command_line.h"
 #include "base/files/file_path.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/download/bubble/download_bubble_controller.h"
 #include "chrome/browser/download/bubble/download_display.h"
 #include "chrome/browser/download/bubble/download_icon_state.h"
@@ -94,7 +95,9 @@
   bool is_active_ = false;
   bool detail_shown_ = false;
   bool is_fullscreen_ = false;
-  DownloadDisplayController* controller_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #constexpr-ctor-field-initializer
+  RAW_PTR_EXCLUSION DownloadDisplayController* controller_ = nullptr;
 };
 
 class FakeDownloadBubbleUIController : public DownloadBubbleUIController {
diff --git a/chrome/browser/download/download_ui_controller_unittest.cc b/chrome/browser/download/download_ui_controller_unittest.cc
index 8c0b74c..71a1337 100644
--- a/chrome/browser/download/download_ui_controller_unittest.cc
+++ b/chrome/browser/download/download_ui_controller_unittest.cc
@@ -11,6 +11,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
@@ -148,9 +149,16 @@
       content::BrowserContext* browser_context);
 
   std::unique_ptr<MockDownloadManager> manager_;
-  content::DownloadManager::Observer* download_history_manager_observer_;
-  content::DownloadManager::Observer* manager_observer_;
-  download::DownloadItem* notified_item_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::DownloadManager::Observer*
+      download_history_manager_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::DownloadManager::Observer* manager_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION download::DownloadItem* notified_item_;
   base::WeakPtrFactory<download::DownloadItem*> notified_item_receiver_factory_;
 
   raw_ptr<HistoryAdapter> history_adapter_;
diff --git a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
index 5dd08b6..30fa28f 100644
--- a/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
+++ b/chrome/browser/extensions/api/context_menus/context_menu_apitest.cc
@@ -5,6 +5,7 @@
 #include <memory>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/app/chrome_command_ids.h"
 #include "chrome/browser/extensions/extension_apitest.h"
@@ -209,7 +210,9 @@
 
   const Extension* extension() { return extension_; }
 
-  ui::MenuModel* top_level_model_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION ui::MenuModel* top_level_model_ = nullptr;
 
  private:
   content::WebContents* GetBackgroundPage(const std::string& extension_id) {
diff --git a/chrome/browser/extensions/api/downloads/downloads_api_unittest.cc b/chrome/browser/extensions/api/downloads/downloads_api_unittest.cc
index fad9f6a..6ce32ca5 100644
--- a/chrome/browser/extensions/api/downloads/downloads_api_unittest.cc
+++ b/chrome/browser/extensions/api/downloads/downloads_api_unittest.cc
@@ -8,6 +8,7 @@
 
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/download/download_core_service_factory.h"
 #include "chrome/browser/download/download_core_service_impl.h"
 #include "chrome/browser/download/download_history.h"
@@ -116,7 +117,10 @@
       content::BrowserContext* browser_context);
 
   std::unique_ptr<MockDownloadManager> manager_;
-  content::DownloadManager::Observer* download_history_manager_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::DownloadManager::Observer*
+      download_history_manager_observer_;
 };
 
 // static
diff --git a/chrome/browser/extensions/api/extension_action/extension_action_api.h b/chrome/browser/extensions/api/extension_action/extension_action_api.h
index aa440233..5253235 100644
--- a/chrome/browser/extensions/api/extension_action/extension_action_api.h
+++ b/chrome/browser/extensions/api/extension_action/extension_action_api.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/observer_list.h"
 #include "base/scoped_observation.h"
 #include "base/values.h"
@@ -143,7 +144,9 @@
   int tab_id_;
 
   // WebContents for |tab_id_| if one exists.
-  content::WebContents* contents_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::WebContents* contents_;
 
   // The extension action for the current extension.
   raw_ptr<ExtensionAction> extension_action_;
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 62ecedd..2a92e8f 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -1675,6 +1675,11 @@
     "expiry_milestone": 120
   },
   {
+      "name": "enable-accessibility-identifier-to-omnibox-leading-image",
+      "owners":["ameurhosni@google.com","bling-flags@google.com"],
+      "expiry_milestone": 114
+  },
+  {
     "name": "enable-accessibility-live-caption",
     "owners": [ "abigailbklein@google.com", "evliu@google.com", "//ui/accessibility/OWNERS" ],
     "expiry_milestone": 120
diff --git a/chrome/browser/icon_loader_browsertest.cc b/chrome/browser/icon_loader_browsertest.cc
index d41aea8e..1bfb226d 100644
--- a/chrome/browser/icon_loader_browsertest.cc
+++ b/chrome/browser/icon_loader_browsertest.cc
@@ -65,7 +65,7 @@
     FILE_PATH_LITERAL("unlikely-to-exist-file.txt");
 
 // Under GTK, the icon providing functions do not return icons.
-IN_PROC_BROWSER_TEST_F(IconLoaderBrowserTest, LoadGroup) {
+IN_PROC_BROWSER_TEST_F(IconLoaderBrowserTest, DISABLED_LoadGroup) {
   float scale = 1.0;
 #if BUILDFLAG(IS_WIN)
   scale = display::win::GetDPIScale();
diff --git a/chrome/browser/metrics/shutdown_watcher_helper.h b/chrome/browser/metrics/shutdown_watcher_helper.h
index 2fc7b8f7..0b28f1d0 100644
--- a/chrome/browser/metrics/shutdown_watcher_helper.h
+++ b/chrome/browser/metrics/shutdown_watcher_helper.h
@@ -4,6 +4,7 @@
 #ifndef CHROME_BROWSER_METRICS_SHUTDOWN_WATCHER_HELPER_H_
 #define CHROME_BROWSER_METRICS_SHUTDOWN_WATCHER_HELPER_H_
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/threading/platform_thread.h"
 #include "base/threading/watchdog.h"
 #include "base/time/time.h"
@@ -35,7 +36,9 @@
 
  private:
   // shutdown_watchdog_ watches for hangs during shutdown.
-  base::Watchdog* shutdown_watchdog_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION base::Watchdog* shutdown_watchdog_;
 
   // The |thread_id_| on which this object is constructed.
   const base::PlatformThreadId thread_id_;
diff --git a/chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h b/chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h
index 0fc415a..9970ecf 100644
--- a/chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h
+++ b/chrome/browser/navigation_predictor/navigation_predictor_keyed_service.h
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/observer_list.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/time/time.h"
@@ -55,7 +56,9 @@
     // The WebContents from where the navigation may happen. Do not use this
     // pointer outside the observer's call stack unless its destruction is also
     // observed.
-    content::WebContents* web_contents_;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION content::WebContents* web_contents_;
 
     // TODO(spelchat): this no longer needs to be optional. Optionality was
     // required because external app predictions didn't provide this field, but
diff --git a/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc b/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
index 9dc5c38..0d0cd373 100644
--- a/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
+++ b/chrome/browser/optimization_guide/hints_fetcher_browsertest.cc
@@ -744,7 +744,8 @@
       1, 1);
 }
 
-IN_PROC_BROWSER_TEST_F(HintsFetcherBrowserTest, HintsFetcherClearFetchedHints) {
+IN_PROC_BROWSER_TEST_F(HintsFetcherBrowserTest,
+                       DISABLED_HintsFetcherClearFetchedHints) {
   const base::HistogramTester* histogram_tester = GetHistogramTester();
   GURL url = https_url();
 
diff --git a/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc b/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
index 8b451169..19d5880 100644
--- a/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
+++ b/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
@@ -6,6 +6,7 @@
 #include <utility>
 
 #include "base/files/file_enumerator.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/path_service.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/autofill/automated_tests/cache_replayer.h"
@@ -263,7 +264,9 @@
   std::unique_ptr<captured_sites_test_utils::ProfileDataController>
       profile_controller_;
   base::test::ScopedFeatureList feature_list_;
-  content::WebContents* web_contents_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::WebContents* web_contents_ = nullptr;
   std::unique_ptr<ServerUrlLoader> server_url_loader_;
 
   base::CallbackListSubscription create_services_subscription_;
diff --git a/chrome/browser/password_manager/password_manager_test_base.h b/chrome/browser/password_manager/password_manager_test_base.h
index faf1dd8..e540ec1 100644
--- a/chrome/browser/password_manager/password_manager_test_base.h
+++ b/chrome/browser/password_manager/password_manager_test_base.h
@@ -8,6 +8,7 @@
 #include <memory>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/weak_ptr.h"
 #include "base/run_loop.h"
 #include "chrome/browser/ssl/cert_verifier_browser_test.h"
@@ -222,7 +223,9 @@
  private:
   net::EmbeddedTestServer https_test_server_;
   // A tab with some hooks injected.
-  content::WebContents* web_contents_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::WebContents* web_contents_;
 
   base::CallbackListSubscription create_services_subscription_;
 };
diff --git a/chrome/browser/password_manager/password_manager_uitest_util.h b/chrome/browser/password_manager/password_manager_uitest_util.h
index 12a2dbe..f34edd74 100644
--- a/chrome/browser/password_manager/password_manager_uitest_util.h
+++ b/chrome/browser/password_manager/password_manager_uitest_util.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_MANAGER_UITEST_UTIL_H_
 #define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_MANAGER_UITEST_UTIL_H_
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/run_loop.h"
 #include "chrome/browser/ui/autofill/autofill_popup_controller_impl.h"
 #include "chrome/browser/ui/autofill/chrome_autofill_client.h"
@@ -44,7 +45,9 @@
   void MaybeQuitRunLoop();
 
   // The loop to be stopped after the popup state change.
-  base::RunLoop* run_loop_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #constexpr-ctor-field-initializer
+  RAW_PTR_EXCLUSION base::RunLoop* run_loop_ = nullptr;
   GenerationPopup popup_showing_ = GenerationPopup::kHidden;
   GenerationUIState state_ =
       PasswordGenerationPopupController::kOfferGeneration;
diff --git a/chrome/browser/permissions/crowd_deny_safe_browsing_request.h b/chrome/browser/permissions/crowd_deny_safe_browsing_request.h
index a142723..29df1294 100644
--- a/chrome/browser/permissions/crowd_deny_safe_browsing_request.h
+++ b/chrome/browser/permissions/crowd_deny_safe_browsing_request.h
@@ -8,6 +8,7 @@
 #include <memory>
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
 #include "base/time/time.h"
@@ -74,7 +75,9 @@
   VerdictCallback callback_;
 
   // For telemetry purposes. The caller guarantees |clock_| to outlive |this|.
-  const base::Clock* clock_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION const base::Clock* clock_;
   const base::Time request_start_time_;
 
   base::WeakPtrFactory<CrowdDenySafeBrowsingRequest> weak_factory_{this};
diff --git a/chrome/browser/policy/status_provider/user_cloud_policy_status_provider.cc b/chrome/browser/policy/status_provider/user_cloud_policy_status_provider.cc
index 19ed145..35b4a342 100644
--- a/chrome/browser/policy/status_provider/user_cloud_policy_status_provider.cc
+++ b/chrome/browser/policy/status_provider/user_cloud_policy_status_provider.cc
@@ -7,9 +7,11 @@
 #include "base/values.h"
 #include "chrome/browser/policy/status_provider/status_provider_util.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
 #include "components/policy/core/browser/webui/policy_status_provider.h"
 #include "components/policy/core/common/cloud/cloud_policy_core.h"
 #include "components/policy/core/common/cloud/cloud_policy_store.h"
+#include "components/signin/public/identity_manager/identity_manager.h"
 
 UserCloudPolicyStatusProvider::UserCloudPolicyStatusProvider(
     policy::CloudPolicyCore* core,
@@ -19,13 +21,22 @@
 UserCloudPolicyStatusProvider::~UserCloudPolicyStatusProvider() = default;
 
 base::Value::Dict UserCloudPolicyStatusProvider::GetStatus() {
-  if (!core_->store()->is_managed())
+  signin::IdentityManager* identity_manager =
+      IdentityManagerFactory::GetForProfile(profile_);
+  const bool is_flex_org =
+      identity_manager && identity_manager
+                              ->FindExtendedAccountInfoByEmailAddress(
+                                  profile_->GetProfileUserName())
+                              .IsMemberOfFlexOrg();
+  if (!is_flex_org && !core_->store()->is_managed()) {
     return {};
+  }
   base::Value::Dict dict =
       policy::PolicyStatusProvider::GetStatusFromCore(core_);
   ExtractDomainFromUsername(&dict);
   GetUserAffiliationStatus(&dict, profile_);
   dict.Set(policy::kPolicyDescriptionKey, kUserPolicyStatusDescription);
+  dict.Set(policy::kFlexOrgWarningKey, is_flex_org);
   SetDomainInUserStatus(dict);
   SetProfileId(&dict, profile_);
   return dict;
diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc
index 65e2fcf..061aed4 100644
--- a/chrome/browser/printing/print_browsertest.cc
+++ b/chrome/browser/printing/print_browsertest.cc
@@ -13,6 +13,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/path_service.h"
 #include "base/ranges/algorithm.h"
 #include "base/run_loop.h"
@@ -387,7 +388,9 @@
 
   const bool wait_for_loaded_;
   raw_ptr<content::WebContents, DanglingUntriaged> preview_dialog_ = nullptr;
-  base::RunLoop* run_loop_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION base::RunLoop* run_loop_ = nullptr;
 };
 
 class TestPrintRenderFrame
@@ -578,7 +581,9 @@
   }
 
  protected:
-  base::RunLoop* run_loop_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION base::RunLoop* run_loop_ = nullptr;
 
  private:
   void PrintPreviewAllowedForTesting() override {
diff --git a/chrome/browser/printing/print_view_manager_unittest.cc b/chrome/browser/printing/print_view_manager_unittest.cc
index dcfe64f..b6a78cc 100644
--- a/chrome/browser/printing/print_view_manager_unittest.cc
+++ b/chrome/browser/printing/print_view_manager_unittest.cc
@@ -8,6 +8,7 @@
 #include "base/auto_reset.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/notreached.h"
 #include "base/run_loop.h"
 #include "base/test/bind.h"
@@ -287,7 +288,9 @@
     return static_cast<TestPrintJob*>(print_job_.get());
   }
 
-  base::RunLoop* run_loop_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION base::RunLoop* run_loop_ = nullptr;
 };
 
 TEST_F(PrintViewManagerTest, PrintSubFrameAndDestroy) {
diff --git a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSettingsBaseFragment.java b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSettingsBaseFragment.java
index e99a868..c72dc09a 100644
--- a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSettingsBaseFragment.java
+++ b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/PrivacySandboxSettingsBaseFragment.java
@@ -86,7 +86,9 @@
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == R.id.menu_id_targeted_help) {
             // Action for the question mark button.
-            openUrlInCct(PrivacySandboxSettingsFragmentV3.PRIVACY_SANDBOX_URL);
+            openUrlInCct(ChromeFeatureList.isEnabled(ChromeFeatureList.PRIVACY_SANDBOX_SETTINGS_4)
+                            ? PrivacySandboxSettingsFragmentV4.HELP_CENTER_URL
+                            : PrivacySandboxSettingsFragmentV3.PRIVACY_SANDBOX_URL);
             return true;
         }
         return false;
diff --git a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/v4/PrivacySandboxSettingsFragmentV4.java b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/v4/PrivacySandboxSettingsFragmentV4.java
index 6bfdc7c..bfb191c 100644
--- a/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/v4/PrivacySandboxSettingsFragmentV4.java
+++ b/chrome/browser/privacy_sandbox/android/java/src/org/chromium/chrome/browser/privacy_sandbox/v4/PrivacySandboxSettingsFragmentV4.java
@@ -21,6 +21,7 @@
     public static final String TOPICS_PREF = "topics";
     public static final String FLEDGE_PREF = "fledge";
     public static final String AD_MEASUREMENT_PREF = "ad_measurement";
+    public static final String HELP_CENTER_URL = "https://support.google.com/chrome/?p=ad_privacy";
 
     private ChromeBasePreference mTopicsPref;
     private ChromeBasePreference mFledgePref;
diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc b/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc
index 94a6467..e55ee8f 100644
--- a/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc
+++ b/chrome/browser/privacy_sandbox/privacy_sandbox_service.cc
@@ -308,10 +308,12 @@
     pref_service_->SetBoolean(prefs::kPrivacySandboxM1AdMeasurementEnabled,
                               true);
   } else if (PromptAction::kConsentAccepted == action) {
+    DCHECK(privacy_sandbox::kPrivacySandboxSettings4ConsentRequired.Get());
     pref_service_->SetBoolean(prefs::kPrivacySandboxM1ConsentDecisionMade,
                               true);
     pref_service_->SetBoolean(prefs::kPrivacySandboxM1TopicsEnabled, true);
   } else if (PromptAction::kConsentDeclined == action) {
+    DCHECK(privacy_sandbox::kPrivacySandboxSettings4ConsentRequired.Get());
     pref_service_->SetBoolean(prefs::kPrivacySandboxM1ConsentDecisionMade,
                               true);
     pref_service_->SetBoolean(prefs::kPrivacySandboxM1TopicsEnabled, false);
diff --git a/chrome/browser/push_messaging/push_messaging_service_impl.cc b/chrome/browser/push_messaging/push_messaging_service_impl.cc
index ca825969..04d7c1d 100644
--- a/chrome/browser/push_messaging/push_messaging_service_impl.cc
+++ b/chrome/browser/push_messaging/push_messaging_service_impl.cc
@@ -945,20 +945,6 @@
     PrefService* prefs,
     Profile* profile) {
   if (app_level_notifications_enabled) {
-    if (prefs->GetTime(kNotificationsPermissionRevocationGracePeriodDate) !=
-        base::Time()) {
-      // Record when the grace period was started so we can adjust the grace
-      // period duration.
-      base::UmaHistogramLongTimes(
-          "Permissions.FCM.Revocation.ResetGracePeriod",
-          base::Time::Now() -
-              prefs->GetTime(
-                  kNotificationsPermissionRevocationGracePeriodDate));
-    }
-
-    base::UmaHistogramEnumeration("Permissions.FCM.Revocation",
-                                  FcmTokenRevocation::kResetGracePeriod);
-
     // Chrome has app-level Notifications permission. Reset the grace period
     // flag and continue as normal.
     prefs->ClearPref(kNotificationsPermissionRevocationGracePeriodDate);
@@ -985,12 +971,6 @@
     // revokes a push message registration token.
     permission_controller->ResetPermission(blink::PermissionType::NOTIFICATIONS,
                                            url::Origin::Create(origin));
-
-    base::UmaHistogramEnumeration("Permissions.FCM.Revocation",
-                                  FcmTokenRevocation::kRevokePermission);
-  } else {
-    base::UmaHistogramEnumeration("Permissions.FCM.Revocation",
-                                  FcmTokenRevocation::kGracePeriodIsNotOver);
   }
 }
 
diff --git a/chrome/browser/push_messaging/push_messaging_service_impl.h b/chrome/browser/push_messaging/push_messaging_service_impl.h
index 524d792..68a1ff70 100644
--- a/chrome/browser/push_messaging/push_messaging_service_impl.h
+++ b/chrome/browser/push_messaging/push_messaging_service_impl.h
@@ -70,13 +70,6 @@
 
 namespace {
 
-enum class FcmTokenRevocation {
-  kResetGracePeriod = 0,
-  kRevokePermission = 1,
-  kGracePeriodIsNotOver = 2,
-  kMaxValue = kGracePeriodIsNotOver,
-};
-
 struct PendingMessage {
   PendingMessage(std::string app_id, gcm::IncomingMessage message);
   PendingMessage(const PendingMessage& other);
diff --git a/chrome/browser/push_messaging/push_messaging_service_unittest.cc b/chrome/browser/push_messaging/push_messaging_service_unittest.cc
index efb72ce..9e937709 100644
--- a/chrome/browser/push_messaging/push_messaging_service_unittest.cc
+++ b/chrome/browser/push_messaging/push_messaging_service_unittest.cc
@@ -523,8 +523,6 @@
 };
 
 TEST_F(FCMRevocationTest, ResetPrefs) {
-  base::HistogramTester histogram_tester;
-
   content::PermissionController* permission_controller =
       profile()->GetPermissionController();
 
@@ -560,10 +558,6 @@
   result = permission_controller->GetPermissionResultForOriginWithoutContext(
       blink::PermissionType::NOTIFICATIONS, GetOrigin());
   EXPECT_EQ(result.status, blink::mojom::PermissionStatus::GRANTED);
-
-  histogram_tester.ExpectBucketCount(
-      "Permissions.FCM.Revocation",
-      static_cast<int>(FcmTokenRevocation::kResetGracePeriod), 1);
 }
 
 // This test verifies that if the grace period is not started, and there is no
@@ -608,8 +602,6 @@
 // Notifications permissions, site-level Notifications permission will be
 // revoked.
 TEST_F(FCMRevocationTest, NoAppLevelPermissionRevocationTest) {
-  base::HistogramTester histogram_tester;
-
   content::PermissionController* permission_controller =
       profile()->GetPermissionController();
 
@@ -650,18 +642,12 @@
   result = permission_controller->GetPermissionResultForOriginWithoutContext(
       blink::PermissionType::NOTIFICATIONS, GetOrigin());
   EXPECT_EQ(result.status, blink::mojom::PermissionStatus::ASK);
-
-  histogram_tester.ExpectBucketCount(
-      "Permissions.FCM.Revocation",
-      static_cast<int>(FcmTokenRevocation::kRevokePermission), 1);
 }
 
 // This test verifies that if the grace period is not over and there is no
 // app-level Notifications permissions, site-level Notifications permission will
 // not be revoked.
 TEST_F(FCMRevocationTest, NoAppLevelPermissionIgnoreTest) {
-  base::HistogramTester histogram_tester;
-
   content::PermissionController* permission_controller =
       profile()->GetPermissionController();
 
@@ -701,17 +687,11 @@
   result = permission_controller->GetPermissionResultForOriginWithoutContext(
       blink::PermissionType::NOTIFICATIONS, GetOrigin());
   EXPECT_EQ(result.status, blink::mojom::PermissionStatus::GRANTED);
-
-  histogram_tester.ExpectBucketCount(
-      "Permissions.FCM.Revocation",
-      static_cast<int>(FcmTokenRevocation::kGracePeriodIsNotOver), 1);
 }
 
 // This test verifies that if the grace period is not over and there is
 // app-level Notifications permissions, the grace period reset will be tracked.
 TEST_F(FCMRevocationTest, ResetAndRecordGracePeriodTest) {
-  base::HistogramTester histogram_tester;
-
   content::PermissionController* permission_controller =
       profile()->GetPermissionController();
 
@@ -751,9 +731,6 @@
   result = permission_controller->GetPermissionResultForOriginWithoutContext(
       blink::PermissionType::NOTIFICATIONS, GetOrigin());
   EXPECT_EQ(result.status, blink::mojom::PermissionStatus::GRANTED);
-
-  histogram_tester.ExpectTimeBucketCount(
-      "Permissions.FCM.Revocation.ResetGracePeriod", base::Days(2), 1);
 }
 
 #endif
diff --git a/chrome/browser/resources/settings/OWNERS b/chrome/browser/resources/settings/OWNERS
index 64eb48f..b95e788 100644
--- a/chrome/browser/resources/settings/OWNERS
+++ b/chrome/browser/resources/settings/OWNERS
@@ -13,3 +13,6 @@
 
 # Happiness Tracking Surveys (HaTS)
 per-file hats_browser_proxy.ts=sauski@google.com
+
+# Metrics browser proxy
+per-file metrics_browser_proxy.ts=sauski@google.com,rainhard@chromium.org,msramek@chromium.org
diff --git a/chrome/browser/safe_browsing/incident_reporting/download_metadata_manager_unittest.cc b/chrome/browser/safe_browsing/incident_reporting/download_metadata_manager_unittest.cc
index fdbf03f..5dd0d20 100644
--- a/chrome/browser/safe_browsing/incident_reporting/download_metadata_manager_unittest.cc
+++ b/chrome/browser/safe_browsing/incident_reporting/download_metadata_manager_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/run_loop.h"
 #include "base/task/sequenced_task_runner.h"
 #include "chrome/test/base/testing_profile.h"
@@ -258,7 +259,9 @@
 
   // The DownloadMetadataManager's content::DownloadManager::Observer. Captured
   // by download_manager_'s AddObserver action.
-  content::DownloadManager::Observer* dm_observer_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::DownloadManager::Observer* dm_observer_ = nullptr;
 };
 
 // A parameterized test that exercises GetDownloadDetails. The parameters
diff --git a/chrome/browser/sessions/tab_loader.h b/chrome/browser/sessions/tab_loader.h
index c97f1c9..f48216b 100644
--- a/chrome/browser/sessions/tab_loader.h
+++ b/chrome/browser/sessions/tab_loader.h
@@ -13,6 +13,7 @@
 #include "base/gtest_prod_util.h"
 #include "base/memory/memory_pressure_listener.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/time/tick_clock.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
@@ -83,7 +84,9 @@
   // loading timeout timer.
   struct LoadingTab {
     base::TimeTicks loading_start_time;
-    content::WebContents* contents;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION content::WebContents* contents;
 
     // For use with sorted STL containers.
     bool operator<(const LoadingTab& rhs) const {
diff --git a/chrome/browser/sharing/sharing_dialog_data.h b/chrome/browser/sharing/sharing_dialog_data.h
index f9b80a891..4dbaacac 100644
--- a/chrome/browser/sharing/sharing_dialog_data.h
+++ b/chrome/browser/sharing/sharing_dialog_data.h
@@ -10,6 +10,7 @@
 #include <vector>
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/sharing/sharing_app.h"
 #include "chrome/browser/sharing/sharing_metrics.h"
 #include "components/sync_device_info/device_info.h"
@@ -29,8 +30,12 @@
   // work on any background color.
   struct HeaderIcons {
     HeaderIcons(const gfx::VectorIcon* light, const gfx::VectorIcon* dark);
-    const gfx::VectorIcon* light;
-    const gfx::VectorIcon* dark;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION const gfx::VectorIcon* light;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION const gfx::VectorIcon* dark;
   };
   SharingDialogData();
   ~SharingDialogData();
diff --git a/chrome/browser/sync/test/integration/contact_info_helper.cc b/chrome/browser/sync/test/integration/contact_info_helper.cc
new file mode 100644
index 0000000..3d10e42
--- /dev/null
+++ b/chrome/browser/sync/test/integration/contact_info_helper.cc
@@ -0,0 +1,59 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/sync/test/integration/contact_info_helper.h"
+
+#include "chrome/browser/autofill/personal_data_manager_factory.h"
+#include "components/autofill/core/browser/autofill_test_utils.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace contact_info_helper {
+
+namespace {
+
+using autofill::AutofillProfile;
+using autofill::PersonalDataManager;
+
+}  // namespace
+
+autofill::AutofillProfile BuildTestAccountProfile() {
+  AutofillProfile profile = autofill::test::GetFullProfile();
+  // The CONTACT_INFO data type is only concerned with kAccount profiles.
+  // kLocalOrSyncable profiles are handled by the AUTOFILL_PROFILE type.
+  profile.set_source_for_testing(AutofillProfile::Source::kAccount);
+  return profile;
+}
+
+PersonalDataManager* GetPersonalDataManager(Profile* profile) {
+  return autofill::PersonalDataManagerFactory::GetForProfile(profile);
+}
+
+PersonalDataManagerProfileChecker::PersonalDataManagerProfileChecker(
+    PersonalDataManager* pdm,
+    const testing::Matcher<std::vector<AutofillProfile>>& matcher)
+    : pdm_(pdm), matcher_(matcher) {
+  pdm_->AddObserver(this);
+}
+
+PersonalDataManagerProfileChecker::~PersonalDataManagerProfileChecker() {
+  pdm_->RemoveObserver(this);
+}
+
+bool PersonalDataManagerProfileChecker::IsExitConditionSatisfied(
+    std::ostream* os) {
+  std::vector<AutofillProfile> profiles;
+  for (AutofillProfile* profile : pdm_->GetProfiles()) {
+    profiles.push_back(*profile);
+  }
+  testing::StringMatchResultListener listener;
+  bool matches = testing::ExplainMatchResult(matcher_, profiles, &listener);
+  *os << listener.str();
+  return matches;
+}
+
+void PersonalDataManagerProfileChecker::OnPersonalDataChanged() {
+  CheckExitCondition();
+}
+
+}  // namespace contact_info_helper
diff --git a/chrome/browser/sync/test/integration/contact_info_helper.h b/chrome/browser/sync/test/integration/contact_info_helper.h
new file mode 100644
index 0000000..9a92f76
--- /dev/null
+++ b/chrome/browser/sync/test/integration/contact_info_helper.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SYNC_TEST_INTEGRATION_CONTACT_INFO_HELPER_H_
+#define CHROME_BROWSER_SYNC_TEST_INTEGRATION_CONTACT_INFO_HELPER_H_
+
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/sync/test/integration/status_change_checker.h"
+#include "components/autofill/core/browser/data_model/autofill_profile.h"
+#include "components/autofill/core/browser/personal_data_manager.h"
+#include "components/autofill/core/browser/personal_data_manager_observer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace contact_info_helper {
+
+autofill::AutofillProfile BuildTestAccountProfile();
+
+autofill::PersonalDataManager* GetPersonalDataManager(Profile* profile);
+
+// Helper class to wait until the PersonalDataManager's profiles match a given
+// predicate.
+class PersonalDataManagerProfileChecker
+    : public StatusChangeChecker,
+      public autofill::PersonalDataManagerObserver {
+ public:
+  PersonalDataManagerProfileChecker(
+      autofill::PersonalDataManager* pdm,
+      const testing::Matcher<std::vector<autofill::AutofillProfile>>& matcher);
+  ~PersonalDataManagerProfileChecker() override;
+
+  // StatusChangeChecker overrides.
+  bool IsExitConditionSatisfied(std::ostream* os) override;
+
+  // PersonalDataManagerObserver overrides.
+  void OnPersonalDataChanged() override;
+
+ private:
+  const base::raw_ptr<autofill::PersonalDataManager> pdm_;
+  const testing::Matcher<std::vector<autofill::AutofillProfile>> matcher_;
+};
+
+}  // namespace contact_info_helper
+
+#endif  // CHROME_BROWSER_SYNC_TEST_INTEGRATION_CONTACT_INFO_HELPER_H_
diff --git a/chrome/browser/sync/test/integration/single_client_contact_info_sync_test.cc b/chrome/browser/sync/test/integration/single_client_contact_info_sync_test.cc
new file mode 100644
index 0000000..5822623
--- /dev/null
+++ b/chrome/browser/sync/test/integration/single_client_contact_info_sync_test.cc
@@ -0,0 +1,135 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <string>
+
+#include "base/test/scoped_feature_list.h"
+#include "chrome/browser/sync/test/integration/contact_info_helper.h"
+#include "chrome/browser/sync/test/integration/fake_server_match_status_checker.h"
+#include "chrome/browser/sync/test/integration/sync_service_impl_harness.h"
+#include "chrome/browser/sync/test/integration/sync_test.h"
+#include "components/autofill/core/browser/contact_info_sync_util.h"
+#include "components/autofill/core/browser/personal_data_manager.h"
+#include "components/autofill/core/common/autofill_features.h"
+#include "components/sync/base/features.h"
+#include "components/sync/base/model_type.h"
+#include "components/sync/engine/loopback_server/persistent_unique_client_entity.h"
+#include "components/sync/protocol/contact_info_specifics.pb.h"
+#include "components/sync/protocol/entity_specifics.pb.h"
+#include "components/sync/test/fake_server.h"
+#include "content/public/test/browser_test.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+using autofill::AutofillProfile;
+using contact_info_helper::BuildTestAccountProfile;
+using contact_info_helper::PersonalDataManagerProfileChecker;
+using testing::UnorderedElementsAre;
+
+// Helper class to wait until the fake server's ContactInfoSpecifics match a
+// given predicate.
+// Unfortunately, since protos don't have an equality operator, the comparisons
+// are based on the `SerializeAsString()` representation of the specifics.
+class FakeServerSpecificsChecker
+    : public fake_server::FakeServerMatchStatusChecker {
+ public:
+  using Matcher = testing::Matcher<std::vector<std::string>>;
+
+  explicit FakeServerSpecificsChecker(const Matcher& matcher)
+      : matcher_(matcher) {}
+
+  // StatusChangeChecker implementation.
+  bool IsExitConditionSatisfied(std::ostream* os) override {
+    std::vector<std::string> specifics;
+    for (const sync_pb::SyncEntity& entity :
+         fake_server()->GetSyncEntitiesByModelType(syncer::CONTACT_INFO)) {
+      specifics.push_back(
+          entity.specifics().contact_info().SerializeAsString());
+    }
+    testing::StringMatchResultListener listener;
+    bool matches = testing::ExplainMatchResult(matcher_, specifics, &listener);
+    *os << listener.str();
+    return matches;
+  }
+
+ private:
+  const Matcher matcher_;
+};
+
+// Since the sync server operates in terms of entity specifics, this helper
+// function converts a given `profile` to the equivalent ContactInfoSpecifics.
+sync_pb::ContactInfoSpecifics AsContactInfoSpecifics(
+    const AutofillProfile& profile) {
+  return autofill::CreateContactInfoEntityDataFromAutofillProfile(profile)
+      ->specifics.contact_info();
+}
+
+// Adds the given `specifics` to the `fake_server` at creation time 0.
+void AddSpecificsToServer(const sync_pb::ContactInfoSpecifics& specifics,
+                          fake_server::FakeServer* fake_server) {
+  sync_pb::EntitySpecifics entity_specifics;
+  entity_specifics.mutable_contact_info()->CopyFrom(specifics);
+  fake_server->InjectEntity(
+      syncer::PersistentUniqueClientEntity::CreateFromSpecificsForTesting(
+          /*non_unique_name=*/"profile", /*client_tag=*/specifics.guid(),
+          /*entity_specifics=*/entity_specifics, /*creation_time=*/0,
+          /*last_modified_time=*/0));
+}
+
+class SingleClientContactInfoSyncTest : public SyncTest {
+ public:
+  SingleClientContactInfoSyncTest() : SyncTest(SINGLE_CLIENT) {
+    // The `PersonalDataManager` only loads `kAccount` profiles when
+    // kAutofillAccountProfilesUnionView is enabled.
+    features_.InitWithFeatures(
+        /*enabled_features=*/{syncer::kSyncEnableContactInfoDataType,
+                              autofill::features::
+                                  kAutofillAccountProfilesUnionView},
+        /*disabled_features=*/{});
+  }
+
+  // In SINGLE_CLIENT tests, there's only a single PersonalDataManager.
+  autofill::PersonalDataManager* GetPersonalDataManager() const {
+    return contact_info_helper::GetPersonalDataManager(GetProfile(0));
+  }
+
+ private:
+  base::test::ScopedFeatureList features_;
+};
+
+IN_PROC_BROWSER_TEST_F(SingleClientContactInfoSyncTest, DownloadInitialData) {
+  const AutofillProfile kProfile = BuildTestAccountProfile();
+  AddSpecificsToServer(AsContactInfoSpecifics(kProfile), GetFakeServer());
+  ASSERT_TRUE(SetupSync());
+  EXPECT_TRUE(PersonalDataManagerProfileChecker(GetPersonalDataManager(),
+                                                UnorderedElementsAre(kProfile))
+                  .Wait());
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientContactInfoSyncTest, UploadProfile) {
+  const AutofillProfile kProfile = BuildTestAccountProfile();
+  ASSERT_TRUE(SetupSync());
+  GetPersonalDataManager()->AddProfile(kProfile);
+  EXPECT_TRUE(FakeServerSpecificsChecker(
+                  UnorderedElementsAre(
+                      AsContactInfoSpecifics(kProfile).SerializeAsString()))
+                  .Wait());
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientContactInfoSyncTest, ClearOnDisableSync) {
+  const AutofillProfile kProfile = BuildTestAccountProfile();
+  AddSpecificsToServer(AsContactInfoSpecifics(kProfile), GetFakeServer());
+  ASSERT_TRUE(SetupSync());
+  ASSERT_TRUE(PersonalDataManagerProfileChecker(GetPersonalDataManager(),
+                                                UnorderedElementsAre(kProfile))
+                  .Wait());
+  GetClient(0)->StopSyncServiceAndClearData();
+  EXPECT_TRUE(PersonalDataManagerProfileChecker(GetPersonalDataManager(),
+                                                testing::IsEmpty())
+                  .Wait());
+}
+
+}  // namespace
diff --git a/chrome/browser/sync/test/integration/sync_integration_tests_sources.gni b/chrome/browser/sync/test/integration/sync_integration_tests_sources.gni
index 93def0e..8acc609 100644
--- a/chrome/browser/sync/test/integration/sync_integration_tests_sources.gni
+++ b/chrome/browser/sync/test/integration/sync_integration_tests_sources.gni
@@ -7,6 +7,7 @@
 sync_integration_tests_sources = [
   "../browser/sync/test/integration/single_client_autofill_profile_sync_test.cc",
   "../browser/sync/test/integration/single_client_bookmarks_sync_test.cc",
+  "../browser/sync/test/integration/single_client_contact_info_sync_test.cc",
   "../browser/sync/test/integration/single_client_custom_passphrase_sync_test.cc",
   "../browser/sync/test/integration/single_client_device_info_sync_test.cc",
   "../browser/sync/test/integration/single_client_history_delete_directives_sync_test.cc",
diff --git a/chrome/browser/ui/browser_tab_strip_tracker.h b/chrome/browser/ui/browser_tab_strip_tracker.h
index 23029286..e084423 100644
--- a/chrome/browser/ui/browser_tab_strip_tracker.h
+++ b/chrome/browser/ui/browser_tab_strip_tracker.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_UI_BROWSER_TAB_STRIP_TRACKER_H_
 #define CHROME_BROWSER_UI_BROWSER_TAB_STRIP_TRACKER_H_
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/browser_list_observer.h"
 
 class BrowserTabStripTrackerDelegate;
@@ -52,8 +53,12 @@
   void OnBrowserAdded(Browser* browser) override;
   void OnBrowserRemoved(Browser* browser) override;
 
-  TabStripModelObserver* const tab_strip_model_observer_;
-  BrowserTabStripTrackerDelegate* const delegate_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION TabStripModelObserver* const tab_strip_model_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION BrowserTabStripTrackerDelegate* const delegate_;
   bool is_processing_initial_browsers_;
 };
 
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller.h b/chrome/browser/ui/exclusive_access/fullscreen_controller.h
index 0a8e5eb..6502b3e 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller.h
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_FULLSCREEN_CONTROLLER_H_
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/weak_ptr.h"
 #include "base/observer_list.h"
 #include "chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h"
@@ -241,7 +242,9 @@
 
   // Set in OnTabDeactivated(). Used to see if we're in the middle of
   // deactivation of a tab.
-  content::WebContents* deactivated_contents_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION content::WebContents* deactivated_contents_ = nullptr;
 
   // Used in testing to set the state to tab fullscreen.
   bool is_tab_fullscreen_for_testing_ = false;
diff --git a/chrome/browser/ui/extensions/extension_settings_overridden_dialog.h b/chrome/browser/ui/extensions/extension_settings_overridden_dialog.h
index f7dc15c..474839b 100644
--- a/chrome/browser/ui/extensions/extension_settings_overridden_dialog.h
+++ b/chrome/browser/ui/extensions/extension_settings_overridden_dialog.h
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/extensions/settings_overridden_dialog_controller.h"
 #include "extensions/common/extension_id.h"
 
@@ -50,7 +51,9 @@
     std::u16string dialog_message;
 
     // The icon to display in the dialog, if any.
-    const gfx::VectorIcon* icon = nullptr;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION const gfx::VectorIcon* icon = nullptr;
   };
 
   ExtensionSettingsOverriddenDialog(Params params, Profile* profile);
diff --git a/chrome/browser/ui/global_media_controls/presentation_request_notification_item.h b/chrome/browser/ui/global_media_controls/presentation_request_notification_item.h
index 692d3a86..5669756b 100644
--- a/chrome/browser/ui/global_media_controls/presentation_request_notification_item.h
+++ b/chrome/browser/ui/global_media_controls/presentation_request_notification_item.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <string>
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/weak_ptr.h"
 #include "components/media_message_center/media_notification_item.h"
 #include "components/media_router/browser/presentation/start_presentation_context.h"
@@ -105,7 +106,10 @@
   void OnFaviconBitmap(const SkBitmap& bitmap);
 
   const std::string id_;
-  global_media_controls::MediaItemManager* const item_manager_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION global_media_controls::MediaItemManager* const
+      item_manager_;
 
   // True if the item is created from a default PresentationRequest, which means
   // |context_| is set to nullptr in the constructor.
@@ -130,7 +134,10 @@
   absl::optional<gfx::ImageSkia> artwork_image_;
   absl::optional<gfx::ImageSkia> favicon_image_;
 
-  media_message_center::MediaNotificationView* view_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION media_message_center::MediaNotificationView* view_ =
+      nullptr;
 
   base::WeakPtrFactory<PresentationRequestNotificationItem> weak_ptr_factory_{
       this};
diff --git a/chrome/browser/ui/startup/startup_browser_creator_browsertest.cc b/chrome/browser/ui/startup/startup_browser_creator_browsertest.cc
index c4fc5705..dabea8e 100644
--- a/chrome/browser/ui/startup/startup_browser_creator_browsertest.cc
+++ b/chrome/browser/ui/startup/startup_browser_creator_browsertest.cc
@@ -16,6 +16,7 @@
 #include "base/json/json_writer.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/path_service.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
@@ -347,7 +348,9 @@
 
   void OnBrowserRemoved(Browser* browser) override {}
 
-  Browser* added_browser_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #constexpr-ctor-field-initializer
+  RAW_PTR_EXCLUSION Browser* added_browser_ = nullptr;
 };
 
 // Test that when there is a popup as the active browser any requests to
diff --git a/chrome/browser/ui/views/commander_frontend_views.cc b/chrome/browser/ui/views/commander_frontend_views.cc
index 734759e..dc45ac0a3 100644
--- a/chrome/browser/ui/views/commander_frontend_views.cc
+++ b/chrome/browser/ui/views/commander_frontend_views.cc
@@ -9,6 +9,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/browser_process.h"
@@ -115,7 +116,9 @@
 
  private:
   views::UnhandledKeyboardEventHandler event_handler_;
-  views::View* owner_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::View* owner_ = nullptr;
 };
 
 BEGIN_METADATA(CommanderWebView, views::WebView)
diff --git a/chrome/browser/ui/views/extensions/browser_action_drag_data.h b/chrome/browser/ui/views/extensions/browser_action_drag_data.h
index 3b3f434..63e37e7 100644
--- a/chrome/browser/ui/views/extensions/browser_action_drag_data.h
+++ b/chrome/browser/ui/views/extensions/browser_action_drag_data.h
@@ -9,6 +9,7 @@
 
 #include <string>
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "ui/base/dragdrop/os_exchange_data.h"
 
 class Profile;
@@ -53,7 +54,9 @@
   bool ReadFromPickle(base::Pickle* pickle);
 
   // The profile we originated from.
-  void* profile_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION void* profile_;
 
   // The id of the view being dragged.
   std::string id_;
diff --git a/chrome/browser/ui/views/extensions/extension_view_views.h b/chrome/browser/ui/views/extensions/extension_view_views.h
index b9150a29..fc6fb3a 100644
--- a/chrome/browser/ui/views/extensions/extension_view_views.h
+++ b/chrome/browser/ui/views/extensions/extension_view_views.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_VIEWS_EXTENSIONS_EXTENSION_VIEW_VIEWS_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/observer_list.h"
 #include "base/observer_list_types.h"
 #include "chrome/browser/extensions/extension_view.h"
@@ -86,7 +87,9 @@
 
   // The container this view is in (not necessarily its direct superview).
   // Note: the view does not own its container.
-  Container* container_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION Container* container_ = nullptr;
 
   // A handler to handle unhandled keyboard messages coming back from the
   // renderer process.
diff --git a/chrome/browser/ui/views/extensions/extensions_menu_main_page_view.h b/chrome/browser/ui/views/extensions/extensions_menu_main_page_view.h
index 7dbbdcf..14d6083 100644
--- a/chrome/browser/ui/views/extensions/extensions_menu_main_page_view.h
+++ b/chrome/browser/ui/views/extensions/extensions_menu_main_page_view.h
@@ -5,6 +5,7 @@
 #ifndef CHROME_BROWSER_UI_VIEWS_EXTENSIONS_EXTENSIONS_MENU_MAIN_PAGE_VIEW_H_
 #define CHROME_BROWSER_UI_VIEWS_EXTENSIONS_EXTENSIONS_MENU_MAIN_PAGE_VIEW_H_
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
 #include "chrome/browser/ui/views/extensions/extensions_menu_page_view.h"
 
@@ -65,7 +66,9 @@
   // The view containing the menu items. This is separated for easy insertion
   // and iteration of menu items. The children are guaranteed to only be
   // InstalledExtensionMenuItemViews.
-  views::View* menu_items_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::View* menu_items_ = nullptr;
 };
 
 BEGIN_VIEW_BUILDER(/* no export */,
diff --git a/chrome/browser/ui/views/extensions/extensions_menu_test_util.h b/chrome/browser/ui/views/extensions/extensions_menu_test_util.h
index 174646d..8797d3de 100644
--- a/chrome/browser/ui/views/extensions/extensions_menu_test_util.h
+++ b/chrome/browser/ui/views/extensions/extensions_menu_test_util.h
@@ -10,6 +10,7 @@
 
 #include "base/auto_reset.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/extensions/extension_action_test_helper.h"
 
 class Browser;
@@ -74,7 +75,9 @@
   std::unique_ptr<ExtensionsMenuView> owned_menu_view_;
 
   // The actual pointer to an ExtensionsMenuView, non-null if alive.
-  ExtensionsMenuView* menu_view_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION ExtensionsMenuView* menu_view_ = nullptr;
 };
 
 #endif  // CHROME_BROWSER_UI_VIEWS_EXTENSIONS_EXTENSIONS_MENU_TEST_UTIL_H_
diff --git a/chrome/browser/ui/views/extensions/extensions_tabbed_menu_view.h b/chrome/browser/ui/views/extensions/extensions_tabbed_menu_view.h
index 5eb9c3f..9a41f3b3 100644
--- a/chrome/browser/ui/views/extensions/extensions_tabbed_menu_view.h
+++ b/chrome/browser/ui/views/extensions/extensions_tabbed_menu_view.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_VIEWS_EXTENSIONS_EXTENSIONS_TABBED_MENU_VIEW_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/extensions/site_permissions_helper.h"
 #include "chrome/browser/ui/extensions/extension_action_view_controller.h"
 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
@@ -207,10 +208,14 @@
   // The view containing the installed menu items in the extensions tab. This is
   // separated for easy insertion and iteration of menu items. The children are
   // guaranteed to only be ExtensionMenuItemViews.
-  views::View* installed_items_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::View* installed_items_ = nullptr;
 
   // The button used to open the webstore page in the extensions tab.
-  HoverButton* discover_more_button_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION HoverButton* discover_more_button_ = nullptr;
 
   // The view containing a message in the site access tab.
   raw_ptr<views::Label> site_access_message_ = nullptr;
diff --git a/chrome/browser/ui/views/find_bar_view.h b/chrome/browser/ui/views/find_bar_view.h
index f2fcffdc..c8d153f 100644
--- a/chrome/browser/ui/views/find_bar_view.h
+++ b/chrome/browser/ui/views/find_bar_view.h
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/views/chrome_views_export.h"
 #include "chrome/browser/ui/views/dropdown_bar_host_delegate.h"
 #include "ui/views/controls/button/button.h"
@@ -115,13 +116,25 @@
   std::u16string last_searched_text_;
 
   // The controls in the window.
-  views::Textfield* find_text_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Textfield* find_text_;
   std::unique_ptr<views::Painter> find_text_border_;
-  FindBarMatchCountLabel* match_count_text_;
-  views::Separator* separator_;
-  views::ImageButton* find_previous_button_;
-  views::ImageButton* find_next_button_;
-  views::ImageButton* close_button_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION FindBarMatchCountLabel* match_count_text_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Separator* separator_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::ImageButton* find_previous_button_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::ImageButton* find_next_button_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::ImageButton* close_button_;
 };
 
 BEGIN_VIEW_BUILDER(/* no export */, FindBarView, views::BoxLayoutView)
diff --git a/chrome/browser/ui/views/frame/contents_web_view.h b/chrome/browser/ui/views/frame/contents_web_view.h
index 99b009b..d806ca1 100644
--- a/chrome/browser/ui/views/frame/contents_web_view.h
+++ b/chrome/browser/ui/views/frame/contents_web_view.h
@@ -7,6 +7,7 @@
 
 #include <memory>
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/views/frame/web_contents_close_handler_delegate.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/skia/include/core/SkColor.h"
@@ -54,7 +55,9 @@
 
  private:
   void UpdateBackgroundColor();
-  StatusBubbleViews* status_bubble_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION StatusBubbleViews* status_bubble_;
 
   bool background_visible_ = true;
 
diff --git a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout_unittest.cc b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout_unittest.cc
index cd86a727..0f607b2 100644
--- a/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout_unittest.cc
+++ b/chrome/browser/ui/views/frame/opaque_browser_frame_view_layout_unittest.cc
@@ -10,6 +10,7 @@
 
 #include "base/command_line.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
@@ -371,7 +372,9 @@
   raw_ptr<views::ImageButton> restore_button_ = nullptr;
   raw_ptr<views::ImageButton> close_button_ = nullptr;
 
-  TabIconView* tab_icon_view_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION TabIconView* tab_icon_view_ = nullptr;
   raw_ptr<views::Label> window_title_ = nullptr;
 };
 
diff --git a/chrome/browser/ui/views/passwords/credential_leak_dialog_view.h b/chrome/browser/ui/views/passwords/credential_leak_dialog_view.h
index b210f2b..752e5db 100644
--- a/chrome/browser/ui/views/passwords/credential_leak_dialog_view.h
+++ b/chrome/browser/ui/views/passwords/credential_leak_dialog_view.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_VIEWS_PASSWORDS_CREDENTIAL_LEAK_DIALOG_VIEW_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/passwords/password_dialog_prompts.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/window/dialog_delegate.h"
@@ -39,7 +40,9 @@
   void InitWindow();
 
   // A weak pointer to the controller.
-  CredentialLeakDialogController* controller_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION CredentialLeakDialogController* controller_ = nullptr;
   const raw_ptr<content::WebContents, DanglingUntriaged> web_contents_ =
       nullptr;
 };
diff --git a/chrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view_browsertest.cc b/chrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view_browsertest.cc
index 4ef9dad..60451f07 100644
--- a/chrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view_browsertest.cc
+++ b/chrome/browser/ui/views/payments/secure_payment_confirmation_dialog_view_browsertest.cc
@@ -157,6 +157,8 @@
                 ::testing::HasSubstr(base::UTF16ToUTF8(opt_out_link_label)));
   }
 
+  // Verify that the data displayed on the view matches what is expected given
+  // the current `model_` state.
   void ExpectViewMatchesModel() {
     ASSERT_NE(test_delegate_->dialog_view(), nullptr);
 
@@ -231,89 +233,6 @@
                      model_.opt_out_link_label());
   }
 
-  void ClickAcceptAndWait() {
-    ResetEventWaiter(DialogEvent::DIALOG_CLOSED);
-
-    test_delegate_->dialog_view()->AcceptDialog();
-    event_waiter_->Wait();
-
-    EXPECT_TRUE(confirm_pressed_);
-    EXPECT_FALSE(cancel_pressed_);
-    EXPECT_FALSE(opt_out_clicked_);
-
-    histogram_tester_.ExpectTotalCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        1);
-    histogram_tester_.ExpectBucketCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        SecurePaymentConfirmationAuthenticationDialogResult::kAccepted, 1);
-  }
-
-  void ClickCancelAndWait() {
-    ResetEventWaiter(DialogEvent::DIALOG_CLOSED);
-
-    test_delegate_->dialog_view()->CancelDialog();
-    event_waiter_->Wait();
-
-    EXPECT_TRUE(cancel_pressed_);
-    EXPECT_FALSE(confirm_pressed_);
-    EXPECT_FALSE(opt_out_clicked_);
-
-    histogram_tester_.ExpectTotalCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        1);
-    histogram_tester_.ExpectBucketCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        SecurePaymentConfirmationAuthenticationDialogResult::kCanceled, 1);
-  }
-
-  void CloseDialogAndWait() {
-    ResetEventWaiter(DialogEvent::DIALOG_CLOSED);
-
-    test_delegate_->CloseDialog();
-    event_waiter_->Wait();
-
-    EXPECT_FALSE(cancel_pressed_);
-    EXPECT_FALSE(confirm_pressed_);
-    EXPECT_FALSE(opt_out_clicked_);
-
-    histogram_tester_.ExpectTotalCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        1);
-    histogram_tester_.ExpectBucketCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        SecurePaymentConfirmationAuthenticationDialogResult::kClosed, 1);
-  }
-
-  void ClickOptOutAndWait() {
-    ResetEventWaiter(DialogEvent::OPT_OUT_CLICKED);
-
-    views::StyledLabel* opt_out_label = static_cast<views::StyledLabel*>(
-        test_delegate_->dialog_view()->GetFootnoteViewForTesting());
-    opt_out_label->ClickFirstLinkForTesting();
-
-    event_waiter_->Wait();
-
-    EXPECT_FALSE(cancel_pressed_);
-    EXPECT_FALSE(confirm_pressed_);
-    EXPECT_TRUE(opt_out_clicked_);
-
-    histogram_tester_.ExpectTotalCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        1);
-    histogram_tester_.ExpectBucketCount(
-        "PaymentRequest.SecurePaymentConfirmation.Funnel."
-        "AuthenticationDialogResult",
-        SecurePaymentConfirmationAuthenticationDialogResult::kOptOut, 1);
-  }
-
   void ResetEventWaiter(DialogEvent event) {
     event_waiter_ = std::make_unique<autofill::EventWaiter<DialogEvent>>(
         std::list<DialogEvent>{event});
@@ -359,75 +278,98 @@
       weak_ptr_factory_{this};
 };
 
+// Basic test that the view matches the model state.
+IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
+                       ViewMatchesModel) {
+  CreateModel();
+  InvokeSecurePaymentConfirmationUI();
+  ExpectViewMatchesModel();
+}
+
+// Test that clicking the 'Accept' button triggers the expected path and closes
+// the dialog.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        AcceptButtonTest) {
   CreateModel();
-
   InvokeSecurePaymentConfirmationUI();
 
-  ExpectViewMatchesModel();
+  ResetEventWaiter(DialogEvent::DIALOG_CLOSED);
+  test_delegate_->dialog_view()->AcceptDialog();
+  event_waiter_->Wait();
 
-  ClickAcceptAndWait();
+  EXPECT_TRUE(confirm_pressed_);
+  EXPECT_FALSE(cancel_pressed_);
+  EXPECT_FALSE(opt_out_clicked_);
+
+  histogram_tester_.ExpectTotalCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      1);
+  histogram_tester_.ExpectBucketCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      SecurePaymentConfirmationAuthenticationDialogResult::kAccepted, 1);
 }
 
+// Test that clicking the 'Cancel' button triggers the expected path and closes
+// the dialog.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        CancelButtonTest) {
   CreateModel();
-
   InvokeSecurePaymentConfirmationUI();
 
-  ExpectViewMatchesModel();
+  ResetEventWaiter(DialogEvent::DIALOG_CLOSED);
+  test_delegate_->dialog_view()->CancelDialog();
+  event_waiter_->Wait();
 
-  ClickCancelAndWait();
+  EXPECT_TRUE(cancel_pressed_);
+  EXPECT_FALSE(confirm_pressed_);
+  EXPECT_FALSE(opt_out_clicked_);
+
+  histogram_tester_.ExpectTotalCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      1);
+  histogram_tester_.ExpectBucketCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      SecurePaymentConfirmationAuthenticationDialogResult::kCanceled, 1);
 }
 
-IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
-                       CloseDialogTest) {
-  CreateModel();
-
-  InvokeSecurePaymentConfirmationUI();
-
-  ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
-}
-
+// Test that the progress bar is visible in the view when requested by the
+// model.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        ProgressBarVisible) {
   CreateModel();
   model_.set_progress_bar_visible(true);
-
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
 }
 
+// Test that the view can be updated to show the progress bar after initial
+// load.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        ShowProgressBar) {
+  // Initially the model should be created with the progress bar hidden.
   CreateModel();
-
   ASSERT_FALSE(model_.progress_bar_visible());
 
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
 
+  // Then update the model to have the bar be visible and check that the view
+  // updates to match.
   model_.set_progress_bar_visible(true);
+  ASSERT_TRUE(model_.progress_bar_visible());
   test_delegate_->dialog_view()->OnModelUpdated();
-
   ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
 }
 
+// Check that the view updates to match model updates.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        OnModelUpdated) {
   CreateModel();
-
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
 
   model_.set_title(u"Test Title");
@@ -443,10 +385,7 @@
   model_.set_cancel_button_label(u"Test cancel");
 
   test_delegate_->dialog_view()->OnModelUpdated();
-
   ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
 }
 
 // Test the two reasons an instrument icon is updated: The model's bitmap
@@ -454,9 +393,7 @@
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        InstrumentIconUpdated) {
   CreateModel();
-
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
 
   // Change the bitmap pointer
@@ -470,10 +407,10 @@
   *instrument_icon_ = CreateMaxSizeInstrumentIcon(SK_ColorRED);
   test_delegate_->dialog_view()->OnModelUpdated();
   ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
 }
 
+// Test that the web contents can be torn down whilst the dialog is visible, and
+// that doing so should close the dialog.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        WebContentsClosed) {
   CreateModel();
@@ -486,11 +423,14 @@
   event_waiter_->Wait();
 }
 
+// TestBrowserUi-provided UI test.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        InvokeUi_default) {
   ShowAndVerifyUi();
 }
 
+// Test that the visible instrument icon is set correctly to the default icon if
+// the model does not provide one.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        DefaultInstrumentIcon) {
   CreateModel();
@@ -500,12 +440,11 @@
   model_.set_instrument_icon(instrument_icon_.get());
 
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
-
-  CloseDialogAndWait();
 }
 
+// Test that the merchant label is formatted correctly based on the input
+// name/origin provided in the model.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        MerchantLabelFormat) {
   CreateModel();
@@ -536,8 +475,6 @@
   ExpectLabelText(
       u"merchant2.com",
       SecurePaymentConfirmationDialogView::DialogViewID::MERCHANT_VALUE);
-
-  CloseDialogAndWait();
 }
 
 // Test that an oversized instrument icon is resized down to the maximum size.
@@ -559,8 +496,6 @@
             image_view->GetImageBounds().width());
   EXPECT_EQ(kSecurePaymentConfirmationInstrumentIconHeightPx,
             image_view->GetImageBounds().height());
-
-  CloseDialogAndWait();
 }
 
 // Test that an undersized instrument icon is resized up to the minimum size.
@@ -582,8 +517,6 @@
             image_view->GetImageBounds().width());
   EXPECT_EQ(kSecurePaymentConfirmationInstrumentIconHeightPx,
             image_view->GetImageBounds().height());
-
-  CloseDialogAndWait();
 }
 
 // Test that a midsized instrument icon is not resized.
@@ -606,10 +539,10 @@
   EXPECT_EQ(width, image_view->GetImageBounds().width());
   EXPECT_EQ(kSecurePaymentConfirmationInstrumentIconHeightPx,
             image_view->GetImageBounds().height());
-
-  CloseDialogAndWait();
 }
 
+// Test that the opt-out link is only shown when explicitly requested, and that
+// clicking it causes the expected path and closes the dialog.
 IN_PROC_BROWSER_TEST_F(SecurePaymentConfirmationDialogViewTest,
                        OptOutShownWhenRequested) {
   CreateModel();
@@ -618,14 +551,32 @@
   // other test is correctly testing the 'no opt out' path.
   ASSERT_FALSE(model_.opt_out_visible());
 
-  // Now set it to true, and invoke SPC.
   model_.set_opt_out_visible(true);
-
   InvokeSecurePaymentConfirmationUI();
-
   ExpectViewMatchesModel();
 
-  ClickOptOutAndWait();
+  // Now click the opt-out link, and verify that the dialog is closed via the
+  // correct path.
+  ResetEventWaiter(DialogEvent::OPT_OUT_CLICKED);
+
+  views::StyledLabel* opt_out_label = static_cast<views::StyledLabel*>(
+      test_delegate_->dialog_view()->GetFootnoteViewForTesting());
+  opt_out_label->ClickFirstLinkForTesting();
+
+  event_waiter_->Wait();
+
+  EXPECT_FALSE(cancel_pressed_);
+  EXPECT_FALSE(confirm_pressed_);
+  EXPECT_TRUE(opt_out_clicked_);
+
+  histogram_tester_.ExpectTotalCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      1);
+  histogram_tester_.ExpectBucketCount(
+      "PaymentRequest.SecurePaymentConfirmation.Funnel."
+      "AuthenticationDialogResult",
+      SecurePaymentConfirmationAuthenticationDialogResult::kOptOut, 1);
 }
 
 }  // namespace payments
diff --git a/chrome/browser/ui/views/profiles/profile_menu_view_base.h b/chrome/browser/ui/views/profiles/profile_menu_view_base.h
index 0bec47d..521e61e4 100644
--- a/chrome/browser/ui/views/profiles/profile_menu_view_base.h
+++ b/chrome/browser/ui/views/profiles/profile_menu_view_base.h
@@ -13,6 +13,7 @@
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/app/vector_icons/vector_icons.h"
 #include "chrome/browser/profiles/profile_metrics.h"
 #include "chrome/browser/ui/browser_window.h"
@@ -76,7 +77,9 @@
     EditButtonParams(const EditButtonParams&);
     ~EditButtonParams();
 
-    const gfx::VectorIcon* edit_icon;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION const gfx::VectorIcon* edit_icon;
     std::u16string edit_tooltip_text;
     base::RepeatingClosure edit_action;
   };
diff --git a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
index ca3155e..cc2fa36 100644
--- a/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
+++ b/chrome/browser/ui/views/relaunch_notification/relaunch_notification_controller_platform_impl_desktop.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_VIEWS_RELAUNCH_NOTIFICATION_RELAUNCH_NOTIFICATION_CONTROLLER_PLATFORM_IMPL_DESKTOP_H_
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/time/time.h"
 #include "chrome/browser/ui/browser_list_observer.h"
 #include "ui/views/widget/widget_observer.h"
@@ -62,7 +63,9 @@
 
   // The widget hosting the bubble or dialog, or nullptr if neither is is
   // currently shown.
-  views::Widget* widget_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION views::Widget* widget_ = nullptr;
 
   // A callback run when the relaunch required notification first becomes
   // visible to the user. This callback is valid only while the instance is
diff --git a/chrome/browser/ui/views/sharing_hub/screenshot/screenshot_captured_bubble.h b/chrome/browser/ui/views/sharing_hub/screenshot/screenshot_captured_bubble.h
index f3659129..4976d332 100644
--- a/chrome/browser/ui/views/sharing_hub/screenshot/screenshot_captured_bubble.h
+++ b/chrome/browser/ui/views/sharing_hub/screenshot/screenshot_captured_bubble.h
@@ -9,6 +9,7 @@
 
 #include "base/files/file_path.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/weak_ptr.h"
 #include "chrome/browser/ui/views/location_bar/location_bar_bubble_delegate_view.h"
 #include "ui/base/metadata/metadata_header_macros.h"
@@ -87,9 +88,15 @@
   base::OnceCallback<void(NavigateParams*)> edit_callback_;
 
   // Pointers to view widgets; weak.
-  views::ImageView* image_view_ = nullptr;
-  views::MdTextButton* download_button_ = nullptr;
-  views::LabelButton* edit_button_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::ImageView* image_view_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::MdTextButton* download_button_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::LabelButton* edit_button_ = nullptr;
 
   base::WeakPtrFactory<ScreenshotCapturedBubble> weak_factory_{this};
 };
diff --git a/chrome/browser/ui/views/status_bubble_views.cc b/chrome/browser/ui/views/status_bubble_views.cc
index 254f687..7ecf8cf4 100644
--- a/chrome/browser/ui/views/status_bubble_views.cc
+++ b/chrome/browser/ui/views/status_bubble_views.cc
@@ -11,6 +11,7 @@
 #include "base/i18n/rtl.h"
 #include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/single_thread_task_runner.h"
@@ -219,7 +220,9 @@
   raw_ptr<StatusBubbleViews> status_bubble_;
 
   // The currently-displayed text.
-  views::Label* text_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Label* text_;
 
   // A timer used to delay destruction of the popup widget. This is meant to
   // balance the performance tradeoffs of rapid creation/destruction and the
diff --git a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
index 88ac366..6681199 100644
--- a/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
+++ b/chrome/browser/ui/views/toolbar/chrome_labs_bubble_view.cc
@@ -7,6 +7,7 @@
 #include "base/callback_list.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "build/build_config.h"
 #include "build/buildflag.h"
 #include "chrome/browser/about_flags.h"
@@ -79,8 +80,12 @@
   }
 
  private:
-  views::MdTextButton* restart_button_;
-  views::Label* restart_label_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::MdTextButton* restart_button_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Label* restart_label_;
 };
 
 BEGIN_METADATA(ChromeLabsFooter, views::View)
diff --git a/chrome/browser/ui/views/toolbar/chrome_labs_item_view.h b/chrome/browser/ui/views/toolbar/chrome_labs_item_view.h
index 5c3a12ca..b75848c 100644
--- a/chrome/browser/ui/views/toolbar/chrome_labs_item_view.h
+++ b/chrome/browser/ui/views/toolbar/chrome_labs_item_view.h
@@ -7,6 +7,7 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/views/view.h"
 
@@ -63,11 +64,15 @@
   raw_ptr<user_education::NewBadgeLabel> experiment_name_;
 
   // Combobox with selected state of the lab.
-  views::Combobox* lab_state_combobox_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Combobox* lab_state_combobox_;
 
   raw_ptr<const flags_ui::FeatureEntry> feature_entry_;
 
-  views::MdTextButton* feedback_button_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::MdTextButton* feedback_button_;
 
   base::RepeatingClosureList combobox_callback_list_;
 };
diff --git a/chrome/browser/ui/views/web_apps/web_app_confirmation_view.h b/chrome/browser/ui/views/web_apps/web_app_confirmation_view.h
index 6952f06..99945d1 100644
--- a/chrome/browser/ui/views/web_apps/web_app_confirmation_view.h
+++ b/chrome/browser/ui/views/web_apps/web_app_confirmation_view.h
@@ -7,6 +7,7 @@
 
 #include <string>
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/browser_dialogs.h"
 #include "chrome/browser/web_applications/web_app_install_info.h"
 #include "ui/base/metadata/metadata_header_macros.h"
@@ -57,15 +58,25 @@
   chrome::AppInstallationAcceptanceCallback callback_;
 
   // Checkbox to launch as a window.
-  views::Checkbox* open_as_window_checkbox_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Checkbox* open_as_window_checkbox_ = nullptr;
 
   // Radio buttons to launch as a tab, window or tabbed window.
-  views::RadioButton* open_as_tab_radio_ = nullptr;
-  views::RadioButton* open_as_window_radio_ = nullptr;
-  views::RadioButton* open_as_tabbed_window_radio_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::RadioButton* open_as_tab_radio_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::RadioButton* open_as_window_radio_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::RadioButton* open_as_tabbed_window_radio_ = nullptr;
 
   // Textfield showing the title of the app.
-  views::Textfield* title_tf_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION views::Textfield* title_tf_ = nullptr;
 };
 
 BEGIN_VIEW_BUILDER(, WebAppConfirmationView, views::DialogDelegateView)
diff --git a/chrome/browser/ui/views/web_apps/web_app_integration_test_driver.h b/chrome/browser/ui/views/web_apps/web_app_integration_test_driver.h
index 51bec2e..1b22699 100644
--- a/chrome/browser/ui/views/web_apps/web_app_integration_test_driver.h
+++ b/chrome/browser/ui/views/web_apps/web_app_integration_test_driver.h
@@ -14,6 +14,7 @@
 #include "base/containers/flat_map.h"
 #include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/run_loop.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/ui/browser_dialogs.h"
@@ -148,9 +149,13 @@
   BrowserState(const BrowserState&);
   bool operator==(const BrowserState& other) const;
 
-  Browser* browser;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION Browser* browser;
   base::flat_map<content::WebContents*, TabState> tabs;
-  content::WebContents* active_tab;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION content::WebContents* active_tab;
   // If this isn't an app browser, `app_id` is empty.
   AppId app_id;
   bool launch_icon_shown;
diff --git a/chrome/browser/ui/web_applications/BUILD.gn b/chrome/browser/ui/web_applications/BUILD.gn
index d8c0d12..33c25f2 100644
--- a/chrome/browser/ui/web_applications/BUILD.gn
+++ b/chrome/browser/ui/web_applications/BUILD.gn
@@ -31,7 +31,6 @@
   testonly = true
 
   sources = [
-    "app_browser_controller_browsertest.cc",
     "commands/launch_web_app_command_browsertest.cc",
     "create_shortcut_browsertest.cc",
     "pwa_mixed_content_browsertest.cc",
diff --git a/chrome/browser/ui/web_applications/app_browser_controller.cc b/chrome/browser/ui/web_applications/app_browser_controller.cc
index 4dd6b85..629ae23 100644
--- a/chrome/browser/ui/web_applications/app_browser_controller.cc
+++ b/chrome/browser/ui/web_applications/app_browser_controller.cc
@@ -377,13 +377,6 @@
 }
 
 absl::optional<SkColor> AppBrowserController::GetThemeColor() const {
-  ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
-  if (native_theme->InForcedColorsMode()) {
-    // use system [Window ThemeColor] when enable high contrast
-    return native_theme->GetSystemThemeColor(
-        ui::NativeTheme::SystemThemeColor::kWindow);
-  }
-
   absl::optional<SkColor> result;
   // HTML meta theme-color tag overrides manifest theme_color, see spec:
   // https://www.w3.org/TR/appmanifest/#theme_color-member
diff --git a/chrome/browser/ui/web_applications/app_browser_controller_browsertest.cc b/chrome/browser/ui/web_applications/app_browser_controller_browsertest.cc
deleted file mode 100644
index c0a630e..0000000
--- a/chrome/browser/ui/web_applications/app_browser_controller_browsertest.cc
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "build/build_config.h"
-#include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/web_applications/app_browser_controller.h"
-#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
-#include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h"
-#include "content/public/test/browser_test.h"
-#include "ui/native_theme/native_theme.h"
-
-namespace web_app {
-
-class AppBrowserControllerBrowserTest : public WebAppControllerBrowserTest {
- public:
-  AppBrowserControllerBrowserTest() = default;
-  ~AppBrowserControllerBrowserTest() override = default;
-};
-
-#if BUILDFLAG(IS_WIN)
-IN_PROC_BROWSER_TEST_F(AppBrowserControllerBrowserTest,
-                       HighContrastThemeColorWin) {
-  const GURL start_url("https://app.site.test/example/index");
-  const AppId app_id = InstallPWA(start_url);
-
-  Browser* browser = web_app::LaunchWebAppBrowser(profile(), app_id);
-
-  AppBrowserController* controller = browser->app_controller();
-  // Verify theme color is null in browsertests before enabling high contrast.
-  ASSERT_FALSE(controller->GetThemeColor().has_value());
-
-  // Enable high contrast theme.
-  ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
-  // Make sure high contrast is disabled initially.
-  EXPECT_FALSE(native_theme->InForcedColorsMode());
-  native_theme->set_forced_colors(true);
-
-  EXPECT_TRUE(native_theme->InForcedColorsMode());
-
-  EXPECT_TRUE(controller->GetThemeColor().has_value());
-  absl::optional<SkColor> hc_theme_color = native_theme->GetSystemThemeColor(
-      ui::NativeTheme::SystemThemeColor::kWindow);
-  EXPECT_EQ(*controller->GetThemeColor(), hc_theme_color);
-}
-#else
-IN_PROC_BROWSER_TEST_F(AppBrowserControllerBrowserTest,
-                       HighContrastThemeColor) {
-  const GURL start_url("https://app.site.test/example/index");
-  const AppId app_id = InstallPWA(start_url);
-
-  Browser* browser = web_app::LaunchWebAppBrowser(profile(), app_id);
-
-  AppBrowserController* controller = browser->app_controller();
-  // Verify theme color is null in browsertests before enabling high contrast.
-  ASSERT_FALSE(controller->GetThemeColor().has_value());
-
-  // Enable high contrast theme.
-  ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
-  // Make sure high contrast is disabled initially.
-  EXPECT_FALSE(native_theme->InForcedColorsMode());
-  native_theme->set_forced_colors(true);
-
-  EXPECT_TRUE(native_theme->InForcedColorsMode());
-
-  // Mac and Linux don't provide system color.
-  EXPECT_FALSE(controller->GetThemeColor().has_value());
-}
-#endif
-
-}  // namespace web_app
diff --git a/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.h b/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.h
index 8300758..5ea8a469 100644
--- a/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.h
+++ b/chrome/browser/ui/webui/autofill_and_password_manager_internals/internals_ui_handler.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/functional/bind.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "components/autofill/core/browser/logging/log_receiver.h"
 #include "content/public/browser/browsing_data_remover.h"
 #include "content/public/browser/web_ui_message_handler.h"
@@ -49,7 +50,9 @@
   // Implements content::BrowsingDataRemover::Observer.
   void OnBrowsingDataRemoverDone(uint64_t failed_data_types) override;
 
-  content::BrowsingDataRemover* remover_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION content::BrowsingDataRemover* remover_;
   Callback callback_;
 };
 
diff --git a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc
index bfaf4536..3de5bfa 100644
--- a/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc
+++ b/chrome/browser/ui/webui/new_tab_page/new_tab_page_handler_unittest.cc
@@ -7,6 +7,7 @@
 #include <vector>
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/raw_ref.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/memory/scoped_refptr.h"
@@ -287,9 +288,16 @@
   raw_ptr<content::WebContents> web_contents_;  // Weak. Owned by factory_.
   base::HistogramTester histogram_tester_;
   std::unique_ptr<NewTabPageHandler> handler_;
-  ThemeServiceObserver* theme_service_observer_;
-  NtpCustomBackgroundServiceObserver* ntp_custom_background_service_observer_;
-  PromoServiceObserver* promo_service_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION ThemeServiceObserver* theme_service_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION NtpCustomBackgroundServiceObserver*
+      ntp_custom_background_service_observer_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION PromoServiceObserver* promo_service_observer_;
 
  private:
   raw_ptr<MockHatsService> mock_hats_service_;
diff --git a/chrome/browser/ui/webui/policy/policy_ui.cc b/chrome/browser/ui/webui/policy/policy_ui.cc
index fdea02b..d5ab74f 100644
--- a/chrome/browser/ui/webui/policy/policy_ui.cc
+++ b/chrome/browser/ui/webui/policy/policy_ui.cc
@@ -38,6 +38,7 @@
     {"labelClientId", IDS_POLICY_LABEL_CLIENT_ID},
     {"labelDirectoryApiId", IDS_POLICY_LABEL_DIRECTORY_API_ID},
     {"labelError", IDS_POLICY_LABEL_ERROR},
+    {"labelWarning", IDS_POLICY_HEADER_WARNING},
     {"labelGaiaId", IDS_POLICY_LABEL_GAIA_ID},
     {"labelIsAffiliated", IDS_POLICY_LABEL_IS_AFFILIATED},
     {"labelLastCloudReportSentTimestamp",
@@ -77,6 +78,7 @@
     {"signinProfile", IDS_POLICY_SIGNIN_PROFILE},
     {"status", IDS_POLICY_STATUS},
     {"statusErrorManagedNoPolicy", IDS_POLICY_STATUS_ERROR_MANAGED_NO_POLICY},
+    {"statusFlexOrgNoPolicy", IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY},
     {"statusDevice", IDS_POLICY_STATUS_DEVICE},
     {"statusMachine", IDS_POLICY_STATUS_MACHINE},
 #if BUILDFLAG(IS_WIN) && BUILDFLAG(GOOGLE_CHROME_BRANDING)
@@ -93,9 +95,7 @@
       source, base::make_span(kPolicyResources, kPolicyResourcesSize),
       IDR_POLICY_POLICY_HTML);
 
-  source->OverrideContentSecurityPolicy(
-      network::mojom::CSPDirectiveName::TrustedTypes,
-      "trusted-types static-types;");
+  webui::EnableTrustedTypesCSP(source);
 }
 
 }  // namespace
diff --git a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler_unittest.cc b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler_unittest.cc
index 1cec47ec..c9b93b0 100644
--- a/chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler_unittest.cc
+++ b/chrome/browser/ui/webui/side_panel/customize_chrome/customize_chrome_page_handler_unittest.cc
@@ -299,12 +299,16 @@
   std::unique_ptr<TestingProfile> profile_;
   testing::NiceMock<MockNtpCustomBackgroundService>
       mock_ntp_custom_background_service_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
   NtpCustomBackgroundServiceObserver* ntp_custom_background_service_observer_;
   network::TestURLLoaderFactory test_url_loader_factory_;
   raw_ptr<MockNtpBackgroundService> mock_ntp_background_service_;
   content::TestWebContentsFactory web_contents_factory_;
   raw_ptr<content::WebContents> web_contents_;
   testing::NiceMock<MockPage> mock_page_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
   NtpBackgroundServiceObserver* ntp_background_service_observer_;
   raw_ptr<MockThemeService> mock_theme_service_;
   std::unique_ptr<Browser> browser_;
diff --git a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
index d711db2..501336c 100644
--- a/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
+++ b/chrome/browser/ui/webui/tab_search/tab_search_page_handler.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_UI_WEBUI_TAB_SEARCH_TAB_SEARCH_PAGE_HANDLER_H_
 
 #include "base/memory/raw_ptr.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/ui/browser_tab_strip_tracker.h"
 #include "chrome/browser/ui/browser_tab_strip_tracker_delegate.h"
 #include "chrome/browser/ui/tabs/tab_group.h"
@@ -95,8 +96,12 @@
     TabDetails(Browser* browser, TabStripModel* tab_strip_model, int index)
         : browser(browser), tab_strip_model(tab_strip_model), index(index) {}
 
-    Browser* browser;
-    TabStripModel* tab_strip_model;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION Browser* browser;
+    // This field is not a raw_ptr<> because it was filtered by the rewriter
+    // for: #union
+    RAW_PTR_EXCLUSION TabStripModel* tab_strip_model;
     int index;
   };
 
diff --git a/chrome/browser/upgrade_detector/installed_version_poller.h b/chrome/browser/upgrade_detector/installed_version_poller.h
index a0bb8bc..80495fc 100644
--- a/chrome/browser/upgrade_detector/installed_version_poller.h
+++ b/chrome/browser/upgrade_detector/installed_version_poller.h
@@ -8,6 +8,7 @@
 #include <memory>
 
 #include "base/functional/callback.h"
+#include "base/memory/raw_ptr_exclusion.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
 #include "base/sequence_checker.h"
@@ -92,7 +93,9 @@
                           InstalledAndCriticalVersion installed_version);
 
   SEQUENCE_CHECKER(sequence_checker_);
-  BuildState* const build_state_;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #union
+  RAW_PTR_EXCLUSION BuildState* const build_state_;
   const GetInstalledVersionCallback get_installed_version_;
   base::OneShotTimer timer_;
 
diff --git a/chrome/browser/vr/elements/vector_icon.cc b/chrome/browser/vr/elements/vector_icon.cc
index 3135b1d..ed7ce0c 100644
--- a/chrome/browser/vr/elements/vector_icon.cc
+++ b/chrome/browser/vr/elements/vector_icon.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/memory/raw_ptr_exclusion.h"
 #include "chrome/browser/vr/elements/ui_texture.h"
 #include "ui/gfx/paint_vector_icon.h"
 #include "ui/gfx/scoped_canvas.h"
@@ -46,7 +47,9 @@
   }
 
   gfx::SizeF size_;
-  const gfx::VectorIcon* icon_ = nullptr;
+  // This field is not a raw_ptr<> because it was filtered by the rewriter for:
+  // #addr-of
+  RAW_PTR_EXCLUSION const gfx::VectorIcon* icon_ = nullptr;
   SkColor color_ = SK_ColorWHITE;
 };
 
diff --git a/chrome/browser/web_applications/BUILD.gn b/chrome/browser/web_applications/BUILD.gn
index 74b4b758..cffb65c9 100644
--- a/chrome/browser/web_applications/BUILD.gn
+++ b/chrome/browser/web_applications/BUILD.gn
@@ -76,6 +76,10 @@
     "isolated_web_apps/isolated_web_app_reader_registry.h",
     "isolated_web_apps/isolated_web_app_reader_registry_factory.cc",
     "isolated_web_apps/isolated_web_app_reader_registry_factory.h",
+    "isolated_web_apps/isolated_web_app_response_reader.cc",
+    "isolated_web_apps/isolated_web_app_response_reader.h",
+    "isolated_web_apps/isolated_web_app_response_reader_factory.cc",
+    "isolated_web_apps/isolated_web_app_response_reader_factory.h",
     "isolated_web_apps/isolated_web_app_trust_checker.cc",
     "isolated_web_apps/isolated_web_app_trust_checker.h",
     "isolated_web_apps/isolated_web_app_url_info.cc",
@@ -595,6 +599,8 @@
     "isolated_web_apps/install_isolated_web_app_from_command_line_unittest.cc",
     "isolated_web_apps/isolated_web_app_reader_registry_factory_unittest.cc",
     "isolated_web_apps/isolated_web_app_reader_registry_unittest.cc",
+    "isolated_web_apps/isolated_web_app_response_reader_factory_unittest.cc",
+    "isolated_web_apps/isolated_web_app_response_reader_unittest.cc",
     "isolated_web_apps/isolated_web_app_trust_checker_unittest.cc",
     "isolated_web_apps/isolated_web_app_url_info_unittest.cc",
     "isolated_web_apps/isolated_web_app_url_loader_factory_unittest.cc",
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.cc
index 8b21ac6..e119c65 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.cc
@@ -16,15 +16,14 @@
 #include "base/strings/strcat.h"
 #include "base/strings/stringprintf.h"
 #include "base/time/time.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
 #include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
 #include "chrome/common/url_constants.h"
 #include "components/web_package/mojom/web_bundle_parser.mojom.h"
 #include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
-#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
 #include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
 #include "services/network/public/cpp/resource_request.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
-#include "third_party/abseil-cpp/absl/types/variant.h"
 #include "url/url_constants.h"
 
 namespace web_app {
@@ -53,8 +52,9 @@
     base::RepeatingCallback<
         std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
         signature_verifier_factory)
-    : validator_(std::move(validator)),
-      signature_verifier_factory_(std::move(signature_verifier_factory)) {}
+    : reader_factory_(std::make_unique<IsolatedWebAppResponseReaderFactory>(
+          std::move(validator),
+          std::move(signature_verifier_factory))) {}
 
 IsolatedWebAppReaderRegistry::~IsolatedWebAppReaderRegistry() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -79,7 +79,7 @@
               : ReaderCacheState::kNotCached);
 
     if (found) {
-      switch (cache_entry_it->second.state) {
+      switch (cache_entry_it->second.state()) {
         case Cache::Entry::State::kPending:
           // If integrity block and metadata are still being read, then the
           // `SignedWebBundleReader` is not yet ready to be used for serving
@@ -101,118 +101,48 @@
       base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator,
                     web_bundle_id.id()}));
 
-  std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>
-      signature_verifier = signature_verifier_factory_.Run();
-  std::unique_ptr<SignedWebBundleReader> reader = SignedWebBundleReader::Create(
-      web_bundle_path, base_url, std::move(signature_verifier));
-
   auto [cache_entry_it, was_insertion] =
-      reader_cache_.Emplace(web_bundle_path, Cache::Entry(std::move(reader)));
+      reader_cache_.Emplace(web_bundle_path, Cache::Entry());
   DCHECK(was_insertion);
   cache_entry_it->second.pending_requests.emplace_back(resource_request,
                                                        std::move(callback));
 
-  cache_entry_it->second.GetReader().StartReading(
-      base::BindOnce(
-          &IsolatedWebAppReaderRegistry::OnIntegrityBlockRead,
-          // `base::Unretained` can be used here since `this` owns `reader`.
-          base::Unretained(this), web_bundle_path, web_bundle_id),
-      base::BindOnce(
-          &IsolatedWebAppReaderRegistry::OnIntegrityBlockAndMetadataRead,
-          // `base::Unretained` can be used here since `this` owns `reader`.
-          base::Unretained(this), web_bundle_path, web_bundle_id));
-}
-
-void IsolatedWebAppReaderRegistry::OnIntegrityBlockRead(
-    const base::FilePath& web_bundle_path,
-    const web_package::SignedWebBundleId& web_bundle_id,
-    const web_package::SignedWebBundleIntegrityBlock integrity_block,
-    base::OnceCallback<void(SignedWebBundleReader::SignatureVerificationAction)>
-        integrity_callback) {
-  validator_->ValidateIntegrityBlock(
-      web_bundle_id, integrity_block,
-      base::BindOnce(&IsolatedWebAppReaderRegistry::OnIntegrityBlockValidated,
-                     weak_ptr_factory_.GetWeakPtr(), web_bundle_path,
-                     web_bundle_id, std::move(integrity_callback)));
-}
-
-void IsolatedWebAppReaderRegistry::OnIntegrityBlockValidated(
-    const base::FilePath& web_bundle_path,
-    const web_package::SignedWebBundleId& web_bundle_id,
-    base::OnceCallback<void(SignedWebBundleReader::SignatureVerificationAction)>
-        integrity_callback,
-    absl::optional<std::string> integrity_block_error) {
-  if (integrity_block_error.has_value()) {
-    // Aborting parsing will trigger a call to `OnIntegrityBlockAndMetadataRead`
-    // with a `SignedWebBundleReader::AbortedByCaller` error.
-    std::move(integrity_callback)
-        .Run(SignedWebBundleReader::SignatureVerificationAction::Abort(
-            *integrity_block_error));
-    return;
-  }
-
+  // If we already verified the signatures of this Signed Web Bundle during
+  // the current browser session, we trust that the Signed Web Bundle has not
+  // been tampered with and don't re-verify signatures.
+  //
   // TODO(crbug.com/1366309): On ChromeOS, we should only verify signatures at
   // install-time. Until this is implemented, we will verify signatures on
   // ChromeOS once per session.
-  if (verified_files_.contains(web_bundle_path)) {
-    // If we already verified the signatures of this Signed Web Bundle during
-    // the current browser session, we trust that the Signed Web Bundle has not
-    // been tampered with and don't re-verify signatures.
-    std::move(integrity_callback)
-        .Run(SignedWebBundleReader::SignatureVerificationAction::
-                 ContinueAndSkipSignatureVerification());
-  } else {
-    std::move(integrity_callback)
-        .Run(SignedWebBundleReader::SignatureVerificationAction::
-                 ContinueAndVerifySignatures());
-  }
+  bool skip_signature_verification = verified_files_.contains(web_bundle_path);
+
+  reader_factory_->CreateResponseReader(
+      web_bundle_path, web_bundle_id, skip_signature_verification,
+      base::BindOnce(&IsolatedWebAppReaderRegistry::OnResponseReaderCreated,
+                     // `base::Unretained` can be used here since `this` owns
+                     // `reader_factory`.
+                     base::Unretained(this), web_bundle_path, web_bundle_id));
 }
 
-void IsolatedWebAppReaderRegistry::OnIntegrityBlockAndMetadataRead(
+void IsolatedWebAppReaderRegistry::OnResponseReaderCreated(
     const base::FilePath& web_bundle_path,
     const web_package::SignedWebBundleId& web_bundle_id,
-    absl::optional<SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>
-        read_integrity_block_and_metadata_error) {
+    base::expected<std::unique_ptr<IsolatedWebAppResponseReader>,
+                   IsolatedWebAppResponseReaderFactory::Error> reader) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
 
   auto cache_entry_it = reader_cache_.Find(web_bundle_path);
   DCHECK(cache_entry_it != reader_cache_.End());
-  DCHECK_EQ(cache_entry_it->second.state, Cache::Entry::State::kPending);
-
-  absl::optional<
-      std::pair<ReadResponseError, ReadIntegrityBlockAndMetadataStatus>>
-      error_and_status;
-  if (read_integrity_block_and_metadata_error.has_value()) {
-    error_and_status = std::make_pair(
-        ReadResponseError::ForError(*read_integrity_block_and_metadata_error),
-        GetStatusFromError(*read_integrity_block_and_metadata_error));
-  }
-
-  SignedWebBundleReader& reader = cache_entry_it->second.GetReader();
-
-  if (!error_and_status.has_value()) {
-    if (auto error_message = validator_->ValidateMetadata(
-            web_bundle_id, reader.GetPrimaryURL(), reader.GetEntries());
-        error_message.has_value()) {
-      error_and_status = std::make_pair(
-          ReadResponseError::ForMetadataValidationError(*error_message),
-          ReadIntegrityBlockAndMetadataStatus::kMetadataValidationError);
-    }
-  }
-
-  base::UmaHistogramEnumeration(
-      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
-      error_and_status.has_value()
-          ? error_and_status->second
-          : ReadIntegrityBlockAndMetadataStatus::kSuccess);
+  DCHECK_EQ(cache_entry_it->second.state(), Cache::Entry::State::kPending);
 
   std::vector<std::pair<network::ResourceRequest, ReadResponseCallback>>
       pending_requests =
           std::exchange(cache_entry_it->second.pending_requests, {});
 
-  if (error_and_status.has_value()) {
+  if (!reader.has_value()) {
     for (auto& [resource_request, callback] : pending_requests) {
-      std::move(callback).Run(base::unexpected(error_and_status->first));
+      std::move(callback).Run(
+          base::unexpected(ReadResponseError::ForError(reader.error())));
     }
     reader_cache_.Erase(cache_entry_it);
     return;
@@ -222,14 +152,15 @@
   // consumers that were waiting for this `SignedWebBundleReader` to become
   // available.
   verified_files_.insert(cache_entry_it->first);
-  cache_entry_it->second.state = Cache::Entry::State::kReady;
+  cache_entry_it->second.set_reader(std::move(*reader));
   for (auto& [resource_request, callback] : pending_requests) {
-    DoReadResponse(reader, resource_request, std::move(callback));
+    DoReadResponse(cache_entry_it->second.GetReader(), resource_request,
+                   std::move(callback));
   }
 }
 
 void IsolatedWebAppReaderRegistry::DoReadResponse(
-    SignedWebBundleReader& reader,
+    IsolatedWebAppResponseReader& reader,
     network::ResourceRequest resource_request,
     ReadResponseCallback callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -257,118 +188,43 @@
       base::BindOnce(
           &IsolatedWebAppReaderRegistry::OnResponseRead,
           // `base::Unretained` can be used here since `this` owns `reader`.
-          base::Unretained(this), reader.AsWeakPtr(), std::move(callback)));
+          base::Unretained(this), std::move(callback)));
 }
 
 void IsolatedWebAppReaderRegistry::OnResponseRead(
-    base::WeakPtr<SignedWebBundleReader> reader,
     ReadResponseCallback callback,
-    base::expected<web_package::mojom::BundleResponsePtr,
-                   SignedWebBundleReader::ReadResponseError> response_head) {
-  base::UmaHistogramEnumeration(
-      "WebApp.Isolated.ReadResponseHeadStatus",
-      response_head.has_value() ? ReadResponseHeadStatus::kSuccess
-                                : GetStatusFromError(response_head.error()));
+    base::expected<IsolatedWebAppResponseReader::Response,
+                   IsolatedWebAppResponseReader::Error> response) {
+  base::UmaHistogramEnumeration("WebApp.Isolated.ReadResponseHeadStatus",
+                                response.has_value()
+                                    ? ReadResponseHeadStatus::kSuccess
+                                    : GetStatusFromError(response.error()));
 
-  if (!response_head.has_value()) {
+  if (!response.has_value()) {
     std::move(callback).Run(
-        base::unexpected(ReadResponseError::ForError(response_head.error())));
+        base::unexpected(ReadResponseError::ForError(response.error())));
     return;
   }
-  // Since `this` owns `reader`, we only pass a weak reference to it to the
-  // `Response` object. If `this` deletes `reader`, it makes sense that the
-  // reference contained in `Response` also becomes invalid.
-  std::move(callback).Run(Response(std::move(*response_head), reader));
-}
-
-IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus
-IsolatedWebAppReaderRegistry::GetStatusFromError(
-    const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error) {
-  return absl::visit(
-      base::Overloaded{
-          [](const web_package::mojom::BundleIntegrityBlockParseErrorPtr&
-                 error) {
-            switch (error->type) {
-              case web_package::mojom::BundleParseErrorType::
-                  kParserInternalError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kIntegrityBlockParserInternalError;
-              case web_package::mojom::BundleParseErrorType::kFormatError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kIntegrityBlockParserFormatError;
-              case web_package::mojom::BundleParseErrorType::kVersionError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kIntegrityBlockParserVersionError;
-            }
-          },
-          [](const SignedWebBundleReader::AbortedByCaller& error) {
-            return ReadIntegrityBlockAndMetadataStatus::
-                kIntegrityBlockValidationError;
-          },
-          [](const web_package::SignedWebBundleSignatureVerifier::Error&
-                 error) {
-            return ReadIntegrityBlockAndMetadataStatus::
-                kSignatureVerificationError;
-          },
-          [](const web_package::mojom::BundleMetadataParseErrorPtr& error) {
-            switch (error->type) {
-              case web_package::mojom::BundleParseErrorType::
-                  kParserInternalError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kMetadataParserInternalError;
-              case web_package::mojom::BundleParseErrorType::kFormatError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kMetadataParserFormatError;
-              case web_package::mojom::BundleParseErrorType::kVersionError:
-                return ReadIntegrityBlockAndMetadataStatus::
-                    kMetadataParserVersionError;
-            }
-          }},
-      error);
+  std::move(callback).Run(std::move(*response));
 }
 
 IsolatedWebAppReaderRegistry::ReadResponseHeadStatus
 IsolatedWebAppReaderRegistry::GetStatusFromError(
-    const SignedWebBundleReader::ReadResponseError& error) {
+    const IsolatedWebAppResponseReader::Error& error) {
   switch (error.type) {
-    case SignedWebBundleReader::ReadResponseError::Type::kParserInternalError:
+    case IsolatedWebAppResponseReader::Error::Type::kParserInternalError:
       return ReadResponseHeadStatus::kResponseHeadParserInternalError;
-    case SignedWebBundleReader::ReadResponseError::Type::kFormatError:
+    case IsolatedWebAppResponseReader::Error::Type::kFormatError:
       return ReadResponseHeadStatus::kResponseHeadParserFormatError;
-    case SignedWebBundleReader::ReadResponseError::Type::kResponseNotFound:
+    case IsolatedWebAppResponseReader::Error::Type::kResponseNotFound:
       return ReadResponseHeadStatus::kResponseNotFoundError;
   }
 }
 
-IsolatedWebAppReaderRegistry::Response::Response(
-    web_package::mojom::BundleResponsePtr head,
-    base::WeakPtr<SignedWebBundleReader> reader)
-    : head_(std::move(head)), reader_(std::move(reader)) {}
-
-IsolatedWebAppReaderRegistry::Response::Response(Response&&) = default;
-
-IsolatedWebAppReaderRegistry::Response&
-IsolatedWebAppReaderRegistry::Response::operator=(Response&&) = default;
-
-IsolatedWebAppReaderRegistry::Response::~Response() = default;
-
-void IsolatedWebAppReaderRegistry::Response::ReadBody(
-    mojo::ScopedDataPipeProducerHandle producer_handle,
-    base::OnceCallback<void(net::Error net_error)> callback) {
-  if (!reader_) {
-    // The weak pointer to `reader_` might no longer be valid when this is
-    // called.
-    std::move(callback).Run(net::ERR_FAILED);
-    return;
-  }
-  reader_->ReadResponseBody(head_->Clone(), std::move(producer_handle),
-                            std::move(callback));
-}
-
 // static
 IsolatedWebAppReaderRegistry::ReadResponseError
 IsolatedWebAppReaderRegistry::ReadResponseError::ForError(
-    const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error) {
+    const IsolatedWebAppResponseReaderFactory::Error& error) {
   return ForOtherError(absl::visit(
       base::Overloaded{
           [](const web_package::mojom::BundleIntegrityBlockParseErrorPtr&
@@ -376,7 +232,7 @@
             return base::StringPrintf("Failed to parse integrity block: %s",
                                       error->message.c_str());
           },
-          [](const SignedWebBundleReader::AbortedByCaller& error) {
+          [](const IntegrityBlockError& error) {
             return base::StringPrintf("Failed to validate integrity block: %s",
                                       error.message.c_str());
           },
@@ -388,30 +244,26 @@
           [](const web_package::mojom::BundleMetadataParseErrorPtr& error) {
             return base::StringPrintf("Failed to parse metadata: %s",
                                       error->message.c_str());
+          },
+          [](const MetadataError& error) {
+            return base::StringPrintf("Failed to validate metadata: %s",
+                                      error.message.c_str());
           }},
       error));
 }
 
 // static
 IsolatedWebAppReaderRegistry::ReadResponseError
-IsolatedWebAppReaderRegistry::ReadResponseError::ForMetadataValidationError(
-    const std::string& error) {
-  return ForOtherError(
-      base::StringPrintf("Failed to validate metadata: %s", error.c_str()));
-}
-
-// static
-IsolatedWebAppReaderRegistry::ReadResponseError
 IsolatedWebAppReaderRegistry::ReadResponseError::ForError(
-    const SignedWebBundleReader::ReadResponseError& error) {
+    const IsolatedWebAppResponseReader::Error& error) {
   switch (error.type) {
-    case SignedWebBundleReader::ReadResponseError::Type::kParserInternalError:
+    case IsolatedWebAppResponseReader::Error::Type::kParserInternalError:
       return ForOtherError(base::StringPrintf(
           "Failed to parse response head: %s", error.message.c_str()));
-    case SignedWebBundleReader::ReadResponseError::Type::kFormatError:
+    case IsolatedWebAppResponseReader::Error::Type::kFormatError:
       return ForOtherError(base::StringPrintf(
           "Failed to parse response head: %s", error.message.c_str()));
-    case SignedWebBundleReader::ReadResponseError::Type::kResponseNotFound:
+    case IsolatedWebAppResponseReader::Error::Type::kResponseNotFound:
       return ForResponseNotFound(base::StringPrintf(
           "Failed to read response: %s", error.message.c_str()));
   }
@@ -490,7 +342,7 @@
             // If a `SignedWebBundleReader` is ready to read responses and has
             // not been used for at least `kCleanupInterval`, remove it from the
             // cache.
-            return cache_entry.state == Entry::State::kReady &&
+            return cache_entry.state() == Entry::State::kReady &&
                    now - cache_entry.last_access() > kCleanupInterval;
           },
           [](const std::pair<base::FilePath, Entry>& entry) -> const Entry& {
@@ -500,9 +352,7 @@
   StopCleanupTimerIfCacheIsEmpty();
 }
 
-IsolatedWebAppReaderRegistry::Cache::Entry::Entry(
-    std::unique_ptr<SignedWebBundleReader> reader)
-    : reader_(std::move(reader)) {}
+IsolatedWebAppReaderRegistry::Cache::Entry::Entry() = default;
 
 IsolatedWebAppReaderRegistry::Cache::Entry::~Entry() = default;
 
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h
index 4b13dd30..4fccb64 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h
@@ -16,14 +16,12 @@
 #include "base/time/time.h"
 #include "base/timer/timer.h"
 #include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
-#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
 #include "components/keyed_service/core/keyed_service.h"
-#include "components/web_package/mojom/web_bundle_parser.mojom-forward.h"
-#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
 #include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
 #include "services/network/public/cpp/resource_request.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace web_package {
 class SignedWebBundleId;
@@ -31,13 +29,13 @@
 
 namespace web_app {
 
-// A registry to create and keep track of `SignedWebBundleReader` instances used
-// to read Isolated Web Apps. At its core, it contains a map from file paths to
-// `SignedWebBundleReader`s to cache them for repeated calls. On non-ChromeOS
-// devices, the first request for a particular file path will also check the
-// integrity of the Signed Web Bundle. On ChromeOS, it is assumed that the
-// Signed Web Bundle has not been corrupted due to its location inside
-// cryptohome, and signatures are not checked.
+// A registry to create and keep track of `IsolatedWebAppResponseReader`
+// instances used to read Isolated Web Apps. At its core, it contains a map from
+// file paths to `IsolatedWebAppResponseReader`s to cache them for repeated
+// calls. On non-ChromeOS devices, the first request for a particular file path
+// will also check the integrity of the Signed Web Bundle. On ChromeOS, it is
+// assumed that the Signed Web Bundle has not been corrupted due to its location
+// inside cryptohome, and signatures are not checked.
 class IsolatedWebAppReaderRegistry : public KeyedService {
  public:
   explicit IsolatedWebAppReaderRegistry(
@@ -51,40 +49,6 @@
   IsolatedWebAppReaderRegistry& operator=(const IsolatedWebAppReaderRegistry&) =
       delete;
 
-  // A `Response` object contains the response head, as well as a `ReadBody`
-  // function to read the response's body. It holds weakly onto a
-  // `SignedWebBundleReader` for reading the response body. This reference will
-  // remain valid until the reader is evicted from the cache of the
-  // `IsolatedWebAppReaderRegistry`.
-  class Response {
-   public:
-    Response(web_package::mojom::BundleResponsePtr head,
-             base::WeakPtr<SignedWebBundleReader> reader);
-
-    Response(const Response&) = delete;
-    Response& operator=(const Response&) = delete;
-
-    Response(Response&&);
-    Response& operator=(Response&&);
-
-    ~Response();
-
-    // Returns the head of the response, which includes status code and response
-    // headers.
-    const web_package::mojom::BundleResponsePtr& head() { return head_; }
-
-    // Reads the body of the response into `producer_handle`, calling `callback`
-    // with `net::OK` on success, and another error code on failure. A failure
-    // may also occur if the `SignedWebBundleReader` that was used to read the
-    // response head has since been evicted from the cache.
-    void ReadBody(mojo::ScopedDataPipeProducerHandle producer_handle,
-                  base::OnceCallback<void(net::Error net_error)> callback);
-
-   private:
-    web_package::mojom::BundleResponsePtr head_;
-    base::WeakPtr<SignedWebBundleReader> reader_;
-  };
-
   struct ReadResponseError {
     enum class Type {
       kOtherError,
@@ -92,13 +56,10 @@
     };
 
     static ReadResponseError ForError(
-        const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error);
-
-    static ReadResponseError ForMetadataValidationError(
-        const std::string& error);
+        const IsolatedWebAppResponseReaderFactory::Error& error);
 
     static ReadResponseError ForError(
-        const SignedWebBundleReader::ReadResponseError& error);
+        const IsolatedWebAppResponseReader::Error& error);
 
     Type type;
     std::string message;
@@ -117,7 +78,8 @@
   };
 
   using ReadResponseCallback = base::OnceCallback<void(
-      base::expected<Response, ReadResponseError> response)>;
+      base::expected<IsolatedWebAppResponseReader::Response, ReadResponseError>
+          response)>;
 
   // Given a path to a Signed Web Bundle, the expected Signed Web Bundle ID, and
   // a request, read the corresponding response from it. The `callback` receives
@@ -128,31 +90,6 @@
                     const network::ResourceRequest& resource_request,
                     ReadResponseCallback callback);
 
-  // This enum represents every error type that can occur during integrity block
-  // and metadata parsing, before responses are read from Signed Web Bundles.
-  //
-  // These values are persisted to logs. Entries should not be renumbered and
-  // numeric values should never be reused.
-  enum class ReadIntegrityBlockAndMetadataStatus {
-    kSuccess = 0,
-    // Integrity Block-related errors
-    kIntegrityBlockParserInternalError = 1,
-    kIntegrityBlockParserFormatError = 2,
-    kIntegrityBlockParserVersionError = 3,
-    kIntegrityBlockValidationError = 4,
-
-    // Signature verification errors
-    kSignatureVerificationError = 5,
-
-    // Metadata-related errors
-    kMetadataParserInternalError = 6,
-    kMetadataParserFormatError = 7,
-    kMetadataParserVersionError = 8,
-    kMetadataValidationError = 9,
-
-    kMaxValue = kMetadataValidationError
-  };
-
   // This enum represents every error type that can occur during response head
   // parsing, after integrity block and metadata have been read successfully.
   //
@@ -170,49 +107,30 @@
   FRIEND_TEST_ALL_PREFIXES(IsolatedWebAppReaderRegistryTest,
                            TestConcurrentRequests);
 
-  void OnIntegrityBlockRead(
+  void OnResponseReaderCreated(
       const base::FilePath& web_bundle_path,
       const web_package::SignedWebBundleId& web_bundle_id,
-      const web_package::SignedWebBundleIntegrityBlock integrity_block,
-      base::OnceCallback<
-          void(SignedWebBundleReader::SignatureVerificationAction)> callback);
+      base::expected<std::unique_ptr<IsolatedWebAppResponseReader>,
+                     IsolatedWebAppResponseReaderFactory::Error> reader);
 
-  void OnIntegrityBlockValidated(
-      const base::FilePath& web_bundle_path,
-      const web_package::SignedWebBundleId& web_bundle_id,
-      base::OnceCallback<
-          void(SignedWebBundleReader::SignatureVerificationAction)>
-          integrity_callback,
-      absl::optional<std::string> integrity_block_error);
-
-  void OnIntegrityBlockAndMetadataRead(
-      const base::FilePath& web_bundle_path,
-      const web_package::SignedWebBundleId& web_bundle_id,
-      absl::optional<SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>
-          read_integrity_block_and_metadata_error);
-
-  void DoReadResponse(SignedWebBundleReader& reader,
+  void DoReadResponse(IsolatedWebAppResponseReader& reader,
                       network::ResourceRequest resource_request,
                       ReadResponseCallback callback);
 
   void OnResponseRead(
-      base::WeakPtr<SignedWebBundleReader> reader,
       ReadResponseCallback callback,
-      base::expected<web_package::mojom::BundleResponsePtr,
-                     SignedWebBundleReader::ReadResponseError> response_head);
-
-  ReadIntegrityBlockAndMetadataStatus GetStatusFromError(
-      const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error);
+      base::expected<IsolatedWebAppResponseReader::Response,
+                     IsolatedWebAppResponseReader::Error> response);
 
   ReadResponseHeadStatus GetStatusFromError(
-      const SignedWebBundleReader::ReadResponseError& error);
+      const IsolatedWebAppResponseReader::Error& error);
 
   enum class ReaderCacheState;
 
   // A thin wrapper around `base::flat_map<base::FilePath, Cache::Entry>` that
   // automatically removes entries from the cache if they have not been accessed
-  // for some time. This makes sure that `SignedWebBundleReader`s are not kept
-  // alive indefinitely, since each of them holds an open file handle and
+  // for some time. This makes sure that `IsolatedWebAppResponseReader`s are not
+  // kept alive indefinitely, since each of them holds an open file handle and
   // memory.
   class Cache {
    public:
@@ -236,13 +154,13 @@
     void Erase(base::flat_map<base::FilePath, Entry>::iterator iterator);
 
     // A cache `Entry` has two states: In its initial `kPending` state, it
-    // caches requests made to a Signed Web Bundle until the
-    // `SignedWebBundleReader` is ready. Once the `SignedWebBundleReader` is
-    // ready to serve responses, all queued requests are run and the state is
-    // updated to `kReady`.
+    // caches requests made to a Signed Web Bundle until an
+    // `IsolatedWebAppResponseReader` is ready. Once the
+    // `IsolatedWebAppResponseReader` is ready and set via `set_reader`, all
+    // queued requests are run and the state is updated to `kReady`.
     class Entry {
      public:
-      explicit Entry(std::unique_ptr<SignedWebBundleReader> reader);
+      Entry();
       ~Entry();
 
       Entry(const Entry& other) = delete;
@@ -251,7 +169,8 @@
       Entry(Entry&& other);
       Entry& operator=(Entry&& other);
 
-      SignedWebBundleReader& GetReader() {
+      IsolatedWebAppResponseReader& GetReader() {
+        DCHECK(reader_);
         last_access_ = base::TimeTicks::Now();
         return *reader_;
       }
@@ -259,7 +178,7 @@
       const base::TimeTicks last_access() const { return last_access_; }
 
       ReaderCacheState AsReaderCacheState() {
-        switch (state) {
+        switch (state()) {
           case State::kPending:
             return ReaderCacheState::kCachedPending;
           case State::kReady:
@@ -268,14 +187,18 @@
       }
 
       enum class State { kPending, kReady };
+      State state() const { return reader_ ? State::kReady : State::kPending; }
 
-      State state = State::kPending;
+      void set_reader(std::unique_ptr<IsolatedWebAppResponseReader> reader) {
+        reader_ = std::move(reader);
+      }
+
       std::vector<std::pair<network::ResourceRequest,
                             IsolatedWebAppReaderRegistry::ReadResponseCallback>>
           pending_requests;
 
      private:
-      std::unique_ptr<SignedWebBundleReader> reader_;
+      std::unique_ptr<IsolatedWebAppResponseReader> reader_;
       // The point in time when the `reader` was last accessed.
       base::TimeTicks last_access_;
     };
@@ -308,10 +231,7 @@
   // if their corresponding `CacheEntry` is cleaned up and later re-created.
   base::flat_set<base::FilePath> verified_files_;
 
-  std::unique_ptr<IsolatedWebAppValidator> validator_;
-  base::RepeatingCallback<
-      std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
-      signature_verifier_factory_;
+  std::unique_ptr<IsolatedWebAppResponseReaderFactory> reader_factory_;
 
   SEQUENCE_CHECKER(sequence_checker_);
   base::WeakPtrFactory<IsolatedWebAppReaderRegistry> weak_ptr_factory_{this};
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_unittest.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_unittest.cc
index 459679e..4375193 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_unittest.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_unittest.cc
@@ -22,6 +22,7 @@
 #include "base/test/task_environment.h"
 #include "base/test/test_future.h"
 #include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
 #include "chrome/browser/web_applications/test/signed_web_bundle_utils.h"
@@ -205,7 +206,7 @@
 };
 
 using ReadResult =
-    base::expected<IsolatedWebAppReaderRegistry::Response,
+    base::expected<IsolatedWebAppResponseReader::Response,
                    IsolatedWebAppReaderRegistry::ReadResponseError>;
 
 TEST_F(IsolatedWebAppReaderRegistryTest, TestSingleRequest) {
@@ -233,13 +234,13 @@
 
   histogram_tester.ExpectBucketCount(
       "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
-      IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
           kSuccess,
       1);
 
   std::string response_body = ReadAndFulfillResponseBody(
       result->head()->payload_length,
-      base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+      base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                      base::Unretained(&*result)));
   EXPECT_EQ(kResponseBody, response_body);
 }
@@ -263,7 +264,7 @@
 
   std::string response_body = ReadAndFulfillResponseBody(
       result->head()->payload_length,
-      base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+      base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                      base::Unretained(&*result)));
   EXPECT_EQ(kResponseBody, response_body);
 }
@@ -293,7 +294,7 @@
   base::test::TestFuture<net::Error> error_future;
   ReadResponseBody(
       result->head()->payload_length,
-      base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+      base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                      base::Unretained(&*result)),
       error_future.GetCallback());
   EXPECT_EQ(net::ERR_FAILED, error_future.Take());
@@ -438,10 +439,10 @@
 
 class IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest
     : public IsolatedWebAppReaderRegistryTest,
-      public ::testing::WithParamInterface<std::pair<
-          web_package::mojom::BundleParseErrorType,
-          IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus>> {
-};
+      public ::testing::WithParamInterface<
+          std::pair<web_package::mojom::BundleParseErrorType,
+                    IsolatedWebAppResponseReaderFactory::
+                        ReadIntegrityBlockAndMetadataStatus>> {};
 
 TEST_P(IsolatedWebAppReaderRegistryIntegrityBlockParserErrorTest,
        TestIntegrityBlockParserError) {
@@ -477,16 +478,17 @@
     ::testing::Values(
         std::make_pair(
             web_package::mojom::BundleParseErrorType::kParserInternalError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kIntegrityBlockParserInternalError),
-        std::make_pair(
-            web_package::mojom::BundleParseErrorType::kVersionError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kIntegrityBlockParserVersionError),
-        std::make_pair(
-            web_package::mojom::BundleParseErrorType::kFormatError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kIntegrityBlockParserFormatError)));
+            IsolatedWebAppResponseReaderFactory::
+                ReadIntegrityBlockAndMetadataStatus::
+                    kIntegrityBlockParserInternalError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kIntegrityBlockParserVersionError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kIntegrityBlockParserFormatError)));
 
 TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidIntegrityBlockContents) {
   base::HistogramTester histogram_tester;
@@ -517,7 +519,7 @@
 
   histogram_tester.ExpectBucketCount(
       "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
-      IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
           kIntegrityBlockValidationError,
       1);
 }
@@ -558,7 +560,7 @@
 
   histogram_tester.ExpectBucketCount(
       "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
-      IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
           kSignatureVerificationError,
       1);
 }
@@ -574,10 +576,10 @@
 
 class IsolatedWebAppReaderRegistryMetadataParserErrorTest
     : public IsolatedWebAppReaderRegistryTest,
-      public ::testing::WithParamInterface<std::pair<
-          web_package::mojom::BundleParseErrorType,
-          IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus>> {
-};
+      public ::testing::WithParamInterface<
+          std::pair<web_package::mojom::BundleParseErrorType,
+                    IsolatedWebAppResponseReaderFactory::
+                        ReadIntegrityBlockAndMetadataStatus>> {};
 
 TEST_P(IsolatedWebAppReaderRegistryMetadataParserErrorTest,
        TestMetadataParserError) {
@@ -614,16 +616,17 @@
     ::testing::Values(
         std::make_pair(
             web_package::mojom::BundleParseErrorType::kParserInternalError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kMetadataParserInternalError),
-        std::make_pair(
-            web_package::mojom::BundleParseErrorType::kVersionError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kMetadataParserVersionError),
-        std::make_pair(
-            web_package::mojom::BundleParseErrorType::kFormatError,
-            IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
-                kMetadataParserFormatError)));
+            IsolatedWebAppResponseReaderFactory::
+                ReadIntegrityBlockAndMetadataStatus::
+                    kMetadataParserInternalError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kMetadataParserVersionError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kMetadataParserFormatError)));
 
 TEST_F(IsolatedWebAppReaderRegistryTest, TestInvalidMetadataPrimaryUrl) {
   base::HistogramTester histogram_tester;
@@ -652,7 +655,7 @@
 
   histogram_tester.ExpectBucketCount(
       "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
-      IsolatedWebAppReaderRegistry::ReadIntegrityBlockAndMetadataStatus::
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
           kMetadataValidationError,
       1);
 }
@@ -771,7 +774,7 @@
 
     std::string response_body = ReadAndFulfillResponseBody(
         result->head()->payload_length,
-        base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+        base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                        base::Unretained(&*result)));
     EXPECT_EQ(kResponseBody, response_body);
   }
@@ -784,7 +787,7 @@
 
     std::string response_body = ReadAndFulfillResponseBody(
         result->head()->payload_length,
-        base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+        base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                        base::Unretained(&*result)));
     EXPECT_EQ(kResponseBody, response_body);
   }
@@ -806,7 +809,7 @@
 
     std::string response_body = ReadAndFulfillResponseBody(
         result->head()->payload_length,
-        base::BindOnce(&IsolatedWebAppReaderRegistry::Response::ReadBody,
+        base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
                        base::Unretained(&*result)));
     EXPECT_EQ(kResponseBody, response_body);
   }
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.cc
new file mode 100644
index 0000000..472b4bd
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.cc
@@ -0,0 +1,104 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+
+#include <memory>
+
+#include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/memory/weak_ptr.h"
+#include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
+#include "components/web_package/mojom/web_bundle_parser.mojom-forward.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "url/gurl.h"
+
+namespace web_app {
+
+namespace {
+
+network::ResourceRequest RemoveQuery(
+    network::ResourceRequest resource_request) {
+  GURL::Replacements replacements;
+  replacements.ClearQuery();
+  resource_request.url = resource_request.url.ReplaceComponents(replacements);
+  return resource_request;
+}
+
+}  // namespace
+
+IsolatedWebAppResponseReader::IsolatedWebAppResponseReader(
+    std::unique_ptr<SignedWebBundleReader> reader)
+    : reader_(std::move(reader)) {
+  CHECK_EQ(reader_->GetState(), SignedWebBundleReader::State::kInitialized);
+}
+
+IsolatedWebAppResponseReader::~IsolatedWebAppResponseReader() = default;
+
+void IsolatedWebAppResponseReader::ReadResponse(
+    const network::ResourceRequest& resource_request,
+    ReadResponseCallback callback) {
+  // Remove query parameters from the request URL, if it has any. Resources
+  // within Signed Web Bundles used for Isolated Web Apps never have username,
+  // password, or fragment, just like resources within Signed Web Bundles and
+  // normal Web Bundles. Removing these from request URLs is done by the
+  // `SignedWebBundleReader`. However, in addition, resources in Signed Web
+  // Bundles used for Isolated Web Apps can also never have query parameters,
+  // which we need to remove here.
+  //
+  // Conceptually, we treat the resources in Signed Web Bundles for Isolated Web
+  // Apps more like files served by a file server (which also strips query
+  // parameters before looking up the file), and not like HTTP exchanges like
+  // they are used for Signed Exchanges (SXG).
+  reader_->ReadResponse(
+      resource_request.url.has_query() ? RemoveQuery(resource_request)
+                                       : resource_request,
+      base::BindOnce(&IsolatedWebAppResponseReader::OnResponseRead,
+                     // `base::Unretained` is safe to use here, since `this`
+                     // owns `reader_`.
+                     base::Unretained(this), std::move(callback)));
+}
+
+void IsolatedWebAppResponseReader::OnResponseRead(
+    ReadResponseCallback callback,
+    base::expected<web_package::mojom::BundleResponsePtr, Error>
+        response_head) {
+  if (!response_head.has_value()) {
+    std::move(callback).Run(base::unexpected(response_head.error()));
+    return;
+  }
+  // Since `this` owns `reader_`, we only pass a weak pointer to it to the
+  // `Response` object. If `this` is deleted, it makes sense that the pointer to
+  // the `reader_` contained in `Response` also becomes invalid.
+  std::move(callback).Run(
+      Response(std::move(*response_head), reader_->AsWeakPtr()));
+}
+
+IsolatedWebAppResponseReader::Response::Response(
+    web_package::mojom::BundleResponsePtr head,
+    base::WeakPtr<SignedWebBundleReader> reader)
+    : head_(std::move(head)), reader_(std::move(reader)) {}
+
+IsolatedWebAppResponseReader::Response::Response(Response&&) = default;
+
+IsolatedWebAppResponseReader::Response&
+IsolatedWebAppResponseReader::Response::operator=(Response&&) = default;
+
+IsolatedWebAppResponseReader::Response::~Response() = default;
+
+void IsolatedWebAppResponseReader::Response::ReadBody(
+    mojo::ScopedDataPipeProducerHandle producer_handle,
+    base::OnceCallback<void(net::Error net_error)> callback) {
+  if (!reader_) {
+    // The weak pointer to `reader_` might no longer be valid when this is
+    // called.
+    std::move(callback).Run(net::ERR_FAILED);
+    return;
+  }
+  reader_->ReadResponseBody(head_->Clone(), std::move(producer_handle),
+                            std::move(callback));
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h
new file mode 100644
index 0000000..b1b33c88
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h
@@ -0,0 +1,85 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_H_
+
+#include <memory>
+
+#include "base/functional/callback_forward.h"
+#include "base/memory/weak_ptr.h"
+#include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
+#include "components/web_package/mojom/web_bundle_parser.mojom-forward.h"
+
+namespace network {
+struct ResourceRequest;
+}
+
+namespace web_app {
+
+// This class is used to read responses for requests from Isolated Web Apps. It
+// is constructed from a `SignedWebBundleReader` instance, which must have
+// already read and validated integrity block and metadata. Usually, this class
+// should be constructed via the `IsolatedWebAppResponseReaderFactory`, which
+// will take care of the necessary validation and verification steps.
+class IsolatedWebAppResponseReader {
+ public:
+  explicit IsolatedWebAppResponseReader(
+      std::unique_ptr<SignedWebBundleReader> reader);
+  ~IsolatedWebAppResponseReader();
+
+  // A `Response` object contains the response head, as well as a `ReadBody`
+  // function to read the response's body. It holds weakly onto a
+  // `SignedWebBundleReader` for reading the response body. This reference will
+  // remain valid until the reader is evicted from the cache of the
+  // `IsolatedWebAppReaderRegistry`.
+  class Response {
+   public:
+    Response(web_package::mojom::BundleResponsePtr head,
+             base::WeakPtr<SignedWebBundleReader> reader);
+
+    Response(const Response&) = delete;
+    Response& operator=(const Response&) = delete;
+
+    Response(Response&&);
+    Response& operator=(Response&&);
+
+    ~Response();
+
+    // Returns the head of the response, which includes status code and response
+    // headers.
+    const web_package::mojom::BundleResponsePtr& head() { return head_; }
+
+    // Reads the body of the response into `producer_handle`, calling `callback`
+    // with `net::OK` on success, and another error code on failure. A failure
+    // may also occur if the `IsolatedWebAppResponseReader` that was used to
+    // read the response head has since been deleted.
+    void ReadBody(mojo::ScopedDataPipeProducerHandle producer_handle,
+                  base::OnceCallback<void(net::Error net_error)> callback);
+
+   private:
+    web_package::mojom::BundleResponsePtr head_;
+    base::WeakPtr<SignedWebBundleReader> reader_;
+  };
+
+  using Error = SignedWebBundleReader::ReadResponseError;
+
+  using ReadResponseCallback =
+      base::OnceCallback<void(base::expected<Response, Error>)>;
+
+  void ReadResponse(const network::ResourceRequest& resource_request,
+                    ReadResponseCallback callback);
+
+ private:
+  void OnResponseRead(ReadResponseCallback callback,
+                      base::expected<web_package::mojom::BundleResponsePtr,
+                                     Error> response_head);
+
+  std::unique_ptr<SignedWebBundleReader> reader_;
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_H_
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.cc
new file mode 100644
index 0000000..a2b5dad
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.cc
@@ -0,0 +1,209 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
+
+#include <memory>
+#include <string>
+
+#include "base/functional/overloaded.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
+#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
+#include "chrome/common/url_constants.h"
+#include "components/web_package/mojom/web_bundle_parser.mojom.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace web_app {
+
+IsolatedWebAppResponseReaderFactory::IsolatedWebAppResponseReaderFactory(
+    std::unique_ptr<IsolatedWebAppValidator> validator,
+    base::RepeatingCallback<
+        std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
+        signature_verifier_factory)
+    : validator_(std::move(validator)),
+      signature_verifier_factory_(std::move(signature_verifier_factory)) {}
+
+IsolatedWebAppResponseReaderFactory::~IsolatedWebAppResponseReaderFactory() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+}
+
+void IsolatedWebAppResponseReaderFactory::CreateResponseReader(
+    const base::FilePath& web_bundle_path,
+    const web_package::SignedWebBundleId& web_bundle_id,
+    bool skip_signature_verification,
+    Callback callback) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK_EQ(web_bundle_id.type(),
+            web_package::SignedWebBundleId::Type::kEd25519PublicKey);
+
+  GURL base_url(
+      base::StrCat({chrome::kIsolatedAppScheme, url::kStandardSchemeSeparator,
+                    web_bundle_id.id()}));
+
+  std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>
+      signature_verifier = signature_verifier_factory_.Run();
+  std::unique_ptr<SignedWebBundleReader> reader = SignedWebBundleReader::Create(
+      web_bundle_path, std::move(base_url), std::move(signature_verifier));
+
+  SignedWebBundleReader& reader_ref = *reader.get();
+  reader_ref.StartReading(
+      base::BindOnce(&IsolatedWebAppResponseReaderFactory::OnIntegrityBlockRead,
+                     weak_ptr_factory_.GetWeakPtr(), web_bundle_id,
+                     skip_signature_verification),
+      base::BindOnce(
+          &IsolatedWebAppResponseReaderFactory::OnIntegrityBlockAndMetadataRead,
+          weak_ptr_factory_.GetWeakPtr(), std::move(reader), web_bundle_path,
+          web_bundle_id, std::move(callback)));
+}
+
+void IsolatedWebAppResponseReaderFactory::OnIntegrityBlockRead(
+    const web_package::SignedWebBundleId& web_bundle_id,
+    bool skip_signature_verification,
+    const web_package::SignedWebBundleIntegrityBlock integrity_block,
+    base::OnceCallback<void(SignedWebBundleReader::SignatureVerificationAction)>
+        integrity_callback) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  validator_->ValidateIntegrityBlock(
+      web_bundle_id, integrity_block,
+      base::BindOnce(
+          &IsolatedWebAppResponseReaderFactory::OnIntegrityBlockValidated,
+          weak_ptr_factory_.GetWeakPtr(), skip_signature_verification,
+          std::move(integrity_callback)));
+}
+
+void IsolatedWebAppResponseReaderFactory::OnIntegrityBlockValidated(
+    bool skip_signature_verification,
+    base::OnceCallback<void(SignedWebBundleReader::SignatureVerificationAction)>
+        integrity_callback,
+    absl::optional<std::string> integrity_block_error) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (integrity_block_error.has_value()) {
+    // Aborting parsing will trigger a call to `OnIntegrityBlockAndMetadataRead`
+    // with a `SignedWebBundleReader::AbortedByCaller` error.
+    std::move(integrity_callback)
+        .Run(SignedWebBundleReader::SignatureVerificationAction::Abort(
+            *integrity_block_error));
+    return;
+  }
+
+  if (skip_signature_verification) {
+    std::move(integrity_callback)
+        .Run(SignedWebBundleReader::SignatureVerificationAction::
+                 ContinueAndSkipSignatureVerification());
+  } else {
+    std::move(integrity_callback)
+        .Run(SignedWebBundleReader::SignatureVerificationAction::
+                 ContinueAndVerifySignatures());
+  }
+}
+
+void IsolatedWebAppResponseReaderFactory::OnIntegrityBlockAndMetadataRead(
+    std::unique_ptr<SignedWebBundleReader> reader,
+    const base::FilePath& web_bundle_path,
+    const web_package::SignedWebBundleId& web_bundle_id,
+    Callback callback,
+    absl::optional<SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>
+        read_integrity_block_and_metadata_error) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  absl::optional<std::pair<Error, ReadIntegrityBlockAndMetadataStatus>>
+      error_and_status;
+  if (read_integrity_block_and_metadata_error.has_value()) {
+    Error error = absl::visit(
+        base::Overloaded{
+            [](const web_package::mojom::BundleIntegrityBlockParseErrorPtr&
+                   error) -> Error { return error->Clone(); },
+            [](const SignedWebBundleReader::AbortedByCaller& error) -> Error {
+              return IntegrityBlockError(error.message);
+            },
+            [](const web_package::SignedWebBundleSignatureVerifier::Error&
+                   error) -> Error { return error; },
+            [](const web_package::mojom::BundleMetadataParseErrorPtr& error)
+                -> Error { return error->Clone(); },
+        },
+        *read_integrity_block_and_metadata_error);
+    error_and_status = std::make_pair(
+        std::move(error),
+        GetStatusFromError(*read_integrity_block_and_metadata_error));
+  }
+
+  if (!error_and_status.has_value()) {
+    if (auto error_message = validator_->ValidateMetadata(
+            web_bundle_id, reader->GetPrimaryURL(), reader->GetEntries());
+        error_message.has_value()) {
+      error_and_status = std::make_pair(
+          MetadataError(*error_message),
+          ReadIntegrityBlockAndMetadataStatus::kMetadataValidationError);
+    }
+  }
+
+  base::UmaHistogramEnumeration(
+      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
+      error_and_status.has_value()
+          ? error_and_status->second
+          : ReadIntegrityBlockAndMetadataStatus::kSuccess);
+
+  if (error_and_status.has_value()) {
+    std::move(callback).Run(
+        base::unexpected(std::move(error_and_status->first)));
+    return;
+  }
+  std::move(callback).Run(
+      std::make_unique<IsolatedWebAppResponseReader>(std::move(reader)));
+}
+
+IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus
+IsolatedWebAppResponseReaderFactory::GetStatusFromError(
+    const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error) {
+  return absl::visit(
+      base::Overloaded{
+          [](const web_package::mojom::BundleIntegrityBlockParseErrorPtr&
+                 error) {
+            switch (error->type) {
+              case web_package::mojom::BundleParseErrorType::
+                  kParserInternalError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kIntegrityBlockParserInternalError;
+              case web_package::mojom::BundleParseErrorType::kFormatError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kIntegrityBlockParserFormatError;
+              case web_package::mojom::BundleParseErrorType::kVersionError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kIntegrityBlockParserVersionError;
+            }
+          },
+          [](const SignedWebBundleReader::AbortedByCaller& error) {
+            return ReadIntegrityBlockAndMetadataStatus::
+                kIntegrityBlockValidationError;
+          },
+          [](const web_package::SignedWebBundleSignatureVerifier::Error&
+                 error) {
+            return ReadIntegrityBlockAndMetadataStatus::
+                kSignatureVerificationError;
+          },
+          [](const web_package::mojom::BundleMetadataParseErrorPtr& error) {
+            switch (error->type) {
+              case web_package::mojom::BundleParseErrorType::
+                  kParserInternalError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kMetadataParserInternalError;
+              case web_package::mojom::BundleParseErrorType::kFormatError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kMetadataParserFormatError;
+              case web_package::mojom::BundleParseErrorType::kVersionError:
+                return ReadIntegrityBlockAndMetadataStatus::
+                    kMetadataParserVersionError;
+            }
+          }},
+      error);
+}
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h
new file mode 100644
index 0000000..d51c60e
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h
@@ -0,0 +1,153 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_FACTORY_H_
+#define CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_FACTORY_H_
+
+#include <memory>
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/functional/callback.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
+#include "components/web_package/mojom/web_bundle_parser.mojom-forward.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace web_package {
+class SignedWebBundleId;
+class SignedWebBundleIntegrityBlock;
+}  // namespace web_package
+
+namespace web_app {
+
+class IsolatedWebAppValidator;
+
+// Simple struct used to represent errors related to the integrity block of the
+// Signed Web Bundle.
+struct IntegrityBlockError {
+  explicit IntegrityBlockError(std::string message)
+      : message(std::move(message)) {}
+
+  std::string message;
+};
+
+// Simple struct used to represent errors related to the metadata of the Signed
+// Web Bundle.
+struct MetadataError {
+  explicit MetadataError(std::string message) : message(std::move(message)) {}
+
+  std::string message;
+};
+
+// Factory for creating instances of `IsolatedWebAppResponseReader` that are
+// ready to read responses from the bundle. Instances returned by this class are
+// guaranteed to have previously read a valid integrity block and metadata, as
+// well as to have verified that the signatures are valid (unless
+// `skip_signature_verification` is set).
+class IsolatedWebAppResponseReaderFactory {
+ public:
+  IsolatedWebAppResponseReaderFactory(
+      std::unique_ptr<IsolatedWebAppValidator> validator,
+      base::RepeatingCallback<
+          std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
+          signature_verifier_factory);
+  ~IsolatedWebAppResponseReaderFactory();
+
+  IsolatedWebAppResponseReaderFactory(
+      const IsolatedWebAppResponseReaderFactory&) = delete;
+  IsolatedWebAppResponseReaderFactory& operator=(
+      const IsolatedWebAppResponseReaderFactory&) = delete;
+
+  using Error = absl::variant<
+      // Triggered when the integrity block of the Signed Web
+      // Bundle does not exist or parsing it fails.
+      web_package::mojom::BundleIntegrityBlockParseErrorPtr,
+      // Triggered when the integrity block is not valid for this Isolated Web
+      // App or when the user agent does not trust the Isolated Web App.
+      IntegrityBlockError,
+      // Triggered when signature verification fails.
+      web_package::SignedWebBundleSignatureVerifier::Error,
+      // Triggered when metadata parsing fails.
+      web_package::mojom::BundleMetadataParseErrorPtr,
+      // Triggered when the metadata is not valid for this Isolated Web App.
+      MetadataError>;
+
+  using Callback = base::OnceCallback<void(
+      base::expected<std::unique_ptr<IsolatedWebAppResponseReader>, Error>)>;
+
+  void CreateResponseReader(const base::FilePath& web_bundle_path,
+                            const web_package::SignedWebBundleId& web_bundle_id,
+                            bool skip_signature_verification,
+                            Callback callback);
+
+  // This enum represents every error type that can occur during integrity block
+  // and metadata parsing, before responses are read from Signed Web Bundles.
+  //
+  // These values are persisted to logs. Entries should not be renumbered and
+  // numeric values should never be reused.
+  enum class ReadIntegrityBlockAndMetadataStatus {
+    kSuccess = 0,
+    // Integrity Block-related errors
+    kIntegrityBlockParserInternalError = 1,
+    kIntegrityBlockParserFormatError = 2,
+    kIntegrityBlockParserVersionError = 3,
+    kIntegrityBlockValidationError = 4,
+
+    // Signature verification errors
+    kSignatureVerificationError = 5,
+
+    // Metadata-related errors
+    kMetadataParserInternalError = 6,
+    kMetadataParserFormatError = 7,
+    kMetadataParserVersionError = 8,
+    kMetadataValidationError = 9,
+
+    kMaxValue = kMetadataValidationError
+  };
+
+ private:
+  void OnIntegrityBlockRead(
+      const web_package::SignedWebBundleId& web_bundle_id,
+      bool skip_signature_verification,
+      const web_package::SignedWebBundleIntegrityBlock integrity_block,
+      base::OnceCallback<
+          void(SignedWebBundleReader::SignatureVerificationAction)> callback);
+
+  void OnIntegrityBlockValidated(
+      bool skip_signature_verification,
+      base::OnceCallback<
+          void(SignedWebBundleReader::SignatureVerificationAction)>
+          integrity_callback,
+      absl::optional<std::string> integrity_block_error);
+
+  void OnIntegrityBlockAndMetadataRead(
+      std::unique_ptr<SignedWebBundleReader> reader,
+      const base::FilePath& web_bundle_path,
+      const web_package::SignedWebBundleId& web_bundle_id,
+      Callback callback,
+      absl::optional<SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>
+          read_integrity_block_and_metadata_error);
+
+  ReadIntegrityBlockAndMetadataStatus GetStatusFromError(
+      const SignedWebBundleReader::ReadIntegrityBlockAndMetadataError& error);
+
+  std::unique_ptr<IsolatedWebAppValidator> validator_;
+  base::RepeatingCallback<
+      std::unique_ptr<web_package::SignedWebBundleSignatureVerifier>()>
+      signature_verifier_factory_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
+  base::WeakPtrFactory<IsolatedWebAppResponseReaderFactory> weak_ptr_factory_{
+      this};
+};
+
+}  // namespace web_app
+
+#endif  // CHROME_BROWSER_WEB_APPLICATIONS_ISOLATED_WEB_APPS_ISOLATED_WEB_APP_RESPONSE_READER_FACTORY_H_
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory_unittest.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory_unittest.cc
new file mode 100644
index 0000000..3d8bd04
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory_unittest.cc
@@ -0,0 +1,480 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_factory.h"
+
+#include <memory>
+
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/bind.h"
+#include "base/functional/callback.h"
+#include "base/functional/callback_helpers.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/repeating_test_future.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "base/types/expected.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_trust_checker.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_validator.h"
+#include "components/prefs/testing_pref_service.h"
+#include "components/web_package/mojom/web_bundle_parser.mojom.h"
+#include "components/web_package/signed_web_bundles/ed25519_public_key.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_signature_verifier.h"
+#include "components/web_package/test_support/mock_web_bundle_parser_factory.h"
+#include "content/public/common/content_features.h"
+#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "third_party/abseil-cpp/absl/types/variant.h"
+
+namespace web_app {
+
+namespace {
+
+using testing::ElementsAre;
+using testing::Eq;
+using testing::IsFalse;
+using testing::IsTrue;
+using testing::NotNull;
+using testing::StartsWith;
+using testing::VariantWith;
+
+constexpr uint8_t kEd25519PublicKey[32] = {0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0,
+                                           0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0,
+                                           0, 0, 0, 0, 2, 2, 2, 0, 0, 0};
+
+constexpr uint8_t kEd25519Signature[64] = {
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 7, 0, 0, 0, 0,
+    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 0, 0};
+
+class FakeIsolatedWebAppValidator : public IsolatedWebAppValidator {
+ public:
+  explicit FakeIsolatedWebAppValidator(
+      absl::optional<std::string> integrity_block_error)
+      : IsolatedWebAppValidator(std::make_unique<IsolatedWebAppTrustChecker>(
+            TestingPrefServiceSimple())),
+        integrity_block_error_(integrity_block_error) {}
+
+  void ValidateIntegrityBlock(
+      const web_package::SignedWebBundleId& web_bundle_id,
+      const web_package::SignedWebBundleIntegrityBlock& integrity_block,
+      base::OnceCallback<void(absl::optional<std::string>)> callback) override {
+    std::move(callback).Run(integrity_block_error_);
+  }
+
+ private:
+  absl::optional<std::string> integrity_block_error_;
+};
+
+class FakeSignatureVerifier
+    : public web_package::SignedWebBundleSignatureVerifier {
+ public:
+  explicit FakeSignatureVerifier(
+      absl::optional<web_package::SignedWebBundleSignatureVerifier::Error>
+          error,
+      base::RepeatingClosure on_verify_signatures = base::DoNothing())
+      : error_(error), on_verify_signatures_(on_verify_signatures) {}
+
+  void VerifySignatures(
+      scoped_refptr<web_package::SharedFile> file,
+      web_package::SignedWebBundleIntegrityBlock integrity_block,
+      SignatureVerificationCallback callback) override {
+    on_verify_signatures_.Run();
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(std::move(callback), error_));
+  }
+
+ private:
+  absl::optional<web_package::SignedWebBundleSignatureVerifier::Error> error_;
+  base::RepeatingClosure on_verify_signatures_;
+};
+
+class IsolatedWebAppResponseReaderFactoryTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    scoped_feature_list_.InitAndEnableFeature(features::kIsolatedWebApps);
+
+    parser_factory_ = std::make_unique<web_package::MockWebBundleParserFactory>(
+        on_create_parser_future_.GetCallback());
+
+    response_ = web_package::mojom::BundleResponse::New();
+    response_->response_code = 200;
+    response_->payload_offset = 0;
+    response_->payload_length = sizeof(kResponseBody) - 1;
+
+    base::flat_map<GURL, web_package::mojom::BundleResponseLocationPtr>
+        requests;
+    requests.insert(
+        {kUrl, web_package::mojom::BundleResponseLocation::New(
+                   response_->payload_offset, response_->payload_length)});
+
+    metadata_ = web_package::mojom::BundleMetadata::New();
+    metadata_->requests = std::move(requests);
+
+    web_package::mojom::BundleIntegrityBlockSignatureStackEntryPtr
+        signature_stack_entry =
+            web_package::mojom::BundleIntegrityBlockSignatureStackEntry::New();
+    signature_stack_entry->public_key = web_package::Ed25519PublicKey::Create(
+        base::make_span(kEd25519PublicKey));
+    signature_stack_entry->signature = web_package::Ed25519Signature::Create(
+        base::make_span(kEd25519Signature));
+
+    std::vector<web_package::mojom::BundleIntegrityBlockSignatureStackEntryPtr>
+        signature_stack;
+    signature_stack.push_back(std::move(signature_stack_entry));
+
+    integrity_block_ = web_package::mojom::BundleIntegrityBlock::New();
+    integrity_block_->size = 42;
+    integrity_block_->signature_stack = std::move(signature_stack);
+
+    factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
+        std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt),
+        base::BindRepeating(
+            []() -> std::unique_ptr<
+                     web_package::SignedWebBundleSignatureVerifier> {
+              return std::make_unique<FakeSignatureVerifier>(absl::nullopt);
+            }));
+
+    std::string test_file_data = kResponseBody;
+    CHECK(temp_dir_.CreateUniqueTempDir());
+    CHECK(CreateTemporaryFileInDir(temp_dir_.GetPath(), &web_bundle_path_));
+    CHECK_EQ(test_file_data.size(), static_cast<size_t>(base::WriteFile(
+                                        web_bundle_path_, test_file_data.data(),
+                                        test_file_data.size())));
+
+    in_process_data_decoder_.service()
+        .SetWebBundleParserFactoryBinderForTesting(base::BindRepeating(
+            &web_package::MockWebBundleParserFactory::AddReceiver,
+            base::Unretained(parser_factory_.get())));
+  }
+
+  void TearDown() override { factory_.reset(); }
+
+  void FulfillIntegrityBlock() {
+    parser_factory_->RunIntegrityBlockCallback(integrity_block_->Clone());
+  }
+
+  void FulfillMetadata() {
+    parser_factory_->RunMetadataCallback(integrity_block_->size,
+                                         metadata_->Clone());
+  }
+
+  void FulfillResponse(const network::ResourceRequest& resource_request) {
+    parser_factory_->RunResponseCallback(
+        web_package::mojom::BundleResponseLocation::New(
+            response_->payload_offset, response_->payload_length),
+        response_->Clone());
+  }
+
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  base::test::ScopedFeatureList scoped_feature_list_;
+  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
+  base::ScopedTempDir temp_dir_;
+  base::FilePath web_bundle_path_;
+  base::test::RepeatingTestFuture<absl::optional<GURL>>
+      on_create_parser_future_;
+
+  const web_package::SignedWebBundleId kWebBundleId =
+      *web_package::SignedWebBundleId::Create(
+          "aaaaaaacaibaaaaaaaaaaaaaaiaaeaaaaaaaaaaaaabaeaqaaaaaaaic");
+  const GURL kUrl = GURL("isolated-app://" + kWebBundleId.id());
+
+  constexpr static char kResponseBody[] = "test";
+
+  constexpr static char kInvalidIsolatedWebAppUrl[] = "isolated-app://foo/";
+
+  std::unique_ptr<IsolatedWebAppResponseReaderFactory> factory_;
+  std::unique_ptr<web_package::MockWebBundleParserFactory> parser_factory_;
+  web_package::mojom::BundleIntegrityBlockPtr integrity_block_;
+  web_package::mojom::BundleMetadataPtr metadata_;
+  web_package::mojom::BundleResponsePtr response_;
+};
+
+using ReaderResult =
+    base::expected<std::unique_ptr<IsolatedWebAppResponseReader>,
+                   IsolatedWebAppResponseReaderFactory::Error>;
+
+class IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest
+    : public IsolatedWebAppResponseReaderFactoryTest,
+      public ::testing::WithParamInterface<
+          std::pair<web_package::mojom::BundleParseErrorType,
+                    IsolatedWebAppResponseReaderFactory::
+                        ReadIntegrityBlockAndMetadataStatus>> {};
+
+TEST_P(IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest,
+       TestIntegrityBlockParserError) {
+  base::HistogramTester histogram_tester;
+
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 /*skip_signature_verification=*/false,
+                                 reader_future.GetCallback());
+
+  auto error = web_package::mojom::BundleIntegrityBlockParseError::New();
+  error->type = GetParam().first;
+  error->message = "test error";
+  parser_factory_->RunIntegrityBlockCallback(nullptr, error->Clone());
+
+  ReaderResult result = reader_future.Take();
+  ASSERT_THAT(result.has_value(), IsFalse());
+  auto* actual_error =
+      absl::get_if<web_package::mojom::BundleIntegrityBlockParseErrorPtr>(
+          &result.error());
+  ASSERT_THAT(actual_error, NotNull());
+  EXPECT_THAT((*actual_error)->type, Eq(error->type));
+  EXPECT_THAT((*actual_error)->message, Eq(error->message));
+
+  histogram_tester.ExpectBucketCount(
+      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second,
+      1);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    IsolatedWebAppResponseReaderFactoryIntegrityBlockParserErrorTest,
+    ::testing::Values(
+        std::make_pair(
+            web_package::mojom::BundleParseErrorType::kParserInternalError,
+            IsolatedWebAppResponseReaderFactory::
+                ReadIntegrityBlockAndMetadataStatus::
+                    kIntegrityBlockParserInternalError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kIntegrityBlockParserVersionError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kIntegrityBlockParserFormatError)));
+
+TEST_F(IsolatedWebAppResponseReaderFactoryTest,
+       TestInvalidIntegrityBlockContents) {
+  base::HistogramTester histogram_tester;
+
+  factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
+      std::make_unique<FakeIsolatedWebAppValidator>("test error"),
+      base::BindRepeating(
+          []() -> std::unique_ptr<
+                   web_package::SignedWebBundleSignatureVerifier> {
+            return std::make_unique<FakeSignatureVerifier>(absl::nullopt);
+          }));
+
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 /*skip_signature_verification=*/false,
+                                 reader_future.GetCallback());
+
+  FulfillIntegrityBlock();
+
+  ReaderResult result = reader_future.Take();
+  ASSERT_THAT(result.has_value(), IsFalse());
+  auto* actual_error = absl::get_if<IntegrityBlockError>(&result.error());
+  ASSERT_THAT(actual_error, NotNull());
+  EXPECT_THAT(actual_error->message, Eq("test error"));
+
+  histogram_tester.ExpectBucketCount(
+      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
+          kIntegrityBlockValidationError,
+      1);
+}
+
+class IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest
+    : public IsolatedWebAppResponseReaderFactoryTest,
+      public ::testing::WithParamInterface<
+          std::tuple<web_package::SignedWebBundleSignatureVerifier::Error,
+                     bool>> {
+ public:
+  IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest()
+      : IsolatedWebAppResponseReaderFactoryTest(),
+        error_(std::get<0>(GetParam())),
+        skip_signature_verification_(std::get<1>(GetParam())) {}
+
+ protected:
+  web_package::SignedWebBundleSignatureVerifier::Error error_;
+  bool skip_signature_verification_;
+};
+
+TEST_P(IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest,
+       SignatureVerificationError) {
+  base::HistogramTester histogram_tester;
+
+  factory_ = std::make_unique<IsolatedWebAppResponseReaderFactory>(
+      std::make_unique<FakeIsolatedWebAppValidator>(absl::nullopt),
+      base::BindRepeating(
+          [](web_package::SignedWebBundleSignatureVerifier::Error error)
+              -> std::unique_ptr<
+                  web_package::SignedWebBundleSignatureVerifier> {
+            return std::make_unique<FakeSignatureVerifier>(error);
+          },
+          error_));
+
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 skip_signature_verification_,
+                                 reader_future.GetCallback());
+
+  FulfillIntegrityBlock();
+
+  // When signature verification is skipped, then the signature verification
+  // error seeded above should not be triggered.
+  if (skip_signature_verification_) {
+    FulfillMetadata();
+
+    ReaderResult result = reader_future.Take();
+    EXPECT_THAT(result.has_value(), IsTrue());
+
+    histogram_tester.ExpectBucketCount(
+        "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
+        IsolatedWebAppResponseReaderFactory::
+            ReadIntegrityBlockAndMetadataStatus::kSignatureVerificationError,
+        0);
+  } else {
+    ReaderResult result = reader_future.Take();
+    ASSERT_THAT(result.has_value(), IsFalse());
+    auto* actual_error =
+        absl::get_if<web_package::SignedWebBundleSignatureVerifier::Error>(
+            &result.error());
+    ASSERT_THAT(actual_error, NotNull());
+    EXPECT_THAT(actual_error->message, Eq(error_.message));
+
+    histogram_tester.ExpectBucketCount(
+        "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
+        IsolatedWebAppResponseReaderFactory::
+            ReadIntegrityBlockAndMetadataStatus::kSignatureVerificationError,
+        1);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    IsolatedWebAppResponseReaderFactorySignatureVerificationErrorTest,
+    ::testing::Combine(
+        ::testing::Values(web_package::SignedWebBundleSignatureVerifier::Error::
+                              ForInternalError("internal error"),
+                          web_package::SignedWebBundleSignatureVerifier::Error::
+                              ForInvalidSignature("invalid signature")),
+        // skip_signature_verification
+        ::testing::Bool()));
+
+class IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest
+    : public IsolatedWebAppResponseReaderFactoryTest,
+      public ::testing::WithParamInterface<
+          std::pair<web_package::mojom::BundleParseErrorType,
+                    IsolatedWebAppResponseReaderFactory::
+                        ReadIntegrityBlockAndMetadataStatus>> {};
+
+TEST_P(IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest,
+       TestMetadataParserError) {
+  base::HistogramTester histogram_tester;
+
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 /*skip_signature_verification=*/false,
+                                 reader_future.GetCallback());
+
+  FulfillIntegrityBlock();
+  auto error = web_package::mojom::BundleMetadataParseError::New();
+  error->message = "test error";
+  error->type = GetParam().first;
+  parser_factory_->RunMetadataCallback(integrity_block_->size, nullptr,
+                                       error->Clone());
+
+  ReaderResult result = reader_future.Take();
+  ASSERT_THAT(result.has_value(), IsFalse());
+  auto* actual_error =
+      absl::get_if<web_package::mojom::BundleMetadataParseErrorPtr>(
+          &result.error());
+  ASSERT_THAT(actual_error, NotNull());
+  EXPECT_THAT((*actual_error)->type, Eq(error->type));
+  EXPECT_THAT((*actual_error)->message, Eq(error->message));
+
+  histogram_tester.ExpectBucketCount(
+      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus", GetParam().second,
+      1);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    IsolatedWebAppResponseReaderFactoryMetadataParserErrorTest,
+    ::testing::Values(
+        std::make_pair(
+            web_package::mojom::BundleParseErrorType::kParserInternalError,
+            IsolatedWebAppResponseReaderFactory::
+                ReadIntegrityBlockAndMetadataStatus::
+                    kMetadataParserInternalError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kVersionError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kMetadataParserVersionError),
+        std::make_pair(web_package::mojom::BundleParseErrorType::kFormatError,
+                       IsolatedWebAppResponseReaderFactory::
+                           ReadIntegrityBlockAndMetadataStatus::
+                               kMetadataParserFormatError)));
+
+TEST_F(IsolatedWebAppResponseReaderFactoryTest, TestInvalidMetadataPrimaryUrl) {
+  base::HistogramTester histogram_tester;
+
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 /*skip_signature_verification=*/false,
+                                 reader_future.GetCallback());
+
+  FulfillIntegrityBlock();
+  auto metadata = metadata_->Clone();
+  metadata->primary_url = kUrl;
+  parser_factory_->RunMetadataCallback(integrity_block_->size,
+                                       std::move(metadata));
+
+  ReaderResult result = reader_future.Take();
+  ASSERT_THAT(result.has_value(), IsFalse());
+  auto* actual_error = absl::get_if<MetadataError>(&result.error());
+  ASSERT_THAT(actual_error, NotNull());
+  EXPECT_THAT(actual_error->message,
+              StartsWith("Primary URL must not be present"));
+
+  histogram_tester.ExpectBucketCount(
+      "WebApp.Isolated.ReadIntegrityBlockAndMetadataStatus",
+      IsolatedWebAppResponseReaderFactory::ReadIntegrityBlockAndMetadataStatus::
+          kMetadataValidationError,
+      1);
+}
+
+TEST_F(IsolatedWebAppResponseReaderFactoryTest,
+       TestInvalidMetadataInvalidExchange) {
+  base::test::TestFuture<ReaderResult> reader_future;
+  factory_->CreateResponseReader(web_bundle_path_, kWebBundleId,
+                                 /*skip_signature_verification=*/false,
+                                 reader_future.GetCallback());
+
+  FulfillIntegrityBlock();
+  auto metadata = metadata_->Clone();
+  metadata->requests.insert_or_assign(
+      GURL(kInvalidIsolatedWebAppUrl),
+      web_package::mojom::BundleResponseLocation::New());
+  parser_factory_->RunMetadataCallback(integrity_block_->size,
+                                       std::move(metadata));
+
+  ReaderResult result = reader_future.Take();
+  ASSERT_THAT(result.has_value(), IsFalse());
+  auto* actual_error = absl::get_if<MetadataError>(&result.error());
+  ASSERT_THAT(actual_error, NotNull());
+  EXPECT_THAT(actual_error->message,
+              StartsWith("The URL of an exchange is invalid"));
+}
+
+}  // namespace
+
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_unittest.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_unittest.cc
new file mode 100644
index 0000000..6e30ae83
--- /dev/null
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader_unittest.cc
@@ -0,0 +1,168 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
+
+#include <memory>
+
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
+#include "chrome/browser/web_applications/isolated_web_apps/signed_web_bundle_reader.h"
+#include "chrome/browser/web_applications/test/signed_web_bundle_utils.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
+#include "components/web_package/signed_web_bundles/signed_web_bundle_integrity_block.h"
+#include "net/base/net_errors.h"
+#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace web_app {
+namespace {
+
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::UnorderedElementsAre;
+
+class IsolatedWebAppResponseReaderTest : public ::testing::Test {
+ protected:
+  void SetUp() override { CHECK(temp_dir_.CreateUniqueTempDir()); }
+
+  base::FilePath CreateSignedBundleAndWriteToDisk() {
+    web_package::WebBundleBuilder builder;
+    builder.AddExchange("/",
+                        {{":status", "200"}, {"content-type", "text/html"}},
+                        "Hello World");
+    auto unsigned_bundle = builder.CreateBundle();
+
+    web_package::WebBundleSigner::KeyPair key_pair(kTestPublicKey,
+                                                   kTestPrivateKey);
+    auto signed_bundle =
+        web_package::WebBundleSigner::SignBundle(unsigned_bundle, {key_pair});
+
+    base::FilePath web_bundle_path;
+    CHECK(CreateTemporaryFileInDir(temp_dir_.GetPath(), &web_bundle_path));
+    CHECK_EQ(static_cast<size_t>(base::WriteFile(
+                 web_bundle_path, reinterpret_cast<char*>(signed_bundle.data()),
+                 signed_bundle.size())),
+             signed_bundle.size());
+
+    return web_bundle_path;
+  }
+
+  absl::optional<SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>
+  ReadIntegrityBlockAndMetadata(SignedWebBundleReader& reader) {
+    base::test::TestFuture<absl::optional<
+        SignedWebBundleReader::ReadIntegrityBlockAndMetadataError>>
+        future;
+    reader.StartReading(
+        base::BindOnce(
+            [](web_package::SignedWebBundleIntegrityBlock integrity_block,
+               base::OnceCallback<void(
+                   SignedWebBundleReader::SignatureVerificationAction)>
+                   callback) {
+              std::move(callback).Run(
+                  SignedWebBundleReader::SignatureVerificationAction::
+                      ContinueAndVerifySignatures());
+            }),
+        future.GetCallback());
+    return future.Take();
+  }
+
+  base::test::TaskEnvironment task_environment_;
+  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
+  base::ScopedTempDir temp_dir_;
+
+  web_package::SignedWebBundleId web_bundle_id_ =
+      *web_package::SignedWebBundleId::Create(kTestEd25519WebBundleId);
+
+  GURL base_url_ =
+      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(web_bundle_id_)
+          .origin()
+          .GetURL();
+};
+
+// Tests that query parameters and fragment are stripped from requests before
+// looking up the corresponding resources inside of the bundle.
+TEST_F(IsolatedWebAppResponseReaderTest,
+       ReadResponseStripsQueryParametersAndFragment) {
+  base::FilePath web_bundle_path = CreateSignedBundleAndWriteToDisk();
+  auto reader = SignedWebBundleReader::Create(web_bundle_path, base_url_);
+  auto error = ReadIntegrityBlockAndMetadata(*reader.get());
+  ASSERT_THAT(error.has_value(), IsFalse());
+
+  auto response_reader =
+      std::make_unique<IsolatedWebAppResponseReader>(std::move(reader));
+
+  {
+    network::ResourceRequest request;
+    request.url = base_url_;
+    base::test::TestFuture<
+        base::expected<IsolatedWebAppResponseReader::Response,
+                       IsolatedWebAppResponseReader::Error>>
+        response_future;
+    response_reader->ReadResponse(request, response_future.GetCallback());
+    EXPECT_THAT(response_future.Get().has_value(), IsTrue());
+  }
+
+  {
+    network::ResourceRequest request;
+    request.url = base_url_.Resolve("/?some-query-parameter#some-fragment");
+    base::test::TestFuture<
+        base::expected<IsolatedWebAppResponseReader::Response,
+                       IsolatedWebAppResponseReader::Error>>
+        response_future;
+    response_reader->ReadResponse(request, response_future.GetCallback());
+    EXPECT_THAT(response_future.Get().has_value(), IsTrue());
+  }
+}
+
+TEST_F(IsolatedWebAppResponseReaderTest, ReadResponseBody) {
+  base::FilePath web_bundle_path = CreateSignedBundleAndWriteToDisk();
+  auto reader = SignedWebBundleReader::Create(web_bundle_path, base_url_);
+  auto error = ReadIntegrityBlockAndMetadata(*reader.get());
+  ASSERT_THAT(error.has_value(), IsFalse());
+
+  auto response_reader =
+      std::make_unique<IsolatedWebAppResponseReader>(std::move(reader));
+
+  network::ResourceRequest request;
+  request.url = base_url_;
+  base::test::TestFuture<base::expected<IsolatedWebAppResponseReader::Response,
+                                        IsolatedWebAppResponseReader::Error>>
+      response_future;
+  response_reader->ReadResponse(request, response_future.GetCallback());
+  ASSERT_THAT(response_future.Get().has_value(), IsTrue());
+
+  IsolatedWebAppResponseReader::Response response = *response_future.Take();
+  EXPECT_THAT(response.head()->response_code, Eq(200));
+
+  {
+    std::string response_content = ReadAndFulfillResponseBody(
+        response.head()->payload_length,
+        base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
+                       base::Unretained(&response)));
+    EXPECT_THAT(response_content, Eq("Hello World"));
+  }
+
+  // If the response_reader is deleted, then reading the response should return
+  // `net::ERR_FAILED`.
+  response_reader.reset();
+  {
+    base::test::TestFuture<net::Error> response_body_future;
+    ReadResponseBody(
+        response.head()->payload_length,
+        base::BindOnce(&IsolatedWebAppResponseReader::Response::ReadBody,
+                       base::Unretained(&response)),
+        response_body_future.GetCallback());
+    EXPECT_THAT(response_body_future.Get(), Eq(net::ERR_FAILED));
+  }
+}
+
+}  // namespace
+}  // namespace web_app
diff --git a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_loader_factory.cc b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_loader_factory.cc
index 6d41b04..e2cc970 100644
--- a/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_loader_factory.cc
+++ b/chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_loader_factory.cc
@@ -18,6 +18,7 @@
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_reader_registry_factory.h"
+#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_response_reader.h"
 #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
 #include "chrome/browser/web_applications/isolated_web_apps/pending_install_info.h"
 #include "chrome/browser/web_applications/isolation_data.h"
@@ -207,7 +208,7 @@
 
  private:
   void OnResponseRead(
-      base::expected<IsolatedWebAppReaderRegistry::Response,
+      base::expected<IsolatedWebAppResponseReader::Response,
                      IsolatedWebAppReaderRegistry::ReadResponseError>
           response) {
     if (!loader_client_.is_connected()) {
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 1e67be3..ae48ca74 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1674453207-c4992292019f3bfbd36e9513ecb2e50bc881f11d.profdata
+chrome-linux-main-1674475200-e498ba800953a7eed586317423d0d187692d6313.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index 75fc9990..2167d4cd 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1674453207-bb91ace36e6081718dc7c1145f79cba7a4ccd9d0.profdata
+chrome-mac-arm-main-1674475200-3d20295ad223daebe66b8dabfb3592056cafe3fb.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index db07b069..ea76ea5 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1674453207-0517fe6223f8388aa804e0c5f195c5f7f5c4d873.profdata
+chrome-mac-main-1674475200-b889e73b0d244ee979b0d9cd05fbea8dddce0f63.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 14a32468..ec15b51 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1674430884-928f55e9916a19f45f957375de0c72d5e12edba2.profdata
+chrome-win32-main-1674442478-3e6bcf93d075e2d11475e54b64c03465e4dca39f.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index be2d9d0..a30f533d 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1674442478-e2eb64c4fa47a77ebcbd4d49c71e8fa51b610776.profdata
+chrome-win64-main-1674464242-24768750bcc93df37fb851a20b34445b7926f40c.profdata
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a098fad..21022c81 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1818,6 +1818,7 @@
       "../browser/devtools/protocol/devtools_protocol_browsertest.cc",
       "../browser/devtools/protocol/devtools_protocol_test_support.cc",
       "../browser/devtools/protocol/devtools_protocol_test_support.h",
+      "../browser/devtools/protocol/form_devtools_issues_browsertest.cc",
       "../browser/direct_sockets/direct_sockets_apitest.cc",
       "../browser/dom_distiller/distillable_page_utils_browsertest.cc",
       "../browser/dom_distiller/dom_distiller_viewer_source_browsertest.cc",
@@ -10284,6 +10285,8 @@
     "../browser/sync/test/integration/committed_all_nudged_changes_checker.h",
     "../browser/sync/test/integration/configuration_refresher.cc",
     "../browser/sync/test/integration/configuration_refresher.h",
+    "../browser/sync/test/integration/contact_info_helper.cc",
+    "../browser/sync/test/integration/contact_info_helper.h",
     "../browser/sync/test/integration/device_info_helper.cc",
     "../browser/sync/test/integration/device_info_helper.h",
     "../browser/sync/test/integration/encryption_helper.cc",
@@ -10385,11 +10388,12 @@
     ]
   }
   public_deps = [
+    ":test_support",
     "//chrome/browser",
     "//chrome/browser/favicon",
+    "//components/autofill/content/browser",
   ]
   deps = [
-    ":test_support",
     "//base",
     "//build:chromeos_buildflags",
     "//chrome/browser/autofill",
diff --git a/chrome/test/data/webui/bookmarks/bookmarks_browsertest.js b/chrome/test/data/webui/bookmarks/bookmarks_browsertest.js
index 1721ba1..90ef836 100644
--- a/chrome/test/data/webui/bookmarks/bookmarks_browsertest.js
+++ b/chrome/test/data/webui/bookmarks/bookmarks_browsertest.js
@@ -64,14 +64,8 @@
   }
 };
 
-// http://crbug.com/803570 : Flaky on Win 7 (dbg)
-GEN('#if BUILDFLAG(IS_WIN) && !defined(NDEBUG)');
-GEN('#define MAYBE_All DISABLED_All');
-GEN('#else');
-GEN('#define MAYBE_All All');
-GEN('#endif');
-
-TEST_F('BookmarksDNDManagerTest', 'MAYBE_All', function() {
+// TODO(https://crbug.com/1409439): Test is flaky.
+TEST_F('BookmarksDNDManagerTest', 'DISABLED_All', function() {
   mocha.run();
 });
 
diff --git a/chrome/test/data/webui/bookmarks/bookmarks_focus_test.js b/chrome/test/data/webui/bookmarks/bookmarks_focus_test.js
index d4d7830..98ad496 100644
--- a/chrome/test/data/webui/bookmarks/bookmarks_focus_test.js
+++ b/chrome/test/data/webui/bookmarks/bookmarks_focus_test.js
@@ -25,7 +25,8 @@
   }
 };
 
-TEST_F('BookmarksFolderNodeFocusTest', 'All', function() {
+GEN('#define MAYBE_All DISABLED_All');
+TEST_F('BookmarksFolderNodeFocusTest', 'MAYBE_All', function() {
   mocha.run();
 });
 
diff --git a/chromeos/ash/components/network/metrics/BUILD.gn b/chromeos/ash/components/network/metrics/BUILD.gn
index 4f33fb5..6a2e0dc 100644
--- a/chromeos/ash/components/network/metrics/BUILD.gn
+++ b/chromeos/ash/components/network/metrics/BUILD.gn
@@ -13,10 +13,12 @@
 
   configs += [ "//chromeos/ash/components/network:network_config" ]
   deps = [
+    "//ash/constants:constants",
     "//base",
     "//chromeos/ash/components/login/login_state",
     "//chromeos/services/network_config/public/mojom",
     "//components/device_event_log",
+    "//components/onc",
   ]
   sources = [
     "cellular_network_metrics_logger.cc",
@@ -40,12 +42,14 @@
   testonly = true
   deps = [
     "//ash/constants",
+    "//ash/constants:constants",
     "//base",
     "//base/test:test_support",
     "//chromeos/ash/components/dbus/shill",
     "//chromeos/ash/components/login/login_state",
     "//chromeos/ash/components/network",
     "//chromeos/ash/components/network:test_support",
+    "//components/onc",
     "//components/prefs",
     "//components/prefs:test_support",
     "//testing/gtest",
diff --git a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.cc b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.cc
index 50c7193..9de6225 100644
--- a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.cc
+++ b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.cc
@@ -4,6 +4,7 @@
 
 #include "chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h"
 
+#include "ash/constants/ash_features.h"
 #include "base/metrics/histogram_functions.h"
 
 #include "chromeos/ash/components/network/metrics/connection_info_metrics_logger.h"
@@ -11,6 +12,7 @@
 #include "chromeos/ash/components/network/network_metadata_store.h"
 #include "chromeos/ash/components/network/network_state_handler.h"
 #include "components/device_event_log/device_event_log.h"
+#include "components/onc/onc_constants.h"
 
 namespace ash {
 namespace {
@@ -171,32 +173,45 @@
       shill_error ? ShillErrorToConnectResult(*shill_error)
                   : ShillConnectResult::kSuccess;
 
+  size_t custom_apns_count = 0u;
   size_t enabled_custom_apns_count = 0u;
   const base::Value::List* custom_apn_list =
       network_metadata_store_->GetCustomApnList(network_state->guid());
   if (custom_apn_list) {
-    // TODO(b/162365553): Filter on enabled custom APNs when the revamp flag is
-    // on.
-    enabled_custom_apns_count = custom_apn_list->size();
+    custom_apns_count = custom_apn_list->size();
+
+    if (ash::features::IsApnRevampEnabled()) {
+      enabled_custom_apns_count = std::count_if(
+          custom_apn_list->begin(), custom_apn_list->end(),
+          [](const base::Value& apn) -> bool {
+            const std::string* apn_type =
+                apn.GetDict().FindString(::onc::cellular_apn::kState);
+            return *apn_type == ::onc::cellular_apn::kStateEnabled;
+          });
+    }
   }
 
   // If the connection was successful, log the number of custom APNs the network
   // has saved for it.
   if (!shill_error) {
-    // TODO(b/162365553): Log the number of enabled/disabled APNs.
-    base::UmaHistogramCounts100("Network.Ash.Cellular.Apn.CustomApns.Count",
-                                enabled_custom_apns_count);
+    base::UmaHistogramCounts100(kCustomApnsCountHistogram, custom_apns_count);
+
+    if (ash::features::IsApnRevampEnabled() && custom_apns_count > 0) {
+      base::UmaHistogramCounts100(kCustomApnsEnabledCountHistogram,
+                                  enabled_custom_apns_count);
+      base::UmaHistogramCounts100(
+          kCustomApnsDisabledCountHistogram,
+          custom_apns_count - enabled_custom_apns_count);
+    }
   }
 
-  if (enabled_custom_apns_count > 0) {
-    base::UmaHistogramEnumeration(
-        "Network.Ash.Cellular.ConnectionResult.HasEnabledCustomApns.All",
-        connect_result);
-    return;
-  }
-
+  // For pre-revamp cases, we consider all custom APNs to be enabled.
+  const bool has_enabled_custom_apns = ash::features::IsApnRevampEnabled()
+                                           ? (enabled_custom_apns_count > 0)
+                                           : (custom_apns_count > 0);
   base::UmaHistogramEnumeration(
-      "Network.Ash.Cellular.ConnectionResult.NoEnabledCustomApns.All",
+      has_enabled_custom_apns ? kConnectResultHasEnabledCustomApnsAllHistogram
+                              : kConnectResultNoEnabledCustomApnsAllHistogram,
       connect_result);
 }
 
diff --git a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h
index 3235e91d..2da60dd3 100644
--- a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h
+++ b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger.h
@@ -53,6 +53,16 @@
       "Network.Ash.Cellular.Apn.DisableCustomApn.Result";
   static constexpr char kDisableCustomApnApnTypesHistogram[] =
       "Network.Ash.Cellular.Apn.DisableCustomApn.ApnTypes";
+  static constexpr char kConnectResultHasEnabledCustomApnsAllHistogram[] =
+      "Network.Ash.Cellular.ConnectionResult.HasEnabledCustomApns.All";
+  static constexpr char kConnectResultNoEnabledCustomApnsAllHistogram[] =
+      "Network.Ash.Cellular.ConnectionResult.NoEnabledCustomApns.All";
+  static constexpr char kCustomApnsCountHistogram[] =
+      "Network.Ash.Cellular.Apn.CustomApns.Count";
+  static constexpr char kCustomApnsEnabledCountHistogram[] =
+      "Network.Ash.Cellular.Apn.CustomApns.Enabled.Count";
+  static constexpr char kCustomApnsDisabledCountHistogram[] =
+      "Network.Ash.Cellular.Apn.CustomApns.Disabled.Count";
 
   CellularNetworkMetricsLogger(
       NetworkStateHandler* network_state_handler,
diff --git a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger_unittest.cc b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger_unittest.cc
index 32d4097..2ed9d62 100644
--- a/chromeos/ash/components/network/metrics/cellular_network_metrics_logger_unittest.cc
+++ b/chromeos/ash/components/network/metrics/cellular_network_metrics_logger_unittest.cc
@@ -6,14 +6,18 @@
 
 #include <memory>
 
+#include "ash/constants/ash_features.h"
 #include "base/run_loop.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
+#include "base/values.h"
 #include "chromeos/ash/components/dbus/shill/shill_service_client.h"
 #include "chromeos/ash/components/network/metrics/connection_results.h"
 #include "chromeos/ash/components/network/network_handler_test_helper.h"
 #include "chromeos/ash/components/network/network_metadata_store.h"
 #include "chromeos/ash/components/network/network_state_handler.h"
+#include "components/onc/onc_constants.h"
 #include "components/prefs/testing_pref_service.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/cros_system_api/dbus/service_constants.h"
@@ -22,14 +26,6 @@
 
 namespace {
 
-const char kCellularCustomApnsCountHistogram[] =
-    "Network.Ash.Cellular.Apn.CustomApns.Count";
-
-const char kCellularConnectResultHasEnabledCustomApnsAllHistogram[] =
-    "Network.Ash.Cellular.ConnectionResult.HasEnabledCustomApns.All";
-const char kCellularConnectResultNoEnabledCustomApnsAllHistogram[] =
-    "Network.Ash.Cellular.ConnectionResult.NoEnabledCustomApns.All";
-
 const char kCellularGuid[] = "test_guid";
 const char kCellularServicePath[] = "/service/network";
 const char kCellularName[] = "network_name";
@@ -38,6 +34,14 @@
 const char kWifiServicePath[] = "/service/network2";
 const char kWifiName[] = "network_name2";
 
+struct ApnHistogramCounts {
+  size_t custom_apns_total_hist_count = 0u;
+  size_t enabled_custom_apns_total_hist_count = 0u;
+  size_t disabled_custom_apns_total_hist_count = 0u;
+  size_t no_enabled_custom_apns = 0u;
+  size_t has_enabled_custom_apns = 0u;
+};
+
 }  // namespace
 
 class CellularNetworkMetricsLoggerTest : public testing::Test {
@@ -98,17 +102,25 @@
     base::RunLoop().RunUntilIdle();
   }
 
-  void AssertHistogramsTotalCount(size_t custom_apns_count,
-                                  size_t no_enabled_custom_apns,
-                                  size_t has_enabled_custom_apns) {
-    histogram_tester_->ExpectTotalCount(kCellularCustomApnsCountHistogram,
-                                        custom_apns_count);
+  void AssertHistogramsTotalCount(const ApnHistogramCounts& counts) {
     histogram_tester_->ExpectTotalCount(
-        kCellularConnectResultNoEnabledCustomApnsAllHistogram,
-        no_enabled_custom_apns);
+        CellularNetworkMetricsLogger::kCustomApnsCountHistogram,
+        counts.custom_apns_total_hist_count);
     histogram_tester_->ExpectTotalCount(
-        kCellularConnectResultHasEnabledCustomApnsAllHistogram,
-        has_enabled_custom_apns);
+        CellularNetworkMetricsLogger::kCustomApnsEnabledCountHistogram,
+        counts.enabled_custom_apns_total_hist_count);
+    histogram_tester_->ExpectTotalCount(
+        CellularNetworkMetricsLogger::kCustomApnsDisabledCountHistogram,
+        counts.disabled_custom_apns_total_hist_count);
+
+    histogram_tester_->ExpectTotalCount(
+        CellularNetworkMetricsLogger::
+            kConnectResultNoEnabledCustomApnsAllHistogram,
+        counts.no_enabled_custom_apns);
+    histogram_tester_->ExpectTotalCount(
+        CellularNetworkMetricsLogger::
+            kConnectResultHasEnabledCustomApnsAllHistogram,
+        counts.has_enabled_custom_apns);
   }
 
   void AssertCustomApnsStatusBucketCount(
@@ -117,10 +129,12 @@
       ash::ShillConnectResult has_enabled_custom_apns_bucket,
       size_t has_enabled_bucket_count) {
     histogram_tester_->ExpectBucketCount(
-        kCellularConnectResultNoEnabledCustomApnsAllHistogram,
+        CellularNetworkMetricsLogger::
+            kConnectResultNoEnabledCustomApnsAllHistogram,
         no_enabled_custom_apns_bucket, no_enabled_bucket_count);
     histogram_tester_->ExpectBucketCount(
-        kCellularConnectResultHasEnabledCustomApnsAllHistogram,
+        CellularNetworkMetricsLogger::
+            kConnectResultHasEnabledCustomApnsAllHistogram,
         has_enabled_custom_apns_bucket, has_enabled_bucket_count);
   }
 
@@ -134,75 +148,163 @@
   TestingPrefServiceSimple local_state_;
 };
 
-TEST_F(CellularNetworkMetricsLoggerTest, AutoStatusTransitions) {
+TEST_F(CellularNetworkMetricsLoggerTest, AutoStatusTransitionsRevampEnabled) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitAndEnableFeature(ash::features::kApnRevamp);
   SetUpGenericCellularNetwork();
+  ApnHistogramCounts counts;
 
-  // Successful connect from disconnected to connected.
   SetShillState(kCellularServicePath, shill::kStateIdle);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/0,
-                             /*no_enabled_custom_apns=*/0,
-                             /*has_enabled_custom_apns=*/0);
+  AssertHistogramsTotalCount(counts);
   SetShillState(kCellularServicePath, shill::kStateOnline);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/1,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/0);
+  counts.custom_apns_total_hist_count++;
+  counts.no_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
   AssertCustomApnsStatusBucketCount(
       ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
       ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/0);
-  histogram_tester_->ExpectBucketCount(kCellularCustomApnsCountHistogram, 0, 1);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsCountHistogram,
+      /*sample=*/0, /*expected_count=*/1);
+
+  // Add an APN to the network.
+  base::Value::Dict apn1;
+  apn1.Set(::onc::cellular_apn::kAccessPointName, "apn1");
+  apn1.Set(::onc::cellular_apn::kState, ::onc::cellular_apn::kStateEnabled);
+  base::Value::List custom_apn_list;
+  custom_apn_list.Append(std::move(apn1));
+  NetworkHandler::Get()->network_metadata_store()->SetCustomApnList(
+      kCellularGuid, custom_apn_list.Clone());
+
+  SetShillState(kCellularServicePath, shill::kStateAssociation);
+  AssertHistogramsTotalCount(counts);
+  SetShillState(kCellularServicePath, shill::kStateOnline);
+
+  counts.custom_apns_total_hist_count++;
+  counts.has_enabled_custom_apns++;
+  counts.enabled_custom_apns_total_hist_count++;
+  counts.disabled_custom_apns_total_hist_count++;
+  AssertHistogramsTotalCount(counts);
+  AssertCustomApnsStatusBucketCount(
+      ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
+      ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/1);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsEnabledCountHistogram,
+      /*sample=*/1, /*expected_count=*/1);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsDisabledCountHistogram,
+      /*sample=*/0,
+      /*expected_count=*/1);
+
+  base::Value::Dict apn2;
+  apn2.Set(::onc::cellular_apn::kAccessPointName, "apn2");
+  apn2.Set(::onc::cellular_apn::kState, ::onc::cellular_apn::kStateDisabled);
+  custom_apn_list.Append(std::move(apn2));
+  NetworkHandler::Get()->network_metadata_store()->SetCustomApnList(
+      kCellularGuid, std::move(custom_apn_list));
+
+  SetShillState(kCellularServicePath, shill::kStateAssociation);
+  AssertHistogramsTotalCount(counts);
+  SetShillState(kCellularServicePath, shill::kStateOnline);
+
+  counts.custom_apns_total_hist_count++;
+  counts.has_enabled_custom_apns++;
+  counts.enabled_custom_apns_total_hist_count++;
+  counts.disabled_custom_apns_total_hist_count++;
+  AssertHistogramsTotalCount(counts);
+  AssertCustomApnsStatusBucketCount(
+      ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
+      ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/2);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsEnabledCountHistogram,
+      /*sample=*/1, /*expected_count=*/2);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsDisabledCountHistogram,
+      /*sample=*/1,
+      /*expected_count=*/1);
+
+  // Fail to connect from disconnecting to disconnected.
+  SetShillState(kCellularServicePath, shill::kStateAssociation);
+  AssertHistogramsTotalCount(counts);
+  SetShillState(kCellularServicePath, shill::kStateDisconnect);
+  AssertHistogramsTotalCount(counts);
+  // Fail to connect from disconnecting to disconnected.
+  SetShillError(kCellularServicePath, shill::kErrorConnectFailed);
+  AssertHistogramsTotalCount(counts);
+  SetShillState(kCellularServicePath, shill::kStateIdle);
+  counts.has_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
+  AssertCustomApnsStatusBucketCount(ShillConnectResult::kSuccess,
+                                    /*no_enabled_bucket_count=*/1,
+                                    ShillConnectResult::kErrorConnectFailed,
+                                    /*has_enabled_bucket_count=*/1);
+}
+
+TEST_F(CellularNetworkMetricsLoggerTest, AutoStatusTransitionsRevampDisabled) {
+  SetUpGenericCellularNetwork();
+  ApnHistogramCounts counts;
+  // Successful connect from disconnected to connected.
+  SetShillState(kCellularServicePath, shill::kStateIdle);
+  AssertHistogramsTotalCount(counts);
+  SetShillState(kCellularServicePath, shill::kStateOnline);
+  counts.custom_apns_total_hist_count++;
+  counts.no_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
+  AssertCustomApnsStatusBucketCount(
+      ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
+      ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/0);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsCountHistogram, 0, 1);
 
   // Add an APN to the network.
   base::Value::Dict apn;
-  apn.Set(shill::kApnProperty, "apn");
+  apn.Set(::onc::cellular_apn::kAccessPointName, "apn1");
+  apn.Set(::onc::cellular_apn::kState, ::onc::cellular_apn::kStateEnabled);
   base::Value::List custom_apn_list;
   custom_apn_list.Append(std::move(apn));
+
   NetworkHandler::Get()->network_metadata_store()->SetCustomApnList(
       kCellularGuid, std::move(custom_apn_list));
 
   // Successful connect from connecting to connected.
   SetShillState(kCellularServicePath, shill::kStateAssociation);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/1,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/0);
+  AssertHistogramsTotalCount(counts);
   SetShillState(kCellularServicePath, shill::kStateOnline);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/2,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/1);
+  counts.custom_apns_total_hist_count++;
+  counts.has_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
+
   AssertCustomApnsStatusBucketCount(
       ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
       ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/1);
-  histogram_tester_->ExpectBucketCount(kCellularCustomApnsCountHistogram, 1, 1);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsCountHistogram, 1, 1);
 
   // Successful connect from connecting to connected again.
   SetShillState(kCellularServicePath, shill::kStateAssociation);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/2,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/1);
+  AssertHistogramsTotalCount(counts);
   SetShillState(kCellularServicePath, shill::kStateOnline);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/3,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/2);
+  counts.custom_apns_total_hist_count++;
+  counts.has_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
+
   AssertCustomApnsStatusBucketCount(
       ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
       ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/2);
-  histogram_tester_->ExpectBucketCount(kCellularCustomApnsCountHistogram, 1, 2);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsCountHistogram, 1, 2);
 
   // Fail to connect from connecting to disconnecting, no valid shill error.
   SetShillState(kCellularServicePath, shill::kStateAssociation);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/3,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/2);
+  AssertHistogramsTotalCount(counts);
   SetShillState(kCellularServicePath, shill::kStateDisconnect);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/3,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/2);
+  AssertHistogramsTotalCount(counts);
 
   // Fail to connect from disconnecting to disconnected.
   SetShillError(kCellularServicePath, shill::kErrorConnectFailed);
   SetShillState(kCellularServicePath, shill::kStateIdle);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/3,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/3);
+  counts.has_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
   AssertCustomApnsStatusBucketCount(ShillConnectResult::kSuccess,
                                     /*no_enabled_bucket_count=*/1,
                                     ShillConnectResult::kErrorConnectFailed,
@@ -212,30 +314,26 @@
 TEST_F(CellularNetworkMetricsLoggerTest, OnlyCellularNetworksStatusRecorded) {
   SetUpGenericCellularNetwork();
   SetUpGenericWifiNetwork();
+  ApnHistogramCounts counts;
 
   SetShillState(kCellularServicePath, shill::kStateIdle);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/0,
-                             /*no_enabled_custom_apns=*/0,
-                             /*has_enabled_custom_apns=*/0);
+  AssertHistogramsTotalCount(counts);
 
   SetShillState(kCellularServicePath, shill::kStateOnline);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/1,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/0);
+  counts.custom_apns_total_hist_count++;
+  counts.no_enabled_custom_apns++;
+  AssertHistogramsTotalCount(counts);
   AssertCustomApnsStatusBucketCount(
       ShillConnectResult::kSuccess, /*no_enabled_bucket_count=*/1,
       ShillConnectResult::kSuccess, /*has_enabled_bucket_count=*/0);
-  histogram_tester_->ExpectBucketCount(kCellularCustomApnsCountHistogram, 0, 1);
+  histogram_tester_->ExpectBucketCount(
+      CellularNetworkMetricsLogger::kCustomApnsCountHistogram, 0, 1);
 
   SetShillState(kWifiServicePath, shill::kStateIdle);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/1,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/0);
+  AssertHistogramsTotalCount(counts);
 
   SetShillState(kWifiServicePath, shill::kStateOnline);
-  AssertHistogramsTotalCount(/*custom_apns_count=*/1,
-                             /*no_enabled_custom_apns=*/1,
-                             /*has_enabled_custom_apns=*/0);
+  AssertHistogramsTotalCount(counts);
 }
 
 }  // namespace ash
diff --git a/components/autofill/core/browser/BUILD.gn b/components/autofill/core/browser/BUILD.gn
index cd4e271..e243aa3 100644
--- a/components/autofill/core/browser/BUILD.gn
+++ b/components/autofill/core/browser/BUILD.gn
@@ -141,8 +141,6 @@
     "data_model/autofill_structured_address_utils.h",
     "data_model/autofill_wallet_usage_data.cc",
     "data_model/autofill_wallet_usage_data.h",
-    "data_model/autofillable_data.cc",
-    "data_model/autofillable_data.h",
     "data_model/birthdate.cc",
     "data_model/birthdate.h",
     "data_model/borrowed_transliterator.cc",
diff --git a/components/autofill/core/browser/contact_info_sync_util.cc b/components/autofill/core/browser/contact_info_sync_util.cc
index 696372c..3b15aa5c 100644
--- a/components/autofill/core/browser/contact_info_sync_util.cc
+++ b/components/autofill/core/browser/contact_info_sync_util.cc
@@ -134,6 +134,8 @@
       profile.modification_date().ToTimeT());
   specifics->set_language_code(profile.language_code());
   specifics->set_profile_label(profile.profile_label());
+  specifics->set_initial_creator_id(profile.initial_creator_id());
+  specifics->set_last_modifier_id(profile.last_modifier_id());
 
   EntryDataSetter s(profile);
   // Set name-related values and statuses.
@@ -201,6 +203,8 @@
       base::Time::FromTimeT(specifics.date_modified_windows_epoch_micros()));
   profile->set_language_code(specifics.language_code());
   profile->set_profile_label(specifics.profile_label());
+  profile->set_initial_creator_id(specifics.initial_creator_id());
+  profile->set_last_modifier_id(specifics.last_modifier_id());
 
   ProfileSetter s(*profile);
   // Set name-related values and statuses.
diff --git a/components/autofill/core/browser/contact_info_sync_util_unittest.cc b/components/autofill/core/browser/contact_info_sync_util_unittest.cc
index c618d4466..308184a7 100644
--- a/components/autofill/core/browser/contact_info_sync_util_unittest.cc
+++ b/components/autofill/core/browser/contact_info_sync_util_unittest.cc
@@ -15,8 +15,9 @@
 
 using sync_pb::ContactInfoSpecifics;
 
-const char kGuid[] = "00000000-0000-0000-0000-000000000001";
-const char kInvalidGuid[] = "1234";
+constexpr char kGuid[] = "00000000-0000-0000-0000-000000000001";
+constexpr char kInvalidGuid[] = "1234";
+constexpr int kNonChromeModifier = 1234;
 const auto kUseDate = base::Time::FromDoubleT(123);
 const auto kModificationDate = base::Time::FromDoubleT(456);
 
@@ -31,6 +32,9 @@
   profile.set_modification_date(kModificationDate);
   profile.set_language_code("en");
   profile.set_profile_label("profile_label");
+  profile.set_initial_creator_id(
+      AutofillProfile::kInitialCreatorOrModifierChrome);
+  profile.set_last_modifier_id(kNonChromeModifier);
 
   // Set name-related values and statuses.
   profile.SetRawInfoWithVerificationStatus(NAME_HONORIFIC_PREFIX, u"Dr.",
@@ -126,6 +130,9 @@
   specifics.set_date_modified_windows_epoch_micros(kModificationDate.ToTimeT());
   specifics.set_language_code("en");
   specifics.set_profile_label("profile_label");
+  specifics.set_initial_creator_id(
+      AutofillProfile::kInitialCreatorOrModifierChrome);
+  specifics.set_last_modifier_id(kNonChromeModifier);
 
   // Set name-related values and statuses.
   SetToken(specifics.mutable_name_honorific(), "Dr.",
diff --git a/components/autofill/core/browser/data_model/autofill_profile.cc b/components/autofill/core/browser/data_model/autofill_profile.cc
index 09fb6ab..459d9170 100644
--- a/components/autofill/core/browser/data_model/autofill_profile.cc
+++ b/components/autofill/core/browser/data_model/autofill_profile.cc
@@ -275,6 +275,8 @@
   has_converted_ = profile.has_converted();
 
   source_ = profile.source_;
+  initial_creator_id_ = profile.initial_creator_id_;
+  last_modifier_id_ = profile.last_modifier_id_;
 
   return *this;
 }
diff --git a/components/autofill/core/browser/data_model/autofill_profile.h b/components/autofill/core/browser/data_model/autofill_profile.h
index f679b1f..5499094 100644
--- a/components/autofill/core/browser/data_model/autofill_profile.h
+++ b/components/autofill/core/browser/data_model/autofill_profile.h
@@ -54,6 +54,10 @@
     kMaxValue = kAccount,
   };
 
+  // The values used to represent Autofill in the `initial_creator_id()` and
+  // `last_modifier_id()`.
+  static constexpr int kInitialCreatorOrModifierChrome = 70073;
+
   AutofillProfile(const std::string& guid,
                   const std::string& origin,
                   Source source = Source::kLocalOrSyncable);
@@ -270,6 +274,16 @@
     source_ = source;
   }
 
+  int initial_creator_id() const { return initial_creator_id_; }
+  void set_initial_creator_id(int creator_id) {
+    initial_creator_id_ = creator_id;
+  }
+
+  int last_modifier_id() const { return last_modifier_id_; }
+  void set_last_modifier_id(int modifier_id) {
+    last_modifier_id_ = modifier_id;
+  }
+
   // Checks for non-empty setting-inaccessible fields and returns all that were
   // found.
   ServerFieldTypeSet FindInaccessibleProfileValues() const;
@@ -353,6 +367,17 @@
   bool has_converted_;
 
   Source source_;
+
+  // Indicates the application that initially created the profile and the
+  // application that performed the last non-metadata modification of it.
+  // Only relevant for `source_ == kAccount` profiles, since `kLocalOrSyncable`
+  // profiles are only used within Autofill.
+  // The integer values represent a server-side enum `BillableService`, which is
+  // not duplicated in Chromium. For Autofill, the exact application that
+  // created/modified the profile is thus opaque. However, Autofill is
+  // represented by the value `kInitialCreatorOrModifierChrome`.
+  int initial_creator_id_;
+  int last_modifier_id_;
 };
 
 // So we can compare AutofillProfiles with EXPECT_EQ().
diff --git a/components/autofill/core/browser/data_model/autofillable_data.cc b/components/autofill/core/browser/data_model/autofillable_data.cc
deleted file mode 100644
index 3a3a840..0000000
--- a/components/autofill/core/browser/data_model/autofillable_data.cc
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "components/autofill/core/browser/data_model/autofillable_data.h"
-
-namespace autofill {
-
-AutofillableData::AutofillableData(const AutofillProfile* profile)
-    : base(profile) {}
-
-AutofillableData::AutofillableData(const CreditCard* card, std::u16string cvc)
-    : base(CreditCardWithCvc{card, std::move(cvc)}) {}
-
-bool AutofillableData::is_profile() const {
-  return absl::holds_alternative<const AutofillProfile*>(*this);
-}
-
-bool AutofillableData::is_credit_card() const {
-  return absl::holds_alternative<CreditCardWithCvc>(*this);
-}
-
-const AutofillProfile& AutofillableData::profile() const {
-  DCHECK(is_profile());
-  return *absl::get<const AutofillProfile*>(*this);
-}
-
-const CreditCard& AutofillableData::credit_card() const {
-  DCHECK(is_credit_card());
-  return *absl::get<CreditCardWithCvc>(*this).credit_card;
-}
-
-const std::u16string& AutofillableData::cvc() const {
-  DCHECK(is_credit_card());
-  return absl::get<CreditCardWithCvc>(*this).cvc;
-}
-
-}  // namespace autofill
diff --git a/components/autofill/core/browser/data_model/autofillable_data.h b/components/autofill/core/browser/data_model/autofillable_data.h
deleted file mode 100644
index e1bd3f8..0000000
--- a/components/autofill/core/browser/data_model/autofillable_data.h
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2021 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_AUTOFILLABLE_DATA_H_
-#define COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_AUTOFILLABLE_DATA_H_
-
-#include <string>
-
-#include "components/autofill/core/browser/data_model/autofill_profile.h"
-#include "components/autofill/core/browser/data_model/credit_card.h"
-#include "third_party/abseil-cpp/absl/types/variant.h"
-
-namespace autofill {
-
-struct CreditCardWithCvc {
-  const CreditCard* credit_card;
-  std::u16string cvc;
-};
-
-class AutofillableData
-    : public absl::variant<const AutofillProfile*, CreditCardWithCvc> {
- public:
-  explicit AutofillableData(const AutofillProfile* profile);
-  explicit AutofillableData(const CreditCard* card, std::u16string cvc = u"");
-
-  using absl::variant<const AutofillProfile*, CreditCardWithCvc>::variant;
-
-  bool is_profile() const;
-  bool is_credit_card() const;
-  const AutofillProfile& profile() const;
-  const CreditCard& credit_card() const;
-  const std::u16string& cvc() const;
-
- private:
-  using base = absl::variant<const AutofillProfile*, CreditCardWithCvc>;
-};
-
-}  // namespace autofill
-
-#endif  // COMPONENTS_AUTOFILL_CORE_BROWSER_DATA_MODEL_AUTOFILLABLE_DATA_H_
diff --git a/components/autofill/core/browser/webdata/autofill_table.cc b/components/autofill/core/browser/webdata/autofill_table.cc
index 7590369c..1dfba32 100644
--- a/components/autofill/core/browser/webdata/autofill_table.cc
+++ b/components/autofill/core/browser/webdata/autofill_table.cc
@@ -290,6 +290,8 @@
 // kDateModified = "date_modified"
 // kLanguageCode = "language_code"
 // kLabel = "label"
+constexpr base::StringPiece kInitialCreatorId = "initial_creator_id";
+constexpr base::StringPiece kLastModifierId = "last_modifier_id";
 
 constexpr base::StringPiece kContactInfoTypeTokensTable =
     "contact_info_type_tokens";
@@ -1054,6 +1056,8 @@
   s.BindInt64(index++, modification_date.ToTimeT());
   s.BindString(index++, profile.language_code());
   s.BindString(index++, profile.profile_label());
+  s.BindInt(index++, profile.initial_creator_id());
+  s.BindInt(index++, profile.last_modifier_id());
 }
 
 // Inserts `profile` into `kContactInfoTable` and `kContactInfoTypeTokensTable`.
@@ -1061,9 +1065,9 @@
                                           const AutofillProfile& profile,
                                           const base::Time& modification_date) {
   sql::Statement s;
-  InsertBuilder(
-      db, s, kContactInfoTable,
-      {kGuid, kUseCount, kUseDate, kDateModified, kLanguageCode, kLabel});
+  InsertBuilder(db, s, kContactInfoTable,
+                {kGuid, kUseCount, kUseDate, kDateModified, kLanguageCode,
+                 kLabel, kInitialCreatorId, kLastModifierId});
   BindAutofillProfileToContactInfoStatement(profile, modification_date, s);
   if (!s.Run())
     return false;
@@ -1087,7 +1091,8 @@
     const std::string& guid) {
   sql::Statement s;
   if (!SelectByGuid(db, s, kContactInfoTable,
-                    {kUseCount, kUseDate, kDateModified, kLanguageCode, kLabel},
+                    {kUseCount, kUseDate, kDateModified, kLanguageCode, kLabel,
+                     kInitialCreatorId, kLastModifierId},
                     guid)) {
     return nullptr;
   }
@@ -1099,6 +1104,8 @@
   profile->set_modification_date(base::Time::FromTimeT(s.ColumnInt64(index++)));
   profile->set_language_code(s.ColumnString(index++));
   profile->set_profile_label(s.ColumnString(index++));
+  profile->set_initial_creator_id(s.ColumnInt(index++));
+  profile->set_last_modifier_id(s.ColumnInt(index++));
 
   if (!SelectByGuid(db, s, kContactInfoTypeTokensTable,
                     {kType, kValue, kVerificationStatus}, guid)) {
@@ -1238,6 +1245,9 @@
     case 109:
       *update_compatible_version = false;
       return MigrateToVersion109AddVirtualCardUsageDataTable();
+    case 110:
+      *update_compatible_version = false;
+      return MigrateToVersion110AddInitialCreatorIdAndLastModifierId();
   }
   return true;
 }
@@ -3341,6 +3351,16 @@
                       {kLastFour, "VARCHAR"}});
 }
 
+bool AutofillTable::MigrateToVersion110AddInitialCreatorIdAndLastModifierId() {
+  sql::Transaction transaction(db_);
+  return db_->DoesTableExist(kContactInfoTable) && transaction.Begin() &&
+         AddColumnIfNotExists(db_, kContactInfoTable, kInitialCreatorId,
+                              "INTEGER") &&
+         AddColumnIfNotExists(db_, kContactInfoTable, kLastModifierId,
+                              "INTEGER") &&
+         transaction.Commit();
+}
+
 bool AutofillTable::AddFormFieldValuesTime(
     const std::vector<FormFieldData>& elements,
     std::vector<AutofillChange>* changes,
@@ -3811,7 +3831,9 @@
                                  {kUseDate, "INTEGER NOT NULL DEFAULT 0"},
                                  {kDateModified, "INTEGER NOT NULL DEFAULT 0"},
                                  {kLanguageCode, "VARCHAR"},
-                                 {kLabel, "VARCHAR"}});
+                                 {kLabel, "VARCHAR"},
+                                 {kInitialCreatorId, "INTEGER"},
+                                 {kLastModifierId, "INTEGER"}});
 }
 
 bool AutofillTable::InitContactInfoTypeTokensTable() {
diff --git a/components/autofill/core/browser/webdata/autofill_table.h b/components/autofill/core/browser/webdata/autofill_table.h
index c3508c7..008610c 100644
--- a/components/autofill/core/browser/webdata/autofill_table.h
+++ b/components/autofill/core/browser/webdata/autofill_table.h
@@ -465,6 +465,11 @@
 //                      help identifying the semantics of the profile. The user
 //                      can choose an arbitrary string in principle, but the
 //                      values '$HOME$' and '$WORK$' indicate a special meaning.
+//   initial_creator_id The application that initially created the profile.
+//                      Represented as an integer. See AutofillProfile.
+//   last_modifier_id   The application that performed the last non-metadata
+//                      modification of the profile.
+//                      Represented as an integer. See AutofillProfile.
 //
 // contact_info_type_tokens
 //                      Contains the values for all relevant ServerFieldTypes of
@@ -811,6 +816,7 @@
   bool MigrateToVersion107AddContactInfoTables();
   bool MigrateToVersion108AddCardIssuerIdColumn();
   bool MigrateToVersion109AddVirtualCardUsageDataTable();
+  bool MigrateToVersion110AddInitialCreatorIdAndLastModifierId();
 
   // Max data length saved in the table, AKA the maximum length allowed for
   // form data.
diff --git a/components/autofill/core/browser/webdata/autofill_wallet_offer_sync_bridge.cc b/components/autofill/core/browser/webdata/autofill_wallet_offer_sync_bridge.cc
index 35b0bec..60dd1da 100644
--- a/components/autofill/core/browser/webdata/autofill_wallet_offer_sync_bridge.cc
+++ b/components/autofill/core/browser/webdata/autofill_wallet_offer_sync_bridge.cc
@@ -89,12 +89,6 @@
     std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
     syncer::EntityChangeList entity_data) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  // All metadata changes have been already written, return early for an error.
-  absl::optional<syncer::ModelError> error = change_processor()->GetError();
-  if (error) {
-    return error;
-  }
-
   MergeRemoteData(std::move(entity_data));
   return absl::nullopt;
 }
diff --git a/components/autofill/core/browser/webdata/autofill_wallet_sync_bridge.cc b/components/autofill/core/browser/webdata/autofill_wallet_sync_bridge.cc
index 1ae512c..e211096 100644
--- a/components/autofill/core/browser/webdata/autofill_wallet_sync_bridge.cc
+++ b/components/autofill/core/browser/webdata/autofill_wallet_sync_bridge.cc
@@ -179,12 +179,6 @@
 absl::optional<syncer::ModelError> AutofillWalletSyncBridge::MergeSyncData(
     std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
     syncer::EntityChangeList entity_data) {
-  // All metadata changes have been already written, return early for an error.
-  absl::optional<syncer::ModelError> error = change_processor()->GetError();
-  if (error) {
-    return error;
-  }
-
   // We want to notify the metadata bridge about all changes so that the
   // metadata bridge can track changes in the data bridge and react accordingly.
   SetSyncData(entity_data, /*notify_metadata_bridge=*/true);
diff --git a/components/autofill_payments_strings.grdp b/components/autofill_payments_strings.grdp
index 49196d11..12d549d 100644
--- a/components/autofill_payments_strings.grdp
+++ b/components/autofill_payments_strings.grdp
@@ -779,17 +779,17 @@
     <message name="IDS_AUTOFILL_CREDIT_CARD_CONTINUE_BUTTON" desc="The label of the button, which would fill in the web form with card credentials." formatter_data="android_java">
       Continue
     </message>
-    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION" desc="Accessibility string read when the bottom sheet is opened. It describes the bottom sheet where a user can pick a credit card to fill into a form." formatter_data="android_java">
-      List of credit cards to be filled on touch.
+    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION" desc="Accessibility string read when the bottom sheet is opened. It describes the bottom sheet where a user can pick a payment method to fill into a form." formatter_data="android_java">
+      Payment methods available to be filled on touch. Keyboard hidden.
     </message>
-    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT" desc="Accessibility string read when the bottom sheet showing a list of the user's credit cards is opened at half height. The sheet will occupy the bottom half the screen." formatter_data="android_java">
-      List of credit cards to be filled on touch opened at half height.
+    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT" desc="Accessibility string read when the bottom sheet showing a list of the user's payment methods is opened at half height. The sheet will occupy the bottom half the screen." formatter_data="android_java">
+      Payment methods available to be filled on touch opened at half height.
     </message>
-    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT" desc="Accessibility string read when the bottom sheet showing a list of the user's credit cards is opened at full height. The sheet will occupy the entire screen." formatter_data="android_java">
-      List of credit cards to be filled on touch opened at full height.
+    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT" desc="Accessibility string read when the bottom sheet showing a list of the user's payment methods is opened at full height. The sheet will occupy the entire screen." formatter_data="android_java">
+      Payment methods available to be filled on touch opened at full height.
     </message>
-    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED" desc="Accessibility string read when the bottom sheet showing a list of the user's credit cards is closed." formatter_data="android_java">
-      List of credit cards is closed.
+    <message name="IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED" desc="Accessibility string read when the bottom sheet showing a list of the user's payment methods is closed." formatter_data="android_java">
+      List of payment methods is closed.
     </message>
   </if>
 
diff --git a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED.png.sha1 b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED.png.sha1
index 75d0e6d..0c008eee 100644
--- a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED.png.sha1
+++ b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CLOSED.png.sha1
@@ -1 +1 @@
-fd77d526608b323ee6edf337cf6b70d7be313c0a
\ No newline at end of file
+c23c11726d4454431b0a969c628e38598add4ecc
\ No newline at end of file
diff --git a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION.png.sha1 b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION.png.sha1
index 75d0e6d..0c008eee 100644
--- a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION.png.sha1
+++ b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_CONTENT_DESCRIPTION.png.sha1
@@ -1 +1 @@
-fd77d526608b323ee6edf337cf6b70d7be313c0a
\ No newline at end of file
+c23c11726d4454431b0a969c628e38598add4ecc
\ No newline at end of file
diff --git a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT.png.sha1 b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT.png.sha1
index 75d0e6d..0c008eee 100644
--- a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT.png.sha1
+++ b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_FULL_HEIGHT.png.sha1
@@ -1 +1 @@
-fd77d526608b323ee6edf337cf6b70d7be313c0a
\ No newline at end of file
+c23c11726d4454431b0a969c628e38598add4ecc
\ No newline at end of file
diff --git a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT.png.sha1 b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT.png.sha1
index d93c1d24..9c36041d 100644
--- a/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT.png.sha1
+++ b/components/autofill_payments_strings_grdp/IDS_AUTOFILL_CREDIT_CARD_BOTTOM_SHEET_HALF_HEIGHT.png.sha1
@@ -1 +1 @@
-eda36378080df1fe7902e36d921c3c07948d4c70
\ No newline at end of file
+9695c69b1ff6b8f50dbe36c7b6eb3e98e6787bf9
\ No newline at end of file
diff --git a/components/commerce/core/commerce_feature_list.cc b/components/commerce/core/commerce_feature_list.cc
index e8292ef..a70cf954 100644
--- a/components/commerce/core/commerce_feature_list.cc
+++ b/components/commerce/core/commerce_feature_list.cc
@@ -26,19 +26,28 @@
 
 namespace {
 
-typedef std::unordered_map<std::string, std::unordered_set<std::string>>
+typedef std::unordered_map<
+    const base::Feature*,
+    std::unordered_map<std::string, std::unordered_set<std::string>>>
     CountryLocaleMap;
 
 // Get a map of enabled countries to the set of allowed locales for that
-// country. Just because a locale is enabled for one country doesn't mean it can
-// or should be enabled in others. The checks using this map should convert all
-// countries and locales to lower case as they may differ depending on the API
-// used to access them.
+// country on a per-feature basis. Just because a locale is enabled for one
+// country doesn't mean it can or should be enabled in others. The checks using
+// this map should convert all countries and locales to lower case as they may
+// differ depending on the API used to access them.
 const CountryLocaleMap& GetAllowedCountryToLocaleMap() {
   // Declaring the variable "static" means it isn't recreated each time this
   // function is called. This gets around the "static initializers" problem.
-  static const base::NoDestructor<CountryLocaleMap> map{{{"us", {"en-us"}}}};
-  return *map;
+  static const base::NoDestructor<CountryLocaleMap> allowed_map([] {
+    CountryLocaleMap map;
+
+    map[&kShoppingListRegionLaunched] = {{"us", {"en-us"}}};
+    map[&kShoppingPDPMetricsRegionLaunched] = {{"us", {"en-us"}}};
+
+    return map;
+  }());
+  return *allowed_map;
 }
 
 constexpr base::FeatureParam<std::string> kRulePartnerMerchantPattern{
@@ -353,14 +362,24 @@
   return country;
 }
 
-bool IsEnabledForCountryAndLocale(std::string country, std::string locale) {
+bool IsEnabledForCountryAndLocale(const base::Feature& feature,
+                                  std::string country,
+                                  std::string locale) {
   const CountryLocaleMap& allowedCountryLocales =
       GetAllowedCountryToLocaleMap();
-  auto it = allowedCountryLocales.find(base::ToLowerASCII(country));
+
+  // First make sure the feature is in the map.
+  auto feature_it = allowedCountryLocales.find(&feature);
+  if (feature_it == allowedCountryLocales.end()) {
+    return false;
+  }
+
+  auto it = feature_it->second.find(base::ToLowerASCII(country));
 
   // If the country isn't in the map, it's not valid.
-  if (it == allowedCountryLocales.end())
+  if (it == feature_it->second.end()) {
     return false;
+  }
 
   // If the set of allowed locales contains our locale, we're considered to be
   // enabled.
diff --git a/components/commerce/core/commerce_feature_list.h b/components/commerce/core/commerce_feature_list.h
index 75bfed8a..4dd6b40c 100644
--- a/components/commerce/core/commerce_feature_list.h
+++ b/components/commerce/core/commerce_feature_list.h
@@ -362,7 +362,9 @@
 
 // Check if commerce features are allowed to run for the specified country
 // and locale.
-bool IsEnabledForCountryAndLocale(std::string country, std::string locale);
+bool IsEnabledForCountryAndLocale(const base::Feature& feature,
+                                  std::string country,
+                                  std::string locale);
 
 #if !BUILDFLAG(IS_ANDROID)
 // Get the time delay between discount fetches.
diff --git a/components/commerce/core/commerce_feature_list_unittest.cc b/components/commerce/core/commerce_feature_list_unittest.cc
index 0869dda..b9822441 100644
--- a/components/commerce/core/commerce_feature_list_unittest.cc
+++ b/components/commerce/core/commerce_feature_list_unittest.cc
@@ -155,15 +155,20 @@
 // "en-us" is an allowed locale for the US.
 TEST_F(CommerceFeatureListTest, TestEnabledForCountryAndLocale) {
   // Check the known success cases with different character cases.
-  ASSERT_TRUE(commerce::IsEnabledForCountryAndLocale("US", "en-us"));
-  ASSERT_TRUE(commerce::IsEnabledForCountryAndLocale("us", "en-US"));
+  ASSERT_TRUE(commerce::IsEnabledForCountryAndLocale(
+      commerce::kShoppingPDPMetricsRegionLaunched, "US", "en-us"));
+  ASSERT_TRUE(commerce::IsEnabledForCountryAndLocale(
+      commerce::kShoppingPDPMetricsRegionLaunched, "us", "en-US"));
 
   // Test allowed country with disallowed (fake) locale.
-  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale("us", "zz-zz"));
+  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale(
+      commerce::kShoppingPDPMetricsRegionLaunched, "us", "zz-zz"));
 
   // Test allowed locale in a disallowed (fake) country.
-  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale("zz", "en-us"));
+  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale(
+      commerce::kShoppingPDPMetricsRegionLaunched, "zz", "en-us"));
 
   // Ensure empty values don't crash.
-  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale("", ""));
+  ASSERT_FALSE(commerce::IsEnabledForCountryAndLocale(
+      commerce::kShoppingPDPMetricsRegionLaunched, "", ""));
 }
diff --git a/components/commerce/core/shopping_service.cc b/components/commerce/core/shopping_service.cc
index 4a8f756..db92181 100644
--- a/components/commerce/core/shopping_service.cc
+++ b/components/commerce/core/shopping_service.cc
@@ -385,7 +385,8 @@
                       commerce::kAddToCartProductImage.Get();
   bool region_launched =
       base::FeatureList::IsEnabled(kShoppingListRegionLaunched) &&
-      IsEnabledForCountryAndLocale(country_on_startup_, locale_on_startup_);
+      IsEnabledForCountryAndLocale(kShoppingListRegionLaunched,
+                                   country_on_startup_, locale_on_startup_);
 
   return flag_enabled || region_launched;
 }
@@ -394,7 +395,8 @@
   bool flag_enabled = base::FeatureList::IsEnabled(kShoppingPDPMetrics);
   bool region_launched =
       base::FeatureList::IsEnabled(kShoppingPDPMetricsRegionLaunched) &&
-      IsEnabledForCountryAndLocale(country_on_startup_, locale_on_startup_);
+      IsEnabledForCountryAndLocale(kShoppingPDPMetricsRegionLaunched,
+                                   country_on_startup_, locale_on_startup_);
 
   return flag_enabled || region_launched;
 }
@@ -735,7 +737,8 @@
   bool flag_enabled = base::FeatureList::IsEnabled(kShoppingList);
   bool region_launched =
       base::FeatureList::IsEnabled(kShoppingListRegionLaunched) &&
-      IsEnabledForCountryAndLocale(country_code, locale);
+      IsEnabledForCountryAndLocale(kShoppingListRegionLaunched, country_code,
+                                   locale);
 
   if (!flag_enabled && !region_launched) {
     return false;
diff --git a/components/component_updater/android/components_info_holder.cc b/components/component_updater/android/components_info_holder.cc
index f7775ee..aa1da28 100644
--- a/components/component_updater/android/components_info_holder.cc
+++ b/components/component_updater/android/components_info_holder.cc
@@ -33,7 +33,7 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   std::vector<ComponentInfo> components_info;
   for (const auto& it : components_) {
-    components_info.emplace_back(it.first, "", std::u16string(), it.second);
+    components_info.emplace_back(it.first, "", std::u16string(), it.second, "");
   }
   return components_info;
 }
diff --git a/components/component_updater/component_updater_service.cc b/components/component_updater/component_updater_service.cc
index 7fd0819..9ce87c9e 100644
--- a/components/component_updater/component_updater_service.cc
+++ b/components/component_updater/component_updater_service.cc
@@ -56,8 +56,13 @@
 ComponentInfo::ComponentInfo(const std::string& id,
                              const std::string& fingerprint,
                              const std::u16string& name,
-                             const base::Version& version)
-    : id(id), fingerprint(fingerprint), name(name), version(version) {}
+                             const base::Version& version,
+                             const std::string& cohort_id)
+    : id(id),
+      fingerprint(fingerprint),
+      name(name),
+      version(version),
+      cohort_id(cohort_id) {}
 ComponentInfo::ComponentInfo(const ComponentInfo& other) = default;
 ComponentInfo& ComponentInfo::operator=(const ComponentInfo& other) = default;
 ComponentInfo::ComponentInfo(ComponentInfo&& other) = default;
@@ -249,10 +254,12 @@
 std::vector<ComponentInfo> CrxUpdateService::GetComponents() const {
   DCHECK(thread_checker_.CalledOnValidThread());
   std::vector<ComponentInfo> result;
+  auto data = std::make_unique<update_client::PersistedData>(
+      config_->GetPrefService(), config_->GetActivityDataService());
   for (const auto& it : components_) {
-    result.push_back(ComponentInfo(it.first, it.second.fingerprint,
-                                   base::UTF8ToUTF16(it.second.name),
-                                   it.second.version));
+    result.push_back(ComponentInfo(
+        it.first, it.second.fingerprint, base::UTF8ToUTF16(it.second.name),
+        it.second.version, data->GetCohort(it.second.app_id)));
   }
   return result;
 }
diff --git a/components/component_updater/component_updater_service.h b/components/component_updater/component_updater_service.h
index 26dec74..1e4b99a 100644
--- a/components/component_updater/component_updater_service.h
+++ b/components/component_updater/component_updater_service.h
@@ -59,7 +59,8 @@
   ComponentInfo(const std::string& id,
                 const std::string& fingerprint,
                 const std::u16string& name,
-                const base::Version& version);
+                const base::Version& version,
+                const std::string& cohort_id);
   ComponentInfo(const ComponentInfo& other);
   ComponentInfo& operator=(const ComponentInfo& other);
   ComponentInfo(ComponentInfo&& other);
@@ -70,6 +71,7 @@
   std::string fingerprint;
   std::u16string name;
   base::Version version;
+  std::string cohort_id;
 };
 
 struct ComponentRegistration {
diff --git a/components/consent_auditor/fake_consent_auditor.h b/components/consent_auditor/fake_consent_auditor.h
index 11f85a8..ce5db61 100644
--- a/components/consent_auditor/fake_consent_auditor.h
+++ b/components/consent_auditor/fake_consent_auditor.h
@@ -85,9 +85,8 @@
  private:
   CoreAccountId account_id_;
 
-  // Holds specific consent information for assistant activity control consent,
-  // account password consent and autofill assistant consent. Does not (yet)
-  // contain recorded sync consent.
+  // Holds specific consent information for assistant activity control consent
+  // and account password consent. Does not (yet) contain recorded sync consent.
   std::vector<sync_pb::UserConsentSpecifics> recorded_consents_;
 
   std::vector<std::vector<int>> recorded_id_vectors_;
diff --git a/components/content_settings/core/browser/content_settings_pref.cc b/components/content_settings/core/browser/content_settings_pref.cc
index a3a34eee..5c3fdd65 100644
--- a/components/content_settings/core/browser/content_settings_pref.cc
+++ b/components/content_settings/core/browser/content_settings_pref.cc
@@ -5,6 +5,7 @@
 #include "components/content_settings/core/browser/content_settings_pref.h"
 
 #include <memory>
+#include <string>
 #include <utility>
 #include <vector>
 
@@ -56,31 +57,36 @@
   return value.is_dict();
 }
 
-// Extract a timestamp from |dictionary[kLastModifiedKey]|.
+// Extract a timestamp from `dict[key]`.
 // Will return base::Time() if no timestamp exists.
-base::Time GetLastModified(const base::Value& dictionary) {
-  return base::ValueToTime(dictionary.FindKey(kLastModifiedKey))
-      .value_or(base::Time());
+base::Time GetTimeFromDictKey(const base::Value::Dict& dict,
+                              const std::string& key) {
+  return base::ValueToTime(dict.Find(key)).value_or(base::Time());
 }
 
-// Extract a timestamp from |dictionary[kExpirationKey]|. Will return
-// base::Time() if no timestamp exists.
-base::Time GetExpiration(const base::Value& dictionary) {
-  return base::ValueToTime(dictionary.FindKey(kExpirationKey))
-      .value_or(base::Time());
+// Extract a timestamp from `dictionary[kLastModifiedKey]`.
+// Will return base::Time() if no timestamp exists.
+base::Time GetLastModified(const base::Value::Dict& dictionary) {
+  return GetTimeFromDictKey(dictionary, kLastModifiedKey);
 }
 
-// Extract a timestamp from |dictionary[kLastVisit]|.
+// Extract a timestamp from `dictionary[kExpirationKey]`.
 // Will return base::Time() if no timestamp exists.
-base::Time GetLastVisit(const base::Value& dictionary) {
-  return base::ValueToTime(dictionary.FindKey(kLastVisitKey))
-      .value_or(base::Time());
+base::Time GetExpiration(const base::Value::Dict& dictionary) {
+  return GetTimeFromDictKey(dictionary, kExpirationKey);
+}
+
+// Extract a timestamp from `dictionary[kLastVisit]`.
+// Will return base::Time() if no timestamp exists.
+base::Time GetLastVisit(const base::Value::Dict& dictionary) {
+  return GetTimeFromDictKey(dictionary, kLastVisitKey);
 }
 
 // Extract a SessionModel from |dictionary[kSessionModelKey]|. Will return
 // SessionModel::Durable if no model exists.
-content_settings::SessionModel GetSessionModel(const base::Value& dictionary) {
-  int model_int = dictionary.FindIntKey(kSessionModelKey).value_or(0);
+content_settings::SessionModel GetSessionModel(
+    const base::Value::Dict& dictionary) {
+  int model_int = dictionary.FindInt(kSessionModelKey).value_or(0);
   if ((model_int >
        static_cast<int>(content_settings::SessionModel::kMaxValue)) ||
       (model_int < 0)) {
@@ -283,15 +289,15 @@
         // multiple non-canonical patterns map to the same canonical pattern,
         // the Preferences updating logic after this loop will preserve the same
         // value in Prefs that this loop ultimately leaves in |value_map_|.
-        non_canonical_patterns_to_canonical_pattern.push_back(
-            {pattern_str, canonicalized_pattern_str});
+        non_canonical_patterns_to_canonical_pattern.emplace_back(
+            pattern_str, canonicalized_pattern_str);
       }
     }
 
     // Get settings dictionary for the current pattern string, and read
     // settings from the dictionary.
     DCHECK(i.second.is_dict());
-    const base::Value& settings_dictionary = i.second;
+    const base::Value::Dict& settings_dictionary = i.second.GetDict();
 
     // Check to see if the setting is expired or not. This may be due to a past
     // expiration date or a SessionModel of UserSession.
@@ -303,7 +309,7 @@
       continue;
     }
 
-    const base::Value* value = settings_dictionary.FindKey(kSettingKey);
+    const base::Value* value = settings_dictionary.Find(kSettingKey);
     if (value) {
       base::Time last_modified;
       base::Time last_visited;
diff --git a/components/content_settings/core/browser/content_settings_pref_unittest.cc b/components/content_settings/core/browser/content_settings_pref_unittest.cc
index b9bb76b..35e5b81 100644
--- a/components/content_settings/core/browser/content_settings_pref_unittest.cc
+++ b/components/content_settings/core/browser/content_settings_pref_unittest.cc
@@ -5,6 +5,7 @@
 #include "components/content_settings/core/browser/content_settings_pref.h"
 
 #include <memory>
+#include <string>
 #include <utility>
 
 #include "base/functional/callback_helpers.h"
@@ -66,15 +67,16 @@
 
 // Given the JSON dictionary representing the "setting" stored under a content
 // setting exception value, returns the tag.
-std::string GetTagFromDummyContentSetting(const base::Value& setting) {
-  const auto* tag = setting.FindKey(kTagKey);
-  return tag ? tag->GetString() : std::string();
+std::string GetTagFromDummyContentSetting(const base::Value::Dict& setting) {
+  const std::string* tag = setting.FindString(kTagKey);
+  return tag ? *tag : std::string();
 }
 
 // Given the JSON dictionary representing a content setting exception value,
 // returns the tag.
-std::string GetTagFromDummyContentSettingValue(const base::Value& pref_value) {
-  const auto* setting = pref_value.FindKey(kSettingKey);
+std::string GetTagFromDummyContentSettingValue(
+    const base::Value::Dict& pref_value) {
+  const base::Value::Dict* setting = pref_value.FindDict(kSettingKey);
   return setting ? GetTagFromDummyContentSetting(*setting) : std::string();
 }
 
@@ -142,7 +144,7 @@
     auto rule = rule_iterator->Next();
     patterns_to_tags_in_memory.emplace_back(
         CreatePatternString(rule.primary_pattern, rule.secondary_pattern),
-        GetTagFromDummyContentSetting(rule.value));
+        GetTagFromDummyContentSetting(rule.value.GetDict()));
   }
 
   EXPECT_THAT(patterns_to_tags_in_memory,
@@ -156,7 +158,8 @@
   ASSERT_TRUE(canonical_pref_value->is_dict());
   for (auto key_value : canonical_pref_value->DictItems()) {
     patterns_to_tags_in_prefs.emplace_back(
-        key_value.first, GetTagFromDummyContentSettingValue(key_value.second));
+        key_value.first,
+        GetTagFromDummyContentSettingValue(key_value.second.GetDict()));
   }
 
   EXPECT_THAT(patterns_to_tags_in_prefs,
@@ -209,7 +212,7 @@
     auto rule = rule_iterator->Next();
     patterns_to_tags_in_memory.emplace_back(
         CreatePatternString(rule.primary_pattern, rule.secondary_pattern),
-        GetTagFromDummyContentSetting(rule.value));
+        GetTagFromDummyContentSetting(rule.value.GetDict()));
   }
 
   EXPECT_THAT(patterns_to_tags_in_memory,
@@ -222,7 +225,8 @@
   ASSERT_TRUE(canonical_pref_value->is_dict());
   for (auto key_value : canonical_pref_value->DictItems()) {
     patterns_to_tags_in_prefs.emplace_back(
-        key_value.first, GetTagFromDummyContentSettingValue(key_value.second));
+        key_value.first,
+        GetTagFromDummyContentSettingValue(key_value.second.GetDict()));
   }
 
   EXPECT_THAT(patterns_to_tags_in_prefs,
diff --git a/components/cronet/android/BUILD.gn b/components/cronet/android/BUILD.gn
index 43caf01..63c0eb9 100644
--- a/components/cronet/android/BUILD.gn
+++ b/components/cronet/android/BUILD.gn
@@ -6,6 +6,7 @@
 import("//build/buildflag_header.gni")
 import("//build/config/android/config.gni")
 import("//build/config/android/rules.gni")
+import("//build/config/cronet/config.gni")
 import("//build/config/zip.gni")
 import("//build/util/lastchange.gni")
 import("//build/util/process_version.gni")
@@ -19,6 +20,7 @@
 import("//url/features.gni")
 
 _templates_dir = "$target_gen_dir/templates"
+_gn_path = "//buildtools/linux64/gn"
 
 declare_args() {
   # In integrated mode, CronetEngine will use the shared network task runner by
@@ -1644,6 +1646,29 @@
     ]
   }
 
+  action("dependencies_checks") {
+    script = "//components/cronet/tools/check_cronet_dependencies.py"
+    inputs = [ "//components/cronet/android/dependencies.txt" ]
+    sources = [
+      "$_gn_path",
+      "//components/cronet/tools/check_cronet_dependencies.sh",
+    ]
+    outputs = [ "$target_gen_dir/$target_name.stamp" ]
+    args = [
+      "--new_dependencies_script",
+      rebase_path("//components/cronet/tools/check_cronet_dependencies.sh",
+                  root_build_dir),
+      "--old_dependencies",
+      rebase_path(inputs[0], root_build_dir),
+      "--stamp",
+      rebase_path(outputs[0], root_build_dir),
+    ]
+    if (is_cronet_build) {
+      args += [ "--is_cronet_build" ]
+    }
+    deps = [ ":cronet" ]
+  }
+
   group("cronet_package_android") {
     # Marked as testonly as it contains test-only targets too.
     testonly = true
@@ -1662,6 +1687,7 @@
         ":cronet_package_copy_resources",
         ":cronet_sizes",
         ":cronet_test_package",
+        ":dependencies_checks",
         ":generate_javadoc",
         ":generate_licenses",
         ":jar_cronet_api_source",
diff --git a/components/cronet/android/dependencies.txt b/components/cronet/android/dependencies.txt
new file mode 100644
index 0000000..96488e45
--- /dev/null
+++ b/components/cronet/android/dependencies.txt
@@ -0,0 +1,71 @@
+//base
+//base/allocator
+//base/allocator/partition_allocator
+//base/android/jni_generator
+//base/numerics
+//base/third_party/double_conversion
+//base/third_party/dynamic_annotations
+//base/third_party/symbolize
+//base/third_party/xdg_mime
+//base/third_party/xdg_user_dirs
+//base/tracing/protos
+//build
+//build/android
+//build/android/bytecode
+//build/config
+//build/config/clang
+//build/config/compiler
+//build/config/sanitizers
+//build/rust
+//buildtools/third_party/libc++
+//buildtools/third_party/libc++abi
+//buildtools/third_party/libunwind
+//build/win
+//clank/third_party/google3
+//components/cronet
+//components/cronet/android
+//components/cronet/native
+//components/grpc_support
+//components/metrics
+//components/nacl/common
+//components/prefs
+//components/prefs/android
+//crypto
+//net
+//net/android
+//net/base/registry_controlled_domains
+//net/cert
+//net/data/ssl/chrome_root_store
+//net/dns
+//net/dns/public
+//net/http
+//net/third_party/quiche
+//net/tools/huffman_trie
+//net/tools/root_store_tool
+//net/tools/transport_security_state_generator
+//net/traffic_annotation
+//third_party/abseil-cpp
+//third_party/android_deps
+//third_party/android_ndk
+//third_party/android_sdk
+//third_party/androidx
+//third_party/ashmem
+//third_party/boringssl
+//third_party/boringssl/src/third_party/fiat
+//third_party/brotli
+//third_party/ced
+//third_party/icu
+//third_party/ijar
+//third_party/jdk
+//third_party/jsoncpp
+//third_party/libevent
+//third_party/metrics_proto
+//third_party/modp_b64
+//third_party/perfetto
+//third_party/perfetto/protos/third_party/chromium
+//third_party/protobuf
+//third_party/zlib
+//tools/android/errorprone_plugin
+//tools/grit
+//tools/gritsettings
+//url
diff --git a/components/cronet/tools/check_cronet_dependencies.py b/components/cronet/tools/check_cronet_dependencies.py
new file mode 100755
index 0000000..1bab798
--- /dev/null
+++ b/components/cronet/tools/check_cronet_dependencies.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""check_cronet_dependencies.py - Keep track of Cronet's dependencies."""
+
+import argparse
+import os
+import subprocess
+import sys
+
+REPOSITORY_ROOT = os.path.abspath(
+    os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
+
+sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp'))
+from util import build_utils  # pylint: disable=wrong-import-position
+
+
+def normalize_third_party_dep(dependency):
+  third_party_str = 'third_party/'
+  if third_party_str not in dependency:
+    raise ValueError('Dependency is not third_party dependency')
+  root_end_index = dependency.rfind(third_party_str) + len(third_party_str)
+  dependency_name_end_index = dependency.find("/", root_end_index)
+  if dependency_name_end_index == -1:
+    return dependency
+  return dependency[:dependency_name_end_index]
+
+
+def dedup_third_party_deps_internal(dependencies, root_deps=None):
+  if root_deps is None:
+    root_deps = set()
+  deduped_deps = []
+  for dependency in dependencies:
+    # crbug.com(1406537): `gn desc deps` can spit out non-deps stuff if it finds unknown
+    # GN args
+    if not dependency or not dependency.startswith('//'):
+      continue
+    if dependency[-1] == '/':
+      raise ValueError('Dependencies must not have a trailing forward slash')
+
+    if 'third_party/' not in dependency:
+      # We don't apply any filtering to non third_party deps.
+      deduped_deps.append(dependency)
+      continue
+
+    # Take the last occurrence to consider //third_party/foo and
+    # //third_party/foo/third_party/bar as two distinct dependencies.
+    third_party_dep_segments = dependency.split('third_party/')
+    third_party_dep = third_party_dep_segments[-1]
+    if '/' not in third_party_dep:
+      # Root dependencies are always unique.
+      # Note: We append the amount of splits to differentiate between
+      # //third_party/foo and //third_party/bar/third_party/foo.
+      root_dep = str(len(third_party_dep_segments)) + third_party_dep
+      root_deps.add(root_dep)
+      deduped_deps.append(normalize_third_party_dep(dependency))
+    else:
+      third_party_dep_root = (str(len(third_party_dep_segments)) +
+                              third_party_dep.split('/')[0])
+      if third_party_dep_root not in root_deps:
+        root_deps.add(third_party_dep_root)
+        deduped_deps.append(normalize_third_party_dep(dependency))
+  return (deduped_deps, root_deps)
+
+
+def dedup_third_party_deps(old_dependencies, new_dependencies):
+  """Maintains only a single target for each third_party dependency."""
+  (_, root_deps) = dedup_third_party_deps_internal(old_dependencies)
+  (deduped_deps, _) = dedup_third_party_deps_internal(new_dependencies,
+                                                      root_deps=root_deps)
+  return '\n'.join(deduped_deps)
+
+
+def main():
+  parser = argparse.ArgumentParser(
+      prog='Check cronet dependencies',
+      description=
+      "Checks whether Cronet's current dependencies match the known ones.")
+  parser.add_argument(
+      '--new_dependencies_script',
+      type=str,
+      help='Relative path to the script that outputs new dependencies',
+      required=True,
+  )
+  parser.add_argument(
+      '--old_dependencies',
+      type=str,
+      help='Relative path to file that contains the old dependencies',
+      required=True,
+  )
+  parser.add_argument(
+      '--stamp',
+      type=str,
+      help='Path to touch on success',
+  )
+  parser.add_argument('--is_cronet_build',
+                      action='store_true',
+                      help='Whether this is an official Cronet build or not')
+  parser.set_defaults(is_cronet_build=False)
+  args = parser.parse_args()
+
+  if not args.is_cronet_build:
+    build_utils.Touch(args.stamp)
+    return
+
+  new_dependencies = subprocess.check_output(
+      [args.new_dependencies_script, args.old_dependencies]).decode('utf-8')
+  with open(args.old_dependencies, 'r') as f:
+    new_dependencies = dedup_third_party_deps(f.read().splitlines(),
+                                              new_dependencies.splitlines())
+  if new_dependencies != '':
+    print('New dependencies detected:')
+    print(new_dependencies)
+    print('Please update: ' + args.old_dependencies)
+    sys.exit(-1)
+  else:
+    build_utils.Touch(args.stamp)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/components/cronet/tools/check_cronet_dependencies.sh b/components/cronet/tools/check_cronet_dependencies.sh
new file mode 100755
index 0000000..fd39d9d
--- /dev/null
+++ b/components/cronet/tools/check_cronet_dependencies.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# check_cronet_dependencies.sh - Prints new Cronet's deps to stdout.
+#
+# Arguments:
+#   $1: relative path to file containing old/known Cronet's dependencies.
+#
+# Output:
+#   Exit code 0: deduped list of new Cronet's dependencies to stdout.
+#   Otherwise: something went wrong.
+
+set -e
+
+OLD_DEPS=$(realpath "$1")
+GN_PATH=$(realpath "../../buildtools/linux64/gn")
+TMP_DIR=$(mktemp -d -p ./)
+trap "rm -rf ${TMP_DIR}" EXIT
+
+# gn desc *might* modify the target build dir. To be sure that doesn't affect
+# the current build, create a new build dir with the same gn args and run gn
+# desc there instead.
+GN_ARGS=$(cat args.gn | tr '\n' ' ' | sed "s/ = /=/g")
+"$GN_PATH" gen "$TMP_DIR" --args="$GN_ARGS" &>/dev/null
+
+(
+cd $TMP_DIR
+NEW_DEPS=$(mktemp -p ./)
+
+# Create new dependencies list and drop duplicates.
+"$GN_PATH" desc ./ //components/cronet/android:cronet deps --all |
+  awk -F ':' '{new_deps[$1]=1}; END{for(dep in new_deps) print dep}' |
+  sort > "$NEW_DEPS"
+
+# Filter out dependencies that already existed. I.e., print new dependencies to
+# stdout.
+awk 'NR==FNR {old_deps[$0]=1; next} {if(!old_deps[$0]) print $0}' \
+  "$OLD_DEPS" "$NEW_DEPS" |
+  sort
+)
diff --git a/components/metrics/component_metrics_provider.cc b/components/metrics/component_metrics_provider.cc
index 6109431..bf7b04a 100644
--- a/components/metrics/component_metrics_provider.cc
+++ b/components/metrics/component_metrics_provider.cc
@@ -5,6 +5,7 @@
 #include "components/metrics/component_metrics_provider.h"
 
 #include "base/containers/fixed_flat_map.h"
+#include "base/hash/hash.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_piece.h"
 #include "components/component_updater/component_updater_service.h"
@@ -180,6 +181,8 @@
     proto->set_component_id(id);
     proto->set_version(component.version.GetString());
     proto->set_omaha_fingerprint(Trim(component.fingerprint));
+    proto->set_cohort_hash(base::PersistentHash(
+        component.cohort_id.substr(0, component.cohort_id.find_last_of(":"))));
   }
 }
 
diff --git a/components/metrics/component_metrics_provider_unittest.cc b/components/metrics/component_metrics_provider_unittest.cc
index 8b660f2..01d9e5f1c 100644
--- a/components/metrics/component_metrics_provider_unittest.cc
+++ b/components/metrics/component_metrics_provider_unittest.cc
@@ -4,6 +4,7 @@
 
 #include "components/metrics/component_metrics_provider.h"
 
+#include "base/hash/hash.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/version.h"
 #include "components/component_updater/component_updater_service.h"
@@ -47,15 +48,15 @@
       ComponentInfo(
           "hfnkpimlhhgieaddgfemjhofmfblmnib",
           "1.0846414bf2025bbc067b6fa5b61b16eda2269d8712b8fec0973b4c71fdc65ca0",
-          u"name1", base::Version("1.2.3.4")),
+          u"name1", base::Version("1.2.3.4"), ""),
       ComponentInfo(
           "oimompecagnajdejgnnjijobebaeigek",
           "1.adc9207a4a88ee98bf9ddf0330f35818386f1adc006bc8eee94dc59d43c0f5d6",
-          u"name2", base::Version("5.6.7.8")),
+          u"name2", base::Version("5.6.7.8"), "1:1bcZ:55@0.33,67@0.5"),
       ComponentInfo(
           "thiscomponentfilteredfromresults",
           "1.b5268dc93e08d68d0be26bd8fbbb15c7b7f805cc06b4abd9d49381bc178e78cf",
-          u"name3", base::Version("9.9.9.9"))};
+          u"name3", base::Version("9.9.9.9"), "")};
 
   ComponentMetricsProvider component_provider(
       std::make_unique<TestComponentMetricsProviderDelegate>(components));
@@ -72,6 +73,10 @@
   EXPECT_EQ("5.6.7.8", system_profile.chrome_component(1).version());
   EXPECT_EQ(2915639418u,
             system_profile.chrome_component(1).omaha_fingerprint());
+  EXPECT_EQ(system_profile.chrome_component(0).cohort_hash(),
+            base::PersistentHash(""));
+  EXPECT_EQ(system_profile.chrome_component(1).cohort_hash(),
+            base::PersistentHash("1:1bcZ"));
 }
 
 }  // namespace metrics
diff --git a/components/policy/core/browser/webui/policy_status_provider.cc b/components/policy/core/browser/webui/policy_status_provider.cc
index f39006a..aab0f5b 100644
--- a/components/policy/core/browser/webui/policy_status_provider.cc
+++ b/components/policy/core/browser/webui/policy_status_provider.cc
@@ -27,6 +27,7 @@
 namespace policy {
 
 const char kPolicyDescriptionKey[] = "policyDescriptionKey";
+const char kFlexOrgWarningKey[] = "flexOrgWarning";
 
 const char kAssetIdKey[] = "assetId";
 const char kLocationKey[] = "location";
diff --git a/components/policy/core/browser/webui/policy_status_provider.h b/components/policy/core/browser/webui/policy_status_provider.h
index 7e11479..a19a7775 100644
--- a/components/policy/core/browser/webui/policy_status_provider.h
+++ b/components/policy/core/browser/webui/policy_status_provider.h
@@ -29,6 +29,7 @@
 class CloudPolicyStore;
 
 POLICY_EXPORT extern const char kPolicyDescriptionKey[];
+POLICY_EXPORT extern const char kFlexOrgWarningKey[];
 
 // The following constants identify top-level keys in the dictionary returned by
 // PolicyStatusProvider.
diff --git a/components/policy/resources/webui/status_box.html b/components/policy/resources/webui/status_box.html
index 0e54524..1a06180 100644
--- a/components/policy/resources/webui/status_box.html
+++ b/components/policy/resources/webui/status_box.html
@@ -26,6 +26,16 @@
 div.status-entry:last-child {
   margin-bottom: 0;
 }
+
+a {
+  color: rgb(26, 115, 232);
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.warning {
+  max-width: 312px;
+}
 </style>
 <fieldset>
   <legend class="legend"></legend>
@@ -121,4 +131,8 @@
     <div class="label">$i18n{labelError}:</div>
     <div class="error"></div>
   </div>
+  <div class="status-entry" hidden>
+    <div class="label">$i18n{labelWarning}:</div>
+    <div class="warning"></div>
+  </div>
 </fieldset>
diff --git a/components/policy/resources/webui/status_box.js b/components/policy/resources/webui/status_box.js
index 7ab352d..64f0b7f0 100644
--- a/components/policy/resources/webui/status_box.js
+++ b/components/policy/resources/webui/status_box.js
@@ -6,6 +6,7 @@
 
 import {CustomElement} from 'chrome://resources/js/custom_element.js';
 import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
+import {sanitizeInnerHtml} from 'chrome://resources/js/parse_html_subset.js';
 
 import {getTemplate} from './status_box.html.js';
 
@@ -32,6 +33,23 @@
   }
 
   /**
+   * Sets the text of a particular named label element in the status box
+   * and updates the visibility if needed.
+   * @param {string} labelName The name of the label element that is being
+   *     updated.
+   * @param {string} labelValue The new text content for the label.
+   * @param {boolean=} needsToBeShown True if we want to show the label
+   *     False otherwise.
+   */
+  setLabelInnerHTMLAndShow_(labelName, labelValue, needsToBeShown = true) {
+    const labelElement = this.shadowRoot.querySelector(labelName);
+    labelElement.innerHTML = sanitizeInnerHtml(` ${labelValue}`);
+    if (needsToBeShown) {
+      labelElement.parentElement.hidden = false;
+    }
+  }
+
+  /**
    * Populate the box with the given cloud policy status.
    * @param {string} scope The policy scope, either "device", "machine",
    *     "user", or "updater".
@@ -43,7 +61,11 @@
     // Set appropriate box legend based on status key
     this.shadowRoot.querySelector('.legend').textContent =
         loadTimeData.getString(status.policyDescriptionKey);
-
+    if (status.flexOrgWarning) {
+      this.setLabelInnerHTMLAndShow_(
+          '.warning', loadTimeData.getString('statusFlexOrgNoPolicy'), true);
+      return;
+    }
     if (scope === 'device') {
       // Populate the device naming information.
       // Populate the asset identifier.
diff --git a/components/policy_strings.grdp b/components/policy_strings.grdp
index f4c2dc75..358e239a 100644
--- a/components/policy_strings.grdp
+++ b/components/policy_strings.grdp
@@ -419,6 +419,9 @@
   <message name="IDS_POLICY_STATUS_ERROR_MANAGED_NO_POLICY" desc="Message to display when a managed device does not have a policy loaded.">
     Managed user or device has no policy loaded.
   </message>
+  <message name="IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY" desc="Message to display when a managed account does not have a policy loaded because the account if from a flex org.">
+    No user policies applied. To add user policies, your organization must <ph name="LINK_BEGIN">&lt;a target="_blank" href="https://support.google.com/chrome/a/answer/9122284"&gt;</ph>verify your domain<ph name="LINK_END">&lt;/a&gt;</ph>.
+  </message>
   <message name="IDS_POLICY_LABEL_MACHINE_ENROLLMENT_DOMAIN" desc="Label for the enrollment domain in the machine policy status box.">
     Enrollment domain:
   </message>
diff --git a/components/policy_strings_grdp/IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY.png.sha1 b/components/policy_strings_grdp/IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY.png.sha1
new file mode 100644
index 0000000..859dcd4
--- /dev/null
+++ b/components/policy_strings_grdp/IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY.png.sha1
@@ -0,0 +1 @@
+3b4959df04806b7a376943ec365a92cf4c56ccc9
\ No newline at end of file
diff --git a/components/safe_browsing/android/remote_database_manager.cc b/components/safe_browsing/android/remote_database_manager.cc
index 113e52f..1234807 100644
--- a/components/safe_browsing/android/remote_database_manager.cc
+++ b/components/safe_browsing/android/remote_database_manager.cc
@@ -78,8 +78,9 @@
     SBThreatType matched_threat_type,
     const ThreatMetadata& metadata) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  if (!req)
+  if (!req) {
     return;  // Previously canceled
+  }
   req->OnRequestDone(matched_threat_type, metadata);
 }
 
@@ -197,13 +198,15 @@
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
   DCHECK(!threat_types.empty());
   DCHECK(SBThreatTypeSetIsValidForCheckBrowseUrl(threat_types));
-  if (!enabled_)
+  if (!enabled_) {
     return true;
+  }
 
   bool can_check_url = CanCheckUrl(url);
   UMA_HISTOGRAM_BOOLEAN("SB2.RemoteCall.CanCheckUrl", can_check_url);
-  if (!can_check_url)
+  if (!can_check_url) {
     return true;  // Safe, continue right away.
+  }
 
   std::unique_ptr<ClientRequest> req(new ClientRequest(client, this, url));
 
@@ -241,11 +244,13 @@
 }
 
 bool RemoteSafeBrowsingDatabaseManager::CheckUrlForHighConfidenceAllowlist(
-    const GURL& url) {
+    const GURL& url,
+    const std::string& metric_variation) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
 
-  if (!enabled_ || !CanCheckUrl(url))
+  if (!enabled_ || !CanCheckUrl(url)) {
     return false;
+  }
 
   if (base::FeatureList::IsEnabled(kComponentUpdaterAndroidProtegoAllowlist)) {
     // SafeBrowsingComponentUpdaterAndroidProtegoAllowlist is enabled.
@@ -265,8 +270,9 @@
     Client* client) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
 
-  if (!enabled_ || !CanCheckUrl(url))
+  if (!enabled_ || !CanCheckUrl(url)) {
     return true;
+  }
 
   std::unique_ptr<ClientRequest> req(new ClientRequest(client, this, url));
 
diff --git a/components/safe_browsing/android/remote_database_manager.h b/components/safe_browsing/android/remote_database_manager.h
index 6293e01..0c2f3087 100644
--- a/components/safe_browsing/android/remote_database_manager.h
+++ b/components/safe_browsing/android/remote_database_manager.h
@@ -53,7 +53,9 @@
                          Client* client) override;
   AsyncMatch CheckCsdAllowlistUrl(const GURL& url, Client* client) override;
   bool CheckResourceUrl(const GURL& url, Client* client) override;
-  bool CheckUrlForHighConfidenceAllowlist(const GURL& url) override;
+  bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& url,
+      const std::string& metric_variation) override;
   bool CheckUrlForSubresourceFilter(const GURL& url, Client* client) override;
   bool MatchDownloadAllowlistUrl(const GURL& url) override;
   bool MatchMalwareIP(const std::string& ip_address) override;
diff --git a/components/safe_browsing/core/browser/BUILD.gn b/components/safe_browsing/core/browser/BUILD.gn
index f4acb9ed0..5383e936 100644
--- a/components/safe_browsing/core/browser/BUILD.gn
+++ b/components/safe_browsing/core/browser/BUILD.gn
@@ -111,9 +111,11 @@
     "//components/prefs",
     "//components/safe_browsing/core/browser:sync_observer",
     "//components/safe_browsing/core/browser/db:v4_protocol_manager_util",
+    "//components/safe_browsing/core/browser/hashprefix_realtime:hash_realtime_cache",
     "//components/safe_browsing/core/common",
     "//components/safe_browsing/core/common/proto:csd_proto",
     "//components/safe_browsing/core/common/proto:realtimeapi_proto",
+    "//components/safe_browsing/core/common/proto:safebrowsingv5_alpha1_proto",
     "//url",
   ]
 }
@@ -131,6 +133,7 @@
     "//components/safe_browsing/core/common:safe_browsing_prefs",
     "//components/safe_browsing/core/common/proto:csd_proto",
     "//components/safe_browsing/core/common/proto:realtimeapi_proto",
+    "//components/safe_browsing/core/common/proto:safebrowsingv5_alpha1_proto",
     "//components/sync_preferences:test_support",
     "//testing/gtest",
   ]
diff --git a/components/safe_browsing/core/browser/db/database_manager.h b/components/safe_browsing/core/browser/db/database_manager.h
index ea94e97b..23196a8 100644
--- a/components/safe_browsing/core/browser/db/database_manager.h
+++ b/components/safe_browsing/core/browser/db/database_manager.h
@@ -184,8 +184,11 @@
   // matches the allowlist, and is false if it does not. The high confidence
   // allowlist is a list of full hashes of URLs that are expected to be safe so
   // in the case of a match on this list, the realtime full URL Safe Browsing
-  // lookup isn't performed.
-  virtual bool CheckUrlForHighConfidenceAllowlist(const GURL& url) = 0;
+  // lookup isn't performed. |metric_variation| is used for logging purposes to
+  // specify the consumer mechanism performing this check in histograms.
+  virtual bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& url,
+      const std::string& metric_variation) = 0;
 
   //
   // Match*(): Methods to synchronously check if various types are safe.
diff --git a/components/safe_browsing/core/browser/db/test_database_manager.cc b/components/safe_browsing/core/browser/db/test_database_manager.cc
index ee0f2a2c..03cd293c 100644
--- a/components/safe_browsing/core/browser/db/test_database_manager.cc
+++ b/components/safe_browsing/core/browser/db/test_database_manager.cc
@@ -69,7 +69,8 @@
 }
 
 bool TestSafeBrowsingDatabaseManager::CheckUrlForHighConfidenceAllowlist(
-    const GURL& url) {
+    const GURL& url,
+    const std::string& metric_variation) {
   NOTIMPLEMENTED();
   return false;
 }
diff --git a/components/safe_browsing/core/browser/db/test_database_manager.h b/components/safe_browsing/core/browser/db/test_database_manager.h
index 3a6bd04..b8b8acd 100644
--- a/components/safe_browsing/core/browser/db/test_database_manager.h
+++ b/components/safe_browsing/core/browser/db/test_database_manager.h
@@ -41,7 +41,9 @@
   bool CheckExtensionIDs(const std::set<std::string>& extension_ids,
                          Client* client) override;
   bool CheckResourceUrl(const GURL& url, Client* client) override;
-  bool CheckUrlForHighConfidenceAllowlist(const GURL& url) override;
+  bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& url,
+      const std::string& metric_variation) override;
   bool CheckUrlForSubresourceFilter(const GURL& url, Client* client) override;
   bool MatchDownloadAllowlistUrl(const GURL& url) override;
   bool MatchMalwareIP(const std::string& ip_address) override;
diff --git a/components/safe_browsing/core/browser/db/v4_local_database_manager.cc b/components/safe_browsing/core/browser/db/v4_local_database_manager.cc
index 53511cf9..aefdbec 100644
--- a/components/safe_browsing/core/browser/db/v4_local_database_manager.cc
+++ b/components/safe_browsing/core/browser/db/v4_local_database_manager.cc
@@ -21,6 +21,7 @@
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/ranges/algorithm.h"
+#include "base/strings/strcat.h"
 #include "base/strings/string_piece.h"
 #include "base/strings/string_tokenizer.h"
 #include "base/task/sequenced_task_runner.h"
@@ -222,8 +223,9 @@
 };
 
 void RecordTimeSinceLastUpdateHistograms(const base::Time& last_response_time) {
-  if (last_response_time.is_null())
+  if (last_response_time.is_null()) {
     return;
+  }
 
   base::TimeDelta time_since_update = base::Time::Now() - last_response_time;
   UMA_HISTOGRAM_LONG_TIMES_100(
@@ -231,6 +233,19 @@
       time_since_update);
 }
 
+void RecordCheckUrlForHighConfidenceAllowlistBoolean(
+    const std::string& metric_name,
+    const std::string& metric_variation,
+    bool value) {
+  auto histogram_name =
+      base::StrCat({"SafeBrowsing.", metric_variation, ".", metric_name});
+  DCHECK(histogram_name == "SafeBrowsing.RT.AllStoresAvailable" ||
+         histogram_name == "SafeBrowsing.HPRT.AllStoresAvailable" ||
+         histogram_name == "SafeBrowsing.RT.AllowlistSizeTooSmall" ||
+         histogram_name == "SafeBrowsing.HPRT.AllowlistSizeTooSmall");
+  base::UmaHistogramBoolean(histogram_name, value);
+}
+
 // Renames the file at |old_path| to |new_path|. Executes on a task runner.
 void RenameStoreFile(const base::FilePath& old_path,
                      const base::FilePath& new_path) {
@@ -498,20 +513,21 @@
 }
 
 bool V4LocalDatabaseManager::CheckUrlForHighConfidenceAllowlist(
-    const GURL& url) {
+    const GURL& url,
+    const std::string& metric_variation) {
   DCHECK(io_task_runner()->RunsTasksInCurrentSequence());
 
   StoresToCheck stores_to_check({GetUrlHighConfidenceAllowlistId()});
   bool all_stores_available = AreAllStoresAvailableNow(stores_to_check);
-  UMA_HISTOGRAM_BOOLEAN("SafeBrowsing.RT.AllStoresAvailable",
-                        all_stores_available);
+  RecordCheckUrlForHighConfidenceAllowlistBoolean(
+      "AllStoresAvailable", metric_variation, all_stores_available);
   bool is_artificial_prefix_empty =
       artificially_marked_store_and_hash_prefixes_.empty();
   bool is_allowlist_too_small =
       IsStoreTooSmall(GetUrlHighConfidenceAllowlistId(), kBytesPerFullHashEntry,
                       kHighConfidenceAllowlistMinimumEntryCount);
-  UMA_HISTOGRAM_BOOLEAN("SafeBrowsing.RT.AllowlistSizeTooSmall",
-                        is_allowlist_too_small);
+  RecordCheckUrlForHighConfidenceAllowlistBoolean(
+      "AllowlistSizeTooSmall", metric_variation, is_allowlist_too_small);
   if (!enabled_ || (is_allowlist_too_small && is_artificial_prefix_empty) ||
       !CanCheckUrl(url) ||
       (!all_stores_available && is_artificial_prefix_empty)) {
@@ -643,8 +659,9 @@
   // Delete the V4Database. Any pending writes to disk are completed.
   // This operation happens on the task_runner on which v4_database_ operates
   // and doesn't block the IO thread.
-  if (v4_database_)
+  if (v4_database_) {
     v4_database_->StopOnIO();
+  }
   v4_database_.reset();
 
   // Delete the V4UpdateProtocolManager.
diff --git a/components/safe_browsing/core/browser/db/v4_local_database_manager.h b/components/safe_browsing/core/browser/db/v4_local_database_manager.h
index f5ae88c..54e1367 100644
--- a/components/safe_browsing/core/browser/db/v4_local_database_manager.h
+++ b/components/safe_browsing/core/browser/db/v4_local_database_manager.h
@@ -77,7 +77,9 @@
   bool CheckExtensionIDs(const std::set<FullHashStr>& extension_ids,
                          Client* client) override;
   bool CheckResourceUrl(const GURL& url, Client* client) override;
-  bool CheckUrlForHighConfidenceAllowlist(const GURL& url) override;
+  bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& url,
+      const std::string& metric_variation) override;
   bool CheckUrlForSubresourceFilter(const GURL& url, Client* client) override;
   bool MatchDownloadAllowlistUrl(const GURL& url) override;
   bool MatchMalwareIP(const std::string& ip_address) override;
diff --git a/components/safe_browsing/core/browser/db/v4_local_database_manager_unittest.cc b/components/safe_browsing/core/browser/db/v4_local_database_manager_unittest.cc
index ac47ad8..31eadab 100644
--- a/components/safe_browsing/core/browser/db/v4_local_database_manager_unittest.cc
+++ b/components/safe_browsing/core/browser/db/v4_local_database_manager_unittest.cc
@@ -131,8 +131,9 @@
       StoreAndHashPrefixes* store_and_hash_prefixes) override {
     store_and_hash_prefixes->clear();
     for (const StoreAndHashPrefix& stored_sahp : store_and_hash_prefixes_) {
-      if (stores_to_check.count(stored_sahp.list_id) == 0)
+      if (stores_to_check.count(stored_sahp.list_id) == 0) {
         continue;
+      }
       const PrefixSize& prefix_size = stored_sahp.hash_prefix.size();
       if (!full_hash.compare(0, prefix_size, stored_sahp.hash_prefix)) {
         store_and_hash_prefixes->push_back(stored_sahp);
@@ -461,6 +462,19 @@
     WaitForTasksOnTaskRunner();
   }
 
+  void ValidateHighConfidenceAllowlistHistograms(
+      bool expected_all_stores_available_sample,
+      bool expected_allowlist_too_small_sample) {
+    histogram_tester_.ExpectUniqueSample(
+        "SafeBrowsing.HPRT.AllStoresAvailable",
+        /*sample=*/expected_all_stores_available_sample,
+        /*expected_bucket_count=*/1);
+    histogram_tester_.ExpectUniqueSample(
+        "SafeBrowsing.HPRT.AllowlistSizeTooSmall",
+        /*sample=*/expected_allowlist_too_small_sample,
+        /*expected_bucket_count=*/1);
+  }
+
   const SBThreatTypeSet usual_threat_types_ = CreateSBThreatTypeSet(
       {SB_THREAT_TYPE_URL_PHISHING, SB_THREAT_TYPE_URL_MALWARE,
        SB_THREAT_TYPE_URL_UNWANTED});
@@ -472,6 +486,7 @@
   ExtendedReportingLevelCallback erl_callback_;
   scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
   base::test::TaskEnvironment task_environment_;
+  base::HistogramTester histogram_tester_;
   scoped_refptr<V4LocalDatabaseManager> v4_local_database_manager_;
 };
 
@@ -685,7 +700,11 @@
   // Confirm there is no match and the full hash check is not performed.
   const GURL url_check("https://" + url_safe_no_scheme);
   EXPECT_FALSE(v4_local_database_manager_->CheckUrlForHighConfidenceAllowlist(
-      url_check));
+      url_check, "HPRT"));
+  ValidateHighConfidenceAllowlistHistograms(
+      /*expected_all_stores_available_sample=*/true,
+      /*expected_allowlist_too_small_sample=*/false);
+
   WaitForTasksOnTaskRunner();
   EXPECT_FALSE(FakeV4LocalDatabaseManager::PerformFullHashCheckCalled(
       v4_local_database_manager_));
@@ -708,7 +727,10 @@
   // Confirm there is a match and the full hash check is not performed.
   const GURL url_check("https://" + url_safe_no_scheme);
   EXPECT_TRUE(v4_local_database_manager_->CheckUrlForHighConfidenceAllowlist(
-      url_check));
+      url_check, "HPRT"));
+  ValidateHighConfidenceAllowlistHistograms(
+      /*expected_all_stores_available_sample=*/true,
+      /*expected_allowlist_too_small_sample=*/false);
   WaitForTasksOnTaskRunner();
   EXPECT_FALSE(FakeV4LocalDatabaseManager::PerformFullHashCheckCalled(
       v4_local_database_manager_));
@@ -730,7 +752,10 @@
   // Confirm there is no match and the full hash check is not performed.
   const GURL url_check("https://example.com/other/");
   EXPECT_FALSE(v4_local_database_manager_->CheckUrlForHighConfidenceAllowlist(
-      url_check));
+      url_check, "HPRT"));
+  ValidateHighConfidenceAllowlistHistograms(
+      /*expected_all_stores_available_sample=*/true,
+      /*expected_allowlist_too_small_sample=*/false);
   WaitForTasksOnTaskRunner();
   EXPECT_FALSE(FakeV4LocalDatabaseManager::PerformFullHashCheckCalled(
       v4_local_database_manager_));
@@ -747,7 +772,10 @@
   // Confirm there is a match and the full hash check is not performed.
   const GURL url_check("https://example.com/safe");
   EXPECT_TRUE(v4_local_database_manager_->CheckUrlForHighConfidenceAllowlist(
-      url_check));
+      url_check, "HPRT"));
+  ValidateHighConfidenceAllowlistHistograms(
+      /*expected_all_stores_available_sample=*/false,
+      /*expected_allowlist_too_small_sample=*/false);
   WaitForTasksOnTaskRunner();
   EXPECT_FALSE(FakeV4LocalDatabaseManager::PerformFullHashCheckCalled(
       v4_local_database_manager_));
@@ -767,14 +795,16 @@
   // Confirm there is a match and the full hash check is not performed.
   const GURL url_check("https://example.com/safe");
   EXPECT_TRUE(v4_local_database_manager_->CheckUrlForHighConfidenceAllowlist(
-      url_check));
+      url_check, "HPRT"));
+  ValidateHighConfidenceAllowlistHistograms(
+      /*expected_all_stores_available_sample=*/true,
+      /*expected_allowlist_too_small_sample=*/true);
   WaitForTasksOnTaskRunner();
   EXPECT_FALSE(FakeV4LocalDatabaseManager::PerformFullHashCheckCalled(
       v4_local_database_manager_));
 }
 
 TEST_F(V4LocalDatabaseManagerTest, TestGetSeverestThreatTypeAndMetadata) {
-  base::HistogramTester histograms;
   WaitForTasksOnTaskRunner();
 
   FullHashStr fh_malware("Malware");
@@ -820,7 +850,7 @@
   EXPECT_EQ("malware_popid", metadata.population_id);
   EXPECT_EQ(fh_malware, matching_full_hash);
 
-  histograms.ExpectUniqueSample(
+  histogram_tester_.ExpectUniqueSample(
       "SafeBrowsing.V4LocalDatabaseManager.ThreatInfoSize",
       /* sample */ 2, /* expected_count */ 2);
 }
@@ -1402,8 +1432,9 @@
 
   std::vector<ListIdentifier> synced_lists;
   for (const auto& info : v4_local_database_manager_->list_infos_) {
-    if (info.fetch_updates())
+    if (info.fetch_updates()) {
       synced_lists.push_back(info.list_id());
+    }
   }
   EXPECT_EQ(expected_lists, synced_lists);
 }
@@ -1420,11 +1451,10 @@
   const std::string rename_status_histogram =
       "SafeBrowsing.V4Store.RenameStatus." + new_store_name;
 
-  base::HistogramTester histograms;
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 0);
-  histograms.ExpectTotalCount(old_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(new_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(rename_status_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
   auto old_store_path =
       base_dir_.GetPath().AppendASCII(old_store_name + ".store");
@@ -1441,17 +1471,17 @@
       base_dir_.GetPath().AppendASCII(new_store_name + ".store");
   ASSERT_TRUE(base::PathExists(new_store_path));
 
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 1);
-  histograms.ExpectBucketCount(old_name_in_use_histogram, false, 1);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_in_use_histogram, false, 1);
 
-  histograms.ExpectTotalCount(old_name_exists_histogram, 1);
-  histograms.ExpectBucketCount(old_name_exists_histogram, true, 1);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_exists_histogram, true, 1);
 
-  histograms.ExpectTotalCount(new_name_exists_histogram, 1);
-  histograms.ExpectBucketCount(new_name_exists_histogram, false, 1);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 1);
+  histogram_tester_.ExpectBucketCount(new_name_exists_histogram, false, 1);
 
-  histograms.ExpectTotalCount(rename_status_histogram, 1);
-  histograms.ExpectBucketCount(rename_status_histogram, 0, 1);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 1);
+  histogram_tester_.ExpectBucketCount(rename_status_histogram, 0, 1);
 
   // Cleanup
   base::DeleteFile(new_store_path);
@@ -1469,20 +1499,19 @@
           {"UrlCsdWhitelist", "UrlCsdAllowlist"},
       });
 
-  base::HistogramTester histograms;
   for (auto const& pair : kStoreFilesToRename) {
     const std::string& old_store_name = pair.first;
     const std::string& new_store_name = pair.second;
 
     std::string old_name_in_use_histogram = old_name_in_use + old_store_name;
-    histograms.ExpectTotalCount(old_name_in_use_histogram, 0);
+    histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 0);
     std::string old_name_exists_histogram = old_name_exists + old_store_name;
-    histograms.ExpectTotalCount(old_name_exists_histogram, 0);
+    histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 0);
 
     std::string new_name_exists_histogram = new_name_exists + new_store_name;
-    histograms.ExpectTotalCount(new_name_exists_histogram, 0);
+    histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 0);
     std::string rename_status_histogram = rename_status + new_store_name;
-    histograms.ExpectTotalCount(rename_status_histogram, 0);
+    histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
     auto old_store_path =
         base_dir_.GetPath().AppendASCII(old_store_name + ".store");
@@ -1511,20 +1540,20 @@
     ASSERT_TRUE(base::PathExists(new_store_path));
 
     std::string old_name_in_use_histogram = old_name_in_use + old_store_name;
-    histograms.ExpectTotalCount(old_name_in_use_histogram, 1);
-    histograms.ExpectBucketCount(old_name_in_use_histogram, false, 1);
+    histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 1);
+    histogram_tester_.ExpectBucketCount(old_name_in_use_histogram, false, 1);
 
     std::string old_name_exists_histogram = old_name_exists + old_store_name;
-    histograms.ExpectTotalCount(old_name_exists_histogram, 1);
-    histograms.ExpectBucketCount(old_name_exists_histogram, true, 1);
+    histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 1);
+    histogram_tester_.ExpectBucketCount(old_name_exists_histogram, true, 1);
 
     std::string new_name_exists_histogram = new_name_exists + new_store_name;
-    histograms.ExpectTotalCount(new_name_exists_histogram, 1);
-    histograms.ExpectBucketCount(new_name_exists_histogram, false, 1);
+    histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 1);
+    histogram_tester_.ExpectBucketCount(new_name_exists_histogram, false, 1);
 
     std::string rename_status_histogram = rename_status + new_store_name;
-    histograms.ExpectTotalCount(rename_status_histogram, 1);
-    histograms.ExpectBucketCount(rename_status_histogram, 0, 1);
+    histogram_tester_.ExpectTotalCount(rename_status_histogram, 1);
+    histogram_tester_.ExpectBucketCount(rename_status_histogram, 0, 1);
 
     // Cleanup
     base::DeleteFile(new_store_path);
@@ -1544,11 +1573,10 @@
   const std::string rename_status_histogram =
       "SafeBrowsing.V4Store.RenameStatus." + new_store_name;
 
-  base::HistogramTester histograms;
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 0);
-  histograms.ExpectTotalCount(old_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(new_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(rename_status_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
   auto old_store_path =
       base_dir_.GetPath().AppendASCII(old_store_name + ".store");
@@ -1556,14 +1584,14 @@
 
   WaitForTasksOnTaskRunner();
 
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 1);
-  histograms.ExpectBucketCount(old_name_in_use_histogram, false, 1);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_in_use_histogram, false, 1);
 
-  histograms.ExpectTotalCount(old_name_exists_histogram, 1);
-  histograms.ExpectBucketCount(old_name_exists_histogram, false, 1);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_exists_histogram, false, 1);
 
-  histograms.ExpectTotalCount(new_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(rename_status_histogram, 0);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
   // Cleanup
   base::DeleteFile(old_store_path);
@@ -1581,11 +1609,10 @@
   const std::string rename_status_histogram =
       "SafeBrowsing.V4Store.RenameStatus." + new_store_name;
 
-  base::HistogramTester histograms;
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 0);
-  histograms.ExpectTotalCount(old_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(new_name_exists_histogram, 0);
-  histograms.ExpectTotalCount(rename_status_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 0);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 0);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
   auto old_store_path =
       base_dir_.GetPath().AppendASCII(old_store_name + ".store");
@@ -1605,16 +1632,16 @@
 
   WaitForTasksOnTaskRunner();
 
-  histograms.ExpectTotalCount(old_name_in_use_histogram, 1);
-  histograms.ExpectBucketCount(old_name_in_use_histogram, false, 1);
+  histogram_tester_.ExpectTotalCount(old_name_in_use_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_in_use_histogram, false, 1);
 
-  histograms.ExpectTotalCount(old_name_exists_histogram, 1);
-  histograms.ExpectBucketCount(old_name_exists_histogram, true, 1);
+  histogram_tester_.ExpectTotalCount(old_name_exists_histogram, 1);
+  histogram_tester_.ExpectBucketCount(old_name_exists_histogram, true, 1);
 
-  histograms.ExpectTotalCount(new_name_exists_histogram, 1);
-  histograms.ExpectBucketCount(new_name_exists_histogram, true, 1);
+  histogram_tester_.ExpectTotalCount(new_name_exists_histogram, 1);
+  histogram_tester_.ExpectBucketCount(new_name_exists_histogram, true, 1);
 
-  histograms.ExpectTotalCount(rename_status_histogram, 0);
+  histogram_tester_.ExpectTotalCount(rename_status_histogram, 0);
 
   // Cleanup
   base::DeleteFile(old_store_path);
diff --git a/components/safe_browsing/core/browser/hash_realtime_mechanism.cc b/components/safe_browsing/core/browser/hash_realtime_mechanism.cc
index 75e2e010..cf6aff3b 100644
--- a/components/safe_browsing/core/browser/hash_realtime_mechanism.cc
+++ b/components/safe_browsing/core/browser/hash_realtime_mechanism.cc
@@ -4,6 +4,7 @@
 
 #include "components/safe_browsing/core/browser/hash_realtime_mechanism.h"
 
+#include "base/metrics/histogram_functions.h"
 #include "base/task/sequenced_task_runner.h"
 #include "components/safe_browsing/core/browser/db/database_manager.h"
 #include "components/safe_browsing/core/browser/db/util.h"
@@ -40,7 +41,10 @@
   }
 
   bool has_allowlist_match =
-      database_manager_->CheckUrlForHighConfidenceAllowlist(url_);
+      database_manager_->CheckUrlForHighConfidenceAllowlist(url_, "HPRT");
+  base::UmaHistogramEnumeration(
+      "SafeBrowsing.HPRT.LocalMatch.Result",
+      has_allowlist_match ? AsyncMatch::MATCH : AsyncMatch::NO_MATCH);
   base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE,
       base::BindOnce(
@@ -78,6 +82,8 @@
     scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
   bool is_lookup_service_available =
       lookup_service_on_ui && !lookup_service_on_ui->IsInBackoffMode();
+  base::UmaHistogramBoolean("SafeBrowsing.HPRT.IsLookupServiceAvailable",
+                            is_lookup_service_available);
   if (!is_lookup_service_available) {
     io_task_runner->PostTask(
         FROM_HERE, base::BindOnce(&HashRealTimeMechanism::PerformHashBasedCheck,
diff --git a/components/safe_browsing/core/browser/hash_realtime_mechanism_unittest.cc b/components/safe_browsing/core/browser/hash_realtime_mechanism_unittest.cc
index b5f4deac..585918f 100644
--- a/components/safe_browsing/core/browser/hash_realtime_mechanism_unittest.cc
+++ b/components/safe_browsing/core/browser/hash_realtime_mechanism_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/containers/contains.h"
 #include "base/functional/bind.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/task_environment.h"
 #include "components/safe_browsing/core/browser/db/test_database_manager.h"
@@ -28,7 +29,9 @@
 class MockHashRealTimeService : public HashRealTimeService {
  public:
   MockHashRealTimeService()
-      : HashRealTimeService(/*url_loader_factory=*/nullptr) {}
+      : HashRealTimeService(
+            /*url_loader_factory=*/nullptr,
+            /*cache_manager=*/nullptr) {}
   base::WeakPtr<MockHashRealTimeService> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
   }
@@ -120,7 +123,9 @@
   // Returns the allowlist match result previously set by
   // |SetAllowlistResultForUrl|. It crashes if the allowlist match result for
   // the |gurl| is not set in advance.
-  bool CheckUrlForHighConfidenceAllowlist(const GURL& gurl) override {
+  bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& gurl,
+      const std::string& metric_variation) override {
     std::string url = gurl.spec();
     DCHECK(base::Contains(urls_allowlist_match_, url));
     return urls_allowlist_match_[url];
@@ -201,10 +206,37 @@
         hash_rt_service_->GetWeakPtr());
   }
 
+  void CheckHashRealTimeMetrics(
+      absl::optional<bool> expected_local_match_result,
+      absl::optional<bool> expected_is_service_available) {
+    if (!expected_local_match_result.has_value()) {
+      histogram_tester_->ExpectTotalCount(
+          /*name=*/"SafeBrowsing.HPRT.LocalMatch.Result", /*expected_count=*/0);
+    } else {
+      histogram_tester_->ExpectUniqueSample(
+          /*name=*/"SafeBrowsing.HPRT.LocalMatch.Result",
+          /*sample=*/expected_local_match_result.value() ? AsyncMatch::MATCH
+                                                         : AsyncMatch::NO_MATCH,
+          /*expected_bucket_count=*/1);
+    }
+    if (!expected_is_service_available.has_value()) {
+      histogram_tester_->ExpectTotalCount(
+          /*name=*/"SafeBrowsing.HPRT.IsLookupServiceAvailable",
+          /*expected_count=*/0);
+    } else {
+      histogram_tester_->ExpectUniqueSample(
+          /*name=*/"SafeBrowsing.HPRT.IsLookupServiceAvailable",
+          /*sample=*/expected_is_service_available.value(),
+          /*expected_bucket_count=*/1);
+    }
+  }
+
  protected:
   base::test::TaskEnvironment task_environment_;
   scoped_refptr<MockSafeBrowsingDatabaseManager> database_manager_;
   std::unique_ptr<MockHashRealTimeService> hash_rt_service_;
+  std::unique_ptr<base::HistogramTester> histogram_tester_ =
+      std::make_unique<base::HistogramTester>();
 };
 
 MATCHER_P2(Matches, url, threat_type, "") {
@@ -224,6 +256,8 @@
   EXPECT_EQ(result.is_safe_synchronously, true);
 
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/absl::nullopt,
+                           /*expected_is_service_available=*/absl::nullopt);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_AllowlistMatchSafe) {
@@ -240,6 +274,8 @@
 
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_SAFE))).Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/true,
+                           /*expected_is_service_available=*/absl::nullopt);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_AllowlistMatchUnsafe) {
@@ -257,6 +293,8 @@
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_URL_PHISHING)))
       .Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/true,
+                           /*expected_is_service_available=*/absl::nullopt);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_SafeLookup) {
@@ -273,6 +311,8 @@
 
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_SAFE))).Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/false,
+                           /*expected_is_service_available=*/true);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_UnsafeLookup) {
@@ -290,6 +330,8 @@
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_URL_PHISHING)))
       .Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/false,
+                           /*expected_is_service_available=*/true);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_BackoffMode) {
@@ -308,6 +350,8 @@
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_URL_PHISHING)))
       .Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/false,
+                           /*expected_is_service_available=*/false);
 }
 
 TEST_F(HashRealTimeMechanismTest, CheckUrl_HashRealTime_UnsuccessfulLookup) {
@@ -327,6 +371,8 @@
   EXPECT_CALL(callback, Run(Matches(url, SB_THREAT_TYPE_URL_PHISHING)))
       .Times(1);
   task_environment_.RunUntilIdle();
+  CheckHashRealTimeMetrics(/*expected_local_match_result=*/false,
+                           /*expected_is_service_available=*/true);
 }
 
 }  // namespace safe_browsing
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/BUILD.gn b/components/safe_browsing/core/browser/hashprefix_realtime/BUILD.gn
index 31d5913..98b7b52 100644
--- a/components/safe_browsing/core/browser/hashprefix_realtime/BUILD.gn
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/BUILD.gn
@@ -12,6 +12,7 @@
     ":hash_realtime_utils",
     "//base:base",
     "//components/keyed_service/core",
+    "//components/safe_browsing/core/browser:verdict_cache_manager",
     "//components/safe_browsing/core/browser/db:v4_protocol_manager_util",
     "//components/safe_browsing/core/browser/utils",
     "//components/safe_browsing/core/common",
@@ -22,6 +23,19 @@
   ]
 }
 
+static_library("hash_realtime_cache") {
+  sources = [
+    "hash_realtime_cache.cc",
+    "hash_realtime_cache.h",
+  ]
+
+  deps = [
+    ":hash_realtime_utils",
+    "//base:base",
+    "//components/safe_browsing/core/common/proto:safebrowsingv5_alpha1_proto",
+  ]
+}
+
 static_library("hash_realtime_utils") {
   sources = [
     "hash_realtime_utils.cc",
@@ -37,13 +51,16 @@
 source_set("unit_tests") {
   testonly = true
   sources = [
+    "hash_realtime_cache_unittest.cc",
     "hash_realtime_service_unittest.cc",
     "hash_realtime_utils_unittest.cc",
   ]
   deps = [
+    ":hash_realtime_cache",
     ":hash_realtime_service",
     ":hash_realtime_utils",
     "//base/test:test_support",
+    "//components/safe_browsing/core/browser:verdict_cache_manager",
     "//components/safe_browsing/core/browser/db:v4_protocol_manager_util",
     "//components/safe_browsing/core/common/proto:safebrowsingv5_alpha1_proto",
     "//components/sync_preferences:test_support",
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.cc b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.cc
new file mode 100644
index 0000000..eebb31d
--- /dev/null
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.cc
@@ -0,0 +1,114 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h"
+
+#include "base/metrics/histogram_functions.h"
+#include "base/notreached.h"
+#include "base/time/time.h"
+#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_utils.h"
+#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
+
+namespace safe_browsing {
+
+namespace {
+
+void LogCacheHitOrMiss(bool is_hit) {
+  base::UmaHistogramBoolean("SafeBrowsing.HPRT.CacheHit", is_hit);
+}
+
+}  // namespace
+
+HashRealTimeCache::HashRealTimeCache() = default;
+
+HashRealTimeCache::~HashRealTimeCache() = default;
+
+HashRealTimeCache::FullHashesAndDetails::FullHashesAndDetails() = default;
+HashRealTimeCache::FullHashesAndDetails::~FullHashesAndDetails() = default;
+
+std::unordered_map<std::string, std::vector<V5::FullHash>>
+HashRealTimeCache::SearchCache(
+    const std::set<std::string>& hash_prefixes) const {
+  std::unordered_map<std::string, std::vector<V5::FullHash>> results;
+  for (const auto& hash_prefix : hash_prefixes) {
+    auto cached_result_it = cache_.find(hash_prefix);
+    if (cached_result_it != cache_.end() &&
+        cached_result_it->second.expiration_time > base::Time::Now()) {
+      results[hash_prefix] = cached_result_it->second.full_hash_and_details;
+      LogCacheHitOrMiss(/*is_hit=*/true);
+    } else {
+      LogCacheHitOrMiss(/*is_hit=*/false);
+    }
+  }
+  return results;
+}
+
+void HashRealTimeCache::CacheSearchHashesResponse(
+    const std::vector<std::string>& requested_hash_prefixes,
+    const std::vector<V5::FullHash>& response_full_hashes,
+    const V5::Duration& cache_duration) {
+  // First, wipe all the results for the relevant hash prefixes, and set the
+  // latest expiry.
+  for (const auto& hash_prefix : requested_hash_prefixes) {
+    FullHashesAndDetails entry;
+    entry.expiration_time = base::Time::Now() +
+                            base::Seconds(cache_duration.seconds()) +
+                            base::Nanoseconds(cache_duration.nanos());
+    cache_[hash_prefix] = entry;
+  }
+  // Then, add all matching and relevant full hashes into the cache. Hash
+  // prefixes only sometimes have matching full hashes, so some may remain empty
+  // due to the wiping that occurred above.
+  for (const auto& fh : response_full_hashes) {
+    // Narrow down each full hash's results to just the threat types that are
+    // relevant for hash-prefix real-time lookups.
+    V5::FullHash full_hash_to_store;
+    full_hash_to_store.set_full_hash(fh.full_hash());
+    for (const auto& fhd : fh.full_hash_details()) {
+      if (hash_realtime_utils::IsThreatTypeRelevant(fhd.threat_type())) {
+        auto* fhd_to_store = full_hash_to_store.add_full_hash_details();
+        fhd_to_store->set_threat_type(fhd.threat_type());
+        for (auto i = 0; i < fhd.attributes_size(); ++i) {
+          fhd_to_store->add_attributes(fhd.attributes(i));
+        }
+      }
+    }
+    // If none of the threat types were relevant for the full hash, don't store
+    // it in the cache.
+    if (full_hash_to_store.full_hash_details().empty()) {
+      continue;
+    }
+    // Update the cache with the remaining results for the associated hash
+    // prefix.
+    auto hash_prefix = hash_realtime_utils::GetHashPrefix(fh.full_hash());
+    auto cached_result_it = cache_.find(hash_prefix);
+    if (cached_result_it != cache_.end()) {
+      cached_result_it->second.full_hash_and_details.push_back(
+          full_hash_to_store);
+    } else {
+      // There should always be a hash prefix associated with the full hash.
+      NOTREACHED();
+    }
+  }
+}
+
+void HashRealTimeCache::ClearExpiredResults() {
+  int num_hash_prefixes = cache_.size();
+  int num_full_hashes = 0;
+  auto it = cache_.begin();
+  while (it != cache_.end()) {
+    num_full_hashes += it->second.full_hash_and_details.size();
+    if (it->second.expiration_time <= base::Time::Now()) {
+      it = cache_.erase(it);
+    } else {
+      ++it;
+    }
+  }
+  base::UmaHistogramCounts10000("SafeBrowsing.HPRT.Cache.HashPrefixCount",
+                                num_hash_prefixes);
+  base::UmaHistogramCounts10000("SafeBrowsing.HPRT.Cache.FullHashCount",
+                                num_full_hashes);
+}
+
+}  // namespace safe_browsing
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h
new file mode 100644
index 0000000..45bddfe4
--- /dev/null
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h
@@ -0,0 +1,60 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_SAFE_BROWSING_CORE_BROWSER_HASHPREFIX_REALTIME_HASH_REALTIME_CACHE_H_
+#define COMPONENTS_SAFE_BROWSING_CORE_BROWSER_HASHPREFIX_REALTIME_HASH_REALTIME_CACHE_H_
+
+#include "base/time/time.h"
+#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
+
+namespace safe_browsing {
+
+// This class manages the cache for hash-prefix real-time lookups.
+class HashRealTimeCache {
+ public:
+  HashRealTimeCache();
+  HashRealTimeCache(const HashRealTimeCache&) = delete;
+  HashRealTimeCache& operator=(const HashRealTimeCache&) = delete;
+  ~HashRealTimeCache();
+
+  struct FullHashesAndDetails {
+    FullHashesAndDetails();
+    ~FullHashesAndDetails();
+
+    // The time at which this cache entry is no longer considered up-to-date.
+    base::Time expiration_time;
+
+    // The list of all full hashes (and related info) that start with a
+    // particular hash prefix and are known to be unsafe. This vector may be
+    // empty if there are no unsafe matches.
+    std::vector<V5::FullHash> full_hash_and_details;
+  };
+
+  // Returns a map, where the key is a requested hash prefix and the value is
+  // the matching result in the cache. If a requested hash prefix was not in the
+  // cache (or has expired), then it is not in the returned map.
+  std::unordered_map<std::string, std::vector<V5::FullHash>> SearchCache(
+      const std::set<std::string>& hash_prefixes) const;
+
+  // Adds the responses to the cache.
+  void CacheSearchHashesResponse(
+      const std::vector<std::string>& requested_hash_prefixes,
+      const std::vector<V5::FullHash>& response_full_hashes,
+      const V5::Duration& cache_duration);
+
+  // Remove any entries from the cache that are expired. The purpose of this is
+  // for memory management.
+  void ClearExpiredResults();
+
+ private:
+  friend class HashRealTimeCacheTest;
+  friend class VerdictCacheManagerTest;
+  // Map of hash prefix -> a |FullHashesAndDetails| object, representing the
+  // matching unsafe full hashes.
+  std::unordered_map<std::string, FullHashesAndDetails> cache_;
+};
+
+}  // namespace safe_browsing
+
+#endif  // COMPONENTS_SAFE_BROWSING_CORE_BROWSER_HASHPREFIX_REALTIME_HASH_REALTIME_CACHE_H_
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache_unittest.cc b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache_unittest.cc
new file mode 100644
index 0000000..be7c3508
--- /dev/null
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache_unittest.cc
@@ -0,0 +1,592 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h"
+
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/task_environment.h"
+#include "base/time/time.h"
+#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+namespace safe_browsing {
+
+class HashRealTimeCacheTest : public PlatformTest {
+ protected:
+  V5::Duration CreateCacheDuration(int seconds, int nanos) {
+    V5::Duration cache_duration;
+    cache_duration.set_seconds(seconds);
+    cache_duration.set_nanos(nanos);
+    return cache_duration;
+  }
+  // Does not populate the "attributes" field.
+  V5::FullHash CreateBasicFullHash(std::string full_hash_str,
+                                   std::vector<V5::ThreatType> threat_types) {
+    V5::FullHash full_hash_object;
+    full_hash_object.set_full_hash(full_hash_str);
+    for (const auto& threat_type : threat_types) {
+      auto* details = full_hash_object.add_full_hash_details();
+      details->set_threat_type(threat_type);
+    }
+    return full_hash_object;
+  }
+  void AddThreatTypeAndAttributes(V5::FullHash& full_hash_object,
+                                  V5::ThreatType threat_type,
+                                  std::vector<V5::ThreatAttribute> attributes) {
+    auto* details = full_hash_object.add_full_hash_details();
+    details->set_threat_type(threat_type);
+    for (const auto& attribute : attributes) {
+      details->add_attributes(attribute);
+    }
+  }
+  void CheckAndResetCacheHitsAndMisses(int num_hits, int num_misses) {
+    histogram_tester_->ExpectBucketCount("SafeBrowsing.HPRT.CacheHit",
+                                         /*sample=*/true,
+                                         /*expected_count=*/num_hits);
+    histogram_tester_->ExpectBucketCount("SafeBrowsing.HPRT.CacheHit",
+                                         /*sample=*/false,
+                                         /*expected_count=*/num_misses);
+    histogram_tester_ = std::make_unique<base::HistogramTester>();
+  }
+  void CheckAndResetCacheSizeOnClear(int num_hash_prefixes,
+                                     int num_full_hashes) {
+    histogram_tester_->ExpectBucketCount(
+        "SafeBrowsing.HPRT.Cache.HashPrefixCount",
+        /*sample=*/num_hash_prefixes,
+        /*expected_count=*/1);
+    histogram_tester_->ExpectBucketCount(
+        "SafeBrowsing.HPRT.Cache.FullHashCount",
+        /*sample=*/num_full_hashes,
+        /*expected_count=*/1);
+    histogram_tester_ = std::make_unique<base::HistogramTester>();
+  }
+  int GetNumCacheEntries(std::unique_ptr<HashRealTimeCache>& cache) {
+    // This includes expired entries that have not yet been cleaned up too.
+    return cache->cache_.size();
+  }
+  void CacheEntry(std::unique_ptr<HashRealTimeCache>& cache_internal,
+                  std::string full_hash,
+                  int cache_duration_seconds) {
+    cache_internal->CacheSearchHashesResponse(
+        {full_hash.substr(0, 4)},
+        {CreateBasicFullHash(full_hash, {V5::ThreatType::MALWARE})},
+        CreateCacheDuration(cache_duration_seconds, 0));
+  }
+
+  base::test::TaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  std::unique_ptr<base::HistogramTester> histogram_tester_ =
+      std::make_unique<base::HistogramTester>();
+};
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_EmptyCache) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  EXPECT_TRUE(cache->SearchCache({}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/0);
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_TRUE(cache->SearchCache({"aaaa", "bbbb"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
+}
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_BasicFunctionality) {
+  base::HistogramTester histogram_tester;
+  auto cache = std::make_unique<HashRealTimeCache>();
+  // The below is done within a block to ensure that the cache works even once
+  // the inputs to CacheSearchHashesResponse have been destructed.
+  {
+    std::vector<std::string> requested_hash_prefixes = {"aaaa", "bbbb", "cccc",
+                                                        "dddd"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash(
+            "aaaa1111111111111111111111111111",
+            {V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
+             V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
+        CreateBasicFullHash("aaaa2222222222222222222222222222",
+                            {V5::ThreatType::MALWARE}),
+        CreateBasicFullHash("aaaa3333333333333333333333333333",
+                            {V5::ThreatType::API_ABUSE}),
+        CreateBasicFullHash("cccc1111111111111111111111111111",
+                            {V5::ThreatType::API_ABUSE,
+                             V5::ThreatType::ABUSIVE_EXPERIENCE_VIOLATION,
+                             V5::ThreatType::BETTER_ADS_VIOLATION,
+                             V5::ThreatType::ABUSIVE_EXPERIENCE_VIOLATION,
+                             V5::ThreatType::POTENTIALLY_HARMFUL_APPLICATION,
+                             V5::ThreatType::SOCIAL_ENGINEERING_ADS}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+
+  // Searching for no prefix or for prefixes not in the request should yield
+  // empty cache results.
+  EXPECT_TRUE(cache->SearchCache({}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/0);
+  EXPECT_TRUE(cache->SearchCache({"eeee"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_TRUE(cache->SearchCache({"eeee", "ffff"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
+
+  std::set<std::string> hash_prefixes_to_search = {"aaaa", "bbbb", "cccc",
+                                                   "dddd", "eeee", "ffff"};
+  auto cache_results = cache->SearchCache(hash_prefixes_to_search);
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/4, /*num_misses=*/2);
+
+  // Don't expect cache results for eeee and ffff, since they are not in the
+  // cache. Expect cache results for all other prefixes.
+  EXPECT_EQ(cache_results.size(), 4u);
+  EXPECT_TRUE(base::Contains(cache_results, "aaaa"));
+  EXPECT_TRUE(base::Contains(cache_results, "bbbb"));
+  EXPECT_TRUE(base::Contains(cache_results, "cccc"));
+  EXPECT_TRUE(base::Contains(cache_results, "dddd"));
+  EXPECT_FALSE(base::Contains(cache_results, "eeee"));
+  EXPECT_FALSE(base::Contains(cache_results, "ffff"));
+
+  // bbbb and dddd should both have empty results, because they did not have any
+  // corresponding full hashes.
+  EXPECT_TRUE(cache_results["bbbb"].empty());
+  EXPECT_TRUE(cache_results["dddd"].empty());
+  // cccc should also have empty results, because the threat types returned by
+  // the server for that full hash were not relevant for hash-prefix real-time
+  // lookups.
+  EXPECT_TRUE(cache_results["cccc"].empty());
+
+  // aaaa should match both aaaa...1 and aaaa...2, but not aaaa....3 due to
+  // irrelevant threat types.
+  EXPECT_EQ(cache_results["aaaa"].size(), 2u);
+  // aaaa...1 should only contain relevant threat types.
+  auto aaaa1_results = cache_results["aaaa"][0];
+  EXPECT_EQ(aaaa1_results.full_hash(), "aaaa1111111111111111111111111111");
+  auto aaaa1_details = aaaa1_results.full_hash_details();
+  EXPECT_EQ(aaaa1_details.size(), 3);
+  EXPECT_EQ(aaaa1_details[0].threat_type(), V5::ThreatType::SOCIAL_ENGINEERING);
+  EXPECT_TRUE(aaaa1_details[0].attributes().empty());
+  EXPECT_EQ(aaaa1_details[1].threat_type(), V5::ThreatType::MALWARE);
+  EXPECT_TRUE(aaaa1_details[1].attributes().empty());
+  EXPECT_EQ(aaaa1_details[2].threat_type(), V5::ThreatType::UNWANTED_SOFTWARE);
+  EXPECT_TRUE(aaaa1_details[2].attributes().empty());
+  // aaaa...2 should have one threat type (malware).
+  auto aaaa2_results = cache_results["aaaa"][1];
+  EXPECT_EQ(aaaa2_results.full_hash(), "aaaa2222222222222222222222222222");
+  auto aaaa2_details = aaaa2_results.full_hash_details();
+  EXPECT_EQ(aaaa2_details.size(), 1);
+  EXPECT_EQ(aaaa2_details[0].threat_type(), V5::ThreatType::MALWARE);
+  EXPECT_TRUE(aaaa2_details[0].attributes().empty());
+}
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_Expiration) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  // The below are done within blocks to ensure that the cache works even once
+  // the inputs to CacheSearchHashesResponse have been destructed.
+  {
+    std::vector<std::string> requested_hash_prefixes = {"aaaa"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash(
+            "aaaa1111111111111111111111111111",
+            {V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
+             V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+  task_environment_.FastForwardBy(base::Seconds(299));
+  {
+    std::vector<std::string> requested_hash_prefixes = {"cccc"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash("cccc1111111111111111111111111111",
+                            {V5::ThreatType::MALWARE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+
+  // aaaa expires at 300 seconds. cccc expires at 599 seconds.
+  // Current time = 299 seconds. aaaa and cccc have not expired.
+  EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 2u);
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/2, /*num_misses=*/0);
+  // Current time = 300 seconds. aaaa has expired. cccc has not expired.
+  task_environment_.FastForwardBy(base::Seconds(1));
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 1u);
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/1);
+  // Current time = 598 seconds. aaaa has expired. cccc has not expired.
+  task_environment_.FastForwardBy(base::Seconds(298));
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 1u);
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/1);
+  // Current time = 599 seconds. aaaa and cccc have expired.
+  task_environment_.FastForwardBy(base::Seconds(1));
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_TRUE(cache->SearchCache({"cccc"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+  EXPECT_TRUE(cache->SearchCache({"aaaa", "cccc"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
+}
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_ExpirationNanos) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  // The below are done within blocks to ensure that the cache works even once
+  // the inputs to CacheSearchHashesResponse have been destructed.
+  {
+    std::vector<std::string> requested_hash_prefixes = {"aaaa"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash(
+            "aaaa1111111111111111111111111111",
+            {V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
+             V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 500000000));
+  }
+  task_environment_.FastForwardBy(base::Seconds(300));
+
+  // aaaa expires at 300.5 seconds.
+  // Current time = 300.0 seconds. aaaa has not expired.
+  EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  // Current time = 300.5 seconds. aaaa has expired.
+  task_environment_.FastForwardBy(base::Nanoseconds(500000000));
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+}
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_Attributes) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  // The below is done within a block to ensure that the cache works even once
+  // the inputs to CacheSearchHashesResponse have been destructed.
+  {
+    std::vector<std::string> requested_hash_prefixes = {"aaaa", "bbbb"};
+    auto full_hash_1 =
+        CreateBasicFullHash("aaaa1111111111111111111111111111", {});
+    AddThreatTypeAndAttributes(
+        full_hash_1, V5::ThreatType::SOCIAL_ENGINEERING,
+        {V5::ThreatAttribute::CANARY, V5::ThreatAttribute::FRAME_ONLY});
+    AddThreatTypeAndAttributes(full_hash_1, V5::ThreatType::MALWARE,
+                               {V5::ThreatAttribute::CANARY});
+    AddThreatTypeAndAttributes(
+        full_hash_1, V5::ThreatType::API_ABUSE,
+        {V5::ThreatAttribute::CANARY, V5::ThreatAttribute::FRAME_ONLY});
+    AddThreatTypeAndAttributes(full_hash_1, V5::ThreatType::UNWANTED_SOFTWARE,
+                               {});
+    std::vector<V5::FullHash> response_full_hashes = {
+        full_hash_1, CreateBasicFullHash("aaaa2222222222222222222222222222",
+                                         {V5::ThreatType::MALWARE})};
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+
+  std::set<std::string> hash_prefixes_to_search = {"aaaa", "bbbb"};
+  auto cache_results = cache->SearchCache(hash_prefixes_to_search);
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/2, /*num_misses=*/0);
+
+  // Sanity check that adding attributes for aaaa hashes does not change the
+  // fact that there should be no bbbb full hashes / associated attributes.
+  EXPECT_TRUE(base::Contains(cache_results, "bbbb"));
+  EXPECT_TRUE(cache_results["bbbb"].empty());
+
+  // We expect aaaa...1 and aaaa...2 both to be in the cache.
+  EXPECT_EQ(cache_results["aaaa"].size(), 2u);
+  // aaaa...1 should be filtered down to relevant threat types, meaning some
+  // attributes get filtered out too since they are associated with a specific
+  // threat type.
+  auto aaaa1_results = cache_results["aaaa"][0];
+  EXPECT_EQ(aaaa1_results.full_hash(), "aaaa1111111111111111111111111111");
+  auto aaaa1_details = aaaa1_results.full_hash_details();
+  EXPECT_EQ(aaaa1_details.size(), 3);
+  EXPECT_EQ(aaaa1_details[0].threat_type(), V5::ThreatType::SOCIAL_ENGINEERING);
+  EXPECT_EQ(aaaa1_details[0].attributes().size(), 2);
+  EXPECT_EQ(aaaa1_details[0].attributes()[0], V5::ThreatAttribute::CANARY);
+  EXPECT_EQ(aaaa1_details[0].attributes()[1], V5::ThreatAttribute::FRAME_ONLY);
+  EXPECT_EQ(aaaa1_details[1].threat_type(), V5::ThreatType::MALWARE);
+  EXPECT_EQ(aaaa1_details[1].attributes().size(), 1);
+  EXPECT_EQ(aaaa1_details[1].attributes()[0], V5::ThreatAttribute::CANARY);
+  EXPECT_EQ(aaaa1_details[2].threat_type(), V5::ThreatType::UNWANTED_SOFTWARE);
+  EXPECT_TRUE(aaaa1_details[2].attributes().empty());
+  // Sanity check that aaaa...2 has no attributes in spite of aaaa...1 having
+  // attributes.
+  auto aaaa2_results = cache_results["aaaa"][1];
+  EXPECT_EQ(aaaa2_results.full_hash(), "aaaa2222222222222222222222222222");
+  auto aaaa2_details = aaaa2_results.full_hash_details();
+  EXPECT_EQ(aaaa2_details.size(), 1);
+  EXPECT_EQ(aaaa2_details[0].threat_type(), V5::ThreatType::MALWARE);
+  EXPECT_TRUE(aaaa2_details[0].attributes().empty());
+}
+
+TEST_F(HashRealTimeCacheTest, TestCacheMatching_OverwrittenEntry) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  // The below are done within blocks to ensure that the cache works even once
+  // the inputs to CacheSearchHashesResponse have been destructed.
+  {
+    // Set up the cache for Request #1.
+    std::vector<std::string> requested_hash_prefixes = {"aaaa"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash(
+            "aaaa1111111111111111111111111111",
+            {V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
+             V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+  // Confirm the cache has the expected results.
+  auto cache_results_1 = cache->SearchCache({"aaaa"});
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_EQ(cache_results_1.size(), 1u);
+  EXPECT_EQ(cache_results_1["aaaa"].size(), 1u);
+  EXPECT_EQ(cache_results_1["aaaa"][0].full_hash(),
+            "aaaa1111111111111111111111111111");
+  EXPECT_EQ(cache_results_1["aaaa"][0].full_hash_details_size(), 3);
+
+  {
+    // Set up the cache for Request #2, overwriting the results of Request #1.
+    std::vector<std::string> requested_hash_prefixes = {"aaaa"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash("aaaa2222222222222222222222222222",
+                            {V5::ThreatType::MALWARE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+
+  // If there is a race where there are two outgoing hash-prefix real-time
+  // requests for the same prefix, the later-responding result replaces the
+  // earlier-responding result. In practice, the two results are expected to be
+  // the same almost always, but if they are not, this is how the cache behaves.
+  auto cache_results_2 = cache->SearchCache({"aaaa"});
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  EXPECT_EQ(cache_results_2.size(), 1u);
+  EXPECT_EQ(cache_results_2["aaaa"].size(), 1u);
+  EXPECT_EQ(cache_results_2["aaaa"][0].full_hash(),
+            "aaaa2222222222222222222222222222");
+  EXPECT_EQ(cache_results_2["aaaa"][0].full_hash_details_size(), 1);
+
+  task_environment_.FastForwardBy(base::Seconds(150));
+  {
+    // Set up the cache for Request #3, overwriting the results of Request #2.
+    // The main overwriting here is just the cache duration, since 150 seconds
+    // have passed.
+    std::vector<std::string> requested_hash_prefixes = {"aaaa"};
+    std::vector<V5::FullHash> response_full_hashes = {
+        CreateBasicFullHash("aaaa2222222222222222222222222222",
+                            {V5::ThreatType::MALWARE}),
+    };
+    cache->CacheSearchHashesResponse(requested_hash_prefixes,
+                                     response_full_hashes,
+                                     CreateCacheDuration(300, 0));
+  }
+
+  // Confirm caching Request #3 overwrote the cache duration. If it didn't, then
+  // the results of Request #2 would already have expired.
+  task_environment_.FastForwardBy(base::Seconds(150));
+  EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+
+  // Confirm Request #3's cache duration is respected.
+  task_environment_.FastForwardBy(base::Seconds(149));
+  EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
+  task_environment_.FastForwardBy(base::Seconds(1));
+  EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
+  CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_EmptyCache) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  EXPECT_EQ(GetNumCacheEntries(cache), 0);
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 0);
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_NoExpiredResults) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
+  CacheEntry(cache, "cccc1111111111111111111111111111", 500);
+
+  EXPECT_EQ(GetNumCacheEntries(cache), 2);
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 2);
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_OneExpiredResult) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
+  CacheEntry(cache, "cccc1111111111111111111111111111", 500);
+
+  // After 400 seconds, aaaa is expired but not cccc.
+  task_environment_.FastForwardBy(base::Seconds(400));
+  EXPECT_EQ(GetNumCacheEntries(cache), 2);
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 1);
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_SomeExpiredResults) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  auto soon = 300;
+  auto later = 500;
+  CacheEntry(cache, "aaaa1111111111111111111111111111", soon);
+  CacheEntry(cache, "bbbb1111111111111111111111111111", later);
+  CacheEntry(cache, "cccc1111111111111111111111111111", soon);
+  CacheEntry(cache, "dddd1111111111111111111111111111", soon);
+  CacheEntry(cache, "eeee1111111111111111111111111111", soon);
+  CacheEntry(cache, "ffff1111111111111111111111111111", later);
+  CacheEntry(cache, "gggg1111111111111111111111111111", later);
+  CacheEntry(cache, "hhhh1111111111111111111111111111", soon);
+
+  auto validate_cache_contents = [](std::unique_ptr<HashRealTimeCache>&
+                                        cache_internal) {
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"aaaa"}), "aaaa"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"bbbb"}), "bbbb"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"cccc"}), "cccc"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"dddd"}), "dddd"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"eeee"}), "eeee"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"ffff"}), "ffff"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"gggg"}), "gggg"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"hhhh"}), "hhhh"));
+  };
+
+  // After 400 seconds, all of the "soon" prefixes have expired, and none of the
+  // "later" prefixes have.
+  task_environment_.FastForwardBy(base::Seconds(400));
+  EXPECT_EQ(GetNumCacheEntries(cache), 8);
+  validate_cache_contents(cache);
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 3);
+  validate_cache_contents(cache);
+}
+
+TEST_F(HashRealTimeCacheTest,
+       TestClearExpiredResults_SomeExpiredResultsReversed) {
+  // The main difference between TestClearExpiredResults_SomeExpiredResults
+  // above and this one is that whether an entry is expired is reversed. This is
+  // to confirm that the iterative deletion in ClearExpiredResults works as
+  // expected regardless of ordering.
+  auto cache = std::make_unique<HashRealTimeCache>();
+  auto soon = 300;
+  auto later = 500;
+  CacheEntry(cache, "aaaa1111111111111111111111111111", later);
+  CacheEntry(cache, "bbbb1111111111111111111111111111", soon);
+  CacheEntry(cache, "cccc1111111111111111111111111111", later);
+  CacheEntry(cache, "dddd1111111111111111111111111111", later);
+  CacheEntry(cache, "eeee1111111111111111111111111111", later);
+  CacheEntry(cache, "ffff1111111111111111111111111111", soon);
+  CacheEntry(cache, "gggg1111111111111111111111111111", soon);
+  CacheEntry(cache, "hhhh1111111111111111111111111111", later);
+
+  auto validate_cache_contents = [](std::unique_ptr<HashRealTimeCache>&
+                                        cache_internal) {
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"aaaa"}), "aaaa"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"bbbb"}), "bbbb"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"cccc"}), "cccc"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"dddd"}), "dddd"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"eeee"}), "eeee"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"ffff"}), "ffff"));
+    EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"gggg"}), "gggg"));
+    EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"hhhh"}), "hhhh"));
+  };
+
+  // After 400 seconds, all of the "soon" prefixes have expired, and none of the
+  // "later" prefixes have.
+  task_environment_.FastForwardBy(base::Seconds(400));
+  EXPECT_EQ(GetNumCacheEntries(cache), 8);
+  validate_cache_contents(cache);
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 5);
+  validate_cache_contents(cache);
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_AllExpiredResults) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+  CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
+  CacheEntry(cache, "cccc1111111111111111111111111111", 500);
+
+  // After 500 seconds, both have expired.
+  task_environment_.FastForwardBy(base::Seconds(500));
+  EXPECT_EQ(GetNumCacheEntries(cache), 2);
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+  cache->ClearExpiredResults();
+  EXPECT_EQ(GetNumCacheEntries(cache), 0);
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
+  EXPECT_FALSE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
+}
+
+TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_Logging) {
+  auto cache = std::make_unique<HashRealTimeCache>();
+
+  // Cache is empty.
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/0, /*num_full_hashes=*/0);
+
+  // Cache has 1 hash prefix with 1 full hash in it.
+  cache->CacheSearchHashesResponse(
+      {"aaaa"},
+      {CreateBasicFullHash("aaaa1111111111111111111111111111",
+                           {V5::ThreatType::MALWARE})},
+      CreateCacheDuration(300, 0));
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/1);
+
+  // Cache has 2 hash prefixes and 3 full hashes (aaaa entry from above remains
+  // included).
+  cache->CacheSearchHashesResponse(
+      {"bbbb"},
+      {CreateBasicFullHash("bbbb1111111111111111111111111111",
+                           {V5::ThreatType::MALWARE}),
+       CreateBasicFullHash("bbbb2222222222222222222222222222",
+                           {V5::ThreatType::MALWARE})},
+      CreateCacheDuration(500, 0));
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/2, /*num_full_hashes=*/3);
+
+  // 400 seconds later, the first addition to the cache has expired. The logs
+  // should still report 2 hash prefixes and 3 full hashes, because they report
+  // the size at the time the cache started being cleared, not afterwards.
+  task_environment_.FastForwardBy(base::Seconds(400));
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/2, /*num_full_hashes=*/3);
+
+  // Clearing the expired results again now displays the size with just the
+  // second addition to the cache.
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/2);
+
+  // 100 seconds later, the second addition to the cache has expired. The log
+  // still includes it in the size (same rationale as above).
+  task_environment_.FastForwardBy(base::Seconds(100));
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/2);
+
+  // Clearing the expired results again now logs that the cache is empty.
+  cache->ClearExpiredResults();
+  CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/0, /*num_full_hashes=*/0);
+}
+
+}  // namespace safe_browsing
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.cc b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.cc
index 84da70d..3fed5c34 100644
--- a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.cc
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.cc
@@ -5,10 +5,13 @@
 #include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h"
 
 #include "base/base64url.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/histogram_macros.h"
 #include "base/strings/escape.h"
 #include "base/strings/stringprintf.h"
 #include "base/task/sequenced_task_runner.h"
 #include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_utils.h"
+#include "components/safe_browsing/core/browser/verdict_cache_manager.h"
 #include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
 #include "components/safe_browsing/core/common/utils.h"
 #include "google_apis/google_api_keys.h"
@@ -61,8 +64,10 @@
 }  // namespace
 
 HashRealTimeService::HashRealTimeService(
-    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
+    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+    VerdictCacheManager* cache_manager)
     : url_loader_factory_(url_loader_factory),
+      cache_manager_(cache_manager),
       backoff_operator_(std::make_unique<BackoffOperator>(
           /*num_failures_to_enforce_backoff=*/kNumFailuresToEnforceBackoff,
           /*min_backoff_reset_duration_in_seconds=*/
@@ -88,11 +93,13 @@
                                         url_full_hashes_vector.end());
   SBThreatType sb_threat_type = SBThreatType::SB_THREAT_TYPE_SAFE;
   int threat_severity = kLeastSeverity;
+  int num_full_hash_matches = 0;
   for (const auto& hash_proto : result_full_hashes) {
     auto it = url_full_hashes.find(hash_proto.full_hash());
     if (url_full_hashes.end() != it) {
       for (const auto& detail : hash_proto.full_hash_details()) {
         if (hash_realtime_utils::IsThreatTypeRelevant(detail.threat_type())) {
+          ++num_full_hash_matches;
           // Note that for hash-prefix real-time checks, there is no need to use
           // the attributes field, because all the checks are for frame URLs.
           if (IsThreatTypeMoreSevere(detail.threat_type(), threat_severity)) {
@@ -103,6 +110,8 @@
       }
     }
   }
+  base::UmaHistogramCounts100("SafeBrowsing.HPRT.ThreatInfoSize",
+                              num_full_hash_matches);
   return sb_threat_type;
 }
 int HashRealTimeService::GetThreatSeverity(const V5::ThreatType& threat_type) {
@@ -134,7 +143,46 @@
 
 bool HashRealTimeService::IsInBackoffMode() const {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  return backoff_operator_->IsInBackoffMode();
+  bool in_backoff = backoff_operator_->IsInBackoffMode();
+  base::UmaHistogramBoolean("SafeBrowsing.HPRT.Backoff.State", in_backoff);
+  return in_backoff;
+}
+
+std::set<std::string> HashRealTimeService::GetHashPrefixesSet(
+    const GURL& url) const {
+  std::vector<std::string> full_hashes;
+  V4ProtocolManagerUtil::UrlToFullHashes(url, &full_hashes);
+  std::set<std::string> hash_prefixes;
+  for (const auto& full_hash : full_hashes) {
+    auto hash_prefix = hash_realtime_utils::GetHashPrefix(full_hash);
+    hash_prefixes.insert(hash_prefix);
+  }
+  return hash_prefixes;
+}
+
+void HashRealTimeService::SearchCache(
+    std::set<std::string> hash_prefixes,
+    std::vector<std::string>* out_missing_hash_prefixes,
+    std::vector<V5::FullHash>* out_cached_full_hashes) const {
+  SCOPED_UMA_HISTOGRAM_TIMER("SafeBrowsing.HPRT.GetCache.Time");
+  auto cached_results =
+      cache_manager_
+          ? cache_manager_->GetCachedHashPrefixRealTimeLookupResults(
+                hash_prefixes)
+          : std::unordered_map<std::string, std::vector<V5::FullHash>>();
+  for (const auto& hash_prefix : hash_prefixes) {
+    auto cached_result_it = cached_results.find(hash_prefix);
+    if (cached_result_it != cached_results.end()) {
+      // If in the cache, keep track of associated full hashes to merge them
+      // with the response results later.
+      for (const auto& cached_full_hash : cached_result_it->second) {
+        out_cached_full_hashes->push_back(cached_full_hash);
+      }
+    } else {
+      // If not in the cache, add the prefix to hash prefixes to request.
+      out_missing_hash_prefixes->push_back(hash_prefix);
+    }
+  }
 }
 
 void HashRealTimeService::StartLookup(
@@ -144,12 +192,27 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(url.is_valid());
 
+  // Search local cache.
+  std::vector<std::string> hash_prefixes_to_request;
+  std::vector<V5::FullHash> cached_full_hashes;
+  SearchCache(GetHashPrefixesSet(url), &hash_prefixes_to_request,
+              &cached_full_hashes);
+  base::UmaHistogramBoolean("SafeBrowsing.HPRT.CacheHitAllPrefixes",
+                            hash_prefixes_to_request.empty());
+  // If all the prefixes are in the cache, no need to send a request. Return
+  // early with the cached results.
+  if (hash_prefixes_to_request.empty()) {
+    auto sb_threat_type = DetermineSBThreatType(url, cached_full_hashes);
+    callback_task_runner->PostTask(
+        FROM_HERE,
+        base::BindOnce(std::move(response_callback),
+                       /*is_lookup_successful=*/true, sb_threat_type));
+    return;
+  }
+
   // Prepare request.
   auto request = std::make_unique<V5::SearchHashesRequest>();
-  std::vector<std::string> full_hashes;
-  V4ProtocolManagerUtil::UrlToFullHashes(url, &full_hashes);
-  for (const auto& full_hash : full_hashes) {
-    auto hash_prefix = hash_realtime_utils::GetHashPrefix(full_hash);
+  for (const auto& hash_prefix : hash_prefixes_to_request) {
     request->add_hash_prefixes(hash_prefix);
   }
 
@@ -157,19 +220,26 @@
   std::unique_ptr<network::SimpleURLLoader> owned_loader =
       network::SimpleURLLoader::Create(GetResourceRequest(std::move(request)),
                                        GetTrafficAnnotationTag());
+  base::UmaHistogramCounts100("SafeBrowsing.HPRT.Request.CountOfPrefixes",
+                              hash_prefixes_to_request.size());
   owned_loader->SetTimeoutDuration(
       base::Seconds(kLookupTimeoutDurationInSeconds));
   owned_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
       url_loader_factory_.get(),
       base::BindOnce(&HashRealTimeService::OnURLLoaderComplete,
-                     weak_factory_.GetWeakPtr(), url, owned_loader.get(),
-                     std::move(callback_task_runner)));
+                     weak_factory_.GetWeakPtr(), url,
+                     std::move(hash_prefixes_to_request),
+                     std::move(cached_full_hashes), owned_loader.get(),
+                     base::TimeTicks::Now(), std::move(callback_task_runner)));
   pending_requests_[owned_loader.release()] = std::move(response_callback);
 }
 
 void HashRealTimeService::OnURLLoaderComplete(
     const GURL& url,
+    const std::vector<std::string>& hash_prefixes_in_request,
+    std::vector<V5::FullHash> result_full_hashes,
     network::SimpleURLLoader* url_loader,
+    base::TimeTicks request_start_time,
     scoped_refptr<base::SequencedTaskRunner> response_callback_task_runner,
     std::unique_ptr<std::string> response_body) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -177,19 +247,34 @@
   auto pending_request_it = pending_requests_.find(url_loader);
   DCHECK(pending_request_it != pending_requests_.end()) << "Request not found";
 
+  base::UmaHistogramTimes("SafeBrowsing.HPRT.Network.Time",
+                          base::TimeTicks::Now() - request_start_time);
+
   int net_error = url_loader->NetError();
   int response_code = 0;
   if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
     response_code = url_loader->ResponseInfo()->headers->response_code();
   }
+  RecordHttpResponseOrErrorCode("SafeBrowsing.HPRT.Network.Result", net_error,
+                                response_code);
 
   auto response = ParseResponseAndUpdateBackoff(net_error, response_code,
                                                 std::move(response_body));
   absl::optional<SBThreatType> sb_threat_type;
   if (response.has_value()) {
-    sb_threat_type =
-        DetermineSBThreatType(url, {response.value()->full_hashes().begin(),
-                                    response.value()->full_hashes().end()});
+    if (cache_manager_) {
+      cache_manager_->CacheHashPrefixRealTimeLookupResults(
+          hash_prefixes_in_request,
+          std::vector<V5::FullHash>(response.value()->full_hashes().begin(),
+                                    response.value()->full_hashes().end()),
+          response.value()->cache_duration());
+    }
+
+    // Merge together the results from the cache and from the response.
+    for (const auto& response_hash : response.value()->full_hashes()) {
+      result_full_hashes.push_back(response_hash);
+    }
+    sb_threat_type = DetermineSBThreatType(url, result_full_hashes);
   }
 
   response_callback_task_runner->PostTask(
@@ -201,17 +286,19 @@
   pending_requests_.erase(pending_request_it);
 }
 
-base::expected<std::unique_ptr<V5::SearchHashesResponse>, bool>
+base::expected<std::unique_ptr<V5::SearchHashesResponse>,
+               HashRealTimeService::OperationResult>
 HashRealTimeService::ParseResponseAndUpdateBackoff(
     int net_error,
     int response_code,
     std::unique_ptr<std::string> response_body) const {
   auto response =
       ParseResponse(net_error, response_code, std::move(response_body));
+  LogOperationResult(response.has_value() ? OperationResult::kSuccess
+                                          : response.error());
   if (response.has_value()) {
     backoff_operator_->ReportSuccess();
-  } else if (!response.error()) {
-    // Error is not retriable, so increase backoff.
+  } else if (response.error() != OperationResult::kRetriableError) {
     backoff_operator_->ReportError();
   }
   return response;
@@ -239,7 +326,8 @@
   }
 }
 
-base::expected<std::unique_ptr<V5::SearchHashesResponse>, bool>
+base::expected<std::unique_ptr<V5::SearchHashesResponse>,
+               HashRealTimeService::OperationResult>
 HashRealTimeService::ParseResponse(
     int net_error,
     int response_code,
@@ -248,19 +336,28 @@
   bool net_and_http_ok = net_error == net::OK && response_code == net::HTTP_OK;
   if (net_and_http_ok && response->ParseFromString(*response_body)) {
     if (!response->has_cache_duration()) {
-      return base::unexpected(/*retriable*/ false);
+      return base::unexpected(OperationResult::kNoCacheDurationError);
     }
     for (const auto& full_hash : response->full_hashes()) {
       if (full_hash.full_hash().length() !=
           hash_realtime_utils::kFullHashLength) {
-        return base::unexpected(/*retriable*/ false);
+        return base::unexpected(OperationResult::kIncorrectFullHashLengthError);
       }
     }
     RemoveFullHashDetailsWithInvalidEnums(response);
     return std::move(response);
+  } else if (net_and_http_ok) {
+    return base::unexpected(OperationResult::kParseError);
+  } else if (ErrorIsRetriable(net_error, response_code)) {
+    return base::unexpected(OperationResult::kRetriableError);
+  } else if (net_error != net::OK) {
+    return base::unexpected(OperationResult::kNetworkError);
+  } else if (response_code != net::HTTP_OK) {
+    return base::unexpected(OperationResult::kHttpError);
+  } else {
+    NOTREACHED();
+    return base::unexpected(OperationResult::kNotReached);
   }
-  return base::unexpected(/*retriable*/ !net_and_http_ok &&
-                          ErrorIsRetriable(net_error, response_code));
 }
 
 std::unique_ptr<network::ResourceRequest>
@@ -297,6 +394,15 @@
     delete pending.first;
   }
   pending_requests_.clear();
+
+  // Clear references to other KeyedServices.
+  cache_manager_ = nullptr;
+}
+
+void HashRealTimeService::LogOperationResult(
+    OperationResult operation_result) const {
+  base::UmaHistogramEnumeration("SafeBrowsing.HPRT.OperationResult",
+                                operation_result);
 }
 
 net::NetworkTrafficAnnotationTag HashRealTimeService::GetTrafficAnnotationTag()
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h
index 26a2fa0..4cb26b3 100644
--- a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h
@@ -35,21 +35,46 @@
 using HPRTLookupResponseCallback =
     base::OnceCallback<void(bool, absl::optional<SBThreatType>)>;
 
-// This class implements the backoff logic and lookup request for hash-prefix
-// real-time lookups. For testing purposes, the request is currently sent to the
-// Safe Browsing server directly. In the future, it will be sent to a proxy via
-// OHTTP.
+class VerdictCacheManager;
+
+// This class implements the backoff logic, cache logic, and lookup request for
+// hash-prefix real-time lookups. For testing purposes, the request is currently
+// sent to the Safe Browsing server directly. In the future, it will be sent to
+// a proxy via OHTTP.
 // TODO(1407283): Update "For testing purposes..." portion of description.
 class HashRealTimeService : public KeyedService {
  public:
   explicit HashRealTimeService(
-      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
+      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+      VerdictCacheManager* cache_manager);
 
   HashRealTimeService(const HashRealTimeService&) = delete;
   HashRealTimeService& operator=(const HashRealTimeService&) = delete;
 
   ~HashRealTimeService() override;
 
+  // These values are persisted to logs. Entries should not be renumbered and
+  // numeric values should never be reused.
+  enum class OperationResult {
+    // The lookup was successful.
+    kSuccess = 0,
+    // Parsing the response to a string failed.
+    kParseError = 1,
+    // There was no cache duration in the parsed response.
+    kNoCacheDurationError = 2,
+    // At least one full hash in the parsed response had the wrong length.
+    kIncorrectFullHashLengthError = 3,
+    // There was a retriable error.
+    kRetriableError = 4,
+    // There was an error in the network stack.
+    kNetworkError = 5,
+    // There was an error in the HTTP response code.
+    kHttpError = 6,
+    // There is a bug in the code leading to a NOTREACHED branch.
+    kNotReached = 7,
+    kMaxValue = kNotReached,
+  };
+
   // Returns whether the |url| is eligible for hash-prefix real-time checks.
   // It's never eligible if the |request_destination| is not mainframe.
   static bool CanCheckUrl(
@@ -89,23 +114,35 @@
 
   // Called when the response from the Safe Browsing V5 remote endpoint is
   // received. This is responsible for parsing the response, determining if
-  // there were errors and updating backoff if relevant, determining the most
-  // severe threat type, and calling the callback.
+  // there were errors and updating backoff if relevant, caching the results,
+  // determining the most severe threat type, and calling the callback.
   //  - |url| is used to match the full hashes in the response with the URL's
   //    full hashes.
+  //  - |hash_prefixes_in_request| is used to cache the mapping of the requested
+  //    hash prefixes to the results.
+  //  - |result_full_hashes| starts out as the initial results from the cache.
+  //    This method mutates this parameter to include the results from the
+  //    server response as well, and then uses the combined results to determine
+  //    the most severe threat type.
   //  - |url_loader| is the loader that was used to send the request.
+  //  - |request_start_time| represents when the request was sent, and is used
+  //    for logging.
   //  - |response_callback_task_runner| is the callback the original caller
   //    passed through that will be called when the method completes.
   //  - |response_body| is the unparsed response from the server.
   void OnURLLoaderComplete(
       const GURL& url,
+      const std::vector<std::string>& hash_prefixes_in_request,
+      std::vector<V5::FullHash> result_full_hashes,
       network::SimpleURLLoader* url_loader,
+      base::TimeTicks request_start_time,
       scoped_refptr<base::SequencedTaskRunner> response_callback_task_runner,
       std::unique_ptr<std::string> response_body);
 
   // Determines the most severe threat type based on |result_full_hashes|, which
-  // contains the server response results. The |url| is required in order to
-  // filter down |result_full_hashes| to ones that match the |url| full hashes.
+  // contains the merged caching and server response results. The |url| is
+  // required in order to filter down |result_full_hashes| to ones that match
+  // the |url| full hashes.
   static SBThreatType DetermineSBThreatType(
       const GURL& url,
       const std::vector<V5::FullHash>& result_full_hashes);
@@ -121,22 +158,25 @@
   static bool IsThreatTypeMoreSevere(const V5::ThreatType& threat_type,
                                      int baseline_severity);
 
+  // Logs whether the lookup succeeded, and if not, why not.
+  void LogOperationResult(OperationResult operation_result) const;
+
   // In addition to attempting to parse the |response_body| as described in the
   // |ParseResponse| function comments, this updates the backoff state depending
   // on the lookup success.
-  base::expected<std::unique_ptr<V5::SearchHashesResponse>, bool>
+  base::expected<std::unique_ptr<V5::SearchHashesResponse>, OperationResult>
   ParseResponseAndUpdateBackoff(
       int net_error,
       int http_error,
       std::unique_ptr<std::string> response_body) const;
 
   // Tries to parse the |response_body| into a |SearchHashesResponse|, and
-  // returns either the response proto or a bool representing whether the error
-  // encountered was retriable.
-  base::expected<std::unique_ptr<V5::SearchHashesResponse>, bool> ParseResponse(
-      int net_error,
-      int http_error,
-      std::unique_ptr<std::string> response_body) const;
+  // returns either the response proto or an |OperationResult| with details on
+  // why the parsing was unsuccessful.
+  base::expected<std::unique_ptr<V5::SearchHashesResponse>, OperationResult>
+  ParseResponse(int net_error,
+                int http_error,
+                std::unique_ptr<std::string> response_body) const;
 
   // Removes any |FullHashDetail| within the |response| that has invalid
   // |ThreatType| or |ThreatAttribute| enums. This is for forward compatibility,
@@ -145,11 +185,26 @@
   void RemoveFullHashDetailsWithInvalidEnums(
       std::unique_ptr<V5::SearchHashesResponse>& response) const;
 
+  // Returns the hash prefixes for the URL's lookup expressions.
+  std::set<std::string> GetHashPrefixesSet(const GURL& url) const;
+
+  // Searches the local cache for the input |hash_prefixes|.
+  //  - |out_missing_hash_prefixes| is an output parameter with a list of which
+  //    hash prefixes were not found in the cache and need to be requested.
+  //  - |out_cached_full_hashes| is an output parameter with a list of unsafe
+  //    full hashes that were found in the cache for any of the |hash_prefixes|.
+  void SearchCache(std::set<std::string> hash_prefixes,
+                   std::vector<std::string>* out_missing_hash_prefixes,
+                   std::vector<V5::FullHash>* out_cached_full_hashes) const;
+
   SEQUENCE_CHECKER(sequence_checker_);
 
   // The URLLoaderFactory we use to issue network requests.
   scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
 
+  // Unowned object used for getting and storing cache entries.
+  raw_ptr<VerdictCacheManager> cache_manager_;
+
   // All requests that are sent but haven't received a response yet.
   PendingHPRTLookupRequests pending_requests_;
 
diff --git a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service_unittest.cc b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service_unittest.cc
index 0bb7f8e..6f5903f 100644
--- a/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service_unittest.cc
+++ b/components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service_unittest.cc
@@ -10,11 +10,14 @@
 #include "base/strings/escape.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/mock_callback.h"
 #include "base/test/task_environment.h"
 #include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
 #include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_utils.h"
+#include "components/safe_browsing/core/browser/verdict_cache_manager.h"
 #include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "google_apis/google_api_keys.h"
 #include "services/network/public/cpp/shared_url_loader_factory.h"
 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
@@ -24,6 +27,14 @@
 
 namespace safe_browsing {
 
+namespace {
+// These two URLs have the same hash prefix. They were determined via a for loop
+// up to 100,000 looking for the first two "https://example.a#" URLs whose hash
+// prefixes matched.
+constexpr char kUrlWithMatchingHashPrefix1[] = "https://example.a23549";
+constexpr char kUrlWithMatchingHashPrefix2[] = "https://example.a3945";
+}  // namespace
+
 class HashRealTimeServiceTest : public PlatformTest {
  public:
   void CreateHashRealTimeService() {
@@ -32,11 +43,24 @@
     test_shared_loader_factory_ =
         base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
             test_url_loader_factory_.get());
-    service_ =
-        std::make_unique<HashRealTimeService>(test_shared_loader_factory_);
+    content_setting_map_ = base::MakeRefCounted<HostContentSettingsMap>(
+        &test_pref_service_, /*is_off_the_record=*/false,
+        /*store_last_modified=*/false, /*restore_session=*/false,
+        /*should_record_metrics=*/false);
+    VerdictCacheManager* cache_manager_ptr = nullptr;
+    if (include_cache_manager_) {
+      cache_manager_ = std::make_unique<VerdictCacheManager>(
+          /*history_service=*/nullptr, content_setting_map_.get(),
+          &test_pref_service_,
+          /*sync_observer=*/nullptr);
+      cache_manager_ptr = cache_manager_.get();
+    }
+    service_ = std::make_unique<HashRealTimeService>(
+        test_shared_loader_factory_, cache_manager_ptr);
   }
   void SetUp() override {
     PlatformTest::SetUp();
+    HostContentSettingsMap::RegisterProfilePrefs(test_pref_service_.registry());
     CreateHashRealTimeService();
     std::string key = google_apis::GetAPIKey();
     key_param_ =
@@ -46,6 +70,10 @@
             : "";
   }
   void TearDown() override {
+    cache_manager_.reset();
+    if (content_setting_map_) {
+      content_setting_map_->ShutdownOnUIThread();
+    }
     service_->Shutdown();
     PlatformTest::TearDown();
   }
@@ -69,6 +97,10 @@
     }
     return hash_prefixes;
   }
+  std::set<HashPrefixStr> UrlToHashPrefixesAsSet(const GURL& url) {
+    auto hash_prefixes = UrlToHashPrefixes(url);
+    return std::set(hash_prefixes.begin(), hash_prefixes.end());
+  }
   FullHashStr UrlToSingleHashPrefix(const GURL& url) {
     auto hash_prefixes = UrlToHashPrefixes(url);
     EXPECT_EQ(hash_prefixes.size(), 1u);
@@ -101,6 +133,42 @@
     cache_duration->set_seconds(300);
     SetUpLookupResponseHelper(request_url, response);
   }
+  void ResetMetrics() {
+    histogram_tester_ = std::make_unique<base::HistogramTester>();
+  }
+  void CheckPreRequestMetrics(bool expect_cache_hit_all_prefixes) {
+    histogram_tester_->ExpectTotalCount(
+        /*name=*/"SafeBrowsing.HPRT.GetCache.Time", /*expected_count=*/1);
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.CacheHitAllPrefixes",
+        /*sample=*/expect_cache_hit_all_prefixes,
+        /*expected_bucket_count=*/1);
+  }
+  void CheckPostRequestMetrics(int expected_threat_info_size) {
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.ThreatInfoSize",
+        /*sample=*/expected_threat_info_size,
+        /*expected_bucket_count=*/1);
+  }
+  void CheckRequestMetrics(
+      int expected_prefix_count,
+      int expected_network_result,
+      HashRealTimeService::OperationResult expected_operation_result) {
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.Request.CountOfPrefixes",
+        /*sample=*/expected_prefix_count,
+        /*expected_bucket_count=*/1);
+    histogram_tester_->ExpectTotalCount(
+        /*name=*/"SafeBrowsing.HPRT.Network.Time", /*expected_count=*/1);
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.Network.Result",
+        /*sample=*/expected_network_result,
+        /*expected_bucket_count=*/1);
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.OperationResult",
+        /*sample=*/expected_operation_result,
+        /*expected_bucket_count=*/1);
+  }
   V5::FullHash CreateFullHashProto(
       std::vector<V5::ThreatType> threat_types,
       std::string full_hash,
@@ -124,13 +192,16 @@
   }
   void StartSuccessRequest(
       const GURL& url,
+      const std::set<FullHashStr>& cached_hash_prefixes,
       base::MockCallback<HPRTLookupResponseCallback>& response_callback,
       const std::vector<V5::FullHash>& response_full_hashes,
       SBThreatType expected_threat_type) {
     // Intercept search hashes request URL.
     auto request = std::make_unique<V5::SearchHashesRequest>();
-    for (const auto& hash_prefix : UrlToHashPrefixes(url)) {
-      request->add_hash_prefixes(hash_prefix);
+    for (const auto& hash_prefix : UrlToHashPrefixesAsSet(url)) {
+      if (!base::Contains(cached_hash_prefixes, hash_prefix)) {
+        request->add_hash_prefixes(hash_prefix);
+      }
     }
     std::string expected_url = GetExpectedRequestUrl(request);
     test_url_loader_factory_->SetInterceptor(base::BindLambdaForTesting(
@@ -152,26 +223,50 @@
     service_->StartLookup(url, response_callback.Get(),
                           base::SequencedTaskRunner::GetCurrentDefault());
   }
+  // Starts a lookup on |url| that is expected to succeed, simulating the server
+  // response as |response_full_hashes|. Confirms that |expected_threat_type| is
+  // returned through the lookup's callback. |cached_hash_prefixes| can be empty
+  // or can specify which hash prefixes are already in the cache and should
+  // therefore not be sent in the request.
   void RunRequestSuccessTest(const GURL& url,
+                             const std::set<FullHashStr>& cached_hash_prefixes,
                              std::vector<V5::FullHash> response_full_hashes,
-                             SBThreatType expected_threat_type) {
+                             SBThreatType expected_threat_type,
+                             int expected_prefix_count,
+                             int expected_threat_info_size) {
     auto num_requests = test_url_loader_factory_->total_requests();
     base::MockCallback<HPRTLookupResponseCallback> response_callback;
-    StartSuccessRequest(url, response_callback, response_full_hashes,
-                        expected_threat_type);
+    StartSuccessRequest(url, cached_hash_prefixes, response_callback,
+                        response_full_hashes, expected_threat_type);
     task_environment_.RunUntilIdle();
+
+    CheckPreRequestMetrics(/*expect_cache_hit_all_prefixes=*/false);
+    CheckRequestMetrics(
+        /*expected_prefix_count=*/expected_prefix_count,
+        /*expected_network_result=*/200,
+        /*expected_operation_result=*/
+        HashRealTimeService::OperationResult::kSuccess);
+    CheckPostRequestMetrics(expected_threat_info_size);
+    ResetMetrics();
+
     EXPECT_EQ(test_url_loader_factory_->total_requests(), num_requests + 1u);
   }
+  // Starts a lookup on |url| that is expected to fail. The simulated server
+  // response body can be specified either by |response_full_hashes| or by
+  // |custom_response|. |status| represents the simulated server response code.
+  // Confirms that the lookup fails.
   void RunRequestFailureTest(
       const GURL& url,
       const absl::optional<std::vector<V5::FullHash>>& response_full_hashes,
       const absl::optional<std::string>& custom_response,
-      net::HttpStatusCode status) {
+      net::HttpStatusCode status,
+      int expected_prefix_count,
+      HashRealTimeService::OperationResult expected_operation_result) {
     auto num_requests = test_url_loader_factory_->total_requests();
 
     // Set up request and response.
     auto request = std::make_unique<V5::SearchHashesRequest>();
-    for (const auto& hash_prefix : UrlToHashPrefixes(url)) {
+    for (const auto& hash_prefix : UrlToHashPrefixesAsSet(url)) {
       request->add_hash_prefixes(hash_prefix);
     }
     std::string expected_url = GetExpectedRequestUrl(request);
@@ -196,13 +291,47 @@
                           base::SequencedTaskRunner::GetCurrentDefault());
     task_environment_.RunUntilIdle();
 
+    CheckPreRequestMetrics(/*expect_cache_hit_all_prefixes=*/false);
+    CheckRequestMetrics(
+        /*expected_prefix_count=*/expected_prefix_count,
+        /*expected_network_result=*/status,
+        /*expected_operation_result=*/
+        expected_operation_result);
+    ResetMetrics();
+
     EXPECT_EQ(test_url_loader_factory_->total_requests(), num_requests + 1u);
   }
+  // Starts a lookup on |url| that should already be found entirely in the cache
+  // and therefore not require a request to be sent. Confirms that the lookup's
+  // callback is called with the |expected_threat_type| and that no requests are
+  // made.
+  void RunFullyCachedRequestTest(const GURL& url,
+                                 SBThreatType expected_threat_type,
+                                 int expected_threat_info_size) {
+    auto num_requests = test_url_loader_factory_->total_requests();
+    base::MockCallback<HPRTLookupResponseCallback> response_callback;
+    // Confirm request response will be called once with the relevant threat
+    // type.
+    EXPECT_CALL(response_callback,
+                Run(/*is_lookup_successful=*/true,
+                    /*sb_threat_type=*/testing::Optional(expected_threat_type)))
+        .Times(1);
+    service_->StartLookup(url, response_callback.Get(),
+                          base::SequencedTaskRunner::GetCurrentDefault());
+    task_environment_.RunUntilIdle();
+
+    CheckPreRequestMetrics(/*expect_cache_hit_all_prefixes=*/true);
+    CheckPostRequestMetrics(
+        /*expected_threat_info_size=*/expected_threat_info_size);
+    ResetMetrics();
+
+    EXPECT_EQ(test_url_loader_factory_->total_requests(), num_requests);
+  }
   void RunSimpleRequest(const GURL& url,
                         const std::vector<V5::FullHash>& response_full_hashes) {
     // Set up request response.
     auto request = std::make_unique<V5::SearchHashesRequest>();
-    for (const auto& hash_prefix : UrlToHashPrefixes(url)) {
+    for (const auto& hash_prefix : UrlToHashPrefixesAsSet(url)) {
       request->add_hash_prefixes(hash_prefix);
     }
     std::string expected_url = GetExpectedRequestUrl(request);
@@ -232,8 +361,19 @@
   std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;
   scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
   std::string key_param_;
+  std::unique_ptr<VerdictCacheManager> cache_manager_;
+  scoped_refptr<HostContentSettingsMap> content_setting_map_;
+  sync_preferences::TestingPrefServiceSyncable test_pref_service_;
   base::test::TaskEnvironment task_environment_{
       base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  std::unique_ptr<base::HistogramTester> histogram_tester_ =
+      std::make_unique<base::HistogramTester>();
+  bool include_cache_manager_ = true;
+};
+
+class HashRealTimeServiceNoCacheManagerTest : public HashRealTimeServiceTest {
+ public:
+  HashRealTimeServiceNoCacheManagerTest() { include_cache_manager_ = false; }
 };
 
 TEST_F(HashRealTimeServiceTest, TestCanCheckUrl) {
@@ -268,86 +408,107 @@
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_Safe) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/{},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE);
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/{},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/0);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_Phishing) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/1);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_Malware) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::MALWARE},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/1);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_UnwantedSoftware) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/1);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_Suspicious) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SUSPICIOUS},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SUSPICIOUS_SITE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SUSPICIOUS_SITE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/1);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_Billing) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::TRICK_TO_BILL},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_BILLING);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_BILLING,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/1);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OneHash_IrrelevantThreatType) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::API_ABUSE},
                            UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/0);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_OverlappingHashPrefixes) {
-  GURL url1 = GURL("https://example.a23549");
-  GURL url2 = GURL("https://example.a3945");
+  GURL url1 = GURL(kUrlWithMatchingHashPrefix1);
+  GURL url2 = GURL(kUrlWithMatchingHashPrefix2);
   // To make sure this test is a useful test, sanity check that the URL hash
   // prefixes are indeed the same.
   EXPECT_EQ(UrlToSingleHashPrefix(url1), UrlToSingleHashPrefix(url2));
   RunRequestSuccessTest(
-      /*url=*/url1, /*response_full_hashes=*/
+      /*url=*/url1, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToSingleFullHash(url2))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/0);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_MaxHashes_Phishing) {
   GURL url = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
+  // |expected_prefix_count| is 30 because this is the maximum number of host
+  // suffix and path prefix combinations, as described in
+  // https://developers.google.com/safe-browsing/v4/urls-hashing#suffixprefix-expressions.
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToFullHashes(url)[0]),
        CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToFullHashes(url)[15])},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/30,
+      /*expected_threat_info_size=*/2);
 }
 
 TEST_F(HashRealTimeServiceTest,
@@ -355,8 +516,11 @@
   GURL url = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
   std::string non_matching_full_hash =
       UrlToHashPrefixes(url)[0] + "1111111111111111111111111111";
+  // |expected_prefix_count| is 30 because this is the maximum number of host
+  // suffix and path prefix combinations, as described in
+  // https://developers.google.com/safe-browsing/v4/urls-hashing#suffixprefix-expressions.
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToFullHashes(url)[0]),
        CreateFullHashProto({V5::ThreatType::MALWARE}, UrlToFullHashes(url)[2]),
@@ -376,14 +540,19 @@
        CreateFullHashProto(
            {V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE},
            non_matching_full_hash)},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/30,
+      /*expected_threat_info_size=*/9);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_MaxHashes_OnlyIrrelevant) {
   GURL url = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
   std::string rest_of_hash = "1111111111111111111111111111";
+  // |expected_prefix_count| is 30 because this is the maximum number of host
+  // suffix and path prefix combinations, as described in
+  // https://developers.google.com/safe-browsing/v4/urls-hashing#suffixprefix-expressions.
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToHashPrefixes(url)[0] + rest_of_hash),
        CreateFullHashProto({V5::ThreatType::MALWARE},
@@ -401,18 +570,22 @@
            UrlToHashPrefixes(url)[8] + rest_of_hash),
        CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToHashPrefixes(url)[15] + rest_of_hash)},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/30,
+      /*expected_threat_info_size=*/0);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_CompetingSeverities) {
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
-      /*url=*/url, /*response_full_hashes=*/
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto(
           {V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::MALWARE,
            V5::ThreatType::SOCIAL_ENGINEERING},
           UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/3);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_Attributes) {
@@ -423,12 +596,14 @@
       {V5::ThreatAttribute::FRAME_ONLY}};
   RunRequestSuccessTest(
       /*url=*/url,
-      /*response_full_hashes=*/
+      /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto(
           {V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::MALWARE,
            V5::ThreatType::SOCIAL_ENGINEERING},
           UrlToSingleFullHash(url), attributes)},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/3);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_InvalidThreatTypes) {
@@ -436,9 +611,11 @@
                          SBThreatType expected_threat_type,
                          int expected_threat_info_size) {
     RunRequestSuccessTest(
-        /*url=*/url, /*response_full_hashes=*/
+        /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
         {CreateFullHashProto(threat_types, UrlToSingleFullHash(url))},
-        /*expected_threat_type=*/expected_threat_type);
+        /*expected_threat_type=*/expected_threat_type,
+        /*expected_prefix_count=*/1,
+        /*expected_threat_info_size=*/expected_threat_info_size);
   };
   // Sanity check the static casting on a valid threat type is not filtered out.
   run_test(GURL("https://example.test1"), {static_cast<V5::ThreatType>(2)},
@@ -474,12 +651,14 @@
         {static_cast<V5::ThreatAttribute>(-2)}};
     RunRequestSuccessTest(
         /*url=*/url,
-        /*response_full_hashes=*/
+        /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
         {CreateFullHashProto(
             {V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::MALWARE,
              V5::ThreatType::SOCIAL_ENGINEERING},
             UrlToSingleFullHash(url), attributes)},
-        /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED);
+        /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED,
+        /*expected_prefix_count=*/1,
+        /*expected_threat_info_size=*/1);
   }
   // Threat types are the same.
   {
@@ -491,13 +670,15 @@
         {static_cast<V5::ThreatAttribute>(-2)}};
     RunRequestSuccessTest(
         /*url=*/url,
-        /*response_full_hashes=*/
+        /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
         {CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE,
                               V5::ThreatType::UNWANTED_SOFTWARE,
                               V5::ThreatType::UNWANTED_SOFTWARE,
                               V5::ThreatType::UNWANTED_SOFTWARE},
                              UrlToSingleFullHash(url), attributes)},
-        /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED);
+        /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_UNWANTED,
+        /*expected_prefix_count=*/1,
+        /*expected_threat_info_size=*/2);
   }
 }
 
@@ -507,23 +688,27 @@
   GURL url1 = GURL("https://example.test1");
   RunRequestSuccessTest(
       /*url=*/url1,
-      /*response_full_hashes=*/
+      /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::MALWARE},
                            UrlToSingleFullHash(url1)),
        CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE},
                            UrlToSingleFullHash(url1))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/2);
   // Run it with the server responses backwards as well to confirm order doesn't
   // matter.
   GURL url2 = GURL("https://example.test2");
   RunRequestSuccessTest(
       /*url=*/url2,
-      /*response_full_hashes=*/
+      /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE},
                            UrlToSingleFullHash(url2)),
        CreateFullHashProto({V5::ThreatType::MALWARE},
                            UrlToSingleFullHash(url2))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/2);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookup_DuplicateFullHashDetailsInResponse) {
@@ -532,12 +717,14 @@
   GURL url = GURL("https://example.test");
   RunRequestSuccessTest(
       /*url=*/url,
-      /*response_full_hashes=*/
+      /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
       {CreateFullHashProto(
           {V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::UNWANTED_SOFTWARE,
            V5::ThreatType::MALWARE, V5::ThreatType::UNWANTED_SOFTWARE},
           UrlToSingleFullHash(url))},
-      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/4);
 }
 
 TEST_F(HashRealTimeServiceTest, TestLookupFailure_Error) {
@@ -545,14 +732,18 @@
   RunRequestFailureTest(
       /*url=*/url, /*response_full_hashes=*/absl::nullopt,
       /*custom_response=*/absl::nullopt,
-      /*status=*/net::HTTP_INTERNAL_SERVER_ERROR);
+      /*status=*/net::HTTP_INTERNAL_SERVER_ERROR, /*expected_prefix_count=*/1,
+      /*expected_operation_result=*/
+      HashRealTimeService::OperationResult::kNetworkError);
 }
 TEST_F(HashRealTimeServiceTest, TestLookupFailure_ParseResponse) {
   GURL url = GURL("https://example.test");
   RunRequestFailureTest(
       /*url=*/url, /*response_full_hashes=*/absl::nullopt,
       /*custom_response=*/"howdy",
-      /*status=*/net::HTTP_OK);
+      /*status=*/net::HTTP_OK, /*expected_prefix_count=*/1,
+      /*expected_operation_result=*/
+      HashRealTimeService::OperationResult::kParseError);
 }
 TEST_F(HashRealTimeServiceTest, TestLookupFailure_IncorrectFullHashLength) {
   GURL url = GURL("https://example.test");
@@ -562,7 +753,9 @@
       absl::optional<std::vector<V5::FullHash>>({CreateFullHashProto(
           {V5::ThreatType::SOCIAL_ENGINEERING}, short_full_hash)}),
       /*custom_response=*/absl::nullopt,
-      /*status=*/net::HTTP_OK);
+      /*status=*/net::HTTP_OK, /*expected_prefix_count=*/1,
+      /*expected_operation_result=*/
+      HashRealTimeService::OperationResult::kIncorrectFullHashLengthError);
 }
 TEST_F(HashRealTimeServiceTest, TestLookupFailure_MissingCacheDuration) {
   GURL url = GURL("https://example.test");
@@ -573,14 +766,155 @@
   RunRequestFailureTest(
       /*url=*/url, /*response_full_hashes=*/{},
       /*custom_response=*/response_str,
-      /*status=*/net::HTTP_OK);
+      /*status=*/net::HTTP_OK, /*expected_prefix_count=*/1,
+      /*expected_operation_result=*/
+      HashRealTimeService::OperationResult::kNoCacheDurationError);
+}
+
+TEST_F(HashRealTimeServiceTest, TestFullyCached_OneHash_Safe) {
+  GURL url = GURL("https://example.test");
+  RunSimpleRequest(
+      /*url=*/url, /*response_full_hashes=*/{});
+  ResetMetrics();
+  RunFullyCachedRequestTest(
+      /*url=*/url,
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_threat_info_size=*/0);
+}
+
+TEST_F(HashRealTimeServiceTest, TestFullyCached_OneHash_Phishing) {
+  GURL url = GURL("https://example.test");
+  RunSimpleRequest(
+      /*url=*/url, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToSingleFullHash(url))});
+  ResetMetrics();
+  RunFullyCachedRequestTest(
+      /*url=*/url,
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_threat_info_size=*/1);
+}
+
+TEST_F(HashRealTimeServiceTest, TestFullyCached_MaxHashes) {
+  GURL url = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
+  RunSimpleRequest(
+      /*url=*/url, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToFullHashes(url)[0]),
+       CreateFullHashProto(
+           {V5::ThreatType::MALWARE, V5::ThreatType::SOCIAL_ENGINEERING},
+           UrlToFullHashes(url)[2]),
+       CreateFullHashProto({V5::ThreatType::API_ABUSE,
+                            V5::ThreatType::POTENTIALLY_HARMFUL_APPLICATION},
+                           UrlToFullHashes(url)[4])});
+  ResetMetrics();
+  RunFullyCachedRequestTest(
+      /*url=*/url,
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_threat_info_size=*/3);
+}
+
+TEST_F(HashRealTimeServiceTest, TestFullyCached_OverlappingHashPrefixes) {
+  GURL url1 = GURL(kUrlWithMatchingHashPrefix1);
+  GURL url2 = GURL(kUrlWithMatchingHashPrefix2);
+  // To make sure this test is a useful test, sanity check that the URL hash
+  // prefixes are indeed the same.
+  EXPECT_EQ(UrlToSingleHashPrefix(url1), UrlToSingleHashPrefix(url2));
+  // Start a lookup for url1, which is a phishing page.
+  RunRequestSuccessTest(
+      /*url=*/url1,
+      /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToSingleFullHash(url1))},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/1, /*expected_threat_info_size=*/1);
+  ResetMetrics();
+  // Start a lookup for url2. This has the same hash prefix as url1, so the
+  // results are fully cached, and no request is sent.
+  RunFullyCachedRequestTest(
+      /*url=*/url2,
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_threat_info_size=*/0);
+}
+
+// Since phishing is more severe than unwanted software, the results from the
+// second request should be used rather than the cached results.
+TEST_F(HashRealTimeServiceTest, TestPartiallyCached_RequestResultsUsed) {
+  GURL url1 = GURL("https://e.f.g/1/2/");
+  RunSimpleRequest(
+      /*url=*/url1, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE},
+                           UrlToFullHashes(url1)[0])});
+  ResetMetrics();
+  GURL url2 = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
+  RunRequestSuccessTest(
+      /*url=*/url2, /*cached_hash_prefixes=*/UrlToHashPrefixesAsSet(url1),
+      /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToFullHashes(url2)[0])},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/24, /*expected_threat_info_size=*/2);
+}
+
+// Since phishing is more severe than unwanted software, the cached results
+// should be used rather than the results from the second request.
+TEST_F(HashRealTimeServiceTest, TestPartiallyCached_CachedResultsUsed) {
+  GURL url1 = GURL("https://e.f.g/1/2/");
+  RunSimpleRequest(
+      /*url=*/url1, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToFullHashes(url1)[0])});
+  ResetMetrics();
+  GURL url2 = GURL("https://a.b.c.d.e.f.g/1/2/3/4/5/6?param=x");
+  RunRequestSuccessTest(
+      /*url=*/url2, /*cached_hash_prefixes=*/UrlToHashPrefixesAsSet(url1),
+      /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::UNWANTED_SOFTWARE},
+                           UrlToFullHashes(url2)[0])},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING,
+      /*expected_prefix_count=*/24, /*expected_threat_info_size=*/2);
+}
+
+TEST_F(HashRealTimeServiceTest, TestCacheDuration) {
+  GURL url = GURL("https://example.test");
+  RunSimpleRequest(
+      /*url=*/url, /*response_full_hashes=*/{});
+  ResetMetrics();
+  // Time = 200 seconds. Re-check the URL, which should be cached.
+  task_environment_.FastForwardBy(base::Seconds(200));
+  RunFullyCachedRequestTest(
+      /*url=*/url,
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_threat_info_size=*/0);
+  // Time = 300 seconds. Re-check the URL, which should no longer be cached and
+  // therefore should send a request.
+  task_environment_.FastForwardBy(base::Seconds(100));
+  RunRequestSuccessTest(
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/{},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/0);
+}
+
+TEST_F(HashRealTimeServiceNoCacheManagerTest, TestNoCaching) {
+  GURL url = GURL("https://example.test");
+  RunSimpleRequest(
+      /*url=*/url, /*response_full_hashes=*/
+      {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
+                           UrlToSingleFullHash(url))});
+  ResetMetrics();
+  RunRequestSuccessTest(
+      /*url=*/url, /*cached_hash_prefixes=*/{}, /*response_full_hashes=*/{},
+      /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_SAFE,
+      /*expected_prefix_count=*/1,
+      /*expected_threat_info_size=*/0);
 }
 
 TEST_F(HashRealTimeServiceTest, TestShutdown) {
   GURL url = GURL("https://example.test");
   // Set up request response.
   auto request = std::make_unique<V5::SearchHashesRequest>();
-  for (const auto& hash_prefix : UrlToHashPrefixes(url)) {
+  for (const auto& hash_prefix : UrlToHashPrefixesAsSet(url)) {
     request->add_hash_prefixes(hash_prefix);
   }
   std::string expected_url = GetExpectedRequestUrl(request);
@@ -601,26 +935,33 @@
   GURL url2 = GURL("https://example.test2");
   base::MockCallback<HPRTLookupResponseCallback> response_callback1;
   StartSuccessRequest(
-      /*url=*/url1,
+      /*url=*/url1, /*cached_hash_prefixes=*/{},
       /*response_callback=*/response_callback1, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::SOCIAL_ENGINEERING},
                            UrlToSingleFullHash(url1))},
       /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
   base::MockCallback<HPRTLookupResponseCallback> response_callback2;
   StartSuccessRequest(
-      /*url=*/url2,
+      /*url=*/url2, /*cached_hash_prefixes=*/{},
       /*response_callback=*/response_callback2, /*response_full_hashes=*/
       {CreateFullHashProto({V5::ThreatType::MALWARE},
                            UrlToSingleFullHash(url2))},
       /*expected_threat_type=*/SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
+
+  histogram_tester_->ExpectTotalCount(
+      /*name=*/"SafeBrowsing.HPRT.Network.Result",
+      /*expected_count=*/0);
   task_environment_.RunUntilIdle();
+  histogram_tester_->ExpectTotalCount(
+      /*name=*/"SafeBrowsing.HPRT.Network.Result",
+      /*expected_count=*/2);
 }
 
 TEST_F(HashRealTimeServiceTest, TestBackoffMode) {
   auto perform_lookup = [this](bool make_fail) {
     GURL url = GURL("https://example.test");
     auto request = std::make_unique<V5::SearchHashesRequest>();
-    for (const auto& hash_prefix : UrlToHashPrefixes(url)) {
+    for (const auto& hash_prefix : UrlToHashPrefixesAsSet(url)) {
       request->add_hash_prefixes(hash_prefix);
     }
     std::string request_url = GetExpectedRequestUrl(request);
@@ -643,6 +984,11 @@
   };
   auto assert_backoff_mode_status = [this](bool expected_backoff_mode_status) {
     EXPECT_EQ(service_->IsInBackoffMode(), expected_backoff_mode_status);
+    histogram_tester_->ExpectUniqueSample(
+        /*name=*/"SafeBrowsing.HPRT.Backoff.State",
+        /*sample=*/expected_backoff_mode_status,
+        /*expected_bucket_count=*/1);
+    ResetMetrics();
   };
 
   // Failing lookups 1 and 2 don't trigger backoff mode. Lookup 3 does.
diff --git a/components/safe_browsing/core/browser/safe_browsing_url_checker_impl_unittest.cc b/components/safe_browsing/core/browser/safe_browsing_url_checker_impl_unittest.cc
index b32c562..dcfe411 100644
--- a/components/safe_browsing/core/browser/safe_browsing_url_checker_impl_unittest.cc
+++ b/components/safe_browsing/core/browser/safe_browsing_url_checker_impl_unittest.cc
@@ -86,7 +86,9 @@
   // Returns the allowlist match result previously set by
   // |SetAllowlistResultForUrl|. It crashes if the allowlist match result for
   // the |gurl| is not set in advance.
-  bool CheckUrlForHighConfidenceAllowlist(const GURL& gurl) override {
+  bool CheckUrlForHighConfidenceAllowlist(
+      const GURL& gurl,
+      const std::string& metric_variation) override {
     std::string url = gurl.spec();
     DCHECK(base::Contains(urls_allowlist_match_, url));
     return urls_allowlist_match_[url];
diff --git a/components/safe_browsing/core/browser/url_realtime_mechanism.cc b/components/safe_browsing/core/browser/url_realtime_mechanism.cc
index 9c54759..17025ec 100644
--- a/components/safe_browsing/core/browser/url_realtime_mechanism.cc
+++ b/components/safe_browsing/core/browser/url_realtime_mechanism.cc
@@ -77,7 +77,7 @@
   bool check_allowlist = can_check_db_ && can_check_high_confidence_allowlist_;
   bool has_allowlist_match =
       check_allowlist &&
-      database_manager_->CheckUrlForHighConfidenceAllowlist(url_);
+      database_manager_->CheckUrlForHighConfidenceAllowlist(url_, "RT");
   RecordLocalMatchResult(has_allowlist_match, request_destination_,
                          url_lookup_service_metric_suffix_);
   base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
diff --git a/components/safe_browsing/core/browser/verdict_cache_manager.cc b/components/safe_browsing/core/browser/verdict_cache_manager.cc
index 2b8810b..ea45c79 100644
--- a/components/safe_browsing/core/browser/verdict_cache_manager.cc
+++ b/components/safe_browsing/core/browser/verdict_cache_manager.cc
@@ -172,8 +172,9 @@
 std::string GetCacheExpressionPath(const std::string& cache_expression) {
   DCHECK(!cache_expression.empty());
   size_t first_slash_pos = cache_expression.find_first_of("/");
-  if (first_slash_pos == std::string::npos)
+  if (first_slash_pos == std::string::npos) {
     return "";
+  }
   return cache_expression.substr(first_slash_pos);
 }
 
@@ -226,8 +227,9 @@
     }
   }
 
-  for (const std::string& key : expired_keys)
+  for (const std::string& key : expired_keys) {
     verdict_dictionary->RemoveKey(key);
+  }
 
   return expired_keys.size();
 }
@@ -323,8 +325,9 @@
     // Ignore any entry that we cannot parse. These invalid entries will be
     // cleaned up during shutdown.
     if (!ParseVerdictEntry<T>(&value, &verdict_received_time, &verdict,
-                              proto_name))
+                              proto_name)) {
       continue;
+    }
     // Since verdict content settings are keyed by origin, we only need to
     // compare the path part of the cache_expression and the given url.
     std::string cache_expression_path =
@@ -425,8 +428,9 @@
       stored_verdict_count_real_time_url_check_(absl::nullopt),
       content_settings_(content_settings),
       sync_observer_(std::move(sync_observer)) {
-  if (history_service)
+  if (history_service) {
     history_service_observation_.Observe(history_service);
+  }
   if (!content_settings->IsOffTheRecord()) {
     ScheduleNextCleanUpAfterInterval(base::Seconds(kCleanUpIntervalInitSecond));
   }
@@ -560,8 +564,9 @@
           ? &stored_verdict_count_password_on_focus_
           : &stored_verdict_count_password_entry_;
   // If we have already computed this, return its value.
-  if (stored_verdict_count->has_value())
+  if (stored_verdict_count->has_value()) {
     return stored_verdict_count->value();
+  }
 
   ContentSettingsForOneType settings;
   content_settings_->GetSettingsForOneType(
@@ -586,8 +591,9 @@
     return 0;
   }
   // If we have already computed this, return its value.
-  if (stored_verdict_count_real_time_url_check_.has_value())
+  if (stored_verdict_count_real_time_url_check_.has_value()) {
     return stored_verdict_count_real_time_url_check_.value();
+  }
 
   ContentSettingsForOneType settings;
   content_settings_->GetSettingsForOneType(
@@ -625,8 +631,9 @@
     // For the same cache_expression, threat_info is in decreasing order of
     // severity. To avoid lower severity threat being overridden by higher one,
     // only store threat info that is first seen for a cache expression.
-    if (base::Contains(visited_cache_expressions, cache_expression))
+    if (base::Contains(visited_cache_expressions, cache_expression)) {
       continue;
+    }
 
     GURL hostname = GetHostNameFromCacheExpression(cache_expression);
     base::Value cache_dictionary_value = content_settings_->GetWebsiteSetting(
@@ -750,6 +757,20 @@
   return has_expired ? ChromeUserPopulation::PageLoadToken() : token;
 }
 
+void VerdictCacheManager::CacheHashPrefixRealTimeLookupResults(
+    const std::vector<std::string>& requested_hash_prefixes,
+    const std::vector<V5::FullHash>& response_full_hashes,
+    const V5::Duration& cache_duration) {
+  hash_realtime_cache_->CacheSearchHashesResponse(
+      requested_hash_prefixes, response_full_hashes, cache_duration);
+}
+
+std::unordered_map<std::string, std::vector<V5::FullHash>>
+VerdictCacheManager::GetCachedHashPrefixRealTimeLookupResults(
+    const std::set<std::string>& hash_prefixes) {
+  return hash_realtime_cache_->SearchCache(hash_prefixes);
+}
+
 void VerdictCacheManager::ScheduleNextCleanUpAfterInterval(
     base::TimeDelta interval) {
   cleanup_timer_.Stop();
@@ -766,6 +787,7 @@
   CleanUpExpiredPhishGuardVerdicts();
   CleanUpExpiredRealTimeUrlCheckVerdicts();
   CleanUpExpiredPageLoadTokens();
+  CleanUpExpiredHashPrefixRealTimeLookupResults();
   ScheduleNextCleanUpAfterInterval(base::Seconds(kCleanUpIntervalSecond));
 }
 
@@ -773,8 +795,9 @@
   if (GetStoredPhishGuardVerdictCount(
           LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) <= 0 &&
       GetStoredPhishGuardVerdictCount(
-          LoginReputationClientRequest::PASSWORD_REUSE_EVENT) <= 0)
+          LoginReputationClientRequest::PASSWORD_REUSE_EVENT) <= 0) {
     return;
+  }
 
   ContentSettingsForOneType password_protection_settings;
   content_settings_->GetSettingsForOneType(
@@ -861,6 +884,10 @@
   page_load_token_map_.clear();
 }
 
+void VerdictCacheManager::CleanUpExpiredHashPrefixRealTimeLookupResults() {
+  hash_realtime_cache_->ClearExpiredResults();
+}
+
 // Overridden from history::HistoryServiceObserver.
 void VerdictCacheManager::OnURLsDeleted(
     history::HistoryService* history_service,
@@ -888,8 +915,9 @@
     base::Value::Dict& cache_dictionary) {
   DCHECK(trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE ||
          trigger_type == LoginReputationClientRequest::PASSWORD_REUSE_EVENT);
-  if (cache_dictionary.empty())
+  if (cache_dictionary.empty()) {
     return false;
+  }
 
   size_t verdicts_removed = 0;
   std::vector<std::string> empty_keys;
@@ -899,21 +927,25 @@
       size_t removed_cnt = RemoveExpiredEntries<LoginReputationClientResponse>(
           &value, kVerdictProto);
       verdicts_removed += removed_cnt;
-      if (stored_verdict_count_password_on_focus_.has_value())
+      if (stored_verdict_count_password_on_focus_.has_value()) {
         stored_verdict_count_password_on_focus_.value() -= removed_cnt;
+      }
     } else {
       size_t removed_cnt = RemoveExpiredEntries<LoginReputationClientResponse>(
           &value, kVerdictProto);
       verdicts_removed += removed_cnt;
-      if (stored_verdict_count_password_entry_.has_value())
+      if (stored_verdict_count_password_entry_.has_value()) {
         stored_verdict_count_password_entry_.value() -= removed_cnt;
+      }
     }
 
-    if (value.GetDict().size() == 0U)
+    if (value.GetDict().size() == 0U) {
       empty_keys.push_back(key);
+    }
   }
-  for (const auto& key : empty_keys)
+  for (const auto& key : empty_keys) {
     cache_dictionary.Remove(key);
+  }
 
   return verdicts_removed > 0U;
 }
@@ -926,13 +958,16 @@
     size_t removed_cnt = RemoveExpiredEntries<RTLookupResponse::ThreatInfo>(
         &value, kRealTimeThreatInfoProto);
     verdicts_removed += removed_cnt;
-    if (stored_verdict_count_real_time_url_check_.has_value())
+    if (stored_verdict_count_real_time_url_check_.has_value()) {
       stored_verdict_count_real_time_url_check_.value() -= removed_cnt;
-    if (value.GetDict().size() == 0U)
+    }
+    if (value.GetDict().size() == 0U) {
       empty_keys.push_back(key);
+    }
   }
-  for (const auto& key : empty_keys)
+  for (const auto& key : empty_keys) {
     cache_dictionary.Remove(key);
+  }
 
   return verdicts_removed > 0U;
 }
@@ -961,8 +996,9 @@
   // We might revisit this logic later to decide if we want to only delete the
   // cached verdict whose cache expression matches this URL.
   for (const history::URLRow& row : deleted_rows) {
-    if (!row.url().SchemeIsHTTPOrHTTPS())
+    if (!row.url().SchemeIsHTTPOrHTTPS()) {
       continue;
+    }
 
     GURL url_key = GetHostNameWithHTTPScheme(row.url());
     stored_verdict_count_password_on_focus_ =
@@ -995,8 +1031,9 @@
   base::Value cache_dictionary_value = content_settings_->GetWebsiteSetting(
       url, GURL(), ContentSettingsType::PASSWORD_PROTECTION, nullptr);
 
-  if (!cache_dictionary_value.is_dict())
+  if (!cache_dictionary_value.is_dict()) {
     return 0;
+  }
 
   int verdict_cnt = 0;
   if (trigger_type == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) {
@@ -1006,8 +1043,9 @@
         password_on_focus_dict ? password_on_focus_dict->DictSize() : 0;
   } else {
     for (auto [key, value] : cache_dictionary_value.GetDict()) {
-      if (key == kPasswordOnFocusCacheKey)
+      if (key == kPasswordOnFocusCacheKey) {
         continue;
+      }
       verdict_cnt += value.GetDict().size();
     }
   }
@@ -1018,8 +1056,9 @@
     const GURL& url) {
   base::Value cache_dictionary_value = content_settings_->GetWebsiteSetting(
       url, GURL(), ContentSettingsType::SAFE_BROWSING_URL_CHECK_DATA, nullptr);
-  if (!cache_dictionary_value.is_dict())
+  if (!cache_dictionary_value.is_dict()) {
     return 0;
+  }
   base::Value* verdict_dictionary =
       cache_dictionary_value.GetDict().Find(kRealTimeUrlCacheKey);
   return verdict_dictionary && verdict_dictionary->is_dict()
@@ -1031,12 +1070,14 @@
   std::string phishing_url_string =
       base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
           kUnsafeUrlFlag);
-  if (phishing_url_string.empty())
+  if (phishing_url_string.empty()) {
     return;
+  }
 
   GURL artificial_unsafe_url(phishing_url_string);
-  if (!artificial_unsafe_url.is_valid())
+  if (!artificial_unsafe_url.is_valid()) {
     return;
+  }
 
   has_artificial_unsafe_url_ = true;
 
@@ -1059,12 +1100,14 @@
   std::string phishing_url_string =
       base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
           kArtificialCachedPhishGuardVerdictFlag);
-  if (phishing_url_string.empty())
+  if (phishing_url_string.empty()) {
     return;
+  }
 
   GURL artificial_unsafe_url(phishing_url_string);
-  if (!artificial_unsafe_url.is_valid())
+  if (!artificial_unsafe_url.is_valid()) {
     return;
+  }
 
   has_artificial_unsafe_url_ = true;
 
diff --git a/components/safe_browsing/core/browser/verdict_cache_manager.h b/components/safe_browsing/core/browser/verdict_cache_manager.h
index 00b5632..4cf04ad 100644
--- a/components/safe_browsing/core/browser/verdict_cache_manager.h
+++ b/components/safe_browsing/core/browser/verdict_cache_manager.h
@@ -18,9 +18,11 @@
 #include "components/keyed_service/core/keyed_service.h"
 #include "components/prefs/pref_change_registrar.h"
 #include "components/prefs/pref_service.h"
+#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h"
 #include "components/safe_browsing/core/browser/safe_browsing_sync_observer.h"
 #include "components/safe_browsing/core/common/proto/csd.pb.h"
 #include "components/safe_browsing/core/common/proto/realtimeapi.pb.h"
+#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
 #include "url/gurl.h"
 
 class HostContentSettingsMap;
@@ -100,6 +102,18 @@
   // token if the token is not found.
   ChromeUserPopulation::PageLoadToken GetPageLoadToken(const GURL& url);
 
+  // Stores the results of a hash-prefix real-time lookup into a cache object.
+  void CacheHashPrefixRealTimeLookupResults(
+      const std::vector<std::string>& requested_hash_prefixes,
+      const std::vector<V5::FullHash>& response_full_hashes,
+      const V5::Duration& cache_duration);
+
+  // Searches the hash-prefix real-time cache object for the requested
+  // |hash_prefixes|.
+  std::unordered_map<std::string, std::vector<V5::FullHash>>
+  GetCachedHashPrefixRealTimeLookupResults(
+      const std::set<std::string>& hash_prefixes);
+
   // Overridden from history::HistoryServiceObserver.
   void OnURLsDeleted(history::HistoryService* history_service,
                      const history::DeletionInfo& deletion_info) override;
@@ -120,6 +134,7 @@
 
  private:
   friend class SafeBrowsingBlockingPageRealTimeUrlCheckTest;
+  friend class VerdictCacheManagerTest;
   FRIEND_TEST_ALL_PREFIXES(VerdictCacheManagerTest, TestCleanUpExpiredVerdict);
   FRIEND_TEST_ALL_PREFIXES(VerdictCacheManagerTest,
                            TestCleanUpExpiredVerdictWithInvalidEntry);
@@ -151,6 +166,7 @@
   void CleanUpExpiredRealTimeUrlCheckVerdicts();
   void CleanUpExpiredPageLoadTokens();
   void CleanUpAllPageLoadTokens(ClearReason reason);
+  void CleanUpExpiredHashPrefixRealTimeLookupResults();
 
   // Helper method to remove content settings when URLs are deleted. If
   // |all_history| is true, removes all cached verdicts. Otherwise it removes
@@ -207,6 +223,10 @@
 
   std::unique_ptr<SafeBrowsingSyncObserver> sync_observer_;
 
+  // The local cache object for hash-prefix real-time lookups.
+  std::unique_ptr<HashRealTimeCache> hash_realtime_cache_ =
+      std::make_unique<HashRealTimeCache>();
+
   bool is_shut_down_ = false;
 
   base::WeakPtrFactory<VerdictCacheManager> weak_factory_{this};
diff --git a/components/safe_browsing/core/browser/verdict_cache_manager_unittest.cc b/components/safe_browsing/core/browser/verdict_cache_manager_unittest.cc
index 8f1ced3c..71c0de7a 100644
--- a/components/safe_browsing/core/browser/verdict_cache_manager_unittest.cc
+++ b/components/safe_browsing/core/browser/verdict_cache_manager_unittest.cc
@@ -13,6 +13,7 @@
 #include "components/safe_browsing/core/browser/safe_browsing_sync_observer.h"
 #include "components/safe_browsing/core/common/proto/csd.pb.h"
 #include "components/safe_browsing/core/common/proto/realtimeapi.pb.h"
+#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "components/sync_preferences/testing_pref_service_syncable.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -85,6 +86,24 @@
                                            verdict_received_time);
   }
 
+  void CacheHashPrefixRealTimeLookupResult(int cache_duration_seconds,
+                                           std::string hash_prefix) {
+    V5::Duration duration;
+    duration.set_seconds(cache_duration_seconds);
+    cache_manager_->CacheHashPrefixRealTimeLookupResults(
+        {hash_prefix}, {V5::FullHash()}, duration);
+  }
+  void ConfirmHashPrefixRealTimeLookupCacheContent(std::string hash_prefix,
+                                                   bool should_expect_entry) {
+    // We cannot call the public SearchCache function because that automatically
+    // filters out expired results. We want to confirm that the cache contents
+    // themselves have been cleaned up as expected, so we access |cache_|
+    // directly.
+    EXPECT_EQ(base::Contains(cache_manager_->hash_realtime_cache_->cache_,
+                             hash_prefix),
+              should_expect_entry);
+  }
+
   void AddThreatInfoToResponse(
       RTLookupResponse& response,
       RTLookupResponse::ThreatInfo::VerdictType verdict_type,
@@ -401,6 +420,14 @@
       GURL("https://www.example1.com"),
       CreatePageLoadToken(now.ToJavaTime(), "token2"));
 
+  CacheHashPrefixRealTimeLookupResult(/*cache_duration_seconds=*/0, "aaaa");
+  CacheHashPrefixRealTimeLookupResult(/*cache_duration_seconds=*/300, "bbbb");
+  // aaaa and bbbb should both be in the cache even though aaaa is expired.
+  ConfirmHashPrefixRealTimeLookupCacheContent(/*hash_prefix=*/"aaaa",
+                                              /*should_expect_entry=*/true);
+  ConfirmHashPrefixRealTimeLookupCacheContent(/*hash_prefix=*/"bbbb",
+                                              /*should_expect_entry=*/true);
+
   cache_manager_->CleanUpExpiredVerdicts();
 
   ASSERT_EQ(1u, cache_manager_->GetStoredPhishGuardVerdictCount(
@@ -470,6 +497,13 @@
   EXPECT_EQ("token2",
             cache_manager_->GetPageLoadToken(GURL("https://www.example1.com/"))
                 .token_value());
+
+  // aaaa is not in the cache because it was expired and has been cleaned up.
+  ConfirmHashPrefixRealTimeLookupCacheContent(/*hash_prefix=*/"aaaa",
+                                              /*should_expect_entry=*/false);
+  // aaaa is still in the cache because it has not expired.
+  ConfirmHashPrefixRealTimeLookupCacheContent(/*hash_prefix=*/"bbbb",
+                                              /*should_expect_entry=*/true);
 }
 
 TEST_F(VerdictCacheManagerTest, TestCleanUpExpiredVerdictWithInvalidEntry) {
@@ -940,4 +974,19 @@
       &out_pg_verdict);
 }
 
+TEST_F(VerdictCacheManagerTest, TestHashPrefixRealTimeLookupCaching) {
+  // Basic test ensuring that the cache manager calls are propagating as
+  // expected to the HashRealTimeCache.
+  EXPECT_TRUE(
+      cache_manager_->GetCachedHashPrefixRealTimeLookupResults({"aaaa", "bbbb"})
+          .empty());
+  CacheHashPrefixRealTimeLookupResult(/*cache_duration_seconds=*/300, "aaaa");
+  CacheHashPrefixRealTimeLookupResult(/*cache_duration_seconds=*/300, "bbbb");
+  auto cache_results = cache_manager_->GetCachedHashPrefixRealTimeLookupResults(
+      {"aaaa", "bbbb", "cccc"});
+  EXPECT_EQ(cache_results.size(), 2u);
+  EXPECT_TRUE(base::Contains(cache_results, "aaaa"));
+  EXPECT_TRUE(base::Contains(cache_results, "bbbb"));
+}
+
 }  // namespace safe_browsing
diff --git a/components/shared_highlighting/core/common/fragment_directives_utils_unittest.cc b/components/shared_highlighting/core/common/fragment_directives_utils_unittest.cc
index 557143a..3649270 100644
--- a/components/shared_highlighting/core/common/fragment_directives_utils_unittest.cc
+++ b/components/shared_highlighting/core/common/fragment_directives_utils_unittest.cc
@@ -64,9 +64,9 @@
   base::Value result = ParseTextFragments(url_with_fragment);
   ASSERT_EQ(2u, result.GetList().size());
   EXPECT_EQ("text 1",
-            result.GetList()[0].FindKey(kFragmentTextStartKey)->GetString());
+            *result.GetList()[0].GetDict().FindString(kFragmentTextStartKey));
   EXPECT_EQ("text 2",
-            result.GetList()[1].FindKey(kFragmentTextStartKey)->GetString());
+            *result.GetList()[1].GetDict().FindString(kFragmentTextStartKey));
 
   GURL url_no_fragment("www.example.com");
   base::Value empty_result = ParseTextFragments(url_no_fragment);
diff --git a/components/shared_highlighting/core/common/text_fragment_unittest.cc b/components/shared_highlighting/core/common/text_fragment_unittest.cc
index d16bc48d..c2280df 100644
--- a/components/shared_highlighting/core/common/text_fragment_unittest.cc
+++ b/components/shared_highlighting/core/common/text_fragment_unittest.cc
@@ -21,90 +21,90 @@
 TEST(TextFragmentTest, FragmentToValueFromEncodedString) {
   // Success cases
   std::string fragment = "start";
-  base::Value result = TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kFragmentPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentTextEndKey));
-  EXPECT_FALSE(result.FindKey(kFragmentSuffixKey));
+  base::Value::Dict result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_FALSE(result.contains(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_FALSE(result.contains(kFragmentTextEndKey));
+  EXPECT_FALSE(result.contains(kFragmentSuffixKey));
 
   fragment = "start,end";
-  result = TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kFragmentPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kFragmentTextEndKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentSuffixKey));
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_FALSE(result.contains(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_EQ("end", *result.FindString(kFragmentTextEndKey));
+  EXPECT_FALSE(result.contains(kFragmentSuffixKey));
 
   fragment = "prefix-,start";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kFragmentPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentTextEndKey));
-  EXPECT_FALSE(result.FindKey(kFragmentSuffixKey));
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_EQ("prefix", *result.FindString(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_FALSE(result.contains(kFragmentTextEndKey));
+  EXPECT_FALSE(result.contains(kFragmentSuffixKey));
 
   fragment = "start,-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kFragmentPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentTextEndKey));
-  EXPECT_EQ("suffix", result.FindKey(kFragmentSuffixKey)->GetString());
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_FALSE(result.contains(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_FALSE(result.contains(kFragmentTextEndKey));
+  EXPECT_EQ("suffix", *result.FindString(kFragmentSuffixKey));
 
   fragment = "prefix-,start,end";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kFragmentPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kFragmentTextEndKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentSuffixKey));
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_EQ("prefix", *result.FindString(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_EQ("end", *result.FindString(kFragmentTextEndKey));
+  EXPECT_FALSE(result.contains(kFragmentSuffixKey));
 
   fragment = "start,end,-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kFragmentPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kFragmentTextEndKey)->GetString());
-  EXPECT_EQ("suffix", result.FindKey(kFragmentSuffixKey)->GetString());
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_FALSE(result.contains(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_EQ("end", *result.FindString(kFragmentTextEndKey));
+  EXPECT_EQ("suffix", *result.FindString(kFragmentSuffixKey));
 
   fragment = "prefix-,start,end,-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ("prefix", result.FindKey(kFragmentPrefixKey)->GetString());
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_EQ("end", result.FindKey(kFragmentTextEndKey)->GetString());
-  EXPECT_EQ("suffix", result.FindKey(kFragmentSuffixKey)->GetString());
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_EQ("prefix", *result.FindString(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_EQ("end", *result.FindString(kFragmentTextEndKey));
+  EXPECT_EQ("suffix", *result.FindString(kFragmentSuffixKey));
 
   // Trailing comma doesn't break otherwise valid fragment
   fragment = "start,";
-  result = TextFragmentToValue(fragment);
-  EXPECT_FALSE(result.FindKey(kFragmentPrefixKey));
-  EXPECT_EQ("start", result.FindKey(kFragmentTextStartKey)->GetString());
-  EXPECT_FALSE(result.FindKey(kFragmentTextEndKey));
-  EXPECT_FALSE(result.FindKey(kFragmentSuffixKey));
+  result = TextFragmentToValue(fragment).TakeDict();
+  EXPECT_FALSE(result.contains(kFragmentPrefixKey));
+  EXPECT_EQ("start", *result.FindString(kFragmentTextStartKey));
+  EXPECT_FALSE(result.contains(kFragmentTextEndKey));
+  EXPECT_FALSE(result.contains(kFragmentSuffixKey));
 
   // Failure Cases
   fragment = "";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  base::Value result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "some,really-,malformed,-thing,with,too,many,commas";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "prefix-,-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "start,prefix-,-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "prefix-,-suffix,start";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "prefix-";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 
   fragment = "-suffix";
-  result = TextFragmentToValue(fragment);
-  EXPECT_EQ(base::Value::Type::NONE, result.type());
+  result_val = TextFragmentToValue(fragment);
+  EXPECT_EQ(base::Value::Type::NONE, result_val.type());
 }
 
 TEST(TextFragmentTest, FragmentToEscapedStringEmpty) {
diff --git a/components/signin/public/identity_manager/account_info.cc b/components/signin/public/identity_manager/account_info.cc
index ca8715c..5933588 100644
--- a/components/signin/public/identity_manager/account_info.cc
+++ b/components/signin/public/identity_manager/account_info.cc
@@ -5,6 +5,7 @@
 #include "components/signin/public/identity_manager/account_info.h"
 
 #include "build/build_config.h"
+#include "components/signin/public/identity_manager/tribool.h"
 #include "google_apis/gaia/gaia_auth_util.h"
 
 #if BUILDFLAG(IS_ANDROID)
@@ -132,6 +133,12 @@
   return !hosted_domain.empty() && hosted_domain != kNoHostedDomainFound;
 }
 
+bool AccountInfo::IsMemberOfFlexOrg() const {
+  return capabilities.is_subject_to_enterprise_policies() ==
+             signin::Tribool::kTrue &&
+         !IsManaged(hosted_domain);
+}
+
 bool AccountInfo::IsManaged() const {
   return IsManaged(hosted_domain);
 }
diff --git a/components/signin/public/identity_manager/account_info.h b/components/signin/public/identity_manager/account_info.h
index dd38ab0..6046396 100644
--- a/components/signin/public/identity_manager/account_info.h
+++ b/components/signin/public/identity_manager/account_info.h
@@ -87,6 +87,10 @@
   // hosted_domain is still unknown (empty), this information will become
   // available asynchronously.
   static bool IsManaged(const std::string& hosted_domain);
+
+  // Returns true if the account has no hosted domain but is a dasher account.
+  bool IsMemberOfFlexOrg() const;
+
   bool IsManaged() const;
 };
 
diff --git a/components/sync/base/data_type_histogram.cc b/components/sync/base/data_type_histogram.cc
index b483ae5f..da07081 100644
--- a/components/sync/base/data_type_histogram.cc
+++ b/components/sync/base/data_type_histogram.cc
@@ -32,6 +32,8 @@
       return "DecryptionPendingForTooLong";
     case UpdateDropReason::kFailedToDecrypt:
       return "FailedToDecrypt";
+    case UpdateDropReason::kDroppedByBridge:
+      return "DroppedByBridge";
   }
 }
 
diff --git a/components/sync/base/data_type_histogram.h b/components/sync/base/data_type_histogram.h
index bbf3717..09b7df8 100644
--- a/components/sync/base/data_type_histogram.h
+++ b/components/sync/base/data_type_histogram.h
@@ -20,7 +20,9 @@
   kTombstoneForNonexistentInIncrementalUpdate,
   kDecryptionPending,
   kDecryptionPendingForTooLong,
-  kFailedToDecrypt
+  kFailedToDecrypt,
+  // This should effectively replace kCannotGenerateStorageKey in the long run.
+  kDroppedByBridge
 };
 
 // Records that a remote update of an entity of type |type| got dropped into a
diff --git a/components/sync/model/client_tag_based_model_type_processor.cc b/components/sync/model/client_tag_based_model_type_processor.cc
index 4ca91c6..861e2a7 100644
--- a/components/sync/model/client_tag_based_model_type_processor.cc
+++ b/components/sync/model/client_tag_based_model_type_processor.cc
@@ -916,9 +916,20 @@
       continue;
     }
 
+    if (!bridge_->IsEntityDataValid(update.entity)) {
+      SyncRecordModelTypeUpdateDropReason(UpdateDropReason::kDroppedByBridge,
+                                          type_);
+      DLOG(WARNING) << "Received entity with invalid update for "
+                    << ModelTypeToDebugString(type_);
+      continue;
+    }
+
     std::string storage_key;
     if (bridge_->SupportsGetStorageKey()) {
       storage_key = bridge_->GetStorageKey(update.entity);
+      // TODO(crbug.com/1057947): Make this a DCHECK as storage keys should not
+      // be empty after IsEntityDataValid() has been implemented by all
+      // bridges.
       if (storage_key.empty()) {
         SyncRecordModelTypeUpdateDropReason(
             UpdateDropReason::kCannotGenerateStorageKey, type_);
@@ -946,6 +957,12 @@
       metadata_changes->UpdateMetadata(storage_key, entity->metadata());
   }
 
+  // If there is already an error (this can happen if one of the metadata
+  // writes failed), don't even send the data to the bridge.
+  if (model_error_) {
+    return model_error_;
+  }
+
   // Let the bridge handle associating and merging the data.
   // For ApplyUpdatesImmediatelyTypes(), no merge is necessary or supported, so
   // call ApplySyncChanges() instead.
diff --git a/components/sync/model/client_tag_based_model_type_processor_unittest.cc b/components/sync/model/client_tag_based_model_type_processor_unittest.cc
index 1d48be5d..def6997 100644
--- a/components/sync/model/client_tag_based_model_type_processor_unittest.cc
+++ b/components/sync/model/client_tag_based_model_type_processor_unittest.cc
@@ -3018,6 +3018,52 @@
       /*expected_count=*/2);
 }
 
+TEST_F(ClientTagBasedModelTypeProcessorTest,
+       ShouldNotProcessInvalidRemoteIncrementalUpdate) {
+  // To ensure the update is not ignored because of empty storage key.
+  bridge()->SetSupportsGetStorageKey(false);
+
+  InitializeToReadyState();
+  UpdateResponseDataList updates;
+  updates.push_back(worker()->GenerateUpdateData(
+      GetPrefHash(kKey1), GeneratePrefSpecifics(kKey1, kValue1)));
+
+  // Force invalidate the next remote update.
+  bridge()->TreatRemoteUpdateAsInvalid(GetPrefHash(kKey1));
+
+  worker()->UpdateFromServer(std::move(updates));
+
+  // Verify that the data wasn't actually stored.
+  EXPECT_EQ(0U, db()->metadata_count());
+  EXPECT_EQ(0U, db()->data_count());
+}
+
+TEST_F(ClientTagBasedModelTypeProcessorTest,
+       ShouldNotProcessInvalidRemoteFullUpdate) {
+  InitializeToMetadataLoaded(/*initial_sync_done=*/false);
+  OnSyncStarting();
+
+  UpdateResponseDataList updates;
+  updates.push_back(worker()->GenerateUpdateData(
+      GetPrefHash(kKey1), GeneratePrefSpecifics(kKey1, kValue1)));
+
+  // Force invalidate the next remote update.
+  bridge()->TreatRemoteUpdateAsInvalid(GetPrefHash(kKey1));
+
+  base::HistogramTester histogram_tester;
+  worker()->UpdateFromServer(std::move(updates));
+
+  // Verify that the data wasn't actually stored.
+  EXPECT_EQ(0U, db()->metadata_count());
+  EXPECT_EQ(0U, db()->data_count());
+
+  // Update was dropped by the bridge.
+  histogram_tester.ExpectBucketCount(
+      "Sync.ModelTypeUpdateDrop.DroppedByBridge",
+      /*bucket=*/ModelTypeHistogramValue(GetModelType()),
+      /*count=*/1);
+}
+
 // The param indicates whether the password notes feature is enabled.
 class PasswordsClientTagBasedModelTypeProcessorTest
     : public testing::WithParamInterface<bool>,
diff --git a/components/sync/model/client_tag_based_remote_update_handler.cc b/components/sync/model/client_tag_based_remote_update_handler.cc
index 4dc5f42..a6b6d20 100644
--- a/components/sync/model/client_tag_based_remote_update_handler.cc
+++ b/components/sync/model/client_tag_based_remote_update_handler.cc
@@ -307,9 +307,14 @@
               ClientTagHash::FromUnhashed(
                   type_, bridge_->GetClientTag(update.entity)));
   }
+  if (!bridge_->IsEntityDataValid(update.entity)) {
+    return nullptr;
+  }
   std::string storage_key;
   if (bridge_->SupportsGetStorageKey()) {
     storage_key = bridge_->GetStorageKey(update.entity);
+    // TODO(crbug.com/1057947): Remove this as storage keys should not be
+    // empty after ValidateRemoteUpdate() has been implemented by all bridges.
     if (storage_key.empty()) {
       return nullptr;
     }
diff --git a/components/sync/model/client_tag_based_remote_update_handler_unittest.cc b/components/sync/model/client_tag_based_remote_update_handler_unittest.cc
index 98bbe91..53010f3 100644
--- a/components/sync/model/client_tag_based_remote_update_handler_unittest.cc
+++ b/components/sync/model/client_tag_based_remote_update_handler_unittest.cc
@@ -346,6 +346,20 @@
   EXPECT_EQ(0U, ProcessorEntityCount());
 }
 
+TEST_F(ClientTagBasedRemoteUpdateHandlerTest,
+       ShouldIgnoreInvalidRemoteUpdates) {
+  // To ensure the update is not ignored because of empty storage key.
+  bridge()->SetSupportsGetStorageKey(false);
+  // Force flag next remote update as invalid.
+  bridge()->TreatRemoteUpdateAsInvalid(GetPrefHash(kKey1));
+
+  ASSERT_EQ(0U, ProcessorEntityCount());
+  ProcessSingleUpdate(GenerateUpdate(GetPrefHash(kKey1), kKey1, kValue1));
+  EXPECT_EQ(0U, db()->data_count());
+  EXPECT_EQ(0U, db()->metadata_count());
+  EXPECT_EQ(0U, ProcessorEntityCount());
+}
+
 }  // namespace
 
 }  // namespace syncer
diff --git a/components/sync/model/model_type_sync_bridge.cc b/components/sync/model/model_type_sync_bridge.cc
index 5985293..83393ac 100644
--- a/components/sync/model/model_type_sync_bridge.cc
+++ b/components/sync/model/model_type_sync_bridge.cc
@@ -76,6 +76,11 @@
   return sync_pb::EntitySpecifics();
 }
 
+bool ModelTypeSyncBridge::IsEntityDataValid(
+    const EntityData& entity_data) const {
+  return true;
+}
+
 ModelTypeChangeProcessor* ModelTypeSyncBridge::change_processor() {
   return change_processor_.get();
 }
diff --git a/components/sync/model/model_type_sync_bridge.h b/components/sync/model/model_type_sync_bridge.h
index 7867f3b..b4cb023c 100644
--- a/components/sync/model/model_type_sync_bridge.h
+++ b/components/sync/model/model_type_sync_bridge.h
@@ -135,9 +135,6 @@
   // type should strive to keep these keys as small as possible.
   // Returning an empty string means the remote creation should be ignored (i.e.
   // it contains invalid data).
-  // TODO(crbug.com/1057947): introduce a dedicated method to validate data from
-  // the server to solve the inconsistency with bridges that don't support
-  // GetStorageKey() and with remote updates which are not creations.
   virtual std::string GetStorageKey(const EntityData& entity_data) = 0;
 
   // Whether or not the bridge is capable of producing a client tag from
@@ -215,6 +212,13 @@
   virtual sync_pb::EntitySpecifics TrimAllSupportedFieldsFromRemoteSpecifics(
       const sync_pb::EntitySpecifics& entity_specifics) const;
 
+  // Returns true if the provided `entity_data` is valid. This method should be
+  // implemented by the bridges and can be used to validate the incoming remote
+  // updates.
+  // TODO(crbug.com/1057947): Mark this method as pure virtual to force all the
+  // bridges to implement this.
+  virtual bool IsEntityDataValid(const EntityData& entity_data) const;
+
   // Needs to be informed about any model change occurring via Delete() and
   // Put(). The changing metadata should be stored to persistent storage
   // before or atomically with the model changes.
diff --git a/components/sync/protocol/README.md b/components/sync/protocol/README.md
index 923a3a8..dc5a9277 100644
--- a/components/sync/protocol/README.md
+++ b/components/sync/protocol/README.md
@@ -18,6 +18,8 @@
 * Deprecating fields:
   * If the field **is** still accessed: Mark it as `[deprecated = true]`. This is the common case, since the browser typically needs to continue supporting the old field for backwards compatibility reasons.
   * If the field **is not** accessed anymore (i.e. no non-ancient clients depend on the field being populated anymore, all migration code has been retired, etc): Remove the field, and add `reserved` entries for both its name and its tag number.
+  * **Note**: If your data type is using the [Protection against data override by old Sync clients](https://www.chromium.org/developers/design-documents/sync/old-sync-clients-data-override-protection/), then even fields that aren't accessed anymore should **not** be removed from the proto definition, since they should still be treated as supported for the purpose of trimming. (Otherwise, the removed fields would forever be carried forward in the data.)
+
 * Deprecating enum values: This is particularly tricky, especially if the default value is not a `FOO_UNSPECIFIED` one (see above). A common pattern is prepending `DEPRECATED_` to the entry name.
 
 For reviewers:
diff --git a/components/sync/protocol/contact_info_specifics.proto b/components/sync/protocol/contact_info_specifics.proto
index 50840366..c6275ae 100644
--- a/components/sync/protocol/contact_info_specifics.proto
+++ b/components/sync/protocol/contact_info_specifics.proto
@@ -76,6 +76,17 @@
   // The value of the label can be freely chosen by the user.
   optional string profile_label = 5;
 
+  // Tracks the application that initially created the profile. The integer
+  // represents a value in the server-side enum `BillableService`. A value of
+  // 70073 represents Chrome (enum value BILLABLE_SERVICE_CHROME_PAYMENTS).
+  optional int32 initial_creator_id = 39;
+  // Tracks the application that applied the last modification to the
+  // non-metadata content of the profile. It represents a value in the same
+  // `BillableService` enum.
+  // All String- and IntegerToken, and the `profile_label` are considered
+  // non-metadata.
+  optional int32 last_modifier_id = 40;
+
   // Contact info name fields.
   optional StringToken name_honorific = 6;
   optional StringToken name_first = 7;
diff --git a/components/sync/protocol/proto_visitors.h b/components/sync/protocol/proto_visitors.h
index e63b4bb..88f024a 100644
--- a/components/sync/protocol/proto_visitors.h
+++ b/components/sync/protocol/proto_visitors.h
@@ -306,6 +306,8 @@
   VISIT(date_modified_windows_epoch_micros);
   VISIT(language_code);
   VISIT(profile_label);
+  VISIT(initial_creator_id);
+  VISIT(last_modifier_id);
   VISIT(name_honorific);
   VISIT(name_first);
   VISIT(name_middle);
diff --git a/components/sync/protocol/webauthn_credential_specifics.proto b/components/sync/protocol/webauthn_credential_specifics.proto
index c2941cc..e224990c 100644
--- a/components/sync/protocol/webauthn_credential_specifics.proto
+++ b/components/sync/protocol/webauthn_credential_specifics.proto
@@ -133,13 +133,21 @@
 
     // The contents of the credential's largeBlob value(*). Unlike with security
     // keys, largeBlob data is not stored in a single lump for all credentials,
-    // but as per-credential data.
+    // but as per-credential data. This data is presented to the authenticator
+    // over CTAP and thus has already had the required DEFLATE compression
+    // applied by the remote platform. The uncompressed size of this data is in
+    // the next field.
     //
     // (*) "large" with respect to embedded devices. Maximum length is 2KiB for
     // Google Password Manager.
     //
     // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorLargeBlobs
     optional bytes large_blob = 4;
+
+    // The claimed uncompressed size of the DEFLATE-compressed data in
+    // `large_blob`. This corresponds to the `origSize` field from the spec:
+    // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#large-blob
+    optional uint64 large_blob_uncompressed_size = 5;
   }
 
   oneof encrypted_data {
diff --git a/components/sync/test/fake_model_type_sync_bridge.cc b/components/sync/test/fake_model_type_sync_bridge.cc
index 93527b3..0c578cf 100644
--- a/components/sync/test/fake_model_type_sync_bridge.cc
+++ b/components/sync/test/fake_model_type_sync_bridge.cc
@@ -382,6 +382,12 @@
   return trimmed_specifics;
 }
 
+bool FakeModelTypeSyncBridge::IsEntityDataValid(
+    const EntityData& entity_data) const {
+  return invalid_remote_updates_.find(entity_data.client_tag_hash) ==
+         invalid_remote_updates_.end();
+}
+
 void FakeModelTypeSyncBridge::SetConflictResolution(
     ConflictResolution resolution) {
   conflict_resolution_ = resolution;
@@ -429,4 +435,9 @@
   values_to_ignore_.insert(value);
 }
 
+void FakeModelTypeSyncBridge::TreatRemoteUpdateAsInvalid(
+    const ClientTagHash& client_tag_hash) {
+  invalid_remote_updates_.insert(client_tag_hash);
+}
+
 }  // namespace syncer
diff --git a/components/sync/test/fake_model_type_sync_bridge.h b/components/sync/test/fake_model_type_sync_bridge.h
index c87c14db..08a84e5 100644
--- a/components/sync/test/fake_model_type_sync_bridge.h
+++ b/components/sync/test/fake_model_type_sync_bridge.h
@@ -7,6 +7,7 @@
 
 #include <map>
 #include <memory>
+#include <set>
 #include <string>
 #include <unordered_set>
 
@@ -119,6 +120,7 @@
       std::unique_ptr<MetadataChangeList> delete_metadata_change_list) override;
   sync_pb::EntitySpecifics TrimAllSupportedFieldsFromRemoteSpecifics(
       const sync_pb::EntitySpecifics& entity_specifics) const override;
+  bool IsEntityDataValid(const EntityData& entity_data) const override;
 
   // Stores a resolution for the next call to ResolveConflict. Note that if this
   // is a USE_NEW resolution, the data will only exist for one resolve call.
@@ -158,6 +160,10 @@
   // if the bridge's ModelType is PREFERENCES.
   void AddPrefValueToIgnore(const std::string& value);
 
+  // Sets the flag to mark entities with client tag hash `client_tag_hash` as
+  // invalid when IsEntityDataValid() is called.
+  void TreatRemoteUpdateAsInvalid(const ClientTagHash& client_tag_hash);
+
   const Store& db() const { return *db_; }
   Store* mutable_db() { return db_.get(); }
   size_t trimmed_specifics_change_count() const {
@@ -188,6 +194,10 @@
   // The preference values that the bridge will ignore.
   std::unordered_set<std::string> values_to_ignore_;
 
+  // The client tag hashes the bridge will mark as invalid in
+  // calls to IsEntityDataValid().
+  std::set<ClientTagHash> invalid_remote_updates_;
+
   // Whether an error should be produced on the next bridge call.
   bool error_next_ = false;
 
diff --git a/components/sync_sessions/session_sync_bridge.cc b/components/sync_sessions/session_sync_bridge.cc
index cc27bd5..3b11bb2 100644
--- a/components/sync_sessions/session_sync_bridge.cc
+++ b/components/sync_sessions/session_sync_bridge.cc
@@ -272,6 +272,11 @@
   return SessionStore::GetStorageKey(entity_data.specifics.session());
 }
 
+bool SessionSyncBridge::IsEntityDataValid(
+    const syncer::EntityData& entity_data) const {
+  return SessionStore::AreValidSpecifics(entity_data.specifics.session());
+}
+
 void SessionSyncBridge::ApplyStopSyncChanges(
     std::unique_ptr<MetadataChangeList> delete_metadata_change_list) {
   DCHECK(store_);
diff --git a/components/sync_sessions/session_sync_bridge.h b/components/sync_sessions/session_sync_bridge.h
index e8c35dd..f3b6922 100644
--- a/components/sync_sessions/session_sync_bridge.h
+++ b/components/sync_sessions/session_sync_bridge.h
@@ -68,6 +68,7 @@
   std::string GetStorageKey(const syncer::EntityData& entity_data) override;
   void ApplyStopSyncChanges(std::unique_ptr<syncer::MetadataChangeList>
                                 delete_metadata_change_list) override;
+  bool IsEntityDataValid(const syncer::EntityData& entity_data) const override;
 
   // LocalSessionEventHandlerImpl::Delegate implementation.
   std::unique_ptr<LocalSessionEventHandlerImpl::WriteBatch>
diff --git a/components/test/data/web_database/version_109.sql b/components/test/data/web_database/version_109.sql
new file mode 100644
index 0000000..e10183a1
--- /dev/null
+++ b/components/test/data/web_database/version_109.sql
@@ -0,0 +1,36 @@
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE meta(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR);
+INSERT INTO meta VALUES('mmap_status','-1');
+INSERT INTO meta VALUES('version','109');
+INSERT INTO meta VALUES('last_compatible_version','106');
+INSERT INTO meta VALUES('Builtin Keyword Version','127');
+CREATE TABLE token_service (service VARCHAR PRIMARY KEY NOT NULL,encrypted_token BLOB);
+CREATE TABLE keywords (id INTEGER PRIMARY KEY,short_name VARCHAR NOT NULL,keyword VARCHAR NOT NULL,favicon_url VARCHAR NOT NULL,url VARCHAR NOT NULL,safe_for_autoreplace INTEGER,originating_url VARCHAR,date_created INTEGER DEFAULT 0,usage_count INTEGER DEFAULT 0,input_encodings VARCHAR,suggest_url VARCHAR,prepopulate_id INTEGER DEFAULT 0,created_by_policy INTEGER DEFAULT 0,last_modified INTEGER DEFAULT 0,sync_guid VARCHAR,alternate_urls VARCHAR,image_url VARCHAR,search_url_post_params VARCHAR,suggest_url_post_params VARCHAR,image_url_post_params VARCHAR,new_tab_url VARCHAR,last_visited INTEGER DEFAULT 0, created_from_play_api INTEGER DEFAULT 0, is_active INTEGER DEFAULT 0, starter_pack_id INTEGER DEFAULT 0);
+CREATE TABLE autofill (name VARCHAR, value VARCHAR, value_lower VARCHAR, date_created INTEGER DEFAULT 0, date_last_used INTEGER DEFAULT 0, count INTEGER DEFAULT 1, PRIMARY KEY (name, value));
+CREATE TABLE credit_cards ( guid VARCHAR PRIMARY KEY, name_on_card VARCHAR, expiration_month INTEGER, expiration_year INTEGER, card_number_encrypted BLOB, date_modified INTEGER NOT NULL DEFAULT 0, origin VARCHAR DEFAULT '', use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, billing_address_id VARCHAR, nickname VARCHAR);
+CREATE TABLE ibans ( guid VARCHAR PRIMARY KEY, use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, value VARCHAR, nickname VARCHAR);
+CREATE TABLE autofill_profiles ( guid VARCHAR PRIMARY KEY, company_name VARCHAR, street_address VARCHAR, dependent_locality VARCHAR, city VARCHAR, state VARCHAR, zipcode VARCHAR, sorting_code VARCHAR, country_code VARCHAR, date_modified INTEGER NOT NULL DEFAULT 0, origin VARCHAR DEFAULT '', language_code VARCHAR, use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, label VARCHAR, disallow_settings_visible_updates INTEGER NOT NULL DEFAULT 0);
+CREATE TABLE autofill_profile_addresses ( guid VARCHAR, street_address VARCHAR, street_name VARCHAR, dependent_street_name VARCHAR, house_number VARCHAR, subpremise VARCHAR, premise_name VARCHAR, street_address_status INTEGER DEFAULT 0, street_name_status INTEGER DEFAULT 0, dependent_street_name_status INTEGER DEFAULT 0, house_number_status INTEGER DEFAULT 0, subpremise_status INTEGER DEFAULT 0, premise_name_status INTEGER DEFAULT 0, dependent_locality VARCHAR, city VARCHAR, state VARCHAR, zip_code VARCHAR, sorting_code VARCHAR, country_code VARCHAR, dependent_locality_status INTEGER DEFAULT 0, city_status INTEGER DEFAULT 0, state_status INTEGER DEFAULT 0, zip_code_status INTEGER DEFAULT 0, sorting_code_status INTEGER DEFAULT 0, country_code_status INTEGER DEFAULT 0, apartment_number VARCHAR, floor VARCHAR, apartment_number_status INTEGER DEFAULT 0, floor_status INTEGER DEFAULT 0);
+CREATE TABLE autofill_profile_names ( guid VARCHAR, first_name VARCHAR, middle_name VARCHAR, last_name VARCHAR, full_name VARCHAR, honorific_prefix VARCHAR, first_last_name VARCHAR, conjunction_last_name VARCHAR, second_last_name VARCHAR, honorific_prefix_status INTEGER DEFAULT 0, first_name_status INTEGER DEFAULT 0, middle_name_status INTEGER DEFAULT 0, last_name_status INTEGER DEFAULT 0, first_last_name_status INTEGER DEFAULT 0, conjunction_last_name_status INTEGER DEFAULT 0, second_last_name_status INTEGER DEFAULT 0, full_name_status INTEGER DEFAULT 0, full_name_with_honorific_prefix VARCHAR, full_name_with_honorific_prefix_status INTEGER DEFAULT 0);
+CREATE TABLE autofill_profile_emails ( guid VARCHAR, email VARCHAR);
+CREATE TABLE autofill_profile_phones ( guid VARCHAR, number VARCHAR);
+CREATE TABLE masked_credit_cards (id VARCHAR,name_on_card VARCHAR,network VARCHAR,last_four VARCHAR,exp_month INTEGER DEFAULT 0,exp_year INTEGER DEFAULT 0, bank_name VARCHAR, nickname VARCHAR, card_issuer INTEGER DEFAULT 0, instrument_id INTEGER DEFAULT 0, virtual_card_enrollment_state INTEGER DEFAULT 0, card_art_url VARCHAR, product_description VARCHAR, card_issuer_id VARCHAR);
+CREATE TABLE unmasked_credit_cards (id VARCHAR,card_number_encrypted VARCHAR,unmask_date INTEGER NOT NULL DEFAULT 0);
+CREATE TABLE virtual_card_usage_data (id VARCHAR PRIMARY KEY, instrument_id INTEGER DEFAULT 0, merchant_domain VARCHAR, last_four VARCHAR);
+CREATE TABLE server_card_metadata (id VARCHAR NOT NULL,use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, billing_address_id VARCHAR);
+CREATE TABLE server_addresses (id VARCHAR,company_name VARCHAR,street_address VARCHAR,address_1 VARCHAR,address_2 VARCHAR,address_3 VARCHAR,address_4 VARCHAR,postal_code VARCHAR,sorting_code VARCHAR,country_code VARCHAR,language_code VARCHAR, recipient_name VARCHAR, phone_number VARCHAR);
+CREATE TABLE server_address_metadata (id VARCHAR NOT NULL,use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, has_converted BOOL NOT NULL DEFAULT FALSE);
+CREATE TABLE autofill_sync_metadata (model_type INTEGER NOT NULL, storage_key VARCHAR NOT NULL, value BLOB, PRIMARY KEY (model_type, storage_key));
+CREATE TABLE autofill_model_type_state (model_type INTEGER NOT NULL PRIMARY KEY, value BLOB);
+CREATE TABLE payments_customer_data (customer_id VARCHAR);
+CREATE TABLE payments_upi_vpa (vpa VARCHAR);
+CREATE TABLE server_card_cloud_token_data ( id VARCHAR, suffix VARCHAR, exp_month INTEGER DEFAULT 0, exp_year INTEGER DEFAULT 0, card_art_url VARCHAR, instrument_token VARCHAR);
+CREATE TABLE offer_data ( offer_id UNSIGNED LONG, offer_reward_amount VARCHAR, expiry UNSIGNED LONG, offer_details_url VARCHAR, merchant_domain VARCHAR, promo_code VARCHAR, value_prop_text VARCHAR, see_details_text VARCHAR, usage_instructions_text VARCHAR);
+CREATE TABLE offer_eligible_instrument ( offer_id UNSIGNED LONG,instrument_id UNSIGNED LONG);
+CREATE TABLE offer_merchant_domain ( offer_id UNSIGNED LONG,merchant_domain VARCHAR);
+CREATE TABLE contact_info( guid VARCHAR PRIMARY KEY, use_count INTEGER NOT NULL DEFAULT 0, use_date INTEGER NOT NULL DEFAULT 0, date_modified INTEGER NOT NULL DEFAULT 0, language_code VARCHAR, label VARCHAR);
+CREATE TABLE contact_info_type_tokens( guid VARCHAR, type INTEGER, value VARCHAR, verification_status INTEGER DEFAULT 0, PRIMARY KEY (guid, type));
+CREATE INDEX autofill_name ON autofill (name);
+CREATE INDEX autofill_name_value_lower ON autofill (name, value_lower);
+COMMIT;
diff --git a/components/webdata/common/BUILD.gn b/components/webdata/common/BUILD.gn
index fd64e91..0a75d6a 100644
--- a/components/webdata/common/BUILD.gn
+++ b/components/webdata/common/BUILD.gn
@@ -47,6 +47,7 @@
     "//components/test/data/web_database/version_106.sql",
     "//components/test/data/web_database/version_107.sql",
     "//components/test/data/web_database/version_108.sql",
+    "//components/test/data/web_database/version_109.sql",
     "//components/test/data/web_database/version_82.sql",
     "//components/test/data/web_database/version_83.sql",
     "//components/test/data/web_database/version_84.sql",
diff --git a/components/webdata/common/web_database.cc b/components/webdata/common/web_database.cc
index 85001526..4f5e8e01 100644
--- a/components/webdata/common/web_database.cc
+++ b/components/webdata/common/web_database.cc
@@ -13,7 +13,7 @@
 // corresponding changes must happen in the unit tests, and new migration test
 // added.  See |WebDatabaseMigrationTest::kCurrentTestedVersionNumber|.
 // static
-const int WebDatabase::kCurrentVersionNumber = 109;
+const int WebDatabase::kCurrentVersionNumber = 110;
 
 const int WebDatabase::kDeprecatedVersionNumber = 82;
 
diff --git a/components/webdata/common/web_database_migration_unittest.cc b/components/webdata/common/web_database_migration_unittest.cc
index 5029114..5de3978 100644
--- a/components/webdata/common/web_database_migration_unittest.cc
+++ b/components/webdata/common/web_database_migration_unittest.cc
@@ -141,7 +141,7 @@
   base::ScopedTempDir temp_dir_;
 };
 
-const int WebDatabaseMigrationTest::kCurrentTestedVersionNumber = 109;
+const int WebDatabaseMigrationTest::kCurrentTestedVersionNumber = 110;
 
 void WebDatabaseMigrationTest::LoadDatabase(
     const base::FilePath::StringType& file) {
@@ -1049,3 +1049,31 @@
     EXPECT_TRUE(connection.DoesTableExist("virtual_card_usage_data"));
   }
 }
+
+// Tests that the initial_creator_id and last_modifier_id columns are added to
+// the contact_info table.
+TEST_F(WebDatabaseMigrationTest, MigrateVersion109ToCurrent) {
+  ASSERT_NO_FATAL_FAILURE(LoadDatabase(FILE_PATH_LITERAL("version_109.sql")));
+  {
+    sql::Database connection;
+    ASSERT_TRUE(connection.Open(GetDatabasePath()));
+    ASSERT_TRUE(sql::MetaTable::DoesTableExist(&connection));
+
+    EXPECT_EQ(109, VersionFromConnection(&connection));
+    EXPECT_FALSE(
+        connection.DoesColumnExist("contact_info", "initial_creator_id"));
+    EXPECT_FALSE(
+        connection.DoesColumnExist("contact_info", "last_modifier_id"));
+  }
+  DoMigration();
+  {
+    sql::Database connection;
+    ASSERT_TRUE(connection.Open(GetDatabasePath()));
+    ASSERT_TRUE(sql::MetaTable::DoesTableExist(&connection));
+
+    EXPECT_EQ(kCurrentTestedVersionNumber, VersionFromConnection(&connection));
+    EXPECT_TRUE(
+        connection.DoesColumnExist("contact_info", "initial_creator_id"));
+    EXPECT_TRUE(connection.DoesColumnExist("contact_info", "last_modifier_id"));
+  }
+}
diff --git a/content/browser/accessibility/browser_accessibility_com_win.cc b/content/browser/accessibility/browser_accessibility_com_win.cc
index b70d1be..68dcc9b 100644
--- a/content/browser/accessibility/browser_accessibility_com_win.cc
+++ b/content/browser/accessibility/browser_accessibility_com_win.cc
@@ -1418,6 +1418,7 @@
       guid_service == IID_IAccessibleTable2 ||
       guid_service == IID_IAccessibleTableCell ||
       guid_service == IID_IAccessibleText ||
+      guid_service == IID_IAccessibleTextSelectionContainer ||
       guid_service == IID_IAccessibleValue ||
       guid_service == IID_ISimpleDOMDocument ||
       guid_service == IID_ISimpleDOMNode ||
diff --git a/content/browser/accessibility/dump_accessibility_scripts_browsertest.cc b/content/browser/accessibility/dump_accessibility_scripts_browsertest.cc
index 7470b3dd..50a3824 100644
--- a/content/browser/accessibility/dump_accessibility_scripts_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_scripts_browsertest.cc
@@ -46,6 +46,8 @@
 constexpr const char kIAccessible[]{"win/ia2/iaccessible"};
 constexpr const char kIAccessible2[]{"win/ia2/iaccessible2"};
 constexpr const char kIAccessibleTable[]{"win/ia2/iaccessibletable"};
+constexpr const char kIAccessibleTextSelectionContainer[]{
+    "win/ia2/iaccessibletextselectioncontainer"};
 
 #endif
 
@@ -607,6 +609,14 @@
   RunTypedTest<kIAccessibleTable>(L"iaccessibletable-selected-columns.html");
 }
 
+// IAccessibleTextSelectionContainer
+
+IN_PROC_BROWSER_TEST_P(DumpAccessibilityScriptTest,
+                       IAccessibleTextSelectionContainerSelections) {
+  RunTypedTest<kIAccessibleTextSelectionContainer>(
+      L"iaccessibletextselectioncontainer-selections.html");
+}
+
 #endif
 
 }  // namespace content
diff --git a/content/browser/attribution_reporting/attribution_storage_sql.cc b/content/browser/attribution_reporting/attribution_storage_sql.cc
index 8ed94782..094ebbd 100644
--- a/content/browser/attribution_reporting/attribution_storage_sql.cc
+++ b/content/browser/attribution_reporting/attribution_storage_sql.cc
@@ -619,6 +619,15 @@
     }
   }
 
+  if (attribution_logic != StoredSource::AttributionLogic::kTruthfully) {
+    if (!rate_limit_table_.AddRateLimitForAttribution(
+            db_.get(), AttributionInfo(std::move(stored_source),
+                                       /*time=*/common_info.source_time(),
+                                       /*debug_key=*/absl::nullopt))) {
+      return StoreSourceResult(StorableSource::Result::kInternalError);
+    }
+  }
+
   if (!transaction.Commit()) {
     return StoreSourceResult(StorableSource::Result::kInternalError);
   }
diff --git a/content/browser/attribution_reporting/attribution_storage_unittest.cc b/content/browser/attribution_reporting/attribution_storage_unittest.cc
index 73bc58d..68ba6c20 100644
--- a/content/browser/attribution_reporting/attribution_storage_unittest.cc
+++ b/content/browser/attribution_reporting/attribution_storage_unittest.cc
@@ -1054,7 +1054,7 @@
                       DefaultAggregatableHistogramContributions()))));
 }
 
-TEST_F(AttributionStorageTest, NeverAttributeImpression_RateLimitsNotChanged) {
+TEST_F(AttributionStorageTest, NeverAttributeImpression_RateLimitsChanged) {
   delegate()->set_rate_limits({
       .time_window = base::TimeDelta::Max(),
       .max_source_registration_reporting_origins =
@@ -1065,31 +1065,20 @@
 
   delegate()->set_randomized_response(
       std::vector<AttributionStorageDelegate::FakeReport>{});
-  storage()->StoreSource(SourceBuilder().SetSourceEventId(5).Build());
+  storage()->StoreSource(TestAggregatableSourceProvider()
+                             .GetBuilder()
+                             .SetSourceEventId(5)
+                             .Build());
   delegate()->set_randomized_response(absl::nullopt);
 
-  const auto conversion = DefaultTrigger();
-  EXPECT_EQ(AttributionTrigger::EventLevelResult::kDroppedForNoise,
-            MaybeCreateAndStoreEventLevelReport(conversion));
-
-  SourceBuilder builder;
-  builder.SetSourceEventId(7);
-  builder.SetPriority(100);
-  storage()->StoreSource(builder.Build());
-  EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess,
-            MaybeCreateAndStoreEventLevelReport(conversion));
-
-  storage()->StoreSource(SourceBuilder().SetSourceEventId(9).Build());
-  EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveAttributions,
-            MaybeCreateAndStoreEventLevelReport(conversion));
-
-  const AttributionReport expected_report =
-      GetExpectedEventLevelReport(builder.BuildStored(), conversion);
-
-  task_environment_.FastForwardBy(kReportDelay);
-
-  EXPECT_THAT(storage()->GetAttributionReports(base::Time::Now()),
-              ElementsAre(expected_report));
+  EXPECT_THAT(
+      storage()->MaybeCreateAndStoreReport(
+          DefaultAggregatableTriggerBuilder().Build()),
+      AllOf(
+          CreateReportEventLevelStatusIs(
+              AttributionTrigger::EventLevelResult::kExcessiveAttributions),
+          CreateReportAggregatableStatusIs(
+              AttributionTrigger::AggregatableResult::kExcessiveAttributions)));
 }
 
 TEST_F(AttributionStorageTest,
@@ -1099,7 +1088,7 @@
       .max_source_registration_reporting_origins =
           std::numeric_limits<int64_t>::max(),
       .max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(),
-      .max_attributions = 1,
+      .max_attributions = 2,
   });
 
   SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder();
@@ -1400,6 +1389,13 @@
 
 TEST_F(AttributionStorageTest, FalselyAttributeImpression_ReportStored) {
   delegate()->set_max_attributions_per_source(1);
+  delegate()->set_rate_limits({
+      .time_window = base::TimeDelta::Max(),
+      .max_source_registration_reporting_origins =
+          std::numeric_limits<int64_t>::max(),
+      .max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(),
+      .max_attributions = 2,
+  });
 
   const base::Time fake_report_time = base::Time::Now() + kReportDelay;
   const base::Time fake_trigger_time = fake_report_time - base::Microseconds(1);
@@ -1442,16 +1438,26 @@
       ElementsAre(SourceActiveStateIs(
           StoredSource::ActiveState::kReachedEventLevelAttributionLimit)));
 
+  AttributionTrigger trigger = DefaultAggregatableTriggerBuilder().Build();
+
   // The falsely attributed impression should only be eligible for further
   // aggregatable reports, but not event-level reports.
   EXPECT_THAT(
-      storage()->MaybeCreateAndStoreReport(
-          DefaultAggregatableTriggerBuilder().Build()),
+      storage()->MaybeCreateAndStoreReport(trigger),
       AllOf(CreateReportEventLevelStatusIs(
                 AttributionTrigger::EventLevelResult::kFalselyAttributedSource),
             CreateReportAggregatableStatusIs(
                 AttributionTrigger::AggregatableResult::kSuccess)));
 
+  // Rate limit changed.
+  EXPECT_THAT(
+      storage()->MaybeCreateAndStoreReport(trigger),
+      AllOf(
+          CreateReportEventLevelStatusIs(
+              AttributionTrigger::EventLevelResult::kFalselyAttributedSource),
+          CreateReportAggregatableStatusIs(
+              AttributionTrigger::AggregatableResult::kExcessiveAttributions)));
+
   // The source's aggregatable budget consumed changes between the two
   // GetAttributionReports() calls due to the aggregatable trigger, which
   // requires a reflection of that change within the event level report
@@ -3099,4 +3105,30 @@
   }
 }
 
+TEST_F(AttributionStorageTest, MaxAttributions_BoundedBySourceTimeWindow) {
+  constexpr base::TimeDelta kTimeWindow = base::Days(1);
+  delegate()->set_rate_limits({
+      .time_window = kTimeWindow,
+      .max_source_registration_reporting_origins =
+          std::numeric_limits<int64_t>::max(),
+      .max_attribution_reporting_origins = std::numeric_limits<int64_t>::max(),
+      .max_attributions = 1,
+  });
+
+  storage()->StoreSource(SourceBuilder().SetExpiry(base::Days(7)).Build());
+
+  AttributionTrigger trigger = DefaultTrigger();
+
+  constexpr base::TimeDelta kTriggerDelay = base::Minutes(1);
+  task_environment_.FastForwardBy(kTriggerDelay);
+  EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess,
+            MaybeCreateAndStoreEventLevelReport(trigger));
+  EXPECT_EQ(AttributionTrigger::EventLevelResult::kExcessiveAttributions,
+            MaybeCreateAndStoreEventLevelReport(trigger));
+
+  task_environment_.FastForwardBy(kTimeWindow - kTriggerDelay);
+  EXPECT_EQ(AttributionTrigger::EventLevelResult::kSuccess,
+            MaybeCreateAndStoreEventLevelReport(trigger));
+}
+
 }  // namespace content
diff --git a/content/browser/attribution_reporting/rate_limit_table.cc b/content/browser/attribution_reporting/rate_limit_table.cc
index 9931bd2..4ac7049d 100644
--- a/content/browser/attribution_reporting/rate_limit_table.cc
+++ b/content/browser/attribution_reporting/rate_limit_table.cc
@@ -106,22 +106,19 @@
 bool RateLimitTable::AddRateLimitForSource(sql::Database* db,
                                            const StoredSource& source) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  return AddRateLimit(db, Scope::kSource, source,
-                      source.common_info().source_time());
+  return AddRateLimit(db, Scope::kSource, source);
 }
 
 bool RateLimitTable::AddRateLimitForAttribution(
     sql::Database* db,
     const AttributionInfo& attribution_info) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  return AddRateLimit(db, Scope::kAttribution, attribution_info.source,
-                      attribution_info.time);
+  return AddRateLimit(db, Scope::kAttribution, attribution_info.source);
 }
 
 bool RateLimitTable::AddRateLimit(sql::Database* db,
                                   Scope scope,
-                                  const StoredSource& source,
-                                  base::Time time) {
+                                  const StoredSource& source) {
   const CommonSourceInfo& common_info = source.common_info();
 
   // Only delete expired rate limits periodically to avoid excessive DB
@@ -161,7 +158,7 @@
   statement.BindString(4, common_info.DestinationSite().Serialize());
   statement.BindString(5, common_info.destination_origin().Serialize());
   statement.BindString(6, common_info.reporting_origin().Serialize());
-  statement.BindTime(7, time);
+  statement.BindTime(7, common_info.source_time());
   statement.BindTime(8, expiry_time);
 
   return statement.Run();
diff --git a/content/browser/attribution_reporting/rate_limit_table.h b/content/browser/attribution_reporting/rate_limit_table.h
index f129607f..05b8f7d 100644
--- a/content/browser/attribution_reporting/rate_limit_table.h
+++ b/content/browser/attribution_reporting/rate_limit_table.h
@@ -99,8 +99,7 @@
  private:
   [[nodiscard]] bool AddRateLimit(sql::Database* db,
                                   Scope scope,
-                                  const StoredSource& source,
-                                  base::Time time)
+                                  const StoredSource& source)
       VALID_CONTEXT_REQUIRED(sequence_checker_);
 
   [[nodiscard]] RateLimitResult AllowedForReportingOriginLimit(
diff --git a/content/browser/attribution_reporting/rate_limit_table_unittest.cc b/content/browser/attribution_reporting/rate_limit_table_unittest.cc
index ca903ea5..b7b34c44 100644
--- a/content/browser/attribution_reporting/rate_limit_table_unittest.cc
+++ b/content/browser/attribution_reporting/rate_limit_table_unittest.cc
@@ -81,8 +81,7 @@
   SourceBuilder NewSourceBuilder() const {
     // Ensure that operations involving attributions use the trigger time, not
     // the source time.
-    auto source_time = scope == RateLimitScope::kSource ? time : base::Time();
-    auto builder = SourceBuilder(source_time);
+    auto builder = SourceBuilder(time);
 
     builder.SetSourceOrigin(*SuitableOrigin::Deserialize(source_origin));
     builder.SetDestinationOrigin(
diff --git a/content/browser/direct_sockets/direct_sockets_open_browsertest.cc b/content/browser/direct_sockets/direct_sockets_open_browsertest.cc
index 2fb82b017e..4d10319f8 100644
--- a/content/browser/direct_sockets/direct_sockets_open_browsertest.cc
+++ b/content/browser/direct_sockets/direct_sockets_open_browsertest.cc
@@ -221,7 +221,8 @@
               ::testing::HasSubstr("keepAliveDelay must be no less than"));
 }
 
-IN_PROC_BROWSER_TEST_F(DirectSocketsOpenBrowserTest, OpenTcp_OptionsOne) {
+IN_PROC_BROWSER_TEST_F(DirectSocketsOpenBrowserTest,
+                       DISABLED_OpenTcp_OptionsOne) {
   base::HistogramTester histogram_tester;
   histogram_tester.ExpectUniqueSample(kTCPNetworkFailuresHistogramName,
                                       -net::Error::ERR_PROXY_CONNECTION_FAILED,
diff --git a/content/browser/loader/object_navigation_fallback_body_loader.cc b/content/browser/loader/object_navigation_fallback_body_loader.cc
index 239f28e..8ef9710 100644
--- a/content/browser/loader/object_navigation_fallback_body_loader.cc
+++ b/content/browser/loader/object_navigation_fallback_body_loader.cc
@@ -109,38 +109,6 @@
                      base::Unretained(this)));
 }
 
-void ObjectNavigationFallbackBodyLoader::MaybeComplete() {
-  // Completion requires receiving the completion status from the `URLLoader`,
-  // as well as the response body being completely drained.
-  if (!status_ || response_body_drainer_)
-    return;
-
-  // At this point, `this` is done and the associated NavigationRequest and
-  // `this` must be cleaned up, no matter what else happens. Running
-  // `completion_closure_` will delete the NavigationRequest, which will delete
-  // `this`.
-  base::ScopedClosureRunner cleanup(std::move(completion_closure_));
-
-  timing_info_->response_end = status_->completion_time;
-  timing_info_->encoded_body_size = status_->encoded_body_length;
-  timing_info_->decoded_body_size = status_->decoded_body_length;
-
-  RenderFrameHostManager* render_manager =
-      navigation_request_->frame_tree_node()->render_manager();
-  if (RenderFrameProxyHost* proxy = render_manager->GetProxyToParent()) {
-    if (proxy->is_render_frame_proxy_live()) {
-      proxy->GetAssociatedRemoteFrame()
-          ->RenderFallbackContentWithResourceTiming(std::move(timing_info_),
-                                                    server_timing_value_);
-    }
-  } else {
-    render_manager->current_frame_host()
-        ->GetAssociatedLocalFrame()
-        ->RenderFallbackContentWithResourceTiming(std::move(timing_info_),
-                                                  server_timing_value_);
-  }
-}
-
 void ObjectNavigationFallbackBodyLoader::BodyLoadFailed() {
   // At this point, `this` is done and the associated NavigationRequest and
   // `this` must be cleaned up, no matter what else happens. Running
@@ -192,16 +160,36 @@
 
 void ObjectNavigationFallbackBodyLoader::OnComplete(
     const network::URLLoaderCompletionStatus& status) {
-  status_ = status;
-  MaybeComplete();
+  response_body_drainer_.reset();
+  // At this point, `this` is done and the associated NavigationRequest and
+  // `this` must be cleaned up, no matter what else happens. Running
+  // `completion_closure_` will delete the NavigationRequest, which will delete
+  // `this`.
+  base::ScopedClosureRunner cleanup(std::move(completion_closure_));
+
+  timing_info_->response_end = status.completion_time;
+  timing_info_->encoded_body_size = status.encoded_body_length;
+  timing_info_->decoded_body_size = status.decoded_body_length;
+
+  RenderFrameHostManager* render_manager =
+      navigation_request_->frame_tree_node()->render_manager();
+  if (RenderFrameProxyHost* proxy = render_manager->GetProxyToParent()) {
+    if (proxy->is_render_frame_proxy_live()) {
+      proxy->GetAssociatedRemoteFrame()
+          ->RenderFallbackContentWithResourceTiming(std::move(timing_info_),
+                                                    server_timing_value_);
+    }
+  } else {
+    render_manager->current_frame_host()
+        ->GetAssociatedLocalFrame()
+        ->RenderFallbackContentWithResourceTiming(std::move(timing_info_),
+                                                  server_timing_value_);
+  }
 }
 
 void ObjectNavigationFallbackBodyLoader::OnDataAvailable(const void* data,
                                                          size_t num_bytes) {}
 
-void ObjectNavigationFallbackBodyLoader::OnDataComplete() {
-  response_body_drainer_.reset();
-  MaybeComplete();
-}
+void ObjectNavigationFallbackBodyLoader::OnDataComplete() {}
 
 }  // namespace content
diff --git a/content/browser/loader/object_navigation_fallback_body_loader.h b/content/browser/loader/object_navigation_fallback_body_loader.h
index 248dab9..34acb77 100644
--- a/content/browser/loader/object_navigation_fallback_body_loader.h
+++ b/content/browser/loader/object_navigation_fallback_body_loader.h
@@ -96,7 +96,6 @@
       network::mojom::URLLoaderClientEndpointsPtr url_loader_client_endpoints,
       base::OnceClosure completion_closure);
 
-  void MaybeComplete();
   void BodyLoadFailed();
 
   // URLLoaderClient overrides:
@@ -124,7 +123,6 @@
   // `response_body_drainer_` will be reset to null when the response body is
   // completely drained.
   std::unique_ptr<mojo::DataPipeDrainer> response_body_drainer_;
-  absl::optional<network::URLLoaderCompletionStatus> status_;
   blink::mojom::ResourceTimingInfoPtr timing_info_;
   std::string server_timing_value_;
   base::OnceClosure completion_closure_;
diff --git a/content/public/common/content_features.cc b/content/public/common/content_features.cc
index 827d40e..95b1e04 100644
--- a/content/public/common/content_features.cc
+++ b/content/public/common/content_features.cc
@@ -343,9 +343,10 @@
 // Enables support for the PPB_VideoDecoder(Dev) API. If this feature is
 // false (and the command-line override is not set in the renderer), the API
 // will appear as unsupported if asked for by a plugin.
+// See crbug.com/1382469 for details.
 BASE_FEATURE(kSupportPepperVideoDecoderDevAPI,
              "SupportPepperVideoDecoderDevAPI",
-             base::FEATURE_ENABLED_BY_DEFAULT);
+             base::FEATURE_DISABLED_BY_DEFAULT);
 
 // Enables service workers on chrome-untrusted:// urls.
 BASE_FEATURE(kEnableServiceWorkersForChromeUntrusted,
diff --git a/content/renderer/pepper/resource_creation_impl_unittest.cc b/content/renderer/pepper/resource_creation_impl_unittest.cc
index 3422d64..61f4f3e 100644
--- a/content/renderer/pepper/resource_creation_impl_unittest.cc
+++ b/content/renderer/pepper/resource_creation_impl_unittest.cc
@@ -92,11 +92,11 @@
   bool was_video_decoder_impl_created_ = false;
 };
 
-TEST_F(ResourceCreationImplTest, APISupportedByDefault) {
+TEST_F(ResourceCreationImplTest, APIUnsupportedByDefault) {
   resource_creation_impl()->CreateVideoDecoderDev(
       0, 0, static_cast<PP_VideoDecoder_Profile>(-1));
 
-  EXPECT_TRUE(was_video_decoder_impl_created());
+  EXPECT_FALSE(was_video_decoder_impl_created());
 }
 
 TEST_F(ResourceCreationImplTest,
diff --git a/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections-expected.txt b/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections-expected.txt
new file mode 100644
index 0000000..21e9203
--- /dev/null
+++ b/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections-expected.txt
@@ -0,0 +1 @@
+input.QueryInterface(IAccessibleTextSelectionContainer).selections=[{startObj: <obj>, startOffset: 3, endObj: <obj>, endOffset: 5}]
diff --git a/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections.html b/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections.html
new file mode 100644
index 0000000..d2a93fd7d
--- /dev/null
+++ b/content/test/data/accessibility/win/ia2/iaccessibletextselectioncontainer/iaccessibletextselectioncontainer-selections.html
@@ -0,0 +1,11 @@
+<!--
+@SCRIPT:
+  input.QueryInterface(IAccessibleTextSelectionContainer).selections
+-->
+<!DOCTYPE html>
+<input id="input" value="My name is Petya">
+<script>
+  let input = document.getElementById("input");
+  input.focus();
+  input.setSelectionRange(3, 5);
+</script>
diff --git a/content/test/data/autofill/autofill_form_devtools_issues_test.html b/content/test/data/autofill/autofill_form_devtools_issues_test.html
new file mode 100644
index 0000000..919f905
--- /dev/null
+++ b/content/test/data/autofill/autofill_form_devtools_issues_test.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Autofill Form</title>
+  </head>
+  <body>
+    <form id="testform" method="post">
+      <!-- for attribute associated to an input name. -->
+      <label for="name">I should be associated to an id attribute.</label>
+      <input type="text" name="name" id="input_1">
+      <!-- Inputs with duplicated ids. -->
+      <input aria-label="I have a label" type="text" id="not_unique_id">
+      <input aria-label="I have a label" type="text" id="not_unique_id">
+      <!-- Inputs with no label. -->
+      <input type="text" id="input_4">
+    </form>
+  </body>
+</html>
diff --git a/device/bluetooth/bluetooth_gatt_notify_session.cc b/device/bluetooth/bluetooth_gatt_notify_session.cc
index 1f5dd75..798e567 100644
--- a/device/bluetooth/bluetooth_gatt_notify_session.cc
+++ b/device/bluetooth/bluetooth_gatt_notify_session.cc
@@ -11,12 +11,19 @@
 
 namespace device {
 
+// static
+BluetoothGattNotifySession::Id BluetoothGattNotifySession::GetNextId() {
+  static Id::Generator generator;
+  return generator.GenerateNextId();
+}
+
 BluetoothGattNotifySession::BluetoothGattNotifySession(
     base::WeakPtr<BluetoothRemoteGattCharacteristic> characteristic)
     : characteristic_(characteristic),
       characteristic_id_(characteristic.get() ? characteristic->GetIdentifier()
                                               : std::string()),
-      active_(true) {}
+      active_(true),
+      unique_id_(GetNextId()) {}
 
 BluetoothGattNotifySession::~BluetoothGattNotifySession() {
   if (active_) {
@@ -41,7 +48,7 @@
 void BluetoothGattNotifySession::Stop(base::OnceClosure callback) {
   active_ = false;
   if (characteristic_ != nullptr) {
-    characteristic_->StopNotifySession(this, std::move(callback));
+    characteristic_->StopNotifySession(unique_id(), std::move(callback));
   } else {
     base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
         FROM_HERE, std::move(callback));
diff --git a/device/bluetooth/bluetooth_gatt_notify_session.h b/device/bluetooth/bluetooth_gatt_notify_session.h
index 6d3c73e7..deb85f5 100644
--- a/device/bluetooth/bluetooth_gatt_notify_session.h
+++ b/device/bluetooth/bluetooth_gatt_notify_session.h
@@ -9,6 +9,7 @@
 
 #include "base/functional/callback.h"
 #include "base/memory/weak_ptr.h"
+#include "base/types/id_type.h"
 #include "device/bluetooth/bluetooth_export.h"
 
 namespace device {
@@ -21,6 +22,8 @@
 // BluetoothRemoteGattCharacteristic::StartNotifySession.
 class DEVICE_BLUETOOTH_EXPORT BluetoothGattNotifySession {
  public:
+  using Id = base::IdTypeU64<BluetoothGattNotifySession>;
+
   explicit BluetoothGattNotifySession(
       base::WeakPtr<BluetoothRemoteGattCharacteristic> characteristic);
 
@@ -28,6 +31,8 @@
   BluetoothGattNotifySession& operator=(const BluetoothGattNotifySession&) =
       delete;
 
+  Id unique_id() { return unique_id_; }
+
   // Destructor automatically stops this session.
   virtual ~BluetoothGattNotifySession();
 
@@ -50,10 +55,13 @@
   virtual void Stop(base::OnceClosure callback);
 
  private:
+  static Id GetNextId();
+
   // The associated characteristic.
   base::WeakPtr<BluetoothRemoteGattCharacteristic> characteristic_;
   std::string characteristic_id_;
   bool active_;
+  const Id unique_id_;
 };
 
 }  // namespace device
diff --git a/device/bluetooth/bluetooth_remote_gatt_characteristic.cc b/device/bluetooth/bluetooth_remote_gatt_characteristic.cc
index 0215a6cf..b6ab7af 100644
--- a/device/bluetooth/bluetooth_remote_gatt_characteristic.cc
+++ b/device/bluetooth/bluetooth_remote_gatt_characteristic.cc
@@ -266,9 +266,9 @@
   DCHECK(notify_command_running_);
   pending_notify_commands_.pop();
 
-  std::unique_ptr<device::BluetoothGattNotifySession> notify_session(
-      new BluetoothGattNotifySession(weak_ptr_factory_.GetWeakPtr()));
-  notify_sessions_.insert(notify_session.get());
+  auto notify_session = std::make_unique<device::BluetoothGattNotifySession>(
+      weak_ptr_factory_.GetWeakPtr());
+  notify_sessions_.insert(notify_session->unique_id());
 
   auto this_ptr = GetWeakPtr();
   std::move(callback).Run(std::move(notify_session));
@@ -304,7 +304,7 @@
 }
 
 void BluetoothRemoteGattCharacteristic::StopNotifySession(
-    BluetoothGattNotifySession* session,
+    BluetoothGattNotifySession::Id session,
     base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   auto split_callback = base::SplitOnceCallback(std::move(callback));
@@ -322,7 +322,7 @@
 }
 
 void BluetoothRemoteGattCharacteristic::ExecuteStopNotifySession(
-    BluetoothGattNotifySession* session,
+    BluetoothGattNotifySession::Id session,
     base::OnceClosure callback,
     CommandStatus previous_command) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
@@ -372,8 +372,7 @@
       ccc_descriptor[0],
       base::BindOnce(
           &BluetoothRemoteGattCharacteristic::OnStopNotifySessionSuccess,
-          GetWeakPtr(), base::UnsafeDanglingUntriaged(session),
-          std::move(split_callback.first)),
+          GetWeakPtr(), session, std::move(split_callback.first)),
       base::BindOnce(
           &BluetoothRemoteGattCharacteristic::OnStopNotifySessionError,
           GetWeakPtr(), session, std::move(split_callback.second)));
@@ -387,7 +386,7 @@
 }
 
 void BluetoothRemoteGattCharacteristic::OnStopNotifySessionSuccess(
-    BluetoothGattNotifySession* session,
+    BluetoothGattNotifySession::Id session,
     base::OnceClosure callback) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   DCHECK(notify_command_running_);
@@ -408,7 +407,7 @@
 }
 
 void BluetoothRemoteGattCharacteristic::OnStopNotifySessionError(
-    BluetoothGattNotifySession* session,
+    BluetoothGattNotifySession::Id session,
     base::OnceClosure callback,
     BluetoothGattService::GattErrorCode error) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
diff --git a/device/bluetooth/bluetooth_remote_gatt_characteristic.h b/device/bluetooth/bluetooth_remote_gatt_characteristic.h
index 7539330..f7b3c31c 100644
--- a/device/bluetooth/bluetooth_remote_gatt_characteristic.h
+++ b/device/bluetooth/bluetooth_remote_gatt_characteristic.h
@@ -20,13 +20,13 @@
 #include "base/sequence_checker.h"
 #include "device/bluetooth/bluetooth_export.h"
 #include "device/bluetooth/bluetooth_gatt_characteristic.h"
+#include "device/bluetooth/bluetooth_gatt_notify_session.h"
 #include "device/bluetooth/bluetooth_remote_gatt_service.h"
 #include "device/bluetooth/public/cpp/bluetooth_uuid.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace device {
 
-class BluetoothGattNotifySession;
 class BluetoothRemoteGattDescriptor;
 
 // BluetoothRemoteGattCharacteristic represents a remote GATT characteristic.
@@ -254,7 +254,7 @@
   // notifications/indications. Core Bluetooth Specification [V4.2 Vol 3 Part G
   // Section 3.3.1.1. Characteristic Properties] requires this descriptor to be
   // present when notifications/indications are supported.
-  virtual void StopNotifySession(BluetoothGattNotifySession* session,
+  virtual void StopNotifySession(BluetoothGattNotifySession::Id session,
                                  base::OnceClosure callback);
 
   class NotifySessionCommand {
@@ -290,13 +290,13 @@
   void OnStartNotifySessionError(ErrorCallback error_callback,
                                  BluetoothGattService::GattErrorCode error);
 
-  void ExecuteStopNotifySession(BluetoothGattNotifySession* session,
+  void ExecuteStopNotifySession(BluetoothGattNotifySession::Id session,
                                 base::OnceClosure callback,
                                 CommandStatus previous_command);
   void CancelStopNotifySession(base::OnceClosure callback);
-  void OnStopNotifySessionSuccess(BluetoothGattNotifySession* session,
+  void OnStopNotifySessionSuccess(BluetoothGattNotifySession::Id session,
                                   base::OnceClosure callback);
-  void OnStopNotifySessionError(BluetoothGattNotifySession* session,
+  void OnStopNotifySessionError(BluetoothGattNotifySession::Id session,
                                 base::OnceClosure callback,
                                 BluetoothGattService::GattErrorCode error);
   bool IsNotificationTypeSupported(
@@ -312,7 +312,7 @@
   bool notify_command_running_ = false;
 
   // Set of active notify sessions.
-  std::set<BluetoothGattNotifySession*> notify_sessions_;
+  std::set<BluetoothGattNotifySession::Id> notify_sessions_;
 
   SEQUENCE_CHECKER(sequence_checker_);
 
diff --git a/device/bluetooth/test/mock_bluetooth_gatt_characteristic.h b/device/bluetooth/test/mock_bluetooth_gatt_characteristic.h
index 955df2b..a871d58 100644
--- a/device/bluetooth/test/mock_bluetooth_gatt_characteristic.h
+++ b/device/bluetooth/test/mock_bluetooth_gatt_characteristic.h
@@ -65,12 +65,12 @@
   }
   MOCK_METHOD2(StartNotifySession_,
                void(NotifySessionCallback&, ErrorCallback&));
-  void StopNotifySession(BluetoothGattNotifySession* s,
+  void StopNotifySession(BluetoothGattNotifySession::Id s,
                          base::OnceClosure c) override {
     StopNotifySession_(s, c);
   }
   MOCK_METHOD2(StopNotifySession_,
-               void(BluetoothGattNotifySession*, base::OnceClosure&));
+               void(BluetoothGattNotifySession::Id, base::OnceClosure&));
   void ReadRemoteCharacteristic(ValueCallback c) override {
     ReadRemoteCharacteristic_(c);
   }
diff --git a/docs/webapps/isolated_web_apps.md b/docs/webapps/isolated_web_apps.md
index 10ee9eb9..f0aaf14b 100644
--- a/docs/webapps/isolated_web_apps.md
+++ b/docs/webapps/isolated_web_apps.md
@@ -29,13 +29,13 @@
 3. The `IsolatedWebAppURLLoader` passes the request on to the
    `IsolatedWebAppReaderRegistry::ReadResponse` method.
 4. The behavior of `ReadResponse` depends on whether an instance of
-   `SignedWebBundleReader` for the given Signed Web Bundle has already been
+   `IsolatedWebAppResponseReader` for the given Signed Web Bundle has already been
    cached.
-   - If a `SignedWebBundleReader` is cached, then that reader is used to read
-     the response from the Signed Web Bundle and the response is sent back to
-     the loader. This is very fast, since the reader has a map of URLs to
+   - If a `IsolatedWebAppResponseReader` is cached, then that reader is used to
+     read the response from the Signed Web Bundle and the response is sent back
+     to the loader. This is very fast, since the reader has a map of URLs to
      offsets into the Signed Web Bundle.
-   - If a `SignedWebBundleReader` is not cached, however, the process continues
+   - If a `IsolatedWebAppResponseReader` is not cached, however, the process continues
      and a new reader is created.
 5. The Integrity Block is read from the Signed Web Bundle.
 6. The validity of the Integrity Block is verified by
@@ -52,8 +52,8 @@
    and validated using `IsolatedWebAppValidator::ValidateMetadata`. This
    includes a check that validates that URLs contained in the Signed Web Bundle
    use the `isolated-app:` scheme, and more.
-8. If the metadata is also valid, then the `SignedWebBundleReader` is added to
-   the cache and the response for the given request is read from it.
+8. If the metadata is also valid, then the `IsolatedWebAppResponseReader` is
+   added to the cache and the response for the given request is read from it.
 
 ## Isolated Web Apps vs. Signed Web Bundles
 
@@ -94,9 +94,8 @@
 
 The `SignedWebBundleReader` is supposed to be a generic reader for Signed Web
 Bundles, unrelated to Isolated Web Apps. As such, it does not know anything
-about Isolated Web Apps or the `isolated-app:` scheme. The more specific
-requirements for Signed Web Bundles when used as Isolated Web Apps are checked
-as part of the `IsolatedWebAppValidator`, `IsolatedWebAppReaderRegistry`, and
-`IsolatedWebAppURLLoader`. For example, the `IsolatedWebAppValidator` checks
-that the URLs contained in the Signed Web Bundle do not have query parameters or
-fragments.
+about Isolated Web Apps or the `isolated-app:` scheme. Usually, code dealing
+with Isolated Web Apps should use the `IsolatedWebAppResponseReader(Factory)` to
+read responses from the bundle. It checks the stricter requirements of Signed
+Web Bundles when used as Isolated Web Apps. For example, it checks that the URLs
+contained in the Signed Web Bundle do not have query parameters or fragments.
diff --git a/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test untrusted/properties.json b/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test untrusted/properties.json
new file mode 100644
index 0000000..fdc22938
--- /dev/null
+++ b/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test untrusted/properties.json
@@ -0,0 +1,65 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "Mac Builder reclient scandeps test untrusted",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb",
+                  "goma_use_local"
+                ],
+                "build_config": "Release",
+                "config": "chromium",
+                "target_bits": 64,
+                "target_platform": "mac"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "use_clang_coverage",
+                  "reclient_test"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "Mac Builder reclient scandeps test untrusted",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-untrusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "profiler_service": "reclient-mac",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test/properties.json b/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test/properties.json
new file mode 100644
index 0000000..635b6c4
--- /dev/null
+++ b/infra/config/generated/builders/reclient/Mac Builder reclient scandeps test/properties.json
@@ -0,0 +1,65 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "Mac Builder reclient scandeps test",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb",
+                  "goma_use_local"
+                ],
+                "build_config": "Release",
+                "config": "chromium",
+                "target_bits": 64,
+                "target_platform": "mac"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "use_clang_coverage",
+                  "reclient_test"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "Mac Builder reclient scandeps test",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-trusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "profiler_service": "reclient-mac",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test untrusted/properties.json b/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test untrusted/properties.json
new file mode 100644
index 0000000..ec67cf8
--- /dev/null
+++ b/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test untrusted/properties.json
@@ -0,0 +1,65 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "ios-simulator reclient scandeps test untrusted",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb",
+                  "mac_toolchain"
+                ],
+                "build_config": "Debug",
+                "config": "chromium",
+                "target_bits": 64,
+                "target_platform": "ios"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "use_clang_coverage",
+                  "reclient_test"
+                ],
+                "config": "ios"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "ios-simulator reclient scandeps test untrusted",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-untrusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium",
+  "xcode_build_version": "14c18"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test/properties.json b/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test/properties.json
new file mode 100644
index 0000000..32769eb
--- /dev/null
+++ b/infra/config/generated/builders/reclient/ios-simulator reclient scandeps test/properties.json
@@ -0,0 +1,65 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "ios-simulator reclient scandeps test",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb",
+                  "mac_toolchain"
+                ],
+                "build_config": "Debug",
+                "config": "chromium",
+                "target_bits": 64,
+                "target_platform": "ios"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "use_clang_coverage",
+                  "reclient_test"
+                ],
+                "config": "ios"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "ios-simulator reclient scandeps test",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-trusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium",
+  "xcode_build_version": "14c18"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test untrusted/properties.json b/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test untrusted/properties.json
new file mode 100644
index 0000000..e2fe91f
--- /dev/null
+++ b/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test untrusted/properties.json
@@ -0,0 +1,63 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "mac-arm64-rel reclient scandeps test untrusted",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb"
+                ],
+                "build_config": "Release",
+                "config": "chromium",
+                "target_arch": "arm",
+                "target_bits": 64,
+                "target_platform": "mac"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "reclient_test"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "mac-arm64-rel reclient scandeps test untrusted",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-untrusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test/properties.json b/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test/properties.json
new file mode 100644
index 0000000..8ca9e47
--- /dev/null
+++ b/infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test/properties.json
@@ -0,0 +1,63 @@
+{
+  "$build/chromium_tests_builder_config": {
+    "builder_config": {
+      "builder_db": {
+        "entries": [
+          {
+            "builder_id": {
+              "bucket": "reclient",
+              "builder": "mac-arm64-rel reclient scandeps test",
+              "project": "chromium"
+            },
+            "builder_spec": {
+              "build_gs_bucket": "chromium-fyi-archive",
+              "builder_group": "chromium.reclient.fyi",
+              "execution_mode": "COMPILE_AND_TEST",
+              "legacy_chromium_config": {
+                "apply_configs": [
+                  "mb"
+                ],
+                "build_config": "Release",
+                "config": "chromium",
+                "target_arch": "arm",
+                "target_bits": 64,
+                "target_platform": "mac"
+              },
+              "legacy_gclient_config": {
+                "apply_configs": [
+                  "reclient_test"
+                ],
+                "config": "chromium"
+              }
+            }
+          }
+        ]
+      },
+      "builder_ids": [
+        {
+          "bucket": "reclient",
+          "builder": "mac-arm64-rel reclient scandeps test",
+          "project": "chromium"
+        }
+      ]
+    }
+  },
+  "$build/reclient": {
+    "bootstrap_env": {
+      "GLOG_vmodule": "bridge*=2",
+      "GOMA_COMPILER_PROXY_ENABLE_CRASH_DUMP": "true"
+    },
+    "instance": "rbe-chromium-trusted-test",
+    "metrics_project": "chromium-reclient-metrics",
+    "scandeps_server": true
+  },
+  "$recipe_engine/resultdb/test_presentation": {
+    "column_keys": [],
+    "grouping_keys": [
+      "status",
+      "v.test_suite"
+    ]
+  },
+  "builder_group": "chromium.reclient.fyi",
+  "recipe": "chromium"
+}
\ No newline at end of file
diff --git a/infra/config/generated/luci/cr-buildbucket.cfg b/infra/config/generated/luci/cr-buildbucket.cfg
index 175e146..e0b16b1 100644
--- a/infra/config/generated/luci/cr-buildbucket.cfg
+++ b/infra/config/generated/luci/cr-buildbucket.cfg
@@ -54183,6 +54183,182 @@
       description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
     }
     builders {
+      name: "Mac Builder reclient scandeps test"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/Mac Builder reclient scandeps test/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-trusted-test rbe instance."
+    }
+    builders {
+      name: "Mac Builder reclient scandeps test untrusted"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/Mac Builder reclient scandeps test untrusted/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
+    }
+    builders {
       name: "Mac Builder reclient staging"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -55247,6 +55423,190 @@
       description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
     }
     builders {
+      name: "ios-simulator reclient scandeps test"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/ios-simulator reclient scandeps test/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      caches {
+        name: "xcode_ios_14c18"
+        path: "xcode_ios_14c18.app"
+      }
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-trusted-test rbe instance."
+    }
+    builders {
+      name: "ios-simulator reclient scandeps test untrusted"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/ios-simulator reclient scandeps test untrusted/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      caches {
+        name: "xcode_ios_14c18"
+        path: "xcode_ios_14c18.app"
+      }
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
+    }
+    builders {
       name: "ios-simulator reclient staging"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
@@ -55615,6 +55975,182 @@
       description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
     }
     builders {
+      name: "mac-arm64-rel reclient scandeps test"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-trusted-test rbe instance."
+    }
+    builders {
+      name: "mac-arm64-rel reclient scandeps test untrusted"
+      swarming_host: "chromium-swarm.appspot.com"
+      dimensions: "builderless:1"
+      dimensions: "cpu:x86-64"
+      dimensions: "free_space:standard"
+      dimensions: "os:Mac-12"
+      dimensions: "pool:luci.chromium.ci"
+      exe {
+        cipd_package: "infra/chromium/bootstrapper/${platform}"
+        cipd_version: "latest"
+        cmd: "bootstrapper"
+      }
+      properties:
+        '{'
+        '  "$bootstrap/exe": {'
+        '    "exe": {'
+        '      "cipd_package": "infra/recipe_bundles/chromium.googlesource.com/chromium/tools/build",'
+        '      "cipd_version": "refs/heads/main",'
+        '      "cmd": ['
+        '        "luciexe"'
+        '      ]'
+        '    }'
+        '  },'
+        '  "$bootstrap/properties": {'
+        '    "properties_file": "infra/config/generated/builders/reclient/mac-arm64-rel reclient scandeps test untrusted/properties.json",'
+        '    "top_level_project": {'
+        '      "ref": "refs/heads/main",'
+        '      "repo": {'
+        '        "host": "chromium.googlesource.com",'
+        '        "project": "chromium/src"'
+        '      }'
+        '    }'
+        '  },'
+        '  "builder_group": "chromium.reclient.fyi",'
+        '  "led_builder_is_bootstrapped": true,'
+        '  "recipe": "chromium"'
+        '}'
+      priority: 35
+      execution_timeout_secs: 10800
+      build_numbers: YES
+      service_account: "chromium-ci-builder@chops-service-accounts.iam.gserviceaccount.com"
+      experiments {
+        key: "chromium_swarming.expose_merge_script_failures"
+        value: 100
+      }
+      experiments {
+        key: "luci.buildbucket.omit_python2"
+        value: 100
+      }
+      experiments {
+        key: "luci.recipes.use_python3"
+        value: 100
+      }
+      resultdb {
+        enable: true
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "ci_test_results"
+          test_results {}
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "gpu_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "ninja://chrome/test:telemetry_gpu_integration_test[^/]*/.+"
+            }
+          }
+        }
+        bq_exports {
+          project: "chrome-luci-data"
+          dataset: "chromium"
+          table: "blink_web_tests_ci_test_results"
+          test_results {
+            predicate {
+              test_id_regexp: "(ninja://[^/]*blink_web_tests/.+)|(ninja://[^/]*blink_wpt_tests/.+)"
+            }
+          }
+        }
+        history_options {
+          use_invocation_timestamp: true
+        }
+      }
+      description_html: "Builds chromium using the test version of reclient and the rbe-chromium-untrusted-test rbe instance."
+    }
+    builders {
       name: "mac-arm64-rel reclient staging"
       swarming_host: "chromium-swarm.appspot.com"
       dimensions: "builderless:1"
diff --git a/infra/config/generated/luci/luci-milo.cfg b/infra/config/generated/luci/luci-milo.cfg
index 38f26f9a..ef8c73ba 100644
--- a/infra/config/generated/luci/luci-milo.cfg
+++ b/infra/config/generated/luci/luci-milo.cfg
@@ -13362,6 +13362,11 @@
     short_name: "cmp"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/ios-simulator reclient scandeps test"
+    category: "rbe|ios"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/ios-simulator reclient staging"
     category: "rbe|ios"
     short_name: "rcs"
@@ -13392,6 +13397,11 @@
     short_name: "rcs"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/Mac Builder reclient scandeps test"
+    category: "rbe|mac"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/Mac Builder reclient staging"
     category: "rbe|mac"
     short_name: "rcs"
@@ -13402,6 +13412,11 @@
     short_name: "rcs"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/mac-arm64-rel reclient scandeps test"
+    category: "rbe|mac"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/mac-arm64-rel reclient staging"
     category: "rbe|mac"
     short_name: "rcs"
@@ -13422,6 +13437,11 @@
     short_name: "rcs"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/ios-simulator reclient scandeps test untrusted"
+    category: "rbecq|ios"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/ios-simulator reclient staging untrusted"
     category: "rbecq|ios"
     short_name: "rcs"
@@ -13452,6 +13472,11 @@
     short_name: "rcs"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/Mac Builder reclient scandeps test untrusted"
+    category: "rbecq|mac"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/Mac Builder reclient staging untrusted"
     category: "rbecq|mac"
     short_name: "rcs"
@@ -13462,6 +13487,11 @@
     short_name: "rcs"
   }
   builders {
+    name: "buildbucket/luci.chromium.reclient/mac-arm64-rel reclient scandeps test untrusted"
+    category: "rbecq|mac"
+    short_name: "rcs"
+  }
+  builders {
     name: "buildbucket/luci.chromium.reclient/mac-arm64-rel reclient staging untrusted"
     category: "rbecq|mac"
     short_name: "rcs"
diff --git a/infra/config/generated/luci/luci-scheduler.cfg b/infra/config/generated/luci/luci-scheduler.cfg
index 5f30226..dcffceea 100644
--- a/infra/config/generated/luci/luci-scheduler.cfg
+++ b/infra/config/generated/luci/luci-scheduler.cfg
@@ -1966,6 +1966,24 @@
   }
 }
 job {
+  id: "Mac Builder reclient scandeps test"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "Mac Builder reclient scandeps test"
+  }
+}
+job {
+  id: "Mac Builder reclient scandeps test untrusted"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "Mac Builder reclient scandeps test untrusted"
+  }
+}
+job {
   id: "Mac Builder reclient staging"
   realm: "reclient"
   buildbucket {
@@ -4324,6 +4342,24 @@
   }
 }
 job {
+  id: "ios-simulator reclient scandeps test"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "ios-simulator reclient scandeps test"
+  }
+}
+job {
+  id: "ios-simulator reclient scandeps test untrusted"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "ios-simulator reclient scandeps test untrusted"
+  }
+}
+job {
   id: "ios-simulator reclient staging"
   realm: "reclient"
   buildbucket {
@@ -5252,6 +5288,24 @@
   }
 }
 job {
+  id: "mac-arm64-rel reclient scandeps test"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "mac-arm64-rel reclient scandeps test"
+  }
+}
+job {
+  id: "mac-arm64-rel reclient scandeps test untrusted"
+  realm: "reclient"
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "reclient"
+    builder: "mac-arm64-rel reclient scandeps test untrusted"
+  }
+}
+job {
   id: "mac-arm64-rel reclient staging"
   realm: "reclient"
   buildbucket {
@@ -6432,6 +6486,8 @@
   triggers: "Linux Builder reclient staging untrusted"
   triggers: "Linux Builder reclient test"
   triggers: "Linux Builder reclient test untrusted"
+  triggers: "Mac Builder reclient scandeps test"
+  triggers: "Mac Builder reclient scandeps test untrusted"
   triggers: "Mac Builder reclient staging"
   triggers: "Mac Builder reclient staging untrusted"
   triggers: "Mac Builder reclient test"
@@ -6444,10 +6500,14 @@
   triggers: "Win x64 Builder reclient staging untrusted"
   triggers: "Win x64 Builder reclient test"
   triggers: "Win x64 Builder reclient test untrusted"
+  triggers: "ios-simulator reclient scandeps test"
+  triggers: "ios-simulator reclient scandeps test untrusted"
   triggers: "ios-simulator reclient staging"
   triggers: "ios-simulator reclient staging untrusted"
   triggers: "ios-simulator reclient test"
   triggers: "ios-simulator reclient test untrusted"
+  triggers: "mac-arm64-rel reclient scandeps test"
+  triggers: "mac-arm64-rel reclient scandeps test untrusted"
   triggers: "mac-arm64-rel reclient staging"
   triggers: "mac-arm64-rel reclient staging untrusted"
   triggers: "mac-arm64-rel reclient test"
diff --git a/infra/config/subprojects/reclient/reclient.star b/infra/config/subprojects/reclient/reclient.star
index 238c1d8b..1ef4e15e 100644
--- a/infra/config/subprojects/reclient/reclient.star
+++ b/infra/config/subprojects/reclient/reclient.star
@@ -213,6 +213,33 @@
     reclient_profiler_service = "reclient-mac",
 )
 
+fyi_reclient_test_builder(
+    name = "Mac Builder reclient scandeps test",
+    builder_spec = builder_config.copy_from(
+        "ci/Mac Builder",
+        lambda spec: structs.evolve(
+            spec,
+            gclient_config = structs.extend(
+                spec.gclient_config,
+                apply_configs = [
+                    "reclient_test",
+                ],
+            ),
+            build_gs_bucket = "chromium-fyi-archive",
+        ),
+    ),
+    builderless = True,
+    cores = None,
+    os = os.MAC_DEFAULT,
+    console_view_category = "mac",
+    priority = 35,
+    reclient_bootstrap_env = {
+        "GLOG_vmodule": "bridge*=2",
+    },
+    reclient_profiler_service = "reclient-mac",
+    reclient_scandeps_server = True,
+)
+
 fyi_reclient_staging_builder(
     name = "Win x64 Builder reclient staging",
     builder_spec = builder_config.copy_from(
@@ -325,6 +352,33 @@
     },
 )
 
+fyi_reclient_test_builder(
+    name = "ios-simulator reclient scandeps test",
+    builder_spec = builder_config.copy_from(
+        "ci/ios-simulator",
+        lambda spec: structs.evolve(
+            spec,
+            gclient_config = structs.extend(
+                spec.gclient_config,
+                apply_configs = [
+                    "reclient_test",
+                ],
+            ),
+            build_gs_bucket = "chromium-fyi-archive",
+        ),
+    ),
+    builderless = True,
+    cores = None,
+    os = os.MAC_DEFAULT,
+    xcode = xcode.x14main,
+    console_view_category = "ios",
+    priority = 35,
+    reclient_bootstrap_env = {
+        "GLOG_vmodule": "bridge*=2",
+    },
+    reclient_scandeps_server = True,
+)
+
 fyi_reclient_staging_builder(
     name = "ios-simulator reclient staging",
     builder_spec = builder_config.copy_from(
@@ -401,6 +455,32 @@
     },
 )
 
+fyi_reclient_test_builder(
+    name = "mac-arm64-rel reclient scandeps test",
+    builder_spec = builder_config.copy_from(
+        "ci/mac-arm64-rel",
+        lambda spec: structs.evolve(
+            spec,
+            gclient_config = structs.extend(
+                spec.gclient_config,
+                apply_configs = [
+                    "reclient_test",
+                ],
+            ),
+            build_gs_bucket = "chromium-fyi-archive",
+        ),
+    ),
+    builderless = True,
+    cores = None,
+    os = os.MAC_DEFAULT,
+    console_view_category = "mac",
+    priority = 35,
+    reclient_bootstrap_env = {
+        "GLOG_vmodule": "bridge*=2",
+    },
+    reclient_scandeps_server = True,
+)
+
 ci.builder(
     name = "Comparison Linux (reclient vs reclient remote links)",
     executable = "recipe:reclient_reclient_comparison",
diff --git a/ios/chrome/browser/flags/BUILD.gn b/ios/chrome/browser/flags/BUILD.gn
index 9119ea5..9ae620d 100644
--- a/ios/chrome/browser/flags/BUILD.gn
+++ b/ios/chrome/browser/flags/BUILD.gn
@@ -64,7 +64,7 @@
     "//ios/chrome/browser/promos_manager:features",
     "//ios/chrome/browser/screen_time:buildflags",
     "//ios/chrome/browser/sessions:features",
-    "//ios/chrome/browser/text_selection",
+    "//ios/chrome/browser/text_selection:text_selection_utils",
     "//ios/chrome/browser/ui:feature_flags",
     "//ios/chrome/browser/ui/app_store_rating:features",
     "//ios/chrome/browser/ui/autofill:features",
diff --git a/ios/chrome/browser/flags/about_flags.mm b/ios/chrome/browser/flags/about_flags.mm
index 2b0982cc..52a43b1 100644
--- a/ios/chrome/browser/flags/about_flags.mm
+++ b/ios/chrome/browser/flags/about_flags.mm
@@ -1219,6 +1219,12 @@
     {"promos-manager-uses-fet", flag_descriptions::kPromosManagerUsesFETName,
      flag_descriptions::kPromosManagerUsesFETDescription, flags_ui::kOsIos,
      FEATURE_VALUE_TYPE(kPromosManagerUsesFET)},
+    {"enable-accessibility-identifier-to-omnibox-leading-image",
+     flag_descriptions::kEnableAccessibilityIdentifierToOmniboxLeadingImageName,
+     flag_descriptions::
+         kEnableAccessibilityIdentifierToOmniboxLeadingImageDescription,
+     flags_ui::kOsIos,
+     FEATURE_VALUE_TYPE(kEnableAccessibilityIdentifierToOmniboxLeadingImage)},
 };
 
 bool SkipConditionalFeatureEntry(const flags_ui::FeatureEntry& entry) {
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
index 0b8dda1..f1ecc90b 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.cc
@@ -236,6 +236,11 @@
     "A crash report will be uploaded if the main thread is frozen more than "
     "the time specified by this flag.";
 
+const char kEnableAccessibilityIdentifierToOmniboxLeadingImageName[] =
+    "Enable accessibility identifier leading image";
+const char kEnableAccessibilityIdentifierToOmniboxLeadingImageDescription[] =
+    "Enable accessibility identifier to Omnibox leading image";
+
 const char kEnableDiscoverFeedTopSyncPromoName[] =
     "Enable the sync promo on top of the feed.";
 const char kEnableDiscoverFeedTopSyncPromoDescription[] =
diff --git a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
index d21a175..d7ed429 100644
--- a/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
+++ b/ios/chrome/browser/flags/ios_chrome_flag_descriptions.h
@@ -138,6 +138,12 @@
 extern const char kEmailName[];
 extern const char kEmailDescription[];
 
+// Title and description for enable accessibility identifier to omnibox leading
+// image.
+extern const char kEnableAccessibilityIdentifierToOmniboxLeadingImageName[];
+extern const char
+    kEnableAccessibilityIdentifierToOmniboxLeadingImageDescription[];
+
 // Title and description for the flag to enable phone numbers detection and
 // processing.
 extern const char kPhoneNumberName[];
diff --git a/ios/chrome/browser/text_selection/BUILD.gn b/ios/chrome/browser/text_selection/BUILD.gn
index a73e5dab..75c663d1 100644
--- a/ios/chrome/browser/text_selection/BUILD.gn
+++ b/ios/chrome/browser/text_selection/BUILD.gn
@@ -9,10 +9,9 @@
     "text_classifier_model_service.mm",
     "text_classifier_model_service_factory.h",
     "text_classifier_model_service_factory.mm",
-    "text_selection_util.h",
-    "text_selection_util.mm",
   ]
   deps = [
+    ":text_selection_utils",
     "//base",
     "//components/keyed_service/ios",
     "//components/optimization_guide/core",
@@ -22,6 +21,15 @@
   ]
 }
 
+source_set("text_selection_utils") {
+  configs += [ "//build/config/compiler:enable_arc" ]
+  sources = [
+    "text_selection_util.h",
+    "text_selection_util.mm",
+  ]
+  deps = [ "//base" ]
+}
+
 source_set("test_support") {
   configs += [ "//build/config/compiler:enable_arc" ]
   testonly = true
diff --git a/ios/chrome/browser/ui/autofill/cells/BUILD.gn b/ios/chrome/browser/ui/autofill/cells/BUILD.gn
index eb00e66..2dee930 100644
--- a/ios/chrome/browser/ui/autofill/cells/BUILD.gn
+++ b/ios/chrome/browser/ui/autofill/cells/BUILD.gn
@@ -13,6 +13,7 @@
     "autofill_edit_item.mm",
     "cvc_header_item.h",
     "cvc_header_item.mm",
+    "cvc_item+private.h",
     "cvc_item.h",
     "cvc_item.mm",
     "expiration_date_edit_item+private.h",
diff --git a/ios/chrome/browser/ui/autofill/cells/cvc_item+private.h b/ios/chrome/browser/ui/autofill/cells/cvc_item+private.h
new file mode 100644
index 0000000..64e120a8
--- /dev/null
+++ b/ios/chrome/browser/ui/autofill/cells/cvc_item+private.h
@@ -0,0 +1,13 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_AUTOFILL_CELLS_CVC_ITEM_PRIVATE_H_
+#define IOS_CHROME_BROWSER_UI_AUTOFILL_CELLS_CVC_ITEM_PRIVATE_H_
+
+// Class extension exposing private properties of CVCCell for testing.
+@interface CVCCell ()
+@property(nonatomic, strong) UIView* dateContainerView;
+@end
+
+#endif  // IOS_CHROME_BROWSER_UI_AUTOFILL_CELLS_CVC_ITEM_PRIVATE_H_
diff --git a/ios/chrome/browser/ui/autofill/cells/cvc_item.mm b/ios/chrome/browser/ui/autofill/cells/cvc_item.mm
index ac81761..339bf85 100644
--- a/ios/chrome/browser/ui/autofill/cells/cvc_item.mm
+++ b/ios/chrome/browser/ui/autofill/cells/cvc_item.mm
@@ -9,6 +9,7 @@
 #import "components/autofill/core/common/autofill_features.h"
 #import "components/grit/components_scaled_resources.h"
 #import "components/strings/grit/components_strings.h"
+#import "ios/chrome/browser/ui/autofill/cells/cvc_item+private.h"
 #import "ios/chrome/browser/ui/util/uikit_ui_util.h"
 #import "ios/chrome/common/ui/colors/semantic_color_names.h"
 #import "ios/chrome/grit/ios_strings.h"
@@ -38,7 +39,6 @@
 
 @interface CVCCell ()<UITextFieldDelegate>
 @property(nonatomic, strong) UILabel* dateSeparator;
-@property(nonatomic, strong) UIView* dateContainerView;
 @property(nonatomic, strong) UIView* CVCContainerView;
 @property(nonatomic, strong)
     NSLayoutConstraint* CVCContainerLeadingConstraintWithDate;
diff --git a/ios/chrome/browser/ui/autofill/cells/cvc_item_unittest.mm b/ios/chrome/browser/ui/autofill/cells/cvc_item_unittest.mm
index 7b0f0c5e..801596c 100644
--- a/ios/chrome/browser/ui/autofill/cells/cvc_item_unittest.mm
+++ b/ios/chrome/browser/ui/autofill/cells/cvc_item_unittest.mm
@@ -6,6 +6,7 @@
 
 #import "base/mac/foundation_util.h"
 #import "components/grit/components_scaled_resources.h"
+#import "ios/chrome/browser/ui/autofill/cells/cvc_item+private.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/gtest_mac.h"
 #import "testing/platform_test.h"
@@ -14,10 +15,6 @@
 #error "This file requires ARC support."
 #endif
 
-@interface CVCCell (Private)
-@property(nonatomic, strong) UIView* dateContainerView;
-@end
-
 namespace {
 
 using CVCItemTest = PlatformTest;
diff --git a/ios/chrome/browser/ui/bubble/BUILD.gn b/ios/chrome/browser/ui/bubble/BUILD.gn
index 67693484..677b501 100644
--- a/ios/chrome/browser/ui/bubble/BUILD.gn
+++ b/ios/chrome/browser/ui/bubble/BUILD.gn
@@ -14,6 +14,7 @@
     "bubble_view.mm",
     "bubble_view_controller.h",
     "bubble_view_controller.mm",
+    "bubble_view_controller_presenter+private.h",
     "bubble_view_controller_presenter.h",
     "bubble_view_controller_presenter.mm",
   ]
diff --git a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter+private.h b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter+private.h
new file mode 100644
index 0000000..d4648b1
--- /dev/null
+++ b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter+private.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_BUBBLE_BUBBLE_VIEW_CONTROLLER_PRESENTER_PRIVATE_H_
+#define IOS_CHROME_BROWSER_UI_BUBBLE_BUBBLE_VIEW_CONTROLLER_PRESENTER_PRIVATE_H_
+
+#import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h"
+
+// Class extension exposing private properties of BubbleViewControllerPresenter
+// for testing.
+@interface BubbleViewControllerPresenter ()
+
+// The underlying BubbleViewController managed by this object.
+// `bubbleViewController` manages the BubbleView instance.
+@property(nonatomic, strong) BubbleViewController* bubbleViewController;
+
+// The timer used to dismiss the bubble after a certain length of time. The
+// bubble is dismissed automatically if the user does not dismiss it manually.
+// If the user dismisses it manually, this timer is invalidated. The timer
+// maintains a strong reference to the presenter, so it must be retained weakly
+// to prevent a retain cycle. The run loop retains a strong reference to the
+// timer so it is not deallocated until it is invalidated.
+@property(nonatomic, strong) NSTimer* bubbleDismissalTimer;
+
+// The timer used to reset the user's engagement. The user is considered
+// engaged with the bubble while it is visible and for a certain duration after
+// it disappears. The timer maintains a strong reference to the presenter, so it
+// must be retained weakly to prevent a retain cycle. The run loop retains a
+// strong reference to the timer so it is not deallocated until it is
+// invalidated.
+@property(nonatomic, strong) NSTimer* engagementTimer;
+
+@end
+
+#endif  // IOS_CHROME_BROWSER_UI_BUBBLE_BUBBLE_VIEW_CONTROLLER_PRESENTER_PRIVATE_H_
diff --git a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
index bde816e..dc85b7e 100644
--- a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
+++ b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.mm
@@ -9,6 +9,7 @@
 #import "base/metrics/histogram_macros.h"
 #import "ios/chrome/browser/ui/bubble/bubble_util.h"
 #import "ios/chrome/browser/ui/bubble/bubble_view_controller.h"
+#import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter+private.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
 #error "This file requires ARC support."
@@ -52,9 +53,6 @@
 
 // Redeclared as readwrite so the value can be changed internally.
 @property(nonatomic, assign, readwrite, getter=isUserEngaged) BOOL userEngaged;
-// The underlying BubbleViewController managed by this object.
-// `bubbleViewController` manages the BubbleView instance.
-@property(nonatomic, strong) BubbleViewController* bubbleViewController;
 // The tap gesture recognizer intercepting tap gestures occurring inside the
 // bubble view. Taps inside must be differentiated from taps outside to track
 // UMA metrics.
@@ -66,20 +64,6 @@
 @property(nonatomic, strong) UITapGestureRecognizer* outsideBubbleTapRecognizer;
 // The swipe gesture recognizer to dismiss the bubble on swipes.
 @property(nonatomic, strong) UISwipeGestureRecognizer* swipeRecognizer;
-// The timer used to dismiss the bubble after a certain length of time. The
-// bubble is dismissed automatically if the user does not dismiss it manually.
-// If the user dismisses it manually, this timer is invalidated. The timer
-// maintains a strong reference to the presenter, so it must be retained weakly
-// to prevent a retain cycle. The run loop retains a strong reference to the
-// timer so it is not deallocated until it is invalidated.
-@property(nonatomic, weak) NSTimer* bubbleDismissalTimer;
-// The timer used to reset the user's engagement. The user is considered
-// engaged with the bubble while it is visible and for a certain duration after
-// it disappears. The timer maintains a strong reference to the presenter, so it
-// must be retained weakly to prevent a retain cycle. The run loop retains a
-// strong reference to the timer so it is not deallocated until it is
-// invalidated.
-@property(nonatomic, weak) NSTimer* engagementTimer;
 // The direction the underlying BubbleView's arrow is pointing.
 @property(nonatomic, assign) BubbleArrowDirection arrowDirection;
 // The alignment of the underlying BubbleView's arrow.
diff --git a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter_unittest.mm b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter_unittest.mm
index 46ad83f..7dec40a 100644
--- a/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter_unittest.mm
+++ b/ios/chrome/browser/ui/bubble/bubble_view_controller_presenter_unittest.mm
@@ -8,6 +8,7 @@
 #import "ios/chrome/browser/ui/bubble/bubble_unittest_util.h"
 #import "ios/chrome/browser/ui/bubble/bubble_view.h"
 #import "ios/chrome/browser/ui/bubble/bubble_view_controller.h"
+#import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter+private.h"
 #import "ios/chrome/browser/ui/bubble/bubble_view_controller_presenter.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/platform_test.h"
@@ -17,19 +18,6 @@
 #error "This file requires ARC support."
 #endif
 
-@interface BubbleViewControllerPresenter (Testing)
-
-// Underlying bubble view controller managed by the presenter.
-@property(nonatomic, strong) BubbleViewController* bubbleViewController;
-
-// Underlying timer used to dismiss the bubble automatically.
-@property(nonatomic, strong) NSTimer* bubbleDismissalTimer;
-
-// Underlying timer used to mark the user as no longer engaged automatically.
-@property(nonatomic, strong) NSTimer* engagementTimer;
-
-@end
-
 // Test fixture to test the BubbleViewControllerPresenter.
 class BubbleViewControllerPresenterTest : public PlatformTest {
  public:
diff --git a/ios/chrome/browser/ui/follow/first_follow_view_controller.mm b/ios/chrome/browser/ui/follow/first_follow_view_controller.mm
index 1d6bcd6..c0dd40e 100644
--- a/ios/chrome/browser/ui/follow/first_follow_view_controller.mm
+++ b/ios/chrome/browser/ui/follow/first_follow_view_controller.mm
@@ -86,16 +86,14 @@
 
 #pragma mark - ConfirmationAlertViewController
 
-- (void)updateStylingForSecondaryTitleLabel:(UILabel*)secondaryTitleLabel {
-  secondaryTitleLabel.font =
-      [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
-  secondaryTitleLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
+- (void)customizeSecondaryTitle:(UITextView*)secondaryTitle {
+  secondaryTitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
+  secondaryTitle.textColor = [UIColor colorNamed:kTextSecondaryColor];
 }
 
-- (void)updateStylingForSubtitleLabel:(UILabel*)subtitleLabel {
-  subtitleLabel.font =
-      [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
-  subtitleLabel.textColor = [UIColor colorNamed:kTextTertiaryColor];
+- (void)customizeSubtitle:(UITextView*)subtitle {
+  subtitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
+  subtitle.textColor = [UIColor colorNamed:kTextTertiaryColor];
 }
 
 @end
diff --git a/ios/chrome/browser/ui/main/scene_controller.mm b/ios/chrome/browser/ui/main/scene_controller.mm
index 02f13bb..60569f5 100644
--- a/ios/chrome/browser/ui/main/scene_controller.mm
+++ b/ios/chrome/browser/ui/main/scene_controller.mm
@@ -2506,6 +2506,14 @@
   // they are not visible.
   ios::provider::HideModalViewStack();
 
+  // Exit fullscreen mode for web page when we re-enter app through external
+  // intents.
+  web::WebState* webState =
+      self.mainInterface.browser->GetWebStateList()->GetActiveWebState();
+  if (webState && webState->IsWebPageInFullscreenMode()) {
+    webState->CloseMediaPresentations();
+  }
+
   // ChromeIdentityService is responsible for the dialogs displayed by the
   // services it wraps.
   GetApplicationContext()->GetSystemIdentityManager()->DismissDialogs();
diff --git a/ios/chrome/browser/ui/settings/clear_browsing_data/BUILD.gn b/ios/chrome/browser/ui/settings/clear_browsing_data/BUILD.gn
index 0c830ce4..9b0af0b 100644
--- a/ios/chrome/browser/ui/settings/clear_browsing_data/BUILD.gn
+++ b/ios/chrome/browser/ui/settings/clear_browsing_data/BUILD.gn
@@ -17,6 +17,7 @@
     "clear_browsing_data_ui_constants.h",
     "clear_browsing_data_ui_constants.mm",
     "clear_browsing_data_ui_delegate.h",
+    "time_range_selector_table_view_controller+private.h",
     "time_range_selector_table_view_controller.h",
     "time_range_selector_table_view_controller.mm",
   ]
diff --git a/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller+private.h b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller+private.h
new file mode 100644
index 0000000..0a39380
--- /dev/null
+++ b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller+private.h
@@ -0,0 +1,14 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_UI_SETTINGS_CLEAR_BROWSING_DATA_TIME_RANGE_SELECTOR_TABLE_VIEW_CONTROLLER_PRIVATE_H_
+#define IOS_CHROME_BROWSER_UI_SETTINGS_CLEAR_BROWSING_DATA_TIME_RANGE_SELECTOR_TABLE_VIEW_CONTROLLER_PRIVATE_H_
+
+// Class extension exposing private methods of
+// TimeRangeSelectorTableViewController for testing.
+@interface TimeRangeSelectorTableViewController ()
+- (void)updatePrefValue:(int)prefValue;
+@end
+
+#endif  // IOS_CHROME_BROWSER_UI_SETTINGS_CLEAR_BROWSING_DATA_TIME_RANGE_SELECTOR_TABLE_VIEW_CONTROLLER_PRIVATE_H_
diff --git a/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller.mm b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller.mm
index bebed97..f390c21 100644
--- a/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller.mm
+++ b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller.mm
@@ -9,6 +9,7 @@
 #import "components/browsing_data/core/pref_names.h"
 #import "components/prefs/pref_member.h"
 #import "components/prefs/pref_service.h"
+#import "ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller+private.h"
 #import "ios/chrome/browser/ui/table_view/cells/table_view_detail_text_item.h"
 #import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h"
 #import "ios/chrome/browser/ui/table_view/table_view_utils.h"
diff --git a/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller_unittest.mm b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller_unittest.mm
index e254b674..9942ca7 100644
--- a/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller_unittest.mm
+++ b/ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller_unittest.mm
@@ -10,6 +10,7 @@
 #import "components/prefs/pref_registry_simple.h"
 #import "components/prefs/pref_service.h"
 #import "components/sync_preferences/pref_service_mock_factory.h"
+#import "ios/chrome/browser/ui/settings/clear_browsing_data/time_range_selector_table_view_controller+private.h"
 #import "ios/chrome/browser/ui/table_view/cells/table_view_item.h"
 #import "ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h"
 #import "ios/chrome/grit/ios_strings.h"
@@ -21,10 +22,6 @@
 #error "This file requires ARC support."
 #endif
 
-@interface TimeRangeSelectorTableViewController (ExposedForTesting)
-- (void)updatePrefValue:(int)prefValue;
-@end
-
 namespace {
 
 const NSInteger kNumberOfItems = 5;
diff --git a/ios/chrome/browser/ui/ui_feature_flags.cc b/ios/chrome/browser/ui/ui_feature_flags.cc
index 786fc912..27075d9 100644
--- a/ios/chrome/browser/ui/ui_feature_flags.cc
+++ b/ios/chrome/browser/ui/ui_feature_flags.cc
@@ -138,6 +138,10 @@
              "MultilineFadeTruncatingLabel",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+BASE_FEATURE(kEnableAccessibilityIdentifierToOmniboxLeadingImage,
+             "EnableAccessibilityIdentifierToOmniboxLeadingImage",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 bool IsTabGridSortedByRecency() {
   return base::FeatureList::IsEnabled(kTabGridRecencySort);
 }
diff --git a/ios/chrome/browser/ui/ui_feature_flags.h b/ios/chrome/browser/ui/ui_feature_flags.h
index a7c6b51..5ed534f 100644
--- a/ios/chrome/browser/ui/ui_feature_flags.h
+++ b/ios/chrome/browser/ui/ui_feature_flags.h
@@ -125,6 +125,9 @@
 // Feature to enable multiline gradient support in fade truncating label.
 BASE_DECLARE_FEATURE(kMultilineFadeTruncatingLabel);
 
+// Flag to enable accessibility identifier to omnibox leading image.
+BASE_DECLARE_FEATURE(kEnableAccessibilityIdentifierToOmniboxLeadingImage);
+
 // Whether the tab grid tabs should be sorted by recency.
 bool IsTabGridSortedByRecency();
 
diff --git a/ios/chrome/browser/ui/webui/policy/policy_ui.mm b/ios/chrome/browser/ui/webui/policy/policy_ui.mm
index ecf25de..b37b092 100644
--- a/ios/chrome/browser/ui/webui/policy/policy_ui.mm
+++ b/ios/chrome/browser/ui/webui/policy/policy_ui.mm
@@ -40,6 +40,7 @@
       {"labelClientId", IDS_POLICY_LABEL_CLIENT_ID},
       {"labelDirectoryApiId", IDS_POLICY_LABEL_DIRECTORY_API_ID},
       {"labelError", IDS_POLICY_LABEL_ERROR},
+      {"labelWarning", IDS_POLICY_HEADER_WARNING},
       {"labelGaiaId", IDS_POLICY_LABEL_GAIA_ID},
       {"labelIsAffiliated", IDS_POLICY_LABEL_IS_AFFILIATED},
       {"labelLastCloudReportSentTimestamp",
@@ -80,6 +81,7 @@
       {"signinProfile", IDS_POLICY_SIGNIN_PROFILE},
       {"status", IDS_POLICY_STATUS},
       {"statusErrorManagedNoPolicy", IDS_POLICY_STATUS_ERROR_MANAGED_NO_POLICY},
+      {"statusFlexOrgNoPolicy", IDS_POLICY_STATUS_FLEX_ORG_NO_POLICY},
       {"statusDevice", IDS_POLICY_STATUS_DEVICE},
       {"statusMachine", IDS_POLICY_STATUS_MACHINE},
       {"statusUser", IDS_POLICY_STATUS_USER},
diff --git a/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.h b/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.h
index 97bb7fc..06e05b3 100644
--- a/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.h
+++ b/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.h
@@ -95,13 +95,15 @@
 // The action handler for interactions in this View Controller.
 @property(nonatomic, weak) id<ConfirmationAlertActionHandler> actionHandler;
 
-// Updates the style of the secondary title label. The default implementation
-// does nothing, but subclasses can override to customize the styling if needed.
-- (void)updateStylingForSecondaryTitleLabel:(UILabel*)secondaryTitleLabel;
+// Can be overridden by subclasses to customize the secondary title, e.g. set a
+// different style, or a UITextViewDelegate. The default implementation does
+// nothing.
+- (void)customizeSecondaryTitle:(UITextView*)secondaryTitle;
 
-// Updates the style of the subtitle label. The default implementation does
-// nothing, but subclasses can override to customize the styling if needed.
-- (void)updateStylingForSubtitleLabel:(UILabel*)subtitleLabel;
+// Can be overridden by subclasses to customize the subtitle, e.g. set a
+// different style, or a UITextViewDelegate. The default implementation does
+// nothing.
+- (void)customizeSubtitle:(UITextView*)subtitle;
 
 @end
 
diff --git a/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.mm b/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.mm
index 894a96f..3264344 100644
--- a/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.mm
+++ b/ios/chrome/common/ui/confirmation_alert/confirmation_alert_view_controller.mm
@@ -116,11 +116,11 @@
   }
 
   UILabel* title = [self createTitleLabel];
-  UILabel* subtitle = [self createSubtitleLabel];
+  UITextView* subtitle = [self createSubtitleView];
 
   NSArray* stackSubviews = nil;
   if ([self.secondaryTitleString length] != 0) {
-    UILabel* secondaryTitle = [self createSecondaryTitleLabel];
+    UITextView* secondaryTitle = [self createSecondaryTitleView];
     stackSubviews =
         @[ self.imageContainerView, title, secondaryTitle, subtitle ];
   } else {
@@ -355,14 +355,12 @@
   [super updateViewConstraints];
 }
 
-- (void)updateStylingForSecondaryTitleLabel:(UILabel*)secondaryTitleLabel {
-  // The subclass needs to overwrite this method if it wants a different style
-  // than the default.
+- (void)customizeSecondaryTitle:(UITextView*)secondaryTitle {
+  // Do nothing by default. Subclasses can override this.
 }
 
-- (void)updateStylingForSubtitleLabel:(UILabel*)subtitleLabel {
-  // The subclass need to overwrite this method if it wants a different style
-  // than the default.
+- (void)customizeSubtitle:(UITextView*)subtitle {
+  // Do nothing by default. Subclasses can override this.
 }
 
 #pragma mark - Private
@@ -533,14 +531,16 @@
   return containerView;
 }
 
-// Creates a label with subtitle label defaults.
-- (UILabel*)createLabel {
-  UILabel* label = [[UILabel alloc] init];
-  label.numberOfLines = 0;
-  label.textAlignment = NSTextAlignmentCenter;
-  label.translatesAutoresizingMaskIntoConstraints = NO;
-  label.adjustsFontForContentSizeCategory = YES;
-  return label;
+// Creates a UITextView with subtitle defaults.
+- (UITextView*)createTextView {
+  UITextView* view = [[UITextView alloc] init];
+  view.textAlignment = NSTextAlignmentCenter;
+  view.translatesAutoresizingMaskIntoConstraints = NO;
+  view.adjustsFontForContentSizeCategory = YES;
+  view.editable = NO;
+  view.selectable = NO;
+  view.scrollEnabled = NO;
+  return view;
 }
 
 // Helper to create the title label.
@@ -567,28 +567,28 @@
   return title;
 }
 
-// Helper to create the title description label.
-- (UILabel*)createSecondaryTitleLabel {
-  UILabel* secondaryTitle = [self createLabel];
+// Helper to create the title description view.
+- (UITextView*)createSecondaryTitleView {
+  UITextView* secondaryTitle = [self createTextView];
   secondaryTitle.font =
       [UIFont preferredFontForTextStyle:UIFontTextStyleTitle2];
   secondaryTitle.text = self.secondaryTitleString;
   secondaryTitle.textColor = [UIColor colorNamed:kTextPrimaryColor];
   secondaryTitle.accessibilityIdentifier =
       kConfirmationAlertSecondaryTitleAccessibilityIdentifier;
-  [self updateStylingForSecondaryTitleLabel:secondaryTitle];
+  [self customizeSecondaryTitle:secondaryTitle];
   return secondaryTitle;
 }
 
-// Helper to create the subtitle label.
-- (UILabel*)createSubtitleLabel {
-  UILabel* subtitle = [self createLabel];
+// Helper to create the subtitle view.
+- (UITextView*)createSubtitleView {
+  UITextView* subtitle = [self createTextView];
   subtitle.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
   subtitle.text = self.subtitleString;
   subtitle.textColor = [UIColor colorNamed:kTextSecondaryColor];
   subtitle.accessibilityIdentifier =
       kConfirmationAlertSubtitleAccessibilityIdentifier;
-  [self updateStylingForSubtitleLabel:subtitle];
+  [self customizeSubtitle:subtitle];
   return subtitle;
 }
 
diff --git a/ios/web/navigation/wk_navigation_util.mm b/ios/web/navigation/wk_navigation_util.mm
index 091d154..7490e13 100644
--- a/ios/web/navigation/wk_navigation_util.mm
+++ b/ios/web/navigation/wk_navigation_util.mm
@@ -104,19 +104,19 @@
   // The URLs and titles of the restored entries are stored in two separate
   // lists instead of a single list of objects to reduce the size of the JSON
   // string to be included in the query parameter.
-  base::Value restored_urls(base::Value::Type::LIST);
-  base::Value restored_titles(base::Value::Type::LIST);
+  base::Value::List restored_urls;
+  base::Value::List restored_titles;
   for (int i = first_restored_item_offset;
        i < new_size + first_restored_item_offset; i++) {
     NavigationItem* item = items[i].get();
     restored_urls.Append(item->GetURL().spec());
     restored_titles.Append(item->GetTitle());
   }
-  base::Value session(base::Value::Type::DICTIONARY);
+  base::Value::Dict session;
   int committed_item_offset = new_last_committed_item_index + 1 - new_size;
-  session.SetKey("offset", base::Value(committed_item_offset));
-  session.SetKey("urls", std::move(restored_urls));
-  session.SetKey("titles", std::move(restored_titles));
+  session.Set("offset", committed_item_offset);
+  session.Set("urls", std::move(restored_urls));
+  session.Set("titles", std::move(restored_titles));
 
   std::string session_json;
   base::JSONWriter::Write(session, &session_json);
diff --git a/ios/web/webui/mojo_facade.h b/ios/web/webui/mojo_facade.h
index 43f473b..d572c395c 100644
--- a/ios/web/webui/mojo_facade.h
+++ b/ios/web/webui/mojo_facade.h
@@ -49,7 +49,7 @@
   // Value returned by GetMessageNameAndArguments.
   struct MessageNameAndArguments {
     std::string name;
-    base::Value args;
+    base::Value::Dict args;
   };
 
   // Extracts message name and arguments from the given JSON string obtained
@@ -63,18 +63,18 @@
   //   - "interfaceName" (a string representing an interface name);
   //   - "requestHandle" (a number representing MojoHandle of the interface
   //     request).
-  void HandleMojoBindInterface(base::Value args);
+  void HandleMojoBindInterface(base::Value::Dict args);
 
   // Closes the given handle. `args` is a dictionary which must contain "handle"
   // key, which is a number representing a MojoHandle.
-  void HandleMojoHandleClose(base::Value args);
+  void HandleMojoHandleClose(base::Value::Dict args);
 
   // Creates a Mojo message pipe. `args` is unused.
   // Returns a dictionary with the following keys:
   //   - "result" (a number representing MojoResult);
   //   - "handle0" and "handle1" (the numbers representing two endpoints of the
   //     message pipe).
-  base::Value HandleMojoCreateMessagePipe(base::Value args);
+  base::Value HandleMojoCreateMessagePipe(base::Value::Dict args);
 
   // Writes a message to the message pipe endpoint given by handle. `args` is a
   // dictionary which must contain the following keys:
@@ -84,7 +84,7 @@
   //   - "handles" (an array representing any handles to attach; handles are
   //     transferred and will no longer be valid; may be empty);
   // Returns MojoResult as a number.
-  base::Value HandleMojoHandleWriteMessage(base::Value args);
+  base::Value HandleMojoHandleWriteMessage(base::Value::Dict args);
 
   // Reads a message from the message pipe endpoint given by handle. `args` is
   // a dictionary which must contain the keys "handle" (a number representing
@@ -94,7 +94,7 @@
   //   - "buffer" (an array representing message data; non-empty only on
   //     success);
   //   - "handles" (an array representing MojoHandles received, if any);
-  base::Value HandleMojoHandleReadMessage(base::Value args);
+  base::Value HandleMojoHandleReadMessage(base::Value::Dict args);
 
   // Begins watching a handle for signals to be satisfied or unsatisfiable.
   // `args` is a dictionary which must contain the following keys:
@@ -103,12 +103,12 @@
   //   - "callbackId" (a number representing the id which should be passed to
   //     Mojo.internal.signalWatch call).
   // Returns watch id as a number.
-  base::Value HandleMojoHandleWatch(base::Value args);
+  base::Value HandleMojoHandleWatch(base::Value::Dict args);
 
   // Cancels a handle watch initiated by "MojoHandle.watch". `args` is a
   // dictionary which must contain "watchId" key (a number representing id
   // returned from "MojoHandle.watch").
-  void HandleMojoWatcherCancel(base::Value args);
+  void HandleMojoWatcherCancel(base::Value::Dict args);
 
   // Runs JavaScript on WebUI page.
   WebState* web_state_ = nil;
diff --git a/ios/web/webui/mojo_facade.mm b/ios/web/webui/mojo_facade.mm
index 60dba8cbb..8a7e994 100644
--- a/ios/web/webui/mojo_facade.mm
+++ b/ios/web/webui/mojo_facade.mm
@@ -83,21 +83,21 @@
   CHECK(value_with_error.has_value());
   CHECK(value_with_error->is_dict());
 
-  const std::string* name = value_with_error->FindStringKey("name");
+  base::Value::Dict& dict = value_with_error->GetDict();
+  const std::string* name = dict.FindString("name");
   CHECK(name);
 
-  base::Value* args =
-      value_with_error->FindKeyOfType("args", base::Value::Type::DICTIONARY);
+  base::Value::Dict* args = dict.FindDict("args");
   CHECK(args);
 
   return {*name, std::move(*args)};
 }
 
-void MojoFacade::HandleMojoBindInterface(base::Value args) {
-  const std::string* interface_name = args.FindStringKey("interfaceName");
+void MojoFacade::HandleMojoBindInterface(base::Value::Dict args) {
+  const std::string* interface_name = args.FindString("interfaceName");
   CHECK(interface_name);
 
-  absl::optional<int> raw_handle = args.FindIntKey("requestHandle");
+  absl::optional<int> raw_handle = args.FindInt("requestHandle");
   CHECK(raw_handle.has_value());
 
   mojo::ScopedMessagePipeHandle handle(
@@ -106,49 +106,43 @@
       mojo::GenericPendingReceiver(*interface_name, std::move(handle)));
 }
 
-void MojoFacade::HandleMojoHandleClose(base::Value args) {
-  absl::optional<int> handle = args.FindIntKey("handle");
+void MojoFacade::HandleMojoHandleClose(base::Value::Dict args) {
+  absl::optional<int> handle = args.FindInt("handle");
   CHECK(handle.has_value());
 
   mojo::Handle(*handle).Close();
 }
 
-base::Value MojoFacade::HandleMojoCreateMessagePipe(base::Value args) {
+base::Value MojoFacade::HandleMojoCreateMessagePipe(base::Value::Dict args) {
   mojo::ScopedMessagePipeHandle handle0, handle1;
   MojoResult mojo_result = mojo::CreateMessagePipe(nullptr, &handle0, &handle1);
-  base::Value result(base::Value::Type::DICTIONARY);
-  result.SetKey("result", base::Value(static_cast<int>(mojo_result)));
+  base::Value::Dict result;
+  result.Set("result", static_cast<int>(mojo_result));
   if (mojo_result == MOJO_RESULT_OK) {
-    result.SetKey("handle0",
-                  base::Value(static_cast<int>(handle0.release().value())));
-    result.SetKey("handle1",
-                  base::Value(static_cast<int>(handle1.release().value())));
+    result.Set("handle0", static_cast<int>(handle0.release().value()));
+    result.Set("handle1", static_cast<int>(handle1.release().value()));
   }
-  return result;
+  return base::Value(std::move(result));
 }
 
-base::Value MojoFacade::HandleMojoHandleWriteMessage(base::Value args) {
-  absl::optional<int> handle = args.FindIntKey("handle");
+base::Value MojoFacade::HandleMojoHandleWriteMessage(base::Value::Dict args) {
+  absl::optional<int> handle = args.FindInt("handle");
   CHECK(handle.has_value());
 
-  const base::Value* handles_list =
-      args.FindKeyOfType("handles", base::Value::Type::LIST);
+  const base::Value::List* handles_list = args.FindList("handles");
   CHECK(handles_list);
 
-  const base::Value* buffer =
-      args.FindKeyOfType("buffer", base::Value::Type::STRING);
+  const std::string* buffer = args.FindString("buffer");
   CHECK(buffer);
 
   int flags = MOJO_WRITE_MESSAGE_FLAG_NONE;
 
-  const auto& handles_list_storage = handles_list->GetList();
-  std::vector<MojoHandle> handles(handles_list_storage.size());
-  for (size_t i = 0; i < handles_list_storage.size(); i++) {
-    int one_handle = handles_list_storage[i].GetInt();
+  std::vector<MojoHandle> handles(handles_list->size());
+  for (size_t i = 0; i < handles_list->size(); i++) {
+    int one_handle = (*handles_list)[i].GetInt();
     handles[i] = one_handle;
   }
-  absl::optional<std::vector<uint8_t>> bytes =
-      base::Base64Decode(buffer->GetString());
+  absl::optional<std::vector<uint8_t>> bytes = base::Base64Decode(*buffer);
   if (!bytes) {
     return base::Value(static_cast<int>(MOJO_RESULT_INVALID_ARGUMENT));
   }
@@ -161,8 +155,8 @@
   return base::Value(static_cast<int>(result));
 }
 
-base::Value MojoFacade::HandleMojoHandleReadMessage(base::Value args) {
-  base::Value* handle_as_value = args.FindKey("handle");
+base::Value MojoFacade::HandleMojoHandleReadMessage(base::Value::Dict args) {
+  base::Value* handle_as_value = args.Find("handle");
   CHECK(handle_as_value);
   int handle_as_int = 0;
   if (handle_as_value->is_int()) {
@@ -177,31 +171,31 @@
   MojoResult mojo_result =
       mojo::ReadMessageRaw(handle, &bytes, &handles, flags);
 
-  base::Value result(base::Value::Type::DICTIONARY);
+  base::Value::Dict result;
   if (mojo_result == MOJO_RESULT_OK) {
-    base::Value handles_list(base::Value::Type::LIST);
+    base::Value::List handles_list;
     for (uint32_t i = 0; i < handles.size(); i++) {
       handles_list.Append(static_cast<int>(handles[i].release().value()));
     }
-    result.SetKey("handles", std::move(handles_list));
+    result.Set("handles", std::move(handles_list));
 
-    base::Value buffer(base::Value::Type::LIST);
+    base::Value::List buffer;
     for (uint32_t i = 0; i < bytes.size(); i++) {
       buffer.Append(bytes[i]);
     }
-    result.SetKey("buffer", std::move(buffer));
+    result.Set("buffer", std::move(buffer));
   }
-  result.SetKey("result", base::Value(static_cast<int>(mojo_result)));
+  result.Set("result", static_cast<int>(mojo_result));
 
-  return result;
+  return base::Value(std::move(result));
 }
 
-base::Value MojoFacade::HandleMojoHandleWatch(base::Value args) {
-  absl::optional<int> handle = args.FindIntKey("handle");
+base::Value MojoFacade::HandleMojoHandleWatch(base::Value::Dict args) {
+  absl::optional<int> handle = args.FindInt("handle");
   CHECK(handle.has_value());
-  absl::optional<int> signals = args.FindIntKey("signals");
+  absl::optional<int> signals = args.FindInt("signals");
   CHECK(signals.has_value());
-  absl::optional<int> callback_id = args.FindIntKey("callbackId");
+  absl::optional<int> callback_id = args.FindInt("callbackId");
   CHECK(callback_id.has_value());
 
   mojo::SimpleWatcher::ReadyCallback callback = base::BindRepeating(
@@ -223,8 +217,8 @@
   return base::Value(last_watch_id_);
 }
 
-void MojoFacade::HandleMojoWatcherCancel(base::Value args) {
-  absl::optional<int> watch_id = args.FindIntKey("watchId");
+void MojoFacade::HandleMojoWatcherCancel(base::Value::Dict args) {
+  absl::optional<int> watch_id = args.FindInt("watchId");
   CHECK(watch_id.has_value());
   watchers_.erase(*watch_id);
 }
diff --git a/testing/buildbot/chrome.json b/testing/buildbot/chrome.json
index ec6cb28..cf0b216 100644
--- a/testing/buildbot/chrome.json
+++ b/testing/buildbot/chrome.json
@@ -1627,6 +1627,28 @@
       "chromiumos_preflight"
     ]
   },
+  "chromeos-octopus-chrome-skylab": {
+    "additional_compile_targets": [
+      "chrome",
+      "linux_symbols",
+      "symupload"
+    ],
+    "skylab_tests": [
+      {
+        "args": [],
+        "autotest_name": "tast.chrome-from-tls",
+        "cros_board": "octopus",
+        "cros_img": "octopus-release/R110-15249.0.0",
+        "name": "chrome_all_tast_tests OCTOPUS_RELEASE_CHROME_FROM_TLS",
+        "swarming": {},
+        "tast_expr": "(\"group:mainline\" && \"dep:chrome\" && !informational)",
+        "test": "chrome_all_tast_tests",
+        "test_id_prefix": "ninja://chromeos:chrome_all_tast_tests/",
+        "timeout_sec": 21600,
+        "variant_id": "OCTOPUS_RELEASE_CHROME_FROM_TLS"
+      }
+    ]
+  },
   "chromeos-reven-chrome": {
     "additional_compile_targets": [
       "chromiumos_preflight"
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index bdf597f..e08568b 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -5839,9 +5839,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -5853,8 +5853,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -6010,9 +6010,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -6024,8 +6024,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -6162,9 +6162,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -6176,8 +6176,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index a48f6c4d0..5d42d74 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -80772,9 +80772,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -80786,8 +80786,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
@@ -80913,9 +80913,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -80927,8 +80927,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com"
@@ -81040,9 +81040,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -81054,8 +81054,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "service_account": "chromium-tester@chops-service-accounts.iam.gserviceaccount.com",
@@ -82388,9 +82388,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -82401,8 +82401,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -82559,9 +82559,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -82572,8 +82572,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -82711,9 +82711,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -82724,8 +82724,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -84249,9 +84249,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -84262,8 +84262,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -84420,9 +84420,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -84433,8 +84433,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -84572,9 +84572,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -84585,8 +84585,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -85358,9 +85358,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "merge": {
           "args": [],
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -85371,8 +85371,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/chromium.memory.json b/testing/buildbot/chromium.memory.json
index f1641ca..60dd7c7 100644
--- a/testing/buildbot/chromium.memory.json
+++ b/testing/buildbot/chromium.memory.json
@@ -18532,12 +18532,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.filter;../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18549,8 +18549,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -18723,12 +18723,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18740,8 +18740,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
@@ -18890,12 +18890,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 111.0.5552.0",
+        "description": "Run with ash-chrome version 111.0.5555.0",
         "isolate_profile_data": true,
         "merge": {
           "args": [],
@@ -18907,8 +18907,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v111.0.5552.0",
-              "revision": "version:111.0.5552.0"
+              "location": "lacros_version_skew_tests_v111.0.5555.0",
+              "revision": "version:111.0.5555.0"
             }
           ],
           "dimension_sets": [
diff --git a/testing/buildbot/filters/mac.mac12-arm64-rel.browser_tests.filter b/testing/buildbot/filters/mac.mac12-arm64-rel.browser_tests.filter
index 639babc..66c37a8 100644
--- a/testing/buildbot/filters/mac.mac12-arm64-rel.browser_tests.filter
+++ b/testing/buildbot/filters/mac.mac12-arm64-rel.browser_tests.filter
@@ -42,6 +42,7 @@
 -AmbientAuthenticationTestWithPolicy.IncognitoAndRegular
 -AmbientAuthenticationTestWithPolicy.RegularOnly
 -AttemptRestartTest.AttemptRestartWithOTRProfiles/1
+-AutofillAcrossIframesTest_NestedAndLargeForm.FlattenFormEvenAcrossFramesWithoutFields
 -AutomationApiTest.ForceLayout
 -AvatarMenuBrowserTest.EditProfile_SigninRequired
 -BackForwardCachePageLoadMetricsObserverBrowserTest.CumulativeLayoutShiftAfterBackForwardCacheRestore
@@ -103,6 +104,7 @@
 -InspectUIFencedFrameTest.FencedFrameInFrontEnd
 -InspectUITest.LaunchUIDevtools
 -InteractiveBrowserTestBrowsertest.CheckResult
+-IntentChipButtonSkipIntentPickerBrowserTest.ShowsIntentChipCollapsed
 -LiveCaptionControllerTest.OnAudioStreamEnd
 -ManagementUITest.ManagementStateChange
 -MediaEngagementBrowserTest.SessionMultipleTabsClosingParent
diff --git a/testing/buildbot/test_suites.pyl b/testing/buildbot/test_suites.pyl
index 0dca879..b9ec175 100644
--- a/testing/buildbot/test_suites.pyl
+++ b/testing/buildbot/test_suites.pyl
@@ -6557,6 +6557,14 @@
       'webview_ui_instrumentation_tests': {},
     },
 
+    'chromeos_octopus_skylab_tests': {
+      'chromeos_chrome_all_tast_tests': {
+        'variants': [
+          'CROS_OCTOPUS_RELEASE_CHROME_FROM_TLS',
+        ]
+      },
+    },
+
     'chromeos_trogdor64_skylab_tests': {
       'chromeos_chrome_all_tast_tests': {
         'variants': [
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index f9da711..9b803b99 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -22,16 +22,16 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5552.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v111.0.5555.0/test_ash_chrome',
     ],
-    'description': 'Run with ash-chrome version 111.0.5552.0',
+    'description': 'Run with ash-chrome version 111.0.5555.0',
     'identifier': 'Lacros version skew testing ash canary',
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v111.0.5552.0',
-          'revision': 'version:111.0.5552.0',
+          'location': 'lacros_version_skew_tests_v111.0.5555.0',
+          'revision': 'version:111.0.5555.0',
         },
       ],
     },
@@ -660,6 +660,16 @@
     'enabled': True,
     'identifier': 'OCTOPUS_FULL',
   },
+  'CROS_OCTOPUS_RELEASE_CHROME_FROM_TLS': {
+    'skylab': {
+      'cros_board': 'octopus',
+      'cros_chrome_version': '110.0.5423.0',
+      'cros_img': 'octopus-release/R110-15249.0.0',
+      'autotest_name': 'tast.chrome-from-tls',
+    },
+    'enabled': True,
+    'identifier': 'OCTOPUS_RELEASE_CHROME_FROM_TLS',
+  },
   'CROS_OCTOPUS_RELEASE_LKGM': {
     'skylab': {
       'cros_board': 'octopus',
diff --git a/testing/buildbot/waterfalls.pyl b/testing/buildbot/waterfalls.pyl
index 8974f65..70eb6b71 100644
--- a/testing/buildbot/waterfalls.pyl
+++ b/testing/buildbot/waterfalls.pyl
@@ -168,6 +168,17 @@
         },
         'os_type': 'chromeos'
       },
+      'chromeos-octopus-chrome-skylab': {
+        'additional_compile_targets': [
+          "chrome",
+          "linux_symbols",
+          "symupload",
+        ],
+        'test_suites': {
+          'skylab_tests': 'chromeos_octopus_skylab_tests',
+        },
+        'os_type': 'chromeos',
+      },
       'chromeos-reven-chrome': {
         'additional_compile_targets': [
           'chromiumos_preflight',
diff --git a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
index 3e556db..9dda44a 100644
--- a/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
+++ b/third_party/blink/public/mojom/use_counter/metrics/css_property_id.mojom
@@ -793,6 +793,7 @@
     kAnimationRange = 740,
     kAnimationRangeStart = 741,
     kAnimationRangeEnd = 742,
+    kAnimationComposition = 743,
     // 1. Add new features above this line (don't change the assigned numbers of
     //    the existing items).
     // 2. Run the src/tools/metrics/histograms/update_use_counter_css.py script
diff --git a/third_party/blink/renderer/core/animation/css/css_animation_data.cc b/third_party/blink/renderer/core/animation/css/css_animation_data.cc
index 90eba66..27db687 100644
--- a/third_party/blink/renderer/core/animation/css/css_animation_data.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animation_data.cc
@@ -15,6 +15,7 @@
   direction_list_.push_back(InitialDirection());
   fill_mode_list_.push_back(InitialFillMode());
   play_state_list_.push_back(InitialPlayState());
+  composition_list_.push_back(InitialComposition());
 }
 
 CSSAnimationData::CSSAnimationData(const CSSAnimationData& other) = default;
diff --git a/third_party/blink/renderer/core/animation/css/css_animation_data.h b/third_party/blink/renderer/core/animation/css/css_animation_data.h
index 2a7dca0..36cb50e 100644
--- a/third_party/blink/renderer/core/animation/css/css_animation_data.h
+++ b/third_party/blink/renderer/core/animation/css/css_animation_data.h
@@ -9,6 +9,7 @@
 
 #include "base/memory/ptr_util.h"
 #include "third_party/blink/renderer/core/animation/css/css_timing_data.h"
+#include "third_party/blink/renderer/core/animation/effect_model.h"
 #include "third_party/blink/renderer/core/animation/timing.h"
 #include "third_party/blink/renderer/core/style/computed_style_constants.h"
 #include "third_party/blink/renderer/core/style/style_name_or_keyword.h"
@@ -48,6 +49,17 @@
   const Vector<EAnimPlayState>& PlayStateList() const {
     return play_state_list_;
   }
+  const Vector<EffectModel::CompositeOperation>& CompositionList() const {
+    return composition_list_;
+  }
+
+  EffectModel::CompositeOperation GetComposition(size_t animation_index) const {
+    if (!composition_list_.size()) {
+      return EffectModel::kCompositeReplace;
+    }
+    wtf_size_t index = animation_index % composition_list_.size();
+    return composition_list_[index];
+  }
 
   Vector<AtomicString>& NameList() { return name_list_; }
   Vector<StyleTimeline>& TimelineList() { return timeline_list_; }
@@ -55,6 +67,9 @@
   Vector<Timing::PlaybackDirection>& DirectionList() { return direction_list_; }
   Vector<Timing::FillMode>& FillModeList() { return fill_mode_list_; }
   Vector<EAnimPlayState>& PlayStateList() { return play_state_list_; }
+  Vector<EffectModel::CompositeOperation>& CompositionList() {
+    return composition_list_;
+  }
 
   static const AtomicString& InitialName();
   static const StyleTimeline& InitialTimeline();
@@ -64,6 +79,9 @@
   static Timing::FillMode InitialFillMode() { return Timing::FillMode::NONE; }
   static double InitialIterationCount() { return 1.0; }
   static EAnimPlayState InitialPlayState() { return EAnimPlayState::kPlaying; }
+  static EffectModel::CompositeOperation InitialComposition() {
+    return EffectModel::CompositeOperation::kCompositeReplace;
+  }
 
  private:
   Vector<AtomicString> name_list_;
@@ -72,6 +90,7 @@
   Vector<Timing::PlaybackDirection> direction_list_;
   Vector<Timing::FillMode> fill_mode_list_;
   Vector<EAnimPlayState> play_state_list_;
+  Vector<EffectModel::CompositeOperation> composition_list_;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/animation/css/css_animations.cc b/third_party/blink/renderer/core/animation/css/css_animations.cc
index 6cf927a9..fd7a48d 100644
--- a/third_party/blink/renderer/core/animation/css/css_animations.cc
+++ b/third_party/blink/renderer/core/animation/css/css_animations.cc
@@ -97,7 +97,7 @@
 
 // Processes keyframe rules, extracting the timing function and properties being
 // animated for each keyframe. The extraction process is doing more work that
-// strictly required for the setup to step 5 in the spec
+// strictly required for the setup to step 6 in the spec
 // (https://drafts.csswg.org/css-animations-2/#keyframes) as an optimization
 // to avoid needing to process each rule multiple times to extract different
 // properties.
@@ -150,7 +150,18 @@
           properties.PropertyAt(j);
       CSSPropertyRef ref(property_reference.Name(), document);
       const CSSProperty& property = ref.GetProperty();
-      if (property.PropertyID() == CSSPropertyID::kAnimationTimingFunction) {
+      if (RuntimeEnabledFeatures::CSSAnimationCompositionEnabled() &&
+          property.PropertyID() == CSSPropertyID::kAnimationComposition) {
+        if (const auto* value_list =
+                DynamicTo<CSSValueList>(property_reference.Value())) {
+          if (const auto* identifier_value =
+                  DynamicTo<CSSIdentifierValue>(value_list->Item(0))) {
+            keyframe->SetComposite(
+                identifier_value->ConvertTo<EffectModel::CompositeOperation>());
+          }
+        }
+      } else if (property.PropertyID() ==
+                 CSSPropertyID::kAnimationTimingFunction) {
         const CSSValue& value = property_reference.Value();
         scoped_refptr<TimingFunction> timing_function;
         if (value.IsInheritedValue() && parent_style->Animations()) {
@@ -206,7 +217,8 @@
     const StringKeyframeVector& keyframes,
     wtf_size_t start_index,
     double offset,
-    const TimingFunction& easing) {
+    const TimingFunction& easing,
+    const absl::optional<EffectModel::CompositeOperation>& composite) {
   for (wtf_size_t i = start_index; i < keyframes.size(); i++) {
     StringKeyframe* keyframe = keyframes[i];
 
@@ -215,20 +227,26 @@
     if (offset < keyframe->CheckedOffset())
       break;
 
-    if (easing.ToString() == keyframe->Easing().ToString())
+    if (easing.ToString() != keyframe->Easing().ToString()) {
+      continue;
+    }
+
+    if (composite == keyframe->Composite()) {
       return i;
+    }
   }
   return absl::nullopt;
 }
 
 // Tests conditions for inserting a bounding keyframe, which are outlined in
-// steps 6 and 7 of the spec for keyframe construction.
+// steps 7 and 8 of the spec for keyframe construction.
 // https://drafts.csswg.org/css-animations-2/#keyframes
 bool NeedsBoundaryKeyframe(StringKeyframe* candidate,
                            double offset,
                            const PropertySet& animated_properties,
                            const PropertySet& bounding_properties,
-                           TimingFunction* default_timing_function) {
+                           TimingFunction* default_timing_function,
+                           const EffectModel::CompositeOperation composite) {
   if (!candidate)
     return true;
 
@@ -238,6 +256,14 @@
   if (bounding_properties.size() == animated_properties.size())
     return false;
 
+  // consider no keyframe composite (auto) +
+  // target's animation_composite = replace to be equal to keyframe's composite
+  // to be replace.
+  if (candidate->Composite().value_or(composite) !=
+      EffectModel::kCompositeReplace) {
+    return true;
+  }
+
   return candidate->Easing().ToString() != default_timing_function->ToString();
 }
 
@@ -249,6 +275,7 @@
     const ComputedStyle* parent_style,
     const AtomicString& name,
     TimingFunction* default_timing_function,
+    EffectModel::CompositeOperation composite,
     size_t animation_index,
     AnimationTimeline* timeline) {
   // The algorithm for constructing string keyframes for a CSS animation is
@@ -264,7 +291,9 @@
   //    repeating the list as necessary as described in CSS Animations 1 §4.2
   //    The animation-name property.
 
-  // 2. Find the last @keyframes at-rule in document order with <keyframes-name>
+  // 2. Let default composite be replace.
+
+  // 3. Find the last @keyframes at-rule in document order with <keyframes-name>
   //    matching name.
   //    If there is no @keyframes at-rule with <keyframes-name> matching name,
   //    abort this procedure. In this case no animation is generated, and any
@@ -274,10 +303,10 @@
       resolver->FindKeyframesRule(&element, &animating_element, name);
   DCHECK(keyframes_rule);
 
-  // 3. Let keyframes be an empty sequence of keyframe objects.
+  // 4. Let keyframes be an empty sequence of keyframe objects.
   StringKeyframeVector keyframes;
 
-  // 4. Let animated properties be an empty set of longhand CSS property names.
+  // 5. Let animated properties be an empty set of longhand CSS property names.
   PropertySet animated_properties;
 
   // Start and end properties are also tracked to simplify the process of
@@ -288,7 +317,7 @@
   // Properties that have already been processed at the current keyframe.
   PropertySet current_offset_properties;
 
-  // 5. Perform a stable sort of the keyframe blocks in the @keyframes rule by
+  // 6. Perform a stable sort of the keyframe blocks in the @keyframes rule by
   //    the offset specified in the keyframe selector, and iterate over the
   //    result in reverse applying the following steps:
   bool has_named_range_keyframes = false;
@@ -300,19 +329,25 @@
   double last_offset = 1;
   wtf_size_t merged_frame_count = 0;
   for (wtf_size_t i = keyframes.size(); i > 0; --i) {
-    // 5.1 Let keyframe offset be the value of the keyframe selector converted
+    // 6.1 Let keyframe offset be the value of the keyframe selector converted
     //     to a value in the range 0 ≤ keyframe offset ≤ 1.
     int source_index = i - 1;
     StringKeyframe* rule_keyframe = keyframes[source_index];
     double keyframe_offset = rule_keyframe->CheckedOffset();
 
-    // 5.2 Let keyframe timing function be the value of the last valid
+    // 6.2 Let keyframe timing function be the value of the last valid
     //     declaration of animation-timing-function specified on the keyframe
     //     block, or, if there is no such valid declaration, default timing
     //     function.
     const TimingFunction& easing = rule_keyframe->Easing();
 
-    // 5.3 After converting keyframe timing function to its canonical form (e.g.
+    // 6.3 Let keyframe composite be the value of the last valid declaration of
+    // animation-composition specified on the keyframe block,
+    // or, if there is no such valid declaration, default composite.
+    absl::optional<EffectModel::CompositeOperation> keyframe_composite =
+        rule_keyframe->Composite();
+
+    // 6.4 After converting keyframe timing function to its canonical form (e.g.
     //     such that step-end becomes steps(1, end)) let keyframe refer to the
     //     existing keyframe in keyframes with matching keyframe offset and
     //     timing function, if any.
@@ -327,11 +362,13 @@
       last_offset = keyframe_offset;
     }
 
+    // TODO(crbug.com/1408702): we should merge keyframes to the most left one,
+    // not the most right one.
     // Avoid unnecessary creation of extra keyframes by merging into
     // existing keyframes.
     absl::optional<int> existing_keyframe_index = FindIndexOfMatchingKeyframe(
         keyframes, source_index + merged_frame_count + 1, keyframe_offset,
-        easing);
+        easing, keyframe_composite);
     int target_index;
     if (existing_keyframe_index) {
       // Merge keyframe propoerties.
@@ -346,7 +383,7 @@
       }
     }
 
-    // 5.4 Iterate over all declarations in the keyframe block and add them to
+    // 6.5 Iterate over all declarations in the keyframe block and add them to
     //     keyframe such that:
     //     * All variable references are resolved to their current values.
     //     * Each shorthand property is expanded to its longhand subproperties.
@@ -360,7 +397,7 @@
     //       have already added at this same keyframe offset, they should be
     //       skipped.
     //     * All property values are replaced with their computed values.
-    // 5.5 Add each physical longhand property name that was added to keyframe
+    // 6.6 Add each property name that was added to keyframe
     //     to animated properties.
     StringKeyframe* keyframe = keyframes[target_index];
     for (const auto& property : rule_keyframe->Properties()) {
@@ -390,45 +427,49 @@
   // Compact the vector of keyframes if any keyframes have been merged.
   keyframes.EraseAt(0, merged_frame_count);
 
-  // 6.  If there is no keyframe in keyframes with offset 0, or if amongst the
+  // 7.  If there is no keyframe in keyframes with offset 0, or if amongst the
   //     keyframes in keyframes with offset 0 not all of the properties in
   //     animated properties are present,
   //
-  // 6.1 Let initial keyframe be the keyframe in keyframes with offset 0 and
+  // 7.1 Let initial keyframe be the keyframe in keyframes with offset 0 and
   //     timing function default timing function.
-  // 6.2 If there is no such keyframe, let initial keyframe be a new empty
+  // 7.2 If there is no such keyframe, let initial keyframe be a new empty
   //     keyframe with offset 0, and timing function default timing function,
   //     and add it to keyframes after the last keyframe with offset 0.
-  // 6.3 For each property in animated properties that is not present in some
+  // 7.3 For each property in animated properties that is not present in some
   //     other keyframe with offset 0, add the computed value of that property
   //     for element to the keyframe.
   StringKeyframe* start_keyframe = keyframes.empty() ? nullptr : keyframes[0];
   if (NeedsBoundaryKeyframe(start_keyframe, 0, animated_properties,
-                            start_properties, default_timing_function)) {
+                            start_properties, default_timing_function,
+                            composite)) {
     start_keyframe = MakeGarbageCollected<StringKeyframe>();
     start_keyframe->SetOffset(0);
     start_keyframe->SetEasing(default_timing_function);
+    start_keyframe->SetComposite(EffectModel::kCompositeReplace);
     keyframes.push_front(start_keyframe);
   }
 
-  // 7.  Similarly, if there is no keyframe in keyframes with offset 1, or if
+  // 8.  Similarly, if there is no keyframe in keyframes with offset 1, or if
   //     amongst the keyframes in keyframes with offset 1 not all of the
   //     properties in animated properties are present,
   //
-  // 7.1 Let final keyframe be the keyframe in keyframes with offset 1 and
+  // 8.1 Let final keyframe be the keyframe in keyframes with offset 1 and
   //     timing function default timing function.
-  // 7.2 If there is no such keyframe, let final keyframe be a new empty
+  // 8.2 If there is no such keyframe, let final keyframe be a new empty
   //     keyframe with offset 1, and timing function default timing function,
   //     and add it to keyframes after the last keyframe with offset 1.
-  // 7.3 For each property in animated properties that is not present in some
+  // 8.3 For each property in animated properties that is not present in some
   //     other keyframe with offset 1, add the computed value of that property
   //     for element to the keyframe.
   StringKeyframe* end_keyframe = keyframes[keyframes.size() - 1];
   if (NeedsBoundaryKeyframe(end_keyframe, 1, animated_properties,
-                            end_properties, default_timing_function)) {
+                            end_properties, default_timing_function,
+                            composite)) {
     end_keyframe = MakeGarbageCollected<StringKeyframe>();
     end_keyframe->SetOffset(1);
     end_keyframe->SetEasing(default_timing_function);
+    end_keyframe->SetComposite(EffectModel::kCompositeReplace);
     keyframes.push_back(end_keyframe);
   }
 
@@ -437,7 +478,7 @@
   DCHECK_EQ(keyframes.back()->CheckedOffset(), 1);
 
   auto* model = MakeGarbageCollected<CssKeyframeEffectModel>(
-      keyframes, EffectModel::kCompositeReplace, &start_keyframe->Easing(),
+      keyframes, composite, &start_keyframe->Easing(),
       has_named_range_keyframes);
   if (animation_index > 0 && model->HasSyntheticKeyframes()) {
     UseCounter::Count(element.GetDocument(),
@@ -1172,6 +1213,9 @@
 
       const StyleTimeline& style_timeline = animation_data->GetTimeline(i);
 
+      const EffectModel::CompositeOperation composite =
+          animation_data->GetComposition(i);
+
       const RunningAnimation* existing_animation = nullptr;
       wtf_size_t existing_animation_index = 0;
 
@@ -1250,8 +1294,16 @@
               existing_animation->scroll_offsets !=
               To<ViewTimeline>(timeline)->GetResolvedScrollOffsets();
         }
+        bool composite_changed = false;
+        if (animation->effect()) {
+          if (const auto* model =
+                  To<KeyframeEffect>(animation->effect())->Model()) {
+            composite_changed = model->Composite() != composite;
+          }
+        }
         bool needs_keyframe_model_recalc =
-            has_named_range_keyframes && scroll_offsets_changed;
+            (has_named_range_keyframes && scroll_offsets_changed) ||
+            composite_changed;
 
         if (needs_keyframe_model_recalc ||
             keyframes_rule != existing_animation->style_rule ||
@@ -1309,8 +1361,8 @@
               *MakeGarbageCollected<InertEffect>(
                   CreateKeyframeEffectModel(
                       resolver, element, animating_element, writing_direction,
-                      parent_style, name, keyframe_timing_function.get(), i,
-                      timeline),
+                      parent_style, name, keyframe_timing_function.get(),
+                      composite, i, timeline),
                   timing, is_paused, inherited_time, timeline_duration,
                   animation->playbackRate()),
               specified_timing, keyframes_rule, timeline,
@@ -1337,8 +1389,8 @@
             *MakeGarbageCollected<InertEffect>(
                 CreateKeyframeEffectModel(resolver, element, animating_element,
                                           writing_direction, parent_style, name,
-                                          keyframe_timing_function.get(), i,
-                                          timeline),
+                                          keyframe_timing_function.get(),
+                                          composite, i, timeline),
                 timing, is_paused, inherited_time, timeline_duration, 1.0),
             specified_timing, keyframes_rule, timeline,
             animation_data->PlayStateList());
@@ -2558,6 +2610,7 @@
     case CSSPropertyID::kAlternativeAnimation:
     case CSSPropertyID::kAnimationDelay:
     case CSSPropertyID::kAlternativeAnimationDelay:
+    case CSSPropertyID::kAnimationComposition:
     case CSSPropertyID::kAnimationDelayEnd:
     case CSSPropertyID::kAnimationDelayStart:
     case CSSPropertyID::kAnimationDirection:
diff --git a/third_party/blink/renderer/core/animation/keyframe.h b/third_party/blink/renderer/core/animation/keyframe.h
index 971f69f2..d309bcee 100644
--- a/third_party/blink/renderer/core/animation/keyframe.h
+++ b/third_party/blink/renderer/core/animation/keyframe.h
@@ -75,9 +75,8 @@
   void SetComposite(EffectModel::CompositeOperation composite) {
     composite_ = composite;
   }
-  bool HasComposite() const { return composite_.has_value(); }
-  EffectModel::CompositeOperation Composite() const {
-    return composite_.value();
+  absl::optional<EffectModel::CompositeOperation> Composite() const {
+    return composite_;
   }
 
   void SetEasing(scoped_refptr<TimingFunction> easing) {
diff --git a/third_party/blink/renderer/core/css/css_properties.json5 b/third_party/blink/renderer/core/css/css_properties.json5
index a19b98ca..0af08a4 100644
--- a/third_party/blink/renderer/core/css/css_properties.json5
+++ b/third_party/blink/renderer/core/css/css_properties.json5
@@ -732,6 +732,22 @@
 
     // Animation Priority properties
     {
+      name: "animation-composition",
+      property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"],
+      keywords: ["replace", "add", "accumulate"],
+      style_builder_template: "animation",
+      style_builder_template_args: {
+        attribute: "Composition",
+      },
+      typedom_types: ["Keyword"],
+      separator: ",",
+      include_paths: ["third_party/blink/renderer/core/animation/effect_model.h"],
+      default_value: "EffectModel::kCompositeReplace",
+      type_name: "EffectModel::CompositeOperation",
+      valid_for_marker: true,
+      runtime_flag: "CSSAnimationComposition"
+    },
+    {
       name: "animation-delay",
       property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal", "InitialValue"],
       style_builder_template: "animation",
diff --git a/third_party/blink/renderer/core/css/css_selector.cc b/third_party/blink/renderer/core/css/css_selector.cc
index 8483221..837fa911 100644
--- a/third_party/blink/renderer/core/css/css_selector.cc
+++ b/third_party/blink/renderer/core/css/css_selector.cc
@@ -626,6 +626,12 @@
     return CSSSelector::kPseudoUnknown;
   }
 
+  if (IsTransitionPseudoElement(
+          GetPseudoId(static_cast<CSSSelector::PseudoType>(match->type))) &&
+      !RuntimeEnabledFeatures::ViewTransitionEnabled()) {
+    return CSSSelector::kPseudoUnknown;
+  }
+
   return static_cast<CSSSelector::PseudoType>(match->type);
 }
 
diff --git a/third_party/blink/renderer/core/css/css_value_id_mappings.h b/third_party/blink/renderer/core/css/css_value_id_mappings.h
index b6f0157..c4e7a1e 100644
--- a/third_party/blink/renderer/core/css/css_value_id_mappings.h
+++ b/third_party/blink/renderer/core/css/css_value_id_mappings.h
@@ -6,6 +6,7 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_CSS_CSS_VALUE_ID_MAPPINGS_H_
 
 #include "base/notreached.h"
+#include "third_party/blink/renderer/core/animation/effect_model.h"
 #include "third_party/blink/renderer/core/css/css_value_id_mappings_generated.h"
 #include "third_party/blink/renderer/core/style/computed_style_constants.h"
 
@@ -71,6 +72,35 @@
 }
 
 template <>
+inline CSSValueID PlatformEnumToCSSValueID(EffectModel::CompositeOperation v) {
+  switch (v) {
+    case EffectModel::kCompositeReplace:
+      return CSSValueID::kReplace;
+    case EffectModel::kCompositeAdd:
+      return CSSValueID::kAdd;
+    case EffectModel::kCompositeAccumulate:
+      return CSSValueID::kAccumulate;
+  }
+  NOTREACHED();
+  return CSSValueID::kReplace;
+}
+
+template <>
+inline EffectModel::CompositeOperation CssValueIDToPlatformEnum(CSSValueID v) {
+  switch (v) {
+    case CSSValueID::kReplace:
+      return EffectModel::kCompositeReplace;
+    case CSSValueID::kAdd:
+      return EffectModel::kCompositeAdd;
+    case CSSValueID::kAccumulate:
+      return EffectModel::kCompositeAccumulate;
+    default:
+      NOTREACHED();
+      return EffectModel::kCompositeReplace;
+  }
+}
+
+template <>
 inline ETextOrientation CssValueIDToPlatformEnum(CSSValueID v) {
   if (v == CSSValueID::kSidewaysRight) {  // Legacy -webkit-auto. Eqiuvalent to
                                           // start.
diff --git a/third_party/blink/renderer/core/css/css_value_keywords.json5 b/third_party/blink/renderer/core/css/css_value_keywords.json5
index f2cf9eb..8e75661 100644
--- a/third_party/blink/renderer/core/css/css_value_keywords.json5
+++ b/third_party/blink/renderer/core/css/css_value_keywords.json5
@@ -859,6 +859,12 @@
     "visual",
 
     //
+    // animation-composition
+    //
+    "replace",
+    "accumulate",
+
+    //
     // animation-direction
     //
     "alternate",
diff --git a/third_party/blink/renderer/core/css/cssom/style_property_map.cc b/third_party/blink/renderer/core/css/cssom/style_property_map.cc
index a64363b..0855b9e6 100644
--- a/third_party/blink/renderer/core/css/cssom/style_property_map.cc
+++ b/third_party/blink/renderer/core/css/cssom/style_property_map.cc
@@ -373,6 +373,13 @@
 
   CSSValueList* current_value = nullptr;
   if (const CSSValue* css_value = GetProperty(property_id)) {
+    if (css_value->IsVariableReferenceValue()) {
+      // https://drafts.css-houdini.org/css-typed-om/#dom-stylepropertymap-append
+      // 8. If props[property] contains a var() reference, throw a TypeError.
+      exception_state.ThrowTypeError(
+          "Cannot append to a list containing a variable reference");
+      return;
+    }
     current_value = To<CSSValueList>(css_value)->Copy();
   } else {
     current_value = CssValueListForPropertyID(property_id);
diff --git a/third_party/blink/renderer/core/css/parser/css_selector_parser_test.cc b/third_party/blink/renderer/core/css/parser/css_selector_parser_test.cc
index d16bdc5..5b89746 100644
--- a/third_party/blink/renderer/core/css/parser/css_selector_parser_test.cc
+++ b/third_party/blink/renderer/core/css/parser/css_selector_parser_test.cc
@@ -237,6 +237,8 @@
 }
 
 TEST(CSSSelectorParserTest, TransitionPseudoStyles) {
+  ScopedViewTransitionForTest view_transition_enabled(true);
+
   struct TestCase {
     const char* selector;
     bool valid;
diff --git a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
index 62d9e43..d2d50d2 100644
--- a/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
+++ b/third_party/blink/renderer/core/css/properties/longhands/longhands_custom.cc
@@ -194,6 +194,39 @@
       style.AnchorScroll()->GetName().GetName());
 }
 
+const CSSValue* AnimationComposition::ParseSingleValue(
+    CSSParserTokenRange& range,
+    const CSSParserContext& context,
+    const CSSParserLocalContext&) const {
+  DCHECK(RuntimeEnabledFeatures::CSSAnimationCompositionEnabled());
+  return css_parsing_utils::ConsumeCommaSeparatedList(
+      css_parsing_utils::ConsumeIdent<CSSValueID::kReplace, CSSValueID::kAdd,
+                                      CSSValueID::kAccumulate>,
+      range);
+}
+
+const CSSValue* AnimationComposition::CSSValueFromComputedStyleInternal(
+    const ComputedStyle& style,
+    const LayoutObject*,
+    bool allow_visited_style) const {
+  DCHECK(RuntimeEnabledFeatures::CSSAnimationCompositionEnabled());
+  if (!style.Animations()) {
+    return InitialValue();
+  }
+  CSSValueList* list = CSSValueList::CreateCommaSeparated();
+  const auto& composition_list = style.Animations()->CompositionList();
+  for (const auto& composition : composition_list) {
+    list->Append(*CSSIdentifierValue::Create(composition));
+  }
+  return list;
+}
+
+const CSSValue* AnimationComposition::InitialValue() const {
+  CSSValueList* list = CSSValueList::CreateCommaSeparated();
+  list->Append(*CSSIdentifierValue::Create(CSSValueID::kReplace));
+  return list;
+}
+
 const CSSValue* AnimationDelay::ParseSingleValue(
     CSSParserTokenRange& range,
     const CSSParserContext& context,
diff --git a/third_party/blink/renderer/core/css/resolver/css_to_style_map.cc b/third_party/blink/renderer/core/css/resolver/css_to_style_map.cc
index 0e342a4..a69f386 100644
--- a/third_party/blink/renderer/core/css/resolver/css_to_style_map.cc
+++ b/third_party/blink/renderer/core/css/resolver/css_to_style_map.cc
@@ -30,6 +30,7 @@
 #include "third_party/blink/renderer/core/css/resolver/css_to_style_map.h"
 
 #include "third_party/blink/renderer/core/animation/css/css_animation_data.h"
+#include "third_party/blink/renderer/core/animation/effect_model.h"
 #include "third_party/blink/renderer/core/css/css_border_image_slice_value.h"
 #include "third_party/blink/renderer/core/css/css_custom_ident_value.h"
 #include "third_party/blink/renderer/core/css/css_identifier_value.h"
@@ -508,6 +509,20 @@
   return MapAnimationRange(value);
 }
 
+EffectModel::CompositeOperation CSSToStyleMap::MapAnimationComposition(
+    StyleResolverState& state,
+    const CSSValue& value) {
+  switch (To<CSSIdentifierValue>(value).GetValueID()) {
+    case CSSValueID::kAdd:
+      return EffectModel::kCompositeAdd;
+    case CSSValueID::kAccumulate:
+      return EffectModel::kCompositeAccumulate;
+    case CSSValueID::kReplace:
+    default:
+      return EffectModel::kCompositeReplace;
+  }
+}
+
 CSSTransitionData::TransitionProperty CSSToStyleMap::MapAnimationProperty(
     StyleResolverState& state,
     const CSSValue& value) {
diff --git a/third_party/blink/renderer/core/css/resolver/css_to_style_map.h b/third_party/blink/renderer/core/css/resolver/css_to_style_map.h
index 785b360..cb25cb1 100644
--- a/third_party/blink/renderer/core/css/resolver/css_to_style_map.h
+++ b/third_party/blink/renderer/core/css/resolver/css_to_style_map.h
@@ -91,6 +91,9 @@
   static absl::optional<Timing::TimelineOffset> MapAnimationRangeEnd(
       StyleResolverState&,
       const CSSValue&);
+  static EffectModel::CompositeOperation MapAnimationComposition(
+      StyleResolverState&,
+      const CSSValue&);
   static CSSTransitionData::TransitionProperty MapAnimationProperty(
       StyleResolverState&,
       const CSSValue&);
diff --git a/third_party/blink/renderer/core/editing/ime/stylus_writing_gesture_test.cc b/third_party/blink/renderer/core/editing/ime/stylus_writing_gesture_test.cc
index 93d1c7d..28f78d6 100644
--- a/third_party/blink/renderer/core/editing/ime/stylus_writing_gesture_test.cc
+++ b/third_party/blink/renderer/core/editing/ime/stylus_writing_gesture_test.cc
@@ -6,7 +6,9 @@
 #include <gtest/gtest.h>
 #include "third_party/blink/public/mojom/input/stylus_writing_gesture.mojom-blink.h"
 #include "third_party/blink/renderer/core/css/style_engine.h"
+#include "third_party/blink/renderer/core/dom/text.h"
 #include "third_party/blink/renderer/core/html/forms/html_input_element.h"
+#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
 #include "third_party/blink/renderer/core/testing/sim/sim_request.h"
 #include "third_party/blink/renderer/core/testing/sim/sim_test.h"
 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
@@ -46,6 +48,8 @@
 
   HTMLInputElement* SetUpSingleInput();
 
+  HTMLTextAreaElement* SetUpMultilineInput();
+
   WebFrameWidgetImpl* WidgetImpl() {
     return static_cast<WebFrameWidgetImpl*>(LocalFrameRoot().FrameWidget());
   }
@@ -93,6 +97,42 @@
   return input_element;
 }
 
+HTMLTextAreaElement* StylusWritingGestureTest::SetUpMultilineInput() {
+  SimRequest main_resource("https://example.com", "text/html");
+  SimSubresourceRequest font_resource("https://example.com/Ahem.woff2",
+                                      "font/woff2");
+
+  LoadURL("https://example.com");
+  main_resource.Complete(R"HTML(
+    <!doctype html>
+    <style>
+      @font-face {
+        font-family: custom-font;
+        src: url(https://example.com/Ahem.woff2) format("woff2");
+      }
+      body {
+        margin: 0;
+      }
+      #target {
+        font: 10px/1 custom-font, monospace;
+        padding: none;
+        border: none;
+      }
+    </style>
+    <textarea type='text' id='target' rows='4'/>
+  )HTML");
+
+  Compositor().BeginFrame();
+  // Finish font loading, and trigger invalidations.
+  font_resource.Complete(ReadAhemWoff2());
+  GetDocument().GetStyleEngine().InvalidateStyleAndLayoutForFontUpdates();
+  Compositor().BeginFrame();
+  HTMLTextAreaElement* input_element =
+      DynamicTo<HTMLTextAreaElement>(*GetDocument().getElementById("target"));
+  input_element->Focus();
+  return input_element;
+}
+
 TEST_F(StylusWritingGestureTest, TestGestureDelete) {
   auto* input = SetUpSingleInput();
   input->SetValue("ABCD EFGH");
@@ -127,8 +167,6 @@
   EXPECT_EQ(3, range.EndOffset());
 }
 
-// Re-enable once https://crbug.com/1404969 is fixed and the middle of word
-// logic is implemented in stylus_writing_gesture.cc.
 TEST_F(StylusWritingGestureTest, TestGestureDeleteWithWordGranularity) {
   auto* input = SetUpSingleInput();
 
@@ -166,6 +204,28 @@
   }
 }
 
+// https://crbug.com/1407262
+TEST_F(StylusWritingGestureTest, TestGestureAtEndOfLineWithWordGranularity) {
+  auto* input = SetUpMultilineInput();
+  auto* inner_editor = input->InnerEditorElement();
+  Document& doc = GetDocument();
+  inner_editor->appendChild(Text::Create(doc, "ABCD"));
+  inner_editor->appendChild(Text::Create(doc, "\n"));
+  inner_editor->appendChild(Text::Create(doc, "EFGH"));
+
+  mojom::blink::StylusWritingGestureDataPtr gesture_data(
+      mojom::blink::StylusWritingGestureData::New());
+  gesture_data->action = mojom::blink::StylusWritingGestureAction::DELETE_TEXT;
+  gesture_data->granularity =
+      mojom::blink::StylusWritingGestureGranularity::WORD;
+  gesture_data->start_point = gfx::Point(0, 6);
+  gesture_data->end_point = gfx::Point(60, 6);
+  gesture_data->text_alternative = text_alternative;
+
+  WidgetImpl()->HandleStylusWritingGestureAction(std::move(gesture_data));
+  EXPECT_EQ("\nEFGH", input->Value());
+}
+
 TEST_F(StylusWritingGestureTest, TestGestureRemoveSpaces) {
   auto* input = SetUpSingleInput();
   input->SetValue("ABCD   EFGH");
diff --git a/third_party/blink/renderer/core/editing/selection_adjuster.cc b/third_party/blink/renderer/core/editing/selection_adjuster.cc
index 36e1fb7..155970b 100644
--- a/third_party/blink/renderer/core/editing/selection_adjuster.cc
+++ b/third_party/blink/renderer/core/editing/selection_adjuster.cc
@@ -205,7 +205,7 @@
               passed_end.GetPosition(), ChooseWordSide(original_end));
           const PositionTemplate<Strategy> word_middle =
               MiddleOfWordPosition(word_start, word_end.DeepEquivalent());
-          if (word_middle > passed_end.GetPosition()) {
+          if (word_middle.IsNull() or word_middle > passed_end.GetPosition()) {
             return word_start;
           }
         }
diff --git a/third_party/blink/renderer/core/editing/visible_units_word.cc b/third_party/blink/renderer/core/editing/visible_units_word.cc
index 452c9c9..a17b464 100644
--- a/third_party/blink/renderer/core/editing/visible_units_word.cc
+++ b/third_party/blink/renderer/core/editing/visible_units_word.cc
@@ -347,6 +347,9 @@
 
 PositionInFlatTree MiddleOfWordPosition(const PositionInFlatTree& word_start,
                                         const PositionInFlatTree& word_end) {
+  if (word_start >= word_end) {
+    return PositionInFlatTree(nullptr, 0);
+  }
   unsigned middle =
       TextIteratorAlgorithm<EditingInFlatTreeStrategy>::RangeLength(word_start,
                                                                     word_end) /
@@ -363,7 +366,7 @@
     middle -= length;
   }
   NOTREACHED();
-  return PositionInFlatTree(nullptr, -1);
+  return PositionInFlatTree(nullptr, 0);
 }
 
 Position MiddleOfWordPosition(const Position& word_start,
diff --git a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
index e0a62a0..b8dc5ab 100644
--- a/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
+++ b/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc
@@ -690,6 +690,8 @@
   container_builder_.SetIsBlockInInline();
   container_builder_.SetInlineSize(fragment.InlineSize());
 
+  container_builder_.ClampBreakAppeal(result.BreakAppeal());
+
   if (!result.IsSelfCollapsing()) {
     // Block-in-inline is wrapped in an anonymous block that has no margins.
     const FontHeight metrics = fragment.BaselineMetrics(
diff --git a/third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h b/third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h
index 9557194..5896640 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h
@@ -495,12 +495,6 @@
     return *early_break_;
   }
 
-  // Downgrade the break appeal if the specified break appeal is lower than any
-  // found so far.
-  void ClampBreakAppeal(NGBreakAppeal appeal) {
-    break_appeal_ = std::min(break_appeal_, appeal);
-  }
-
   // Creates the fragment. Can only be called once.
   const NGLayoutResult* ToBoxFragment() {
     DCHECK_NE(BoxType(), NGPhysicalFragment::kInlineBox);
@@ -801,9 +795,6 @@
 
   AtomicString page_name_;
 
-  // The appeal of breaking inside this container.
-  NGBreakAppeal break_appeal_ = kBreakAppealPerfect;
-
   absl::optional<LayoutUnit> first_baseline_;
   absl::optional<LayoutUnit> last_baseline_;
   LayoutUnit math_italic_correction_;
diff --git a/third_party/blink/renderer/core/layout/ng/ng_container_fragment_builder.h b/third_party/blink/renderer/core/layout/ng/ng_container_fragment_builder.h
index fcf2dd3..61ef6a4 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_container_fragment_builder.h
+++ b/third_party/blink/renderer/core/layout/ng/ng_container_fragment_builder.h
@@ -341,6 +341,12 @@
     should_force_same_fragmentation_flow_ = true;
   }
 
+  // Downgrade the break appeal if the specified break appeal is lower than any
+  // found so far.
+  void ClampBreakAppeal(NGBreakAppeal appeal) {
+    break_appeal_ = std::min(break_appeal_, appeal);
+  }
+
   // Specify that all child break tokens be added manually, instead of being
   // added automatically as part of adding child fragments.
   void SetShouldAddBreakTokensManually() {
@@ -438,6 +444,9 @@
 
   const NGEarlyBreak* early_break_ = nullptr;
 
+  // The appeal of breaking inside this container.
+  NGBreakAppeal break_appeal_ = kBreakAppealPerfect;
+
   // See NGLayoutResult::AnnotationOverflow().
   LayoutUnit annotation_overflow_;
   // See NGLayoutResult::BlockEndAnotationSpace().
diff --git a/third_party/blink/renderer/core/layout/ng/ng_layout_result.cc b/third_party/blink/renderer/core/layout/ng/ng_layout_result.cc
index 95d445e..b5b4270 100644
--- a/third_party/blink/renderer/core/layout/ng/ng_layout_result.cc
+++ b/third_party/blink/renderer/core/layout/ng/ng_layout_result.cc
@@ -93,7 +93,6 @@
     bitfields_.is_block_size_for_fragmentation_clamped =
         builder->is_block_size_for_fragmentation_clamped_;
 
-    bitfields_.break_appeal = builder->break_appeal_;
     bitfields_.has_forced_break = builder->has_forced_break_;
   }
   bitfields_.disable_simplified_layout = builder->disable_simplified_layout;
@@ -275,6 +274,8 @@
     bitfields_.is_empty_spanner_parent = builder->is_empty_spanner_parent_;
   }
 
+  bitfields_.break_appeal = builder->break_appeal_;
+
   bitfields_.should_force_same_fragmentation_flow =
       builder->should_force_same_fragmentation_flow_;
 
diff --git a/third_party/blink/renderer/core/svg/svg_length_context.cc b/third_party/blink/renderer/core/svg/svg_length_context.cc
index 6a4a4b47b..8317e0ab 100644
--- a/third_party/blink/renderer/core/svg/svg_length_context.cc
+++ b/third_party/blink/renderer/core/svg/svg_length_context.cc
@@ -63,49 +63,35 @@
   return nullptr;
 }
 
-const ComputedStyle* RootElementStyle(const Node* context) {
-  if (!context) {
-    return nullptr;
+const ComputedStyle* RootElementStyle(const SVGElement& element) {
+  if (auto* document_element = element.GetDocument().documentElement()) {
+    if (element != document_element) {
+      return document_element->GetComputedStyle();
+    }
   }
-
-  const Document& document = context->GetDocument();
-  Node* document_element = document.documentElement();
-  const ComputedStyle* document_style = document.GetComputedStyle();
-  const ComputedStyle* style = document_element && context != document_element
-                                   ? document_element->GetComputedStyle()
-                                   : document_style;
-  if (!style) {
-    style = document_style;
-  }
-  return style;
+  return nullptr;
 }
 
-class CSSToLengthConversionDataContext {
+const ComputedStyle* RootElementStyle(const SVGElement* element) {
+  return element ? RootElementStyle(*element) : nullptr;
+}
+
+class SVGLengthConversionData : public CSSToLengthConversionData {
   STACK_ALLOCATED();
 
  public:
-  explicit CSSToLengthConversionDataContext(const SVGElement* context)
-      : context_(context),
-        style_(ComputedStyleForLengthResolving(context)),
-        root_style_(RootElementStyle(context)) {}
-
-  bool HasStyle() const { return style_; }
-
-  CSSToLengthConversionData MakeConversionData() const {
-    DCHECK(context_);
-    DCHECK(HasStyle());
-    return CSSToLengthConversionData(style_, style_, root_style_,
-                                     context_->GetDocument().GetLayoutView(),
-                                     CSSToLengthConversionData::ContainerSizes(
-                                         context_->ParentOrShadowHostElement()),
-                                     1.0f, ignored_flags_);
-  }
+  SVGLengthConversionData(const SVGElement& context, const ComputedStyle& style)
+      : CSSToLengthConversionData(&style,
+                                  &style,
+                                  RootElementStyle(context),
+                                  context.GetDocument().GetLayoutView(),
+                                  CSSToLengthConversionData::ContainerSizes(
+                                      context.ParentOrShadowHostElement()),
+                                  1.0f,
+                                  ignored_flags_) {}
 
  private:
-  const SVGElement* context_ = nullptr;
-  const ComputedStyle* style_ = nullptr;
-  const ComputedStyle* root_style_ = nullptr;
-  mutable CSSToLengthConversionData::Flags ignored_flags_ = 0;
+  CSSToLengthConversionData::Flags ignored_flags_ = 0;
 };
 
 float ObjectBoundingBoxUnitToUserUnits(const Length& length,
@@ -316,12 +302,11 @@
                                               const SVGLength& width,
                                               const SVGLength& height) {
   DCHECK_NE(SVGUnitTypes::kSvgUnitTypeUnknown, type);
-  CSSToLengthConversionDataContext conversion_context(context);
-  if (!conversion_context.HasStyle()) {
+  const ComputedStyle* style = ComputedStyleForLengthResolving(context);
+  if (!style) {
     return gfx::RectF(0, 0, 0, 0);
   }
-  const CSSToLengthConversionData conversion_data =
-      conversion_context.MakeConversionData();
+  const SVGLengthConversionData conversion_data(*context, *style);
   // Convert SVGLengths to Lengths (preserves percentages).
   const LengthPoint point(
       x.AsCSSPrimitiveValue().ConvertToLength(conversion_data),
@@ -381,32 +366,31 @@
 
 float SVGLengthContext::ResolveValue(const CSSPrimitiveValue& primitive_value,
                                      SVGLengthMode mode) const {
-  CSSToLengthConversionDataContext conversion_context(context_);
-  if (!conversion_context.HasStyle()) {
+  const ComputedStyle* style = ComputedStyleForLengthResolving(context_);
+  if (!style) {
     return 0;
   }
-  const Length& length =
-      primitive_value.ConvertToLength(conversion_context.MakeConversionData());
+  const SVGLengthConversionData conversion_data(*context_, *style);
+  const Length& length = primitive_value.ConvertToLength(conversion_data);
   return ValueForLength(length, 1.0f, mode);
 }
 
 Length SVGLengthContext::ConvertToLength(const SVGLength& length) const {
-  CSSToLengthConversionDataContext conversion_context(context_);
-  if (!conversion_context.HasStyle()) {
+  const ComputedStyle* style = ComputedStyleForLengthResolving(context_);
+  if (!style) {
     return Length::Fixed(0);
   }
-  return length.AsCSSPrimitiveValue().ConvertToLength(
-      conversion_context.MakeConversionData());
+  const SVGLengthConversionData conversion_data(*context_, *style);
+  return length.AsCSSPrimitiveValue().ConvertToLength(conversion_data);
 }
 
 LengthPoint SVGLengthContext::ConvertToLengthPoint(const SVGLength& x,
                                                    const SVGLength& y) const {
-  CSSToLengthConversionDataContext conversion_context(context_);
-  if (!conversion_context.HasStyle()) {
+  const ComputedStyle* style = ComputedStyleForLengthResolving(context_);
+  if (!style) {
     return LengthPoint(Length::Fixed(0), Length::Fixed(0));
   }
-  const CSSToLengthConversionData conversion_data =
-      conversion_context.MakeConversionData();
+  const SVGLengthConversionData conversion_data(*context_, *style);
   return LengthPoint(x.AsCSSPrimitiveValue().ConvertToLength(conversion_data),
                      y.AsCSSPrimitiveValue().ConvertToLength(conversion_data));
 }
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture.cc b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
index 596511af..6e272e0 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
@@ -426,7 +426,18 @@
 // inside the method, https://crbug.com/708723.
 void ImageCapture::SetMediaTrackConstraints(
     ScriptPromiseResolver* resolver,
-    const HeapVector<Member<MediaTrackConstraintSet>>& constraints_vector) {
+    const MediaTrackConstraints* all_constraints) {
+  DCHECK(all_constraints);
+  if (!all_constraints->hasAdvanced() || all_constraints->advanced().empty()) {
+    // TODO(crbug.com/1408091): This is not spec compliant.
+    // If there are no advanced constraints (but only required and optional
+    // constraints), the required and optional constraints should be applied.
+    ClearMediaTrackConstraints();
+    resolver->Resolve();
+    return;
+  }
+
+  const auto& constraints_vector = all_constraints->advanced();
   DCHECK_GT(constraints_vector.size(), 0u);
   // TODO(mcasas): add support more than one single advanced constraint.
   const MediaTrackConstraintSet* constraints = constraints_vector[0];
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture.h b/third_party/blink/renderer/modules/imagecapture/image_capture.h
index b92d967..62a1c32 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture.h
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture.h
@@ -12,7 +12,7 @@
 #include "third_party/blink/renderer/bindings/core/v8/active_script_wrappable.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_capabilities.h"
-#include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_constraint_set.h"
+#include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_constraints.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_settings.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_photo_settings.h"
 #include "third_party/blink/renderer/core/dom/events/event_target.h"
@@ -78,9 +78,8 @@
   ScriptPromise grabFrame(ScriptState*);
 
   void GetMediaTrackCapabilities(MediaTrackCapabilities*) const;
-  void SetMediaTrackConstraints(
-      ScriptPromiseResolver*,
-      const HeapVector<Member<MediaTrackConstraintSet>>&);
+  void SetMediaTrackConstraints(ScriptPromiseResolver*,
+                                const MediaTrackConstraints* constraints);
   const MediaTrackConstraintSet* GetMediaTrackConstraints() const;
   void ClearMediaTrackConstraints();
   void GetMediaTrackSettings(MediaTrackSettings*) const;
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
index 0f74c27f..27fdaa2 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.cc
@@ -759,7 +759,7 @@
       // implementation.
       image_capture_->ClearMediaTrackConstraints();
     } else if (ConstraintsHaveImageCapture(constraints)) {
-      applyConstraintsImageCapture(resolver, constraints);
+      image_capture_->SetMediaTrackConstraints(resolver, constraints);
       return;
     }
   }
@@ -788,18 +788,6 @@
   return;
 }
 
-void MediaStreamTrackImpl::applyConstraintsImageCapture(
-    ScriptPromiseResolver* resolver,
-    const MediaTrackConstraints* constraints) {
-  // |constraints| empty means "remove/clear all current constraints".
-  if (!constraints->hasAdvanced() || constraints->advanced().empty()) {
-    image_capture_->ClearMediaTrackConstraints();
-    resolver->Resolve();
-  } else {
-    image_capture_->SetMediaTrackConstraints(resolver, constraints->advanced());
-  }
-}
-
 bool MediaStreamTrackImpl::Ended() const {
   return (execution_context_ && execution_context_->IsContextDestroyed()) ||
          (ready_state_ == MediaStreamSource::kReadyStateEnded);
diff --git a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
index 8f8ba12a..2a2c5cf 100644
--- a/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
+++ b/third_party/blink/renderer/modules/mediastream/media_stream_track_impl.h
@@ -159,8 +159,6 @@
   void SourceChangedCaptureHandle() override;
 
   void PropagateTrackEnded();
-  void applyConstraintsImageCapture(ScriptPromiseResolver*,
-                                    const MediaTrackConstraints*);
 
   void SendLogMessage(const WTF::String& message);
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
index ed870d3..3464263 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_peer_connection.cc
@@ -1776,35 +1776,52 @@
   return rtp_contributing_source_cache_.value();
 }
 
-RTCRtpTransceiver* RTCPeerConnection::addTransceiver(
-    const V8UnionMediaStreamTrackOrString* track_or_kind,
+absl::optional<webrtc::RtpTransceiverInit> ValidateRtpTransceiverInit(
+    ExecutionContext* execution_context,
+    ExceptionState& exception_state,
     const RTCRtpTransceiverInit* init,
-    ExceptionState& exception_state) {
-  if (ThrowExceptionIfSignalingStateClosed(signaling_state_, &exception_state))
-    return nullptr;
-  auto webrtc_init = ToRtpTransceiverInit(GetExecutionContext(), init);
+    const String kind) {
+  auto webrtc_init = ToRtpTransceiverInit(execution_context, init, kind);
   // Validate sendEncodings.
   for (auto& encoding : webrtc_init.send_encodings) {
     if (encoding.rid.length() > 16) {
       exception_state.ThrowTypeError("Illegal length of rid");
-      return nullptr;
+      return absl::nullopt;
     }
     // Allowed characters: a-z 0-9 _ and -
     if (encoding.rid.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLM"
                                        "NOPQRSTUVWXYZ0123456789-_") !=
         std::string::npos) {
       exception_state.ThrowTypeError("Illegal character in rid");
-      return nullptr;
+      return absl::nullopt;
     }
   }
+  return webrtc_init;
+}
+
+RTCRtpTransceiver* RTCPeerConnection::addTransceiver(
+    const V8UnionMediaStreamTrackOrString* track_or_kind,
+    const RTCRtpTransceiverInit* init,
+    ExceptionState& exception_state) {
+  if (ThrowExceptionIfSignalingStateClosed(signaling_state_,
+                                           &exception_state)) {
+    return nullptr;
+  }
   webrtc::RTCErrorOr<std::unique_ptr<RTCRtpTransceiverPlatform>> result =
       webrtc::RTCError(webrtc::RTCErrorType::UNSUPPORTED_OPERATION);
   switch (track_or_kind->GetContentType()) {
     case V8UnionMediaStreamTrackOrString::ContentType::kMediaStreamTrack: {
       MediaStreamTrack* track = track_or_kind->GetAsMediaStreamTrack();
+
+      auto webrtc_init = ValidateRtpTransceiverInit(
+          GetExecutionContext(), exception_state, init, track->kind());
+      if (!webrtc_init) {
+        return nullptr;
+      }
+
       RegisterTrack(track);
       result = peer_handler_->AddTransceiverWithTrack(track->Component(),
-                                                      std::move(webrtc_init));
+                                                      std::move(*webrtc_init));
       break;
     }
     case V8UnionMediaStreamTrackOrString::ContentType::kString: {
@@ -1822,8 +1839,15 @@
             "MediaStreamTrack kind ('audio' or 'video').");
         return nullptr;
       }
+
+      auto webrtc_init = ValidateRtpTransceiverInit(
+          GetExecutionContext(), exception_state, init, kind);
+      if (!webrtc_init) {
+        return nullptr;
+      }
+
       result = peer_handler_->AddTransceiverWithKind(std::move(kind),
-                                                     std::move(webrtc_init));
+                                                     std::move(*webrtc_init));
       break;
     }
   }
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
index 9ecb4d3..d9ebee9 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.cc
@@ -448,13 +448,14 @@
 std::tuple<Vector<webrtc::RtpEncodingParameters>,
            absl::optional<webrtc::DegradationPreference>>
 ToRtpParameters(ExecutionContext* context,
-                const RTCRtpSendParameters* parameters) {
+                const RTCRtpSendParameters* parameters,
+                const String& kind) {
   Vector<webrtc::RtpEncodingParameters> encodings;
   if (parameters->hasEncodings()) {
     encodings.reserve(parameters->encodings().size());
 
     for (const auto& encoding : parameters->encodings()) {
-      encodings.push_back(ToRtpEncodingParameters(context, encoding));
+      encodings.push_back(ToRtpEncodingParameters(context, encoding, kind));
     }
   }
 
@@ -481,7 +482,8 @@
 
 webrtc::RtpEncodingParameters ToRtpEncodingParameters(
     ExecutionContext* context,
-    const RTCRtpEncodingParameters* encoding) {
+    const RTCRtpEncodingParameters* encoding,
+    const String& kind) {
   webrtc::RtpEncodingParameters webrtc_encoding;
   if (encoding->hasRid()) {
     webrtc_encoding.rid = encoding->rid().Utf8();
@@ -493,21 +495,24 @@
   if (encoding->hasMaxBitrate()) {
     webrtc_encoding.max_bitrate_bps = ClampTo<int>(encoding->maxBitrate());
   }
-  if (encoding->hasScaleResolutionDownBy()) {
-    webrtc_encoding.scale_resolution_down_by =
-        encoding->scaleResolutionDownBy();
+  if (kind == "video") {
+    if (encoding->hasScaleResolutionDownBy()) {
+      webrtc_encoding.scale_resolution_down_by =
+          encoding->scaleResolutionDownBy();
+    }
+    if (encoding->hasMaxFramerate()) {
+      webrtc_encoding.max_framerate = encoding->maxFramerate();
+    }
+    // https://w3c.github.io/webrtc-svc/
+    if (encoding->hasScalabilityMode()) {
+      webrtc_encoding.scalability_mode = encoding->scalabilityMode().Utf8();
+      WebRtcScalabilityMode scalability_mode =
+          ScalabilityModeStringToUMAMode(*webrtc_encoding.scalability_mode);
+      UMA_HISTOGRAM_ENUMERATION("WebRtcScalabilityMode", scalability_mode);
+    }
+  } else if (kind == "audio") {
+    webrtc_encoding.adaptive_ptime = encoding->adaptivePtime();
   }
-  if (encoding->hasMaxFramerate()) {
-    webrtc_encoding.max_framerate = encoding->maxFramerate();
-  }
-  // https://w3c.github.io/webrtc-svc/
-  if (encoding->hasScalabilityMode()) {
-    webrtc_encoding.scalability_mode = encoding->scalabilityMode().Utf8();
-    WebRtcScalabilityMode scalability_mode =
-        ScalabilityModeStringToUMAMode(*webrtc_encoding.scalability_mode);
-    UMA_HISTOGRAM_ENUMERATION("WebRtcScalabilityMode", scalability_mode);
-  }
-  webrtc_encoding.adaptive_ptime = encoding->adaptivePtime();
   return webrtc_encoding;
 }
 
@@ -670,22 +675,25 @@
     if (webrtc_encoding.max_bitrate_bps) {
       encoding->setMaxBitrate(webrtc_encoding.max_bitrate_bps.value());
     }
-    if (webrtc_encoding.scale_resolution_down_by) {
-      encoding->setScaleResolutionDownBy(
-          webrtc_encoding.scale_resolution_down_by.value());
-    }
-    if (webrtc_encoding.max_framerate) {
-      encoding->setMaxFramerate(webrtc_encoding.max_framerate.value());
-    }
     encoding->setPriority(
         PriorityFromDouble(webrtc_encoding.bitrate_priority).c_str());
     encoding->setNetworkPriority(
         PriorityFromEnum(webrtc_encoding.network_priority).c_str());
-    if (webrtc_encoding.scalability_mode) {
-      encoding->setScalabilityMode(
-          webrtc_encoding.scalability_mode.value().c_str());
+    if (kind_ == "video") {
+      if (webrtc_encoding.scale_resolution_down_by) {
+        encoding->setScaleResolutionDownBy(
+            webrtc_encoding.scale_resolution_down_by.value());
+      }
+      if (webrtc_encoding.max_framerate) {
+        encoding->setMaxFramerate(webrtc_encoding.max_framerate.value());
+      }
+      if (webrtc_encoding.scalability_mode) {
+        encoding->setScalabilityMode(
+            webrtc_encoding.scalability_mode.value().c_str());
+      }
+    } else if (kind_ == "audio") {
+      encoding->setAdaptivePtime(webrtc_encoding.adaptive_ptime);
     }
-    encoding->setAdaptivePtime(webrtc_encoding.adaptive_ptime);
     encodings.push_back(encoding);
   }
   parameters->setEncodings(encodings);
@@ -743,7 +751,7 @@
   Vector<webrtc::RtpEncodingParameters> encodings;
   absl::optional<webrtc::DegradationPreference> degradation_preference;
   std::tie(encodings, degradation_preference) =
-      ToRtpParameters(pc_->GetExecutionContext(), parameters);
+      ToRtpParameters(pc_->GetExecutionContext(), parameters, kind_);
 
   auto* request = MakeGarbageCollected<SetParametersRequest>(resolver, this);
   sender_->SetParameters(std::move(encodings), degradation_preference, request);
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
index f0a2281..c7a396f 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_sender.h
@@ -42,7 +42,8 @@
 
 webrtc::RtpEncodingParameters ToRtpEncodingParameters(
     ExecutionContext* context,
-    const RTCRtpEncodingParameters*);
+    const RTCRtpEncodingParameters*,
+    const String& kind);
 RTCRtpHeaderExtensionParameters* ToRtpHeaderExtensionParameters(
     const webrtc::RtpExtension& headers);
 RTCRtpCodecParameters* ToRtpCodecParameters(
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.cc b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.cc
index 518ce57..86e69120 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.cc
@@ -89,7 +89,8 @@
 
 webrtc::RtpTransceiverInit ToRtpTransceiverInit(
     ExecutionContext* context,
-    const RTCRtpTransceiverInit* init) {
+    const RTCRtpTransceiverInit* init,
+    const String& kind) {
   webrtc::RtpTransceiverInit webrtc_init;
   absl::optional<webrtc::RtpTransceiverDirection> direction;
   if (init->hasDirection() &&
@@ -104,7 +105,7 @@
   DCHECK(init->hasSendEncodings());
   for (const auto& encoding : init->sendEncodings()) {
     webrtc_init.send_encodings.push_back(
-        ToRtpEncodingParameters(context, encoding));
+        ToRtpEncodingParameters(context, encoding, kind));
   }
   return webrtc_init;
 }
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.h b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.h
index 6b5904b8..4458457 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_rtp_transceiver.h
@@ -26,7 +26,8 @@
 class RTCRtpSender;
 
 webrtc::RtpTransceiverInit ToRtpTransceiverInit(ExecutionContext* context,
-                                                const RTCRtpTransceiverInit*);
+                                                const RTCRtpTransceiverInit*,
+                                                const String& kind);
 
 class RTCRtpTransceiver final : public ScriptWrappable {
   DEFINE_WRAPPERTYPEINFO();
diff --git a/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_face.cc b/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_face.cc
index b6ee0a8f..d164ac9 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_face.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/harfbuzz_face.cc
@@ -53,6 +53,7 @@
 #include "third_party/blink/renderer/platform/resolution_units.h"
 #include "third_party/blink/renderer/platform/wtf/hash_map.h"
 #include "third_party/blink/renderer/platform/wtf/math_extras.h"
+#include "third_party/blink/renderer/platform/wtf/text/character_names.h"
 #include "third_party/skia/include/core/SkPaint.h"
 #include "third_party/skia/include/core/SkPath.h"
 #include "third_party/skia/include/core/SkPoint.h"
@@ -91,8 +92,30 @@
   if (hb_font_data->range_set_ && !hb_font_data->range_set_->Contains(unicode))
     return false;
 
-  return hb_font_get_glyph(hb_font_get_parent(hb_font), unicode,
-                           variation_selector, glyph);
+  hb_bool_t hb_has_glyph = hb_font_get_glyph(
+      hb_font_get_parent(hb_font), unicode, variation_selector, glyph);
+// MacOS CoreText API synthesizes GlyphID for several unicode codepoints,
+// for example, hyphens and separators for some fonts. HarfBuzz does not
+// synthesize such glyphs, and as it's not found from the last resort font, we
+// end up with displaying tofu, see https://crbug.com/1267606 for details.
+// Chrome uses Times as last resort fallback font and in Times the only visible
+// synthesizing characters are hyphen (0x2010) and non-breaking hyphen (0x2011).
+// For performance reasons, we limit this fallback lookup to the specific
+// missing glyphs for hyphens and only to Mac OS, where we're facing this issue.
+#if BUILDFLAG(IS_MAC)
+  if (!hb_has_glyph) {
+    SkTypeface* typeface = hb_font_data->font_.getTypeface();
+    if (!typeface) {
+      return false;
+    }
+    if (unicode == kHyphenCharacter || unicode == kNonBreakingHyphen) {
+      SkGlyphID sk_glyph_id = typeface->unicharToGlyph(unicode);
+      *glyph = sk_glyph_id;
+      return sk_glyph_id;
+    }
+  }
+#endif
+  return hb_has_glyph;
 }
 
 static hb_bool_t HarfBuzzGetNominalGlyph(hb_font_t* hb_font,
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc
index 5a4f4b8..4676cef7 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats.cc
@@ -24,12 +24,12 @@
 
 namespace blink {
 
-// TODO(https://crbug.com/webrtc/14554): When there exists a flag in WebRTC to
-// not collect deprecated stats in the first place, make use of that flag and
-// unship the filtering mechanism controlled by `WebRtcUnshipDeprecatedStats`.
+// TODO(https://crbug.com/webrtc/14175): When "track" stats no longer exist in
+// the lower layer, delete all the filtering mechanisms gated by this flag since
+// that filtering will become a NO-OP when "track" no longer exists.
 BASE_FEATURE(WebRtcUnshipDeprecatedStats,
              "WebRtcUnshipDeprecatedStats",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 namespace {
 
diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_stats_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_stats_test.cc
index 93732bc..5524d3c6 100644
--- a/third_party/blink/renderer/platform/peerconnection/rtc_stats_test.cc
+++ b/third_party/blink/renderer/platform/peerconnection/rtc_stats_test.cc
@@ -169,52 +169,8 @@
   ASSERT_EQ(4u, all_members_copy->GetStats("id")->MembersCount());
 }
 
-TEST(RTCStatsTest, IncludeDeprecatedByDefault) {
-  rtc::scoped_refptr<webrtc::RTCStatsReport> webrtc_report =
-      webrtc::RTCStatsReport::Create(webrtc::Timestamp::Micros(1234));
-  {
-    auto stats_with_deprecated_foo_id =
-        std::make_unique<TestStats>("NotDeprecated_a", 1234);
-    stats_with_deprecated_foo_id->foo_id = "DEPRECATED_b";
-    webrtc_report->AddStats(std::move(stats_with_deprecated_foo_id));
-  }
-  webrtc_report->AddStats(std::make_unique<TestStats>("DEPRECATED_b", 1234));
-  {
-    auto stats_with_non_deprecated_foo_id =
-        std::make_unique<TestStats>("NotDeprecated_c", 1234);
-    stats_with_non_deprecated_foo_id->foo_id = "NotDeprecated_a";
-    webrtc_report->AddStats(std::move(stats_with_non_deprecated_foo_id));
-  }
-
-  RTCStatsReportPlatform report(webrtc_report.get(), {});
-  EXPECT_TRUE(report.GetStats("DEPRECATED_b"));
-  EXPECT_EQ(report.Size(), 3u);
-  EXPECT_TRUE(report.Next());
-  EXPECT_TRUE(report.Next());
-  EXPECT_TRUE(report.Next());
-  EXPECT_FALSE(report.Next());
-
-  auto stats_with_deprecated_foo_id = report.GetStats("NotDeprecated_a");
-  ASSERT_TRUE(stats_with_deprecated_foo_id);
-  // fooId is included despite referencing something deprecated.
-  EXPECT_EQ(stats_with_deprecated_foo_id->MembersCount(), 3u);
-  EXPECT_EQ(stats_with_deprecated_foo_id->GetMember(0)->GetName(),
-            "standardized");
-  EXPECT_EQ(stats_with_deprecated_foo_id->GetMember(1)->GetName(), "fooId");
-
-  auto stats_with_non_deprecated_foo_id = report.GetStats("NotDeprecated_c");
-  ASSERT_TRUE(stats_with_deprecated_foo_id);
-  // fooId is included, it's not referencing anything deprecated.
-  EXPECT_EQ(stats_with_non_deprecated_foo_id->MembersCount(), 3u);
-  EXPECT_EQ(stats_with_non_deprecated_foo_id->GetMember(0)->GetName(),
-            "standardized");
-  EXPECT_EQ(stats_with_non_deprecated_foo_id->GetMember(1)->GetName(), "fooId");
-}
-
-TEST(RTCStatsTest, ExcludeDeprecatedWithFlag) {
-  base::test::ScopedFeatureList feature_list;
-  feature_list.InitAndEnableFeature(blink::WebRtcUnshipDeprecatedStats);
-
+// WebRtcUnshipDeprecatedStats is enabled-by-default.
+TEST(RTCStatsTest, ExcludeDeprecated) {
   rtc::scoped_refptr<webrtc::RTCStatsReport> webrtc_report =
       webrtc::RTCStatsReport::Create(webrtc::Timestamp::Micros(1234));
   {
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index b6034c8..34a899e 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -614,6 +614,10 @@
       status: "experimental",
     },
     {
+      name: "CSSAnimationComposition",
+      status: "experimental"
+    },
+    {
       // Whether <image> values are allowed as counter style <symbol>
       name: "CSSAtRuleCounterStyleImageSymbols",
     },
diff --git a/third_party/blink/renderer/platform/wtf/text/character_names.h b/third_party/blink/renderer/platform/wtf/text/character_names.h
index 66b82dd..bb7827e 100644
--- a/third_party/blink/renderer/platform/wtf/text/character_names.h
+++ b/third_party/blink/renderer/platform/wtf/text/character_names.h
@@ -144,6 +144,7 @@
 const UChar32 kMathItalicUpperAlpha = 0x1D6E2;
 const UChar kMinusSignCharacter = 0x2212;
 const UChar kNewlineCharacter = 0x000A;
+const UChar kNonBreakingHyphen = 0x2011;
 const UChar32 kNonCharacter = 0xFFFF;
 const UChar kFormFeedCharacter = 0x000C;
 const UChar32 kNabla = 0x2207;
@@ -308,6 +309,7 @@
 using WTF::unicode::kNewlineCharacter;
 using WTF::unicode::kNoBreakSpaceCharacter;
 using WTF::unicode::kNominalDigitShapesCharacter;
+using WTF::unicode::kNonBreakingHyphen;
 using WTF::unicode::kNonCharacter;
 using WTF::unicode::kObjectReplacementCharacter;
 using WTF::unicode::kOverlineCharacter;
diff --git a/third_party/blink/renderer/platform/wtf/text/wtf_string.h b/third_party/blink/renderer/platform/wtf/text/wtf_string.h
index ebd232e..7ef4b9a 100644
--- a/third_party/blink/renderer/platform/wtf/text/wtf_string.h
+++ b/third_party/blink/renderer/platform/wtf/text/wtf_string.h
@@ -685,7 +685,9 @@
 // Table representing common HTML strings of type '\n<space>*'.
 class WTF_EXPORT NewlineThenWhitespaceStringsTable {
  public:
-  static constexpr size_t kTableSize = 128;
+  // The constant is kept small to minimize the overhead of the table (496
+  // bytes).
+  static constexpr size_t kTableSize = 32;
 
   static void Init();
 
diff --git a/third_party/blink/web_tests/FlagExpectations/highdpi b/third_party/blink/web_tests/FlagExpectations/highdpi
index 3797111..97477e8e 100644
--- a/third_party/blink/web_tests/FlagExpectations/highdpi
+++ b/third_party/blink/web_tests/FlagExpectations/highdpi
@@ -453,8 +453,6 @@
 
 crbug.com/1310040 virtual/gpu-rasterization-disable-yuv/images/yuv-decode-eligible/color-profile-layer.html [ Failure Pass ]
 
-crbug.com/1318592 external/wpt/resource-timing/object-not-found-after-TAO-cross-origin-redirect.html [ Failure Pass ]
-
 crbug.com/1324618 external/wpt/css/css-images/object-view-box-fit-fill* [ Pass ]
 
 # crbug.com/1339051: some ref tests generate output with minor differences.
diff --git a/third_party/blink/web_tests/NeverFixTests b/third_party/blink/web_tests/NeverFixTests
index 849b89d..9e3bc13 100644
--- a/third_party/blink/web_tests/NeverFixTests
+++ b/third_party/blink/web_tests/NeverFixTests
@@ -1747,3 +1747,16 @@
 # which often fails, so skip it on this configuration.
 # Use the same reason as the Mac suppressions to avoid an incorrect presubmit check.
 crbug.com/1378476 [ Win ] virtual/webgl-oversized-printing/printing/webgl-oversized-printing.html [ Skip ]
+
+# This test is Mac specific, since it checks cases when hyphen character is
+# being synthesized in Arial font on recent Mac vesrions and therefore should
+# be skipped on other platforms.
+crbug.com/1267606 [ Linux ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Win ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Fuchsia ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Android ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Mac10.13 ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Mac10.14 ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Mac10.15 ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Mac11 ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
+crbug.com/1267606 [ Mac11-arm64 ] wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html [ Skip ]
diff --git a/third_party/blink/web_tests/SlowTests b/third_party/blink/web_tests/SlowTests
index a3f193e2..5ed4a17 100644
--- a/third_party/blink/web_tests/SlowTests
+++ b/third_party/blink/web_tests/SlowTests
@@ -34,7 +34,12 @@
 crbug.com/24182 fast/canvas/canvas-toBlob-toDataURL-race-imageEncoder-webp.html [ Slow ]
 crbug.com/24182 fast/dom/SelectorAPI/resig-SelectorsAPI-test.xhtml [ Slow ]
 crbug.com/24182 fast/frames/cached-frame-counter.html [ Slow ]
-crbug.com/24182 fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Linux ] fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Mac10.15 Release ] fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Mac11 Release ] fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Mac11-arm64 Release ] fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Mac12 ] fast/frames/frame-limit.html [ Slow ]
+crbug.com/24182 [ Release Win ] fast/frames/frame-limit.html [ Slow ]
 crbug.com/24182 fast/overflow/lots-of-sibling-inline-boxes.html [ Slow ] # Particularly slow in Debug: >12x slower!
 crbug.com/24182 http/tests/accessibility/slow-document-load.html [ Slow ]
 crbug.com/24182 http/tests/cache/subresource-expiration-1.html [ Slow ]
@@ -1255,7 +1260,6 @@
 crbug.com/1046784 [ Mac10.14 Release ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
 crbug.com/1046784 [ Mac10.15 Release ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
 crbug.com/1046784 [ Mac11 Release ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
-crbug.com/1046784 [ Mac11-arm64 Release ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
 crbug.com/1046784 [ Mac12 ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
 crbug.com/1046784 [ Release Win ] http/tests/devtools/a11y-axe-core/sources/scope-pane-a11y-test.js [ Slow ]
 crbug.com/1046784 http/tests/inspector-protocol/network/navigate-iframe-out2in.js [ Slow ]
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 30010e2..ea2ba282 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -3033,18 +3033,13 @@
 crbug.com/626703 [ Mac12-arm64 ] wpt_internal/webxr/ar/iframe-oopif.sub.https.html [ Failure Timeout ]
 crbug.com/626703 [ Mac12 ] external/wpt/html/cross-origin-opener-policy/resource-popup.https.html [ Timeout ]
 crbug.com/626703 external/wpt/webcodecs/videoFrame-serialization.crossAgentCluster.html [ Timeout ]
-crbug.com/626703 [ Linux ] external/wpt/resource-timing/response-status-code.html [ Skip Timeout ]
-crbug.com/626703 [ Win ] external/wpt/resource-timing/response-status-code.html [ Skip Timeout ]
 crbug.com/626703 [ Debug Mac12 ] external/wpt/url/a-element.html?exclude=(file|javascript|mailto) [ Crash Failure ]
-crbug.com/626703 virtual/plz-dedicated-worker/external/wpt/resource-timing/response-status-code.html [ Skip Timeout ]
-crbug.com/626703 [ Mac11 ] external/wpt/resource-timing/response-status-code.html [ Skip Timeout ]
 crbug.com/1404285 [ Mac ] virtual/compute-pressure/external/wpt/compute-pressure/compute_pressure_disconnect_immediately.tentative.https.window.html [ Crash Failure ]
 crbug.com/626703 [ Win11 ] wpt_internal/geolocation-api/watchPosition-page-visibility.https.html [ Timeout ]
 crbug.com/626703 [ Win10.20h2 ] external/wpt/web-animations/idlharness.window.html [ Crash Failure ]
 crbug.com/626703 [ Mac11 ] external/wpt/performance-timeline/tentative/include-frames-from-child-cross-origin-grandchild.sub.html [ Timeout ]
 crbug.com/626703 [ Mac ] external/wpt/url/a-element-xhtml.xhtml?exclude=(file|javascript|mailto) [ Crash Failure ]
 crbug.com/626703 [ Mac12-arm64 ] virtual/pending-beacon/external/wpt/pending-beacon/pending_post_beacon-cors.tentative.https.window.html [ Timeout ]
-crbug.com/626703 [ Debug Mac12 ] external/wpt/url/a-element-origin.html [ Crash Failure ]
 crbug.com/626703 [ Win10.20h2 ] wpt_internal/geolocation-api/watchPosition-page-visibility.https.html [ Timeout ]
 crbug.com/626703 [ Mac11-arm64 ] virtual/pending-beacon/external/wpt/pending-beacon/pending_beacon-sendonhidden.tentative.https.window.html [ Timeout ]
 crbug.com/626703 [ Win11 ] external/wpt/credential-management/fedcm-iframe.https.html [ Timeout ]
@@ -3069,7 +3064,6 @@
 crbug.com/626703 [ Linux ] external/wpt/webxr/hit-test/ar_hittest_source_cancel.https.html [ Timeout ]
 crbug.com/626703 [ Linux ] external/wpt/webxr/hit-test/ar_hittest_subscription_inputSources.https.html [ Timeout ]
 crbug.com/626703 [ Linux ] external/wpt/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html [ Timeout ]
-crbug.com/626703 [ Linux ] external/wpt/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html [ Timeout ]
 crbug.com/626703 [ Linux ] external/wpt/webxr/light-estimation/xrWebGLBinding_getReflectionCubeMap.https.html [ Timeout ]
 crbug.com/626703 [ Linux ] external/wpt/webxr/render_state_update.https.html [ Timeout ]
 crbug.com/626703 [ Linux ] external/wpt/webxr/webxr_feature_policy.https.html [ Timeout ]
@@ -3369,7 +3363,8 @@
 crbug.com/626703 external/wpt/css/CSS2/text/white-space-collapsing-bidi-001.xht [ Failure ]
 crbug.com/626703 external/wpt/css/CSS2/text/white-space-mixed-001.xht [ Failure ]
 crbug.com/626703 external/wpt/media-source/mediasource-avtracks.html [ Crash Failure ]
-crbug.com/626703 external/wpt/media-source/mediasource-getvideoplaybackquality.html [ Failure Timeout ]
+crbug.com/626703 [ Mac12-arm64 Release ] external/wpt/media-source/mediasource-getvideoplaybackquality.html [ Failure Timeout ]
+crbug.com/626703 [ Release Win ] external/wpt/media-source/mediasource-getvideoplaybackquality.html [ Failure Timeout ]
 crbug.com/958104 external/wpt/presentation-api/controlling-ua/getAvailability.https.html [ Failure ]
 crbug.com/626703 external/wpt/screen-orientation/onchange-event-subframe.html [ Timeout ]
 
@@ -6080,7 +6075,6 @@
 
 # Sheriff 2022-06-24
 crbug.com/1339211 [ Mac ] external/wpt/css/css-text-decor/text-decoration-thickness-fixed.html [ Failure Pass ]
-crbug.com/1339291 [ Debug Mac12 ] external/wpt/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html [ Failure Pass ]
 crbug.com/1339291 [ Mac12-arm64 Release ] external/wpt/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html [ Failure Pass ]
 crbug.com/1339293 [ Linux ] http/tests/devtools/network/network-initiator.js [ Failure Pass ]
 crbug.com/1339293 [ Linux ] external/wpt/html/semantics/embedded-content/the-img-element/invisible-image.html [ Failure Pass ]
@@ -6154,9 +6148,6 @@
 # Offscreen GPU canvas test flaky
 crbug.com/1286883 [ Linux ] virtual/gpu/fast/canvas/OffscreenCanvas-Bitmaprenderer-toBlob.html [ Failure Pass ]
 
-# Temporarily disabled to unblock https://crrev.com/c/4183652
-crbug.com/1408033 http/tests/devtools/persistence/automapping-sourcemap-nameclash.js [ Pass Timeout ]
-
 # Sheriff 2022-08-01
 # TODO(crbug.com/1225773): Re-enable this test
 crbug.com/1225773 [ Linux ] external/wpt/html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-event-during-parse.html [ Crash Failure Pass Timeout ]
@@ -6926,7 +6917,6 @@
 # fast/dom/Element/scrollTop-scrollLeft-body.html previously also linked with crbug.com/1249176
 crbug.com/1361956 [ Mac12 ] fast/dom/Element/scrollTop-scrollLeft-body.html [ Failure Pass Timeout ]
 crbug.com/1354433 [ Win ] external/wpt/html/browsers/the-window-object/window-properties.https.html [ Failure Pass ]
-crbug.com/1404252 [ Mac11-arm64 Release ] external/wpt/resource-timing/response-status-code.html [ Failure Pass ]
 
 # Sheriff 2023-01-11
 crbug.com/1406380 [ Win ] external/wpt/infrastructure/server/webtransport-h3.https.sub.any.worker.html [ Failure Pass ]
diff --git a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
index a1fc65d..d4708f5 100644
--- a/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
+++ b/third_party/blink/web_tests/external/WPT_BASE_MANIFEST_8.json
@@ -262852,11 +262852,11 @@
   "support": {
    ".cache": {
     "gitignore2.json": [
-     "537fb716112e759c5a0c147325bed01b04887f33",
+     "daa62d6d0d934d1e59e5ca8eb5dcb709cef13182",
      []
     ],
     "mtime.json": [
-     "dbda03f9b2b266ad7fb08b77efdf23414ff958f4",
+     "200ec7bb8d676a173280f8f612a32a48a4a175b9",
      []
     ]
    },
@@ -269586,7 +269586,7 @@
      []
     ],
     "cookieListItem_attributes.https.any.js.ini": [
-     "80c98b3b637957051cd949030a9a6a8a78d67448",
+     "80c87e5b5ecbec52004e97a6506176ecf4273aa1",
      []
     ],
     "cookieStore_get_set_across_origins.sub.https.html.ini": [
@@ -290949,7 +290949,7 @@
       []
      ],
      "cjk-kerning.html.ini": [
-      "cf10f40714d0b64c83f8037bd93071a2b6b95c94",
+      "5f57054ae95563dfd924ace4a18776bd51a65c77",
       []
      ],
      "crash-large-grapheme-cluster.html.ini": [
@@ -305032,7 +305032,7 @@
        []
       ],
       "clip-path-inline-001.html.ini": [
-       "c87eb82357b0de46119bbf273892ce08cf2eac89",
+       "4173d27b5922f3746b41937f04fb8f2fb3f0cd3e",
        []
       ],
       "clip-path-inline-002.html.ini": [
@@ -318918,7 +318918,7 @@
        []
       ],
       "text-transform-capitalize-007.html.ini": [
-       "4dd4f078df482979aef56a6f67251ad514fe3ad0",
+       "a29c6839a53a872c397c39993b88ef3cc8ef182a",
        []
       ],
       "text-transform-capitalize-010.html.ini": [
@@ -320992,6 +320992,10 @@
       "e96898a81ef34410b775d73819deb78eb1665c4f",
       []
      ],
+     "text-decoration-line-spelling-error-color-dynamic-001.optional.html.ini": [
+      "3559642adae08eebbb3583e63dcd6eba59542a1b",
+      []
+     ],
      "text-decoration-serialization.tentative-expected.txt": [
       "0d83f58028ff740b492af6663ff5b135a740b9a1",
       []
@@ -324947,6 +324951,10 @@
        "c3219cfe7561a6d0b5c0c0419b140feaa575c4b1",
        []
       ],
+      "kind-of-widget-fallback-input-reset-background-size-001.html.ini": [
+       "aa68b0a17b6e0931e119e8e7256eacdca0541021",
+       []
+      ],
       "kind-of-widget-fallback-input-reset-border-end-end-radius-001.html.ini": [
        "cfa46fa9706127e00580cf1e1c5f022bc8563538",
        []
@@ -324991,6 +324999,10 @@
        "0a103751c35f49aefdaf0284d14ae70ecf9c3907",
        []
       ],
+      "kind-of-widget-fallback-input-submit-border-image-repeat-001.html.ini": [
+       "29920ec86d393af34a0e6971dbb576aced6a859e",
+       []
+      ],
       "kind-of-widget-fallback-input-submit-border-inline-end-width-001.html.ini": [
        "2a170629651d9defa1951ceceeab784ca50ea285",
        []
@@ -326019,7 +326031,7 @@
       []
      ],
      "outline-025.html.ini": [
-      "35e544d8dabb400df879580cc527dbd154163577",
+      "c5515ab0e129b5c55bd8b4e73f4750c363cae17d",
       []
      ],
      "outline-026.html.ini": [
@@ -349703,7 +349715,7 @@
         []
        ],
        "open-features-non-integer-screeny.html.ini": [
-        "2fbad0dd12c4027691d6194f54a3c83c1bc4651a",
+        "cf4382ffe16f331a58ce9bee4b6cb6804b6c15fd",
         []
        ],
        "open-features-non-integer-top-expected.txt": [
@@ -349756,7 +349768,7 @@
          []
         ],
         "message-opener.html": [
-         "07662c63cd01337e3b6a53ce6001d187b719fbe7",
+         "5c58a93a640dbcb468002a47fc2ad26c90ecd1ae",
          []
         ],
         "tokenization-noopener-noreferrer.js": [
@@ -374931,7 +374943,7 @@
       []
      ],
      "performance-navigation-timing-same-origin-bfcache.tentative.window.js.ini": [
-      "2faeab35f7932b2191cc0d963c0bc95d8cdfd868",
+      "a7414b2e31aaebcdf7373b5217c283682827ab56",
       []
      ],
      "performance-navigation-timing-same-origin-replace.tentative.window.js.ini": [
@@ -375200,7 +375212,7 @@
      []
     ],
     "payment-extension-allowed-by-permissions-policy-attribute.https.sub.html.ini": [
-     "e9534c3dcb5c3418fbfe28a7cebb5802952308a9",
+     "3fabfc4dd20fee4aeb546ab7fa71b35b278ea395",
      []
     ],
     "payment-supported-by-permissions-policy.tentative.html.ini": [
@@ -375603,7 +375615,7 @@
       []
      ],
      "permissions-policy.js": [
-      "00c0bf23265ad20b21952a4a0d71361e7fce64ed",
+      "62f8dcdf9155aebbf1d75ab999f9241d22281093",
       []
      ],
      "picture-in-picture.js": [
@@ -379605,10 +379617,6 @@
      "ee9b192f0b474f31c9bb1b5a64b4b14b01cd2f91",
      []
     ],
-    "getdisplaymedia.https-expected.txt": [
-     "ba1d56a7b63883a3ea4bd62afd17f1c90bdf7e0d",
-     []
-    ],
     "getdisplaymedia.https.html.ini": [
      "1a220b5c2052f4dc086b147923f38c6226354bcc",
      []
@@ -384680,7 +384688,7 @@
        []
       ],
       "screen-capture.https.html": [
-       "55f1995643dfb0505e79bad4ed3eb9c2d1bad28e",
+       "1304b9d74b0b9bb0dbf2af56f60523c570aebe1f",
        []
       ],
       "screen-orientation-lock.https.html": [
@@ -384921,7 +384929,7 @@
       []
      ],
      "restriction-screen-capture.https.html.ini": [
-      "eae5030bd126293522bc44be0c8a546ec4d529db",
+      "9112e68ccfb32b50cae13880e6f735ff6de80091",
       []
      ],
      "restriction-screen-orientation-lock.https.html.ini": [
@@ -388898,7 +388906,7 @@
      []
     ],
     "a-element-xhtml.xhtml.ini": [
-     "b02fb24f6442a4afefa2d283e9810a43da5454b4",
+     "0e8ba0f6c7c337099c4ef9648d3231d7033e704e",
      []
     ],
     "a-element-xhtml_include=javascript-expected.txt": [
@@ -388910,7 +388918,7 @@
      []
     ],
     "a-element.html.ini": [
-     "291c587673ba45df92f7d949c42ab39edfee2c0f",
+     "4ad709f28d93b90cc50be0bf7e5236dcea55119d",
      []
     ],
     "a-element_include=javascript-expected.txt": [
@@ -388963,7 +388971,7 @@
       []
      ],
      "urltestdata.json": [
-      "fe7316fd2e8fb0d28ef34c07b4b3f204c673f613",
+      "0265346a6a62d437cb9e94e55bd93369d92544fa",
       []
      ]
     },
@@ -388976,7 +388984,7 @@
      []
     ],
     "url-constructor.any.js.ini": [
-     "1cbbb92cc79d33580bd8fd6b0a269c1e2db7af4d",
+     "4a51ce1c1ffab8e03de9a0d7f54f77e31a8538f6",
      []
     ],
     "url-constructor.any.worker_include=javascript-expected.txt": [
@@ -398188,7 +398196,7 @@
      []
     ],
     "events_referenceSpace_reset_inline.https.html.ini": [
-     "00c1d56bf0b835c4ab75baee491703b102791134",
+     "642843ad2ceb54ce485e69d0d5c226a8a9240c0f",
      []
     ],
     "events_session_select.https.html.ini": [
@@ -512152,7 +512160,7 @@
         ]
        ],
        "open-features-negative-innerwidth-innerheight.html": [
-        "7f55f1bb1d809a207560a55ed8587329fce10e1e",
+        "019ee9d7309f787220a8c61ebfda2221f33d8afa",
         [
          null,
          {
@@ -512161,7 +512169,7 @@
         ]
        ],
        "open-features-negative-screenx-screeny.html": [
-        "09fb2d6b0a5abdcf1889e20ef67dc431e2565dbf",
+        "6e316db48205ead4b3a5794358aae138775ab25b",
         [
          null,
          {
@@ -512170,7 +512178,7 @@
         ]
        ],
        "open-features-negative-top-left.html": [
-        "15b3103db3cc2e4d81616c7ce49489bc5430844b",
+        "316b01a40192369127b27c5db6bea62943eed631",
         [
          null,
          {
@@ -512179,7 +512187,7 @@
         ]
        ],
        "open-features-negative-width-height.html": [
-        "30b70926f2c7b38336c00e367d3dcd9d667decec",
+        "3cb155620dff754300061d6b5ce0f6a7122b47fa",
         [
          null,
          {
@@ -512188,7 +512196,7 @@
         ]
        ],
        "open-features-non-integer-height.html": [
-        "cd2d019c47d556f3462f25110ba018591a25ce3e",
+        "d8ee866c503512550b692f943779f19e0b8eb50a",
         [
          null,
          {
@@ -512197,7 +512205,7 @@
         ]
        ],
        "open-features-non-integer-innerheight.html": [
-        "5ee752caedc836dd0cb9ab7dd3b27894878ff141",
+        "f191d875d8630b0ff09c770e055a67b768f5deff",
         [
          null,
          {
@@ -512206,7 +512214,7 @@
         ]
        ],
        "open-features-non-integer-innerwidth.html": [
-        "972beef48abbdef58329176ed4d0e9b262a8283d",
+        "d1ddc5e43ab408ee58cba1988155ce0836d1f511",
         [
          null,
          {
@@ -512215,7 +512223,7 @@
         ]
        ],
        "open-features-non-integer-left.html": [
-        "fbb2a30ee22687feced74eae2c438ee30cc4b2da",
+        "c771204dc40c590430fd144100fe6f9e79fdd46a",
         [
          null,
          {
@@ -512224,7 +512232,7 @@
         ]
        ],
        "open-features-non-integer-screenx.html": [
-        "2aeab9bb796eac8d87c77b10414bf62ffcf17edc",
+        "49a37832579982746a26024d2b06fa0f37d3562b",
         [
          null,
          {
@@ -512233,7 +512241,7 @@
         ]
        ],
        "open-features-non-integer-screeny.html": [
-        "cb4e873f26d022fd18a551b11ac755884d171f17",
+        "5183405445150c3e8988086302774ba0dfdc5e33",
         [
          null,
          {
@@ -512242,7 +512250,7 @@
         ]
        ],
        "open-features-non-integer-top.html": [
-        "d4f4e90d96ed638a059d13827c56d21314d990a5",
+        "a7926e748b25fedd57cb5e903ff03b49738729b0",
         [
          null,
          {
@@ -512251,7 +512259,7 @@
         ]
        ],
        "open-features-non-integer-width.html": [
-        "a746abed3f36abe800c6bcd612cbecc07383abdb",
+        "6878063ed765ac4f5fd3d6e5e0cdc0659ea635df",
         [
          null,
          {
@@ -512260,7 +512268,7 @@
         ]
        ],
        "open-features-tokenization-innerheight-innerwidth.html": [
-        "c839c6ca0661efce0ca531b6f35b470be4478d27",
+        "cf3ad25514cb3e03801429b405c91539590abefb",
         [
          null,
          {
@@ -512287,7 +512295,7 @@
         ]
        ],
        "open-features-tokenization-screenx-screeny.html": [
-        "bb6fc4b84bfb6157db6a406251b9bed28d4277f4",
+        "5a53fefc40d986a14488445d2d40f3b0046be0e1",
         [
          null,
          {
@@ -512296,7 +512304,7 @@
         ]
        ],
        "open-features-tokenization-top-left.html": [
-        "ef098a1062c6bc0b5b27a91b42af9aeda41ad8bb",
+        "842cbcf820dca0c6c60bced55cb81f91c2fece9e",
         [
          null,
          {
@@ -512305,7 +512313,7 @@
         ]
        ],
        "open-features-tokenization-width-height.html": [
-        "cac4ae639b2f0e0f0f846497680aede2022538f5",
+        "ff61199179560073590bede06ec52c48950f580c",
         [
          null,
          {
@@ -565774,17 +565782,21 @@
      ]
     ],
     "MediaStreamTrack-iframe-audio-transfer.https.html": [
-     "e1df23a24a135abe8983d5a14c6497d071abfc98",
+     "963f42c4cbbb89a24bd30112daf75dc54613b3cc",
      [
       null,
-      {}
+      {
+       "testdriver": true
+      }
      ]
     ],
     "MediaStreamTrack-iframe-transfer.https.html": [
-     "54fc2b2d164eef7a322891b21b2e483eb55da07a",
+     "745f314e6fbdda505e3d276cca922d281ff1df7c",
      [
       null,
-      {}
+      {
+       "testdriver": true
+      }
      ]
     ],
     "MediaStreamTrack-init.https.html": [
@@ -573710,7 +573722,7 @@
      ]
     ],
     "payment-extension-allowed-by-permissions-policy-attribute.https.sub.html": [
-     "de55531b6c9efe8e7faf84d2de559f2725175e2e",
+     "ef36bf97f10496ca8dac24d5172957ecfb9883a6",
      [
       null,
       {}
@@ -601533,7 +601545,7 @@
       ]
      ],
      "restriction-screen-capture.https.html": [
-      "e4b958d338a9a88eddf26a369993142cec1e2508",
+      "2cd7fb662f2c7e85ee4d0b7ef3c74f93ecc537c4",
       [
        null,
        {
diff --git a/third_party/blink/web_tests/external/wpt/cookie-store/cookieListItem_attributes.https.any.js.ini b/third_party/blink/web_tests/external/wpt/cookie-store/cookieListItem_attributes.https.any.js.ini
index 80c98b3..80c87e5 100644
--- a/third_party/blink/web_tests/external/wpt/cookie-store/cookieListItem_attributes.https.any.js.ini
+++ b/third_party/blink/web_tests/external/wpt/cookie-store/cookieListItem_attributes.https.any.js.ini
@@ -4,3 +4,11 @@
 
   [CookieListItem - cookieStore.set with expires set to a Date 10 years in the future]
     expected: [FAIL, PASS]
+
+
+[cookieListItem_attributes.https.any.html]
+  [CookieListItem - cookieStore.set with expires set to a timestamp 10 years in the future]
+    expected: [PASS, FAIL]
+
+  [CookieListItem - cookieStore.set with expires set to a Date 10 years in the future]
+    expected: [FAIL, PASS]
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/CSSAnimation-effect.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-animations/CSSAnimation-effect.tentative-expected.txt
deleted file mode 100644
index aa98c77..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-animations/CSSAnimation-effect.tentative-expected.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-This is a testharness.js-based test.
-PASS Setting a null effect on a running animation fires an animationend event
-PASS Replacing an animation's effect with an effect that targets a different property should update both properties
-PASS Replacing an animation's effect with a shorter one that should have already finished, the animation finishes immediately
-PASS A play-pending animation's effect whose effect is replaced still exits the pending state
-PASS CSS animation events are dispatched at the original element even after setting an effect with a different target element
-PASS After replacing a finished animation's effect with a longer one it fires an animationstart event
-FAIL Setting animation-composition sets the composite property on the effect assert_equals: expected "add" but got "replace"
-PASS Replacing the effect of a CSSAnimation causes subsequent changes to corresponding animation-* properties to be ignored
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative-expected.txt
index 51b5c21..e7d3535d 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative-expected.txt
@@ -5,8 +5,8 @@
 PASS KeyframeEffect.getKeyframes() returns frames with expected easing values, when the easing is specified on each keyframe
 PASS KeyframeEffect.getKeyframes() returns frames with expected easing values, when the easing is specified on some keyframes
 PASS KeyframeEffect.getKeyframes() returns frames with expected composite values, when the composite is set on the effect using animation-composition on the element
-FAIL KeyframeEffect.getKeyframes() returns frames with expected composite values, when the composite is specified on each keyframe assert_equals: value of 'composite' on ComputedKeyframe #0 expected "replace" but got "auto"
-FAIL KeyframeEffect.getKeyframes() returns frames with expected composite values, when the composite is specified on some keyframes assert_equals: value of 'composite' on ComputedKeyframe #0 expected "add" but got "auto"
+PASS KeyframeEffect.getKeyframes() returns frames with expected composite values, when the composite is specified on each keyframe
+PASS KeyframeEffect.getKeyframes() returns frames with expected composite values, when the composite is specified on some keyframes
 PASS KeyframeEffect.getKeyframes() returns expected frames for a simple animation that specifies a single shorthand property
 PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with a 0% keyframe and no 100% keyframe
 PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with a 100% keyframe and no 0% keyframe
@@ -17,8 +17,8 @@
 PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time, and all with the same easing function
 PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different easing functions
 PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different but equivalent easing functions
-FAIL KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different composite operations assert_equals: Number of keyframes should match expected 3 but got 2
-FAIL KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different easing functions and composite operations assert_equals: Number of keyframes should match expected 4 but got 3
+PASS KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different composite operations
+FAIL KeyframeEffect.getKeyframes() returns expected frames for an animation with multiple keyframes for the same time and with different easing functions and composite operations assert_array_equals: properties on Keyframe #1 should match lengths differ, expected array ["composite", "computedOffset", "easing", "marginTop", "offset", "paddingLeft"] length 6, got ["composite", "computedOffset", "easing", "fontSize", "offset"] length 5
 PASS KeyframeEffect.getKeyframes() returns expected frames for overlapping keyframes
 FAIL KeyframeEffect.getKeyframes() returns expected values for animations with filter properties and missing keyframes assert_equals: value for 'filter' on Keyframe #1 should match expected "blur(5px) sepia(60%) saturate(30%)" but got "blur(5px) sepia(0.6) saturate(0.3)"
 PASS KeyframeEffect.getKeyframes() returns expected values for animation with drop-shadow of filter property
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative.html b/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
index a716745..4547cae 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
@@ -394,10 +394,12 @@
 
   const frames = getKeyframes(div);
 
+  // Final keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
     { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "replace",
       color: "rgb(255, 255, 255)" },
   ];
   assert_frame_lists_equal(frames, expected);
@@ -411,8 +413,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       color: "rgb(255, 255, 255)" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
@@ -428,12 +432,14 @@
 
   const frames = getKeyframes(div);
 
+  // Initial and final keyframes should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0,   computedOffset: 0,   easing: "ease", composite: "auto",
+    { offset: 0,   computedOffset: 0,   easing: "ease", composite: "replace",
       color: "rgb(255, 255, 255)" },
     { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 1,   computedOffset: 1,   easing: "ease", composite: "auto",
+    { offset: 1,   computedOffset: 1,   easing: "ease", composite: "replace",
       color: "rgb(255, 255, 255)" },
   ];
   assert_frame_lists_equal(frames, expected);
@@ -631,8 +637,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       filter: "none" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       filter: "blur(5px) sepia(60%) saturate(30%)" },
@@ -670,8 +678,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       textShadow: "rgb(0, 0, 0) 1px 1px 2px,"
                   + " rgb(0, 0, 255) 0px 0px 16px,"
                   + " rgb(0, 0, 255) 0px 0px 3.2px" },
@@ -694,8 +704,10 @@
 
   assert_equals(frames.length, 2, "number of frames");
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       backgroundSize: "auto" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       backgroundSize: "50% auto, 6px auto, contain" },
@@ -724,8 +736,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       transform: "none" },
     { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       transform: "translate(100px)" },
@@ -740,8 +754,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "replace",
       marginBottom: "0px",
       marginLeft: "0px",
       marginRight: "0px",
@@ -762,8 +778,10 @@
 
   const frames = getKeyframes(div);
 
+  // Initial keyframe should be replace as per sections 7 and 8 of
+  // https://drafts.csswg.org/css-animations-2/#keyframes
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(2, start)", composite: "auto",
+    { offset: 0, computedOffset: 0, easing: "steps(2, start)", composite: "replace",
       color: "rgb(0, 0, 0)" },
     { offset: 1, computedOffset: 1, easing: "steps(2, start)", composite: "auto",
       color: "rgb(0, 255, 0)" },
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative.html.ini b/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative.html.ini
deleted file mode 100644
index 05eb67b..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[animation-composition-computed.tentative.html]
-  [Property animation-composition value 'replace, add, accumulate']
-    expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative.html.ini b/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative.html.ini
deleted file mode 100644
index 749e6ba..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative.html.ini
+++ /dev/null
@@ -1,12 +0,0 @@
-[animation-composition-valid.tentative.html]
-  [e.style['animation-composition'\] = "replace" should set the property value]
-    expected: FAIL
-
-  [e.style['animation-composition'\] = "add" should set the property value]
-    expected: FAIL
-
-  [e.style['animation-composition'\] = "accumulate" should set the property value]
-    expected: FAIL
-
-  [e.style['animation-composition'\] = "replace, add, accumulate" should set the property value]
-    expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-break/block-in-inline-004.html b/third_party/blink/web_tests/external/wpt/css/css-break/block-in-inline-004.html
new file mode 100644
index 0000000..338f4ad
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-break/block-in-inline-004.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1290809">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div style="columns:2; column-fill:auto; gap:0; width:100px; height:100px; orphans:1; widows:1; background:red;">
+  <div style="height:50px; background:green;"></div>
+  <span>
+    <div style="display:inline-block; vertical-align:top; width:100%; height:50px; background:green;"></div>
+    <div style="height:50px; background:green;"></div>
+  </span>
+  <div style="height:50px; background:green;"></div>
+</div>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/cjk-kerning.html.ini b/third_party/blink/web_tests/external/wpt/css/css-fonts/cjk-kerning.html.ini
index cf10f407..5f57054 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-fonts/cjk-kerning.html.ini
+++ b/third_party/blink/web_tests/external/wpt/css/css-fonts/cjk-kerning.html.ini
@@ -6,11 +6,11 @@
 
   [expected match: .default .cjk vs .kernOFF .cjk]
     expected:
-      if (processor == "x86_64") and (flag_specific == "") and (product == "content_shell"): FAIL
+      if (os == "linux") and (flag_specific == "") and (product == "content_shell"): FAIL
 
   [expected mismatch: .kernOFF .cjk vs .kernON .cjk]
     expected:
-      if (processor == "x86_64") and (flag_specific == "") and (product == "content_shell"): PASS
+      if (os == "linux") and (flag_specific == "") and (product == "content_shell"): PASS
       FAIL
 
   [expected mismatch: .paltOFFkernON .cjk vs .paltONkernON .cjk]
diff --git a/third_party/blink/web_tests/external/wpt/css/css-masking/clip-path/clip-path-inline-001.html.ini b/third_party/blink/web_tests/external/wpt/css/css-masking/clip-path/clip-path-inline-001.html.ini
index c87eb823..4173d27b 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-masking/clip-path/clip-path-inline-001.html.ini
+++ b/third_party/blink/web_tests/external/wpt/css/css-masking/clip-path/clip-path-inline-001.html.ini
@@ -1,3 +1,4 @@
 [clip-path-inline-001.html]
   expected:
+    if (product == "content_shell") and (os == "linux") and (version == "Ubuntu 18.04"): FAIL
     if product == "chrome": FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties-expected.txt b/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties-expected.txt
index 04212008..20bd488 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties-expected.txt
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 62 tests; 60 PASS, 2 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 62 tests; 61 PASS, 1 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Property font value 'italic small-caps 900 expanded 25px / 50px Ahem' in ::marker
 PASS Property font-family value 'Ahem' in ::marker
 PASS Property font-feature-settings value '"smcp"' in ::marker
@@ -35,7 +35,7 @@
 PASS Property animation-name value 'anim' in ::marker
 PASS Property animation-play-state value 'paused' in ::marker
 PASS Property animation-timing-function value 'linear' in ::marker
-FAIL Property animation-composition value 'add' in ::marker assert_true: animation-composition doesn't seem to be supported in the computed style expected true got false
+PASS Property animation-composition value 'add' in ::marker
 PASS Property transition value 'display 1s linear 2s' in ::marker
 PASS Property transition-delay value '1s' in ::marker
 PASS Property transition-duration value '2s' in ::marker
diff --git a/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties.html.ini b/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties.html.ini
index db2d9ce..255a365 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties.html.ini
+++ b/third_party/blink/web_tests/external/wpt/css/css-pseudo/parsing/marker-supported-properties.html.ini
@@ -1,6 +1,3 @@
 [marker-supported-properties.html]
   [Property white-space value 'nowrap' in ::marker]
     expected: FAIL
-
-  [Property animation-composition value 'add' in ::marker]
-    expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-text-decor/text-decoration-line-spelling-error-color-dynamic-001.optional.html.ini b/third_party/blink/web_tests/external/wpt/css/css-text-decor/text-decoration-line-spelling-error-color-dynamic-001.optional.html.ini
new file mode 100644
index 0000000..3559642
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-text-decor/text-decoration-line-spelling-error-color-dynamic-001.optional.html.ini
@@ -0,0 +1,2 @@
+[text-decoration-line-spelling-error-color-dynamic-001.optional.html]
+  expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-text/text-transform/text-transform-capitalize-007.html.ini b/third_party/blink/web_tests/external/wpt/css/css-text/text-transform/text-transform-capitalize-007.html.ini
index 4dd4f07..a29c683 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-text/text-transform/text-transform-capitalize-007.html.ini
+++ b/third_party/blink/web_tests/external/wpt/css/css-text/text-transform/text-transform-capitalize-007.html.ini
@@ -1,3 +1,4 @@
 [text-transform-capitalize-007.html]
   expected:
+    if (product == "content_shell") and (os == "linux") and (version == "Ubuntu 18.04"): FAIL
     if product == "chrome": FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-typed-om/the-stylepropertymap/declared/append.tentative.html b/third_party/blink/web_tests/external/wpt/css/css-typed-om/the-stylepropertymap/declared/append.tentative.html
index ced6e81..2ff92b22c 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-typed-om/the-stylepropertymap/declared/append.tentative.html
+++ b/third_party/blink/web_tests/external/wpt/css/css-typed-om/the-stylepropertymap/declared/append.tentative.html
@@ -61,4 +61,9 @@
       [CSS.s(5), CSS.s(10), CSS.s(1), CSS.s(2)]);
 }, 'StylePropertyMap.append is case-insensitive');
 
+test(t => {
+  let styleMap = createDeclaredStyleMap(t, 'transition-duration: var(--a)');
+  assert_throws_js(TypeError, () => styleMap.append('transition-duration', CSS.s(1)));
+}, 'Appending to a list containing a variable reference should throw');
+
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-reset-background-size-001.html.ini b/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-reset-background-size-001.html.ini
new file mode 100644
index 0000000..aa68b0a
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-reset-background-size-001.html.ini
@@ -0,0 +1,2 @@
+[kind-of-widget-fallback-input-reset-background-size-001.html]
+  expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-submit-border-image-repeat-001.html.ini b/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-submit-border-image-repeat-001.html.ini
new file mode 100644
index 0000000..29920ec
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/css-ui/compute-kind-widget-generated/kind-of-widget-fallback-input-submit-border-image-repeat-001.html.ini
@@ -0,0 +1,2 @@
+[kind-of-widget-fallback-input-submit-border-image-repeat-001.html]
+  expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/css/css-ui/outline-025.html.ini b/third_party/blink/web_tests/external/wpt/css/css-ui/outline-025.html.ini
index 35e544d8..c5515ab0 100644
--- a/third_party/blink/web_tests/external/wpt/css/css-ui/outline-025.html.ini
+++ b/third_party/blink/web_tests/external/wpt/css/css-ui/outline-025.html.ini
@@ -1,3 +1,3 @@
 [outline-025.html]
   expected:
-    if flag_specific == "disable-layout-ng": FAIL
+    if (os == "linux") and (version == "Ubuntu 18.04"): FAIL
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html
index 7f55f1b..019ee9d7 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-innerwidth-innerheight.html
@@ -50,7 +50,7 @@
         e.source.close();
         assert_equals(data.width, baselineDimensions.width, `"${feature} is negative and should result in a minimally-wide window"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerWidth=" + baselineDimensions.width, '', featureString);
     }, `features "${feature}" should NOT set "width=404"`);
   });
 
@@ -67,7 +67,7 @@
         e.source.close();
         assert_equals(data.height, baselineDimensions.height, `"${feature} is negative and should result in a minimal-height window"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
     }, `features "${feature}" should NOT set "height=404"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html
index 09fb2d6..6e316db 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-screenx-screeny.html
@@ -43,7 +43,7 @@
         e.source.close();
         assert_equals(data.top, baselineDimensions.top, `"${feature} is negative and should be set to 0"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
     }, `features "${feature}" should NOT set "top=204"`);
   });
 
@@ -59,7 +59,7 @@
         e.source.close();
         assert_equals(data.left, baselineDimensions.left, `"${feature} is negative and should be set to 0"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
     }, `features "${feature}" should NOT set "left=204"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-top-left.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-top-left.html
index 15b3103..316b01a 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-top-left.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-top-left.html
@@ -44,7 +44,7 @@
         e.source.close();
         assert_equals(data.top, baselineDimensions.top, `"${feature} is negative and should be set to 0"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
     }, `features "${feature}" should NOT set "top=204"`);
   });
 
@@ -60,7 +60,7 @@
         e.source.close();
         assert_equals(data.left, baselineDimensions.left, `"${feature} is negative and should be set to 0"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
     }, `features "${feature}" should NOT set "left=204"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-width-height.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-width-height.html
index 30b7092..3cb1556 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-width-height.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-negative-width-height.html
@@ -50,7 +50,7 @@
         e.source.close();
         assert_equals(data.width, baselineDimensions.width, `"${feature} is negative and should result in a minimally-wide window"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
     }, `features "${feature}" should NOT set "width=404"`);
   });
 
@@ -67,7 +67,7 @@
         e.source.close();
         assert_equals(data.height, baselineDimensions.height, `"${feature} is negative and should result in a minimal-height window"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
     }, `features "${feature}" should NOT set "height=404"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-height.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-height.html
index cd2d019..d8ee866 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-height.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-height.html
@@ -42,7 +42,7 @@
         e.source.close();
         assert_equals(data.height, baselineDimensions.height);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', feature);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', feature);
     }, `${feature}: absence of feature "height" should be treated same as "height=0"`);
   });
 
@@ -59,7 +59,7 @@
         e.source.close();
         assert_equals(data.height, baselineDimensions.height, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
     }, `features "${feature}" should NOT set "height=404"`);
   });
 
@@ -82,7 +82,7 @@
         e.source.close();
         assert_equals(data.height, 405, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerHeight=405", '', featureString);
     }, `features "${feature}" should set "height=405"`);
   });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html
index 5ee752c..f191d87 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerheight.html
@@ -44,7 +44,7 @@
         e.source.close();
         assert_equals(data.height, baselineDimensions.height, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=' + baselineDimensions.height, '', featureString);
     }, `features "${feature}" should NOT set "height=404"`);
   });
 
@@ -67,7 +67,7 @@
         e.source.close();
         assert_equals(data.height, 405, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerHeight=405", '', featureString);
     }, `features "${feature}" should set "height=405"`);
   });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html
index 972beef..d1ddc5e 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-innerwidth.html
@@ -44,7 +44,7 @@
         e.source.close();
         assert_equals(data.width, baselineDimensions.width, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
     }, `features "${feature}" should NOT set "width=404"`);
   });
 
@@ -67,7 +67,7 @@
         e.source.close();
         assert_equals(data.width, 405, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=405', '', featureString);
     }, `features "${feature}" should set "width=405"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-left.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-left.html
index fbb2a30..c771204 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-left.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-left.html
@@ -45,7 +45,7 @@
         e.source.close();
         assert_equals(data.left, baselineDimensions.left, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
     }, `features "${feature}" should NOT set "left=104"`);
   });
 
@@ -68,7 +68,7 @@
         e.source.close();
         assert_equals(data.left, 105, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=105', '', featureString);
     }, `features "${feature}" should set "left=105"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html
index 2aeab9bb..49a37832 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screenx.html
@@ -44,8 +44,8 @@
         e.source.close();
         assert_equals(data.left, baselineDimensions.left, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
-    }, `features "${feature}" should NOT set "left=104"`);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=' + baselineDimensions.left, '', featureString);
+    }, `features "${feature}" should NOT set "screenx=104"`);
   });
 
   // Codepoints that are valid ASCII digits should be collected
@@ -67,8 +67,8 @@
         e.source.close();
         assert_equals(data.left, 105, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
-    }, `features "${feature}" should set "left=105"`);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=105', '', featureString);
+    }, `features "${feature}" should set "screenx=105"`);
   });
 }
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html
index cb4e873f..5183405 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html
@@ -44,8 +44,8 @@
         e.source.close();
         assert_equals(data.top, baselineDimensions.top, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
-    }, `features "${feature}" should NOT set "height=404"`);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
+    }, `features "${feature}" should NOT set "screeny=404"`);
   });
 
   // Codepoints that are valid ASCII digits should be collected
@@ -67,8 +67,8 @@
         e.source.close();
         assert_equals(data.top, 405, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
-    }, `features "${feature}" should set "height=405"`);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=405', '', featureString);
+    }, `features "${feature}" should set "screeny=405"`);
   });
 
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html.ini b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html.ini
index 2fbad0d..cf4382f 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html.ini
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-screeny.html.ini
@@ -1,6 +1,4 @@
 [open-features-non-integer-screeny.html]
-  expected:
-    if product == "chrome": [OK, ERROR]
   [features "screeny=405.5" should set "height=405"]
     expected: FAIL
 
@@ -27,3 +25,30 @@
 
   [features "screeny=405e-1" should set "height=405"]
     expected: FAIL
+
+  [features "screeny=405^4" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405/5" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405e1" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405*3" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405LLl" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405.32" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405e-1" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405.5" should set "screeny=405"]
+    expected: FAIL
+
+  [features "screeny=405  " should set "screeny=405"]
+    expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-top.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-top.html
index d4f4e90..a7926e7 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-top.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-top.html
@@ -42,7 +42,7 @@
         e.source.close();
         assert_equals(data.top, baselineDimensions.top, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=' + baselineDimensions.top, '', featureString);
     }, `features "${feature}" should NOT set "top=104"`);
   });
 
@@ -65,7 +65,7 @@
         e.source.close();
         assert_equals(data.top, 105, `"${feature} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=105', '', featureString);
     }, `features "${feature}" should set "top=105"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-width.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-width.html
index a746abe..6878063 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-width.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-non-integer-width.html
@@ -43,7 +43,7 @@
         e.source.close();
         assert_equals(data.width, baselineDimensions.width);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', feature);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', feature);
     }, `${feature}: absence of feature "width" should be treated same as "width=0"`);
   });
 
@@ -60,7 +60,7 @@
         e.source.close();
         assert_equals(data.width, baselineDimensions.width, `"${feature} begins with an invalid character and should be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=' + baselineDimensions.width, '', featureString);
     }, `features "${feature}" should NOT set "width=404"`);
   });
 
@@ -83,7 +83,7 @@
         e.source.close();
         assert_equals(data.width, 405, `"${featureString} value after first non-digit will be ignored"`);
       }));
-      var win = window.open(prefixedMessage.url(windowURL), '', featureString);
+      var win = window.open(prefixedMessage.url(windowURL) + "&expected_innerWidth=405", '', featureString);
     }, `features "${feature}" should set "width=405"`);
   });
 }
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html
index c839c6c..cf3ad25 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-innerheight-innerwidth.html
@@ -30,7 +30,7 @@
       e.source.close();
       assert_equals(data.width, 401);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=401', '', height + features);
   }, `${format_value(features)} should set width of opened window`);
 });
 
@@ -48,7 +48,7 @@
       e.source.close();
       assert_equals(data.height, 402);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402', '', width + features);
   }, `${format_value(features)} should set height of opened window`);
 });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html
index bb6fc4b8..5a53fef 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-screenx-screeny.html
@@ -30,7 +30,7 @@
       e.source.close();
       assert_equals(data.left, 141);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=141', '', width + height + features);
   }, `${format_value(features)} should set left position of opened window`);
 });
 
@@ -48,7 +48,7 @@
       e.source.close();
       assert_equals(data.top, 142);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=142', '', width + height + features);
   }, `${format_value(features)} should set top position of opened window`);
 });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html
index ef098a1..842cbcf8 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-top-left.html
@@ -29,7 +29,7 @@
       e.source.close();
       assert_equals(data.left, 141);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=141', '', width + height + features);
   }, `"${features}" should set left position of opened window`);
 });
 
@@ -46,7 +46,7 @@
       e.source.close();
       assert_equals(data.top, 142);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenY=142', '', width + height + features);
   }, `${format_value(features)} should set top position of opened window`);
 });
 
@@ -62,7 +62,7 @@
       assert_equals(data.top, 152);
       assert_equals(data.left, 152);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_screenX=152&expected_screenY=152', '', width + height + features);
   }, `${format_value(features)} should set top and left position of opened window`);
 });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
index cac4ae63..ff61199 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/open-features-tokenization-width-height.html
@@ -29,7 +29,7 @@
       e.source.close();
       assert_equals(data.width, 401);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', height + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerWidth=401', '', height + features);
   }, `${format_value(features)} should set width of opened window`);
 });
 
@@ -46,7 +46,7 @@
       e.source.close();
       assert_equals(data.height, 402);
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', width + features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402', '', width + features);
   }, `${format_value(features)} should set height of opened window`);
 });
 
@@ -64,7 +64,7 @@
       assert_equals(data.height, 402);
       assert_equals(data.width, 401)
     }));
-    var win = window.open(prefixedMessage.url(windowURL), '', features);
+    var win = window.open(prefixedMessage.url(windowURL) + '&expected_innerHeight=402&expected_innerWidth=401', '', features);
   }, `${format_value(features)} should set height and width of opened window`);
 });
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/resources/message-opener.html b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/resources/message-opener.html
index 07662c6..5c58a93 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/resources/message-opener.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/the-window-object/open-close/resources/message-opener.html
@@ -2,6 +2,14 @@
 <script>
 var prefixedMessage = new PrefixedMessageResource();
 var max = 150, attempts = 0;
+
+const urlParams = new URLSearchParams(location.search);
+const expected_innerWidth = urlParams.get('expected_innerWidth');
+const expected_innerHeight = urlParams.get('expected_innerHeight');
+const expected_screenX = urlParams.get('expected_screenX');
+const expected_screenY = urlParams.get('expected_screenY');
+let should_wait_until_settled = expected_innerWidth === null && expected_innerHeight === null && expected_screenX === null && expected_screenY === null;
+
 function sendCoordinates() {
   // Certain windowing systems position windows asynchronously.
   // As a result, the window may not be positioned yet when the
@@ -11,6 +19,27 @@
     setTimeout(sendCoordinates, 100);
     return;
   }
+  if (expected_innerWidth && window.innerWidth != expected_innerWidth && ++attempts < max) {
+    setTimeout(sendCoordinates, 10);
+    return;
+  }
+  if (expected_innerHeight && window.innerHeight != expected_innerHeight && ++attempts < max) {
+    setTimeout(sendCoordinates, 10);
+    return;
+  }
+  if (expected_screenX && window.screenX != expected_screenX && ++attempts < max) {
+    setTimeout(sendCoordinates, 10);
+    return;
+  }
+  if (expected_screenY && window.screenY != expected_screenY && ++attempts < max) {
+    setTimeout(sendCoordinates, 10);
+    return;
+  }
+  if (should_wait_until_settled) {
+    should_wait_until_settled = false;
+    setTimeout(sendCoordinates, 300);
+    return;
+  }
   prefixedMessage.postToOpener({
     left: window.screenX,
     top: window.screenY,
diff --git a/third_party/blink/web_tests/external/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js.ini b/third_party/blink/web_tests/external/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js.ini
index 2faeab3..a7414b2 100644
--- a/third_party/blink/web_tests/external/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js.ini
+++ b/third_party/blink/web_tests/external/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js.ini
@@ -1,5 +1,5 @@
 [performance-navigation-timing-same-origin-bfcache.tentative.window.html]
   [RemoteContextHelper navigation using BFCache]
     expected:
-      if (os == "linux") and (version == "trusty") and (product == "chrome"): FAIL
-      if (os == "linux") and (version == "Ubuntu 18.04"): FAIL
+      if (processor == "x86_64") and (os == "linux") and (product == "chrome"): FAIL
+      if (processor == "x86_64") and (os == "win"): FAIL
diff --git a/third_party/blink/web_tests/external/wpt/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html.ini b/third_party/blink/web_tests/external/wpt/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html.ini
index e9534c3..3fabfc4 100644
--- a/third_party/blink/web_tests/external/wpt/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html.ini
+++ b/third_party/blink/web_tests/external/wpt/permissions-policy/payment-extension-allowed-by-permissions-policy-attribute.https.sub.html.ini
@@ -8,5 +8,5 @@
 
   [permissions policy "payment" can be enabled in cross-origin iframe using allow="payment" attribute]
     expected:
-      if product == "chrome": TIMEOUT
+      if product == "chrome": [NOTRUN, TIMEOUT]
       FAIL
diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/prefetch/prefetch-traverse-reload.sub.html b/third_party/blink/web_tests/external/wpt/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
new file mode 100644
index 0000000..3f1312e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/speculation-rules/prefetch/prefetch-traverse-reload.sub.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="timeout" content="long">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/dispatcher/dispatcher.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/websockets/constants.sub.js"></script>
+<script src="resources/utils.sub.js"></script>
+<script>
+promise_test(async t => {
+  assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+  let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+  let previousUrl = await agent.execute_script(() => location.href);
+  await agent.execute_script(async () => {
+    window.preventBfcache = new WebSocket('wss://{{ports[wss][0]}}/echo');
+  });
+
+  let nextUrl = agent.getExecutorURL({ protocol: 'https', page: 2 });
+  await agent.navigate(nextUrl);
+
+  await agent.forceSinglePrefetch(previousUrl);
+  await agent.execute_script(() => {
+    window.executor.suspend(() => history.go(-1));
+  });
+
+  assert_equals(previousUrl, await agent.execute_script(() => location.href));
+  assert_prefetched(await agent.getRequestHeaders(), "traversal should use prefetch");
+}, "prefetches can be used for traversal navigations");
+
+promise_test(async t => {
+  assert_implements(HTMLScriptElement.supports('speculationrules'), "Speculation Rules not supported");
+
+  let agent = await spawnWindow(t, { protocol: 'https', pipe: 'header(Cache-Control, no-store)' });
+  let previousUrl = await agent.execute_script(() => location.href);
+  await agent.forceSinglePrefetch(previousUrl);
+  await agent.execute_script(() => {
+    window.executor.suspend(() => location.reload());
+  });
+
+  assert_equals(previousUrl, await agent.execute_script(() => location.href));
+  assert_prefetched(await agent.getRequestHeaders(), "reload should use prefetch");
+}, "prefetches can be used for reload navigations");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/speculation-rules/prerender/restriction-screen-capture.https.html.ini b/third_party/blink/web_tests/external/wpt/speculation-rules/prerender/restriction-screen-capture.https.html.ini
index eae5030b..9112e68 100644
--- a/third_party/blink/web_tests/external/wpt/speculation-rules/prerender/restriction-screen-capture.https.html.ini
+++ b/third_party/blink/web_tests/external/wpt/speculation-rules/prerender/restriction-screen-capture.https.html.ini
@@ -1,7 +1,9 @@
 [restriction-screen-capture.https.html]
   expected:
-    if (product == "content_shell") and (os == "win"): [OK, ERROR]
-    if product == "chrome": TIMEOUT
+    if (os == "linux") and (product == "chrome"): TIMEOUT
   [The access to the Screen Capture API should be deferred until the\n    prerendered page is activated]
     expected:
       if product == "chrome": TIMEOUT
+
+  [prerendering pages should not be able to invoke the Screen Capture API]
+    expected: TIMEOUT
diff --git a/third_party/blink/web_tests/external/wpt/url/a-element-xhtml.xhtml.ini b/third_party/blink/web_tests/external/wpt/url/a-element-xhtml.xhtml.ini
index b02fb24..0e8ba0f 100644
--- a/third_party/blink/web_tests/external/wpt/url/a-element-xhtml.xhtml.ini
+++ b/third_party/blink/web_tests/external/wpt/url/a-element-xhtml.xhtml.ini
@@ -1211,8 +1211,6 @@
 
 
 [a-element-xhtml.xhtml?exclude=(file|javascript|mailto)]
-  expected:
-    if (flag_specific == "") and (os == "linux") and (product == "content_shell"): [OK, CRASH]
   [Parsing: <non-special://test:@test/x> against <about:blank>]
     expected: FAIL
 
@@ -2061,6 +2059,12 @@
     expected:
       if os == "win": FAIL
 
+  [Parsing: <http://%5B::1\]> against <http://other.com/>]
+    expected: FAIL
+
+  [Parsing: <http://[::%31\]> against <http://other.com/>]
+    expected: FAIL
+
 
 [a-element-xhtml.xhtml?include=mailto]
   expected:
diff --git a/third_party/blink/web_tests/external/wpt/url/a-element.html.ini b/third_party/blink/web_tests/external/wpt/url/a-element.html.ini
index 291c587..4ad709f 100644
--- a/third_party/blink/web_tests/external/wpt/url/a-element.html.ini
+++ b/third_party/blink/web_tests/external/wpt/url/a-element.html.ini
@@ -2174,6 +2174,12 @@
   [Parsing: <non-special:x/?#ï¿¿y> against <about:blank>]
     expected: FAIL
 
+  [Parsing: <http://%5B::1\]> against <http://other.com/>]
+    expected: FAIL
+
+  [Parsing: <http://[::%31\]> against <http://other.com/>]
+    expected: FAIL
+
 
 [a-element.html?include=javascript]
   expected:
diff --git a/third_party/blink/web_tests/external/wpt/url/resources/urltestdata.json b/third_party/blink/web_tests/external/wpt/url/resources/urltestdata.json
index fe7316f..0265346 100644
--- a/third_party/blink/web_tests/external/wpt/url/resources/urltestdata.json
+++ b/third_party/blink/web_tests/external/wpt/url/resources/urltestdata.json
@@ -3964,6 +3964,16 @@
     "base": "http://other.com/",
     "failure": true
   },
+  {
+    "input": "http://[::%31]",
+    "base": "http://other.com/",
+    "failure": true
+  },
+  {
+    "input": "http://%5B::1]",
+    "base": "http://other.com/",
+    "failure": true
+  },
   "Misc Unicode",
   {
     "input": "http://foo:💩@example.com/bar",
diff --git a/third_party/blink/web_tests/external/wpt/url/url-constructor.any.js.ini b/third_party/blink/web_tests/external/wpt/url/url-constructor.any.js.ini
index 1cbbb92..4a51ce1 100644
--- a/third_party/blink/web_tests/external/wpt/url/url-constructor.any.js.ini
+++ b/third_party/blink/web_tests/external/wpt/url/url-constructor.any.js.ini
@@ -1861,6 +1861,12 @@
   [Parsing: <non-special:x/?#ï¿¿y> against <about:blank>]
     expected: FAIL
 
+  [Parsing: <http://%5B::1\]> against <http://other.com/>]
+    expected: FAIL
+
+  [Parsing: <http://[::%31\]> against <http://other.com/>]
+    expected: FAIL
+
 
 [url-constructor.any.worker.html?include=javascript]
   [Parsing: <javascript:/../> against <about:blank>]
@@ -2828,6 +2834,12 @@
   [Parsing: <non-special:x/?#ï¿¿y> against <about:blank>]
     expected: FAIL
 
+  [Parsing: <http://%5B::1\]> against <http://other.com/>]
+    expected: FAIL
+
+  [Parsing: <http://[::%31\]> against <http://other.com/>]
+    expected: FAIL
+
 
 [url-constructor.any.html?include=javascript]
   [Parsing: <javascript:/../> against <about:blank>]
diff --git a/third_party/blink/web_tests/external/wpt/webrtc-extensions/RTCRtpParameters-maxFramerate-expected.txt b/third_party/blink/web_tests/external/wpt/webrtc-extensions/RTCRtpParameters-maxFramerate-expected.txt
deleted file mode 100644
index 71a5976..0000000
--- a/third_party/blink/web_tests/external/wpt/webrtc-extensions/RTCRtpParameters-maxFramerate-expected.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-This is a testharness.js-based test.
-PASS addTransceiver() with sendEncoding.maxFramerate field set to less than 0 should reject with RangeError
-FAIL addTransceiver('audio') with sendEncoding.maxFramerate should succeed, but remove the maxFramerate, even if it is invalid Failed to execute 'addTransceiver' on 'RTCPeerConnection': Attempted to set RtpParameters max_framerate to an invalid value. max_framerate must be >= 0.0
-FAIL setParameters with maxFramerate on an audio sender should succeed, but remove the maxFramerate assert_equals: expected 1 but got 0
-FAIL setParameters with an invalid maxFramerate on an audio sender should succeed, but remove the maxFramerate assert_equals: expected 1 but got 0
-PASS setParameters() with encoding.maxFramerate field set to less than 0 should reject with RangeError
-PASS setParameters() with maxFramerate 24->16 should succeed with RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 24->16 should succeed without RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate undefined->16 should succeed with RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate undefined->16 should succeed without RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 24->undefined should succeed with RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 24->undefined should succeed without RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 0->16 should succeed with RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 0->16 should succeed without RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 24->0 should succeed with RTCRtpTransceiverInit
-PASS setParameters() with maxFramerate 24->0 should succeed without RTCRtpTransceiverInit
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/webrtc/RTCRtpParameters-encodings-expected.txt b/third_party/blink/web_tests/external/wpt/webrtc/RTCRtpParameters-encodings-expected.txt
index e849fa7..b0b528e 100644
--- a/third_party/blink/web_tests/external/wpt/webrtc/RTCRtpParameters-encodings-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/webrtc/RTCRtpParameters-encodings-expected.txt
@@ -1,17 +1,17 @@
 This is a testharness.js-based test.
-FAIL getParameters should return RTCRtpEncodingParameters with all required fields assert_equals: expected 1 but got 0
-FAIL addTransceiver(audio) with undefined sendEncodings should have default encoding parameter with active set to true assert_equals: expected 1 but got 0
-FAIL addTransceiver(video) with undefined sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1 assert_equals: expected 1 but got 0
-FAIL addTransceiver(audio) with empty list sendEncodings should have default encoding parameter with active set to true assert_equals: expected 1 but got 0
-FAIL addTransceiver(video) with empty list sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1 assert_equals: expected 1 but got 0
+PASS getParameters should return RTCRtpEncodingParameters with all required fields
+PASS addTransceiver(audio) with undefined sendEncodings should have default encoding parameter with active set to true
+FAIL addTransceiver(video) with undefined sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1 assert_equals: expected (number) 1 but got (undefined) undefined
+PASS addTransceiver(audio) with empty list sendEncodings should have default encoding parameter with active set to true
+FAIL addTransceiver(video) with empty list sendEncodings should have default encoding parameter with active set to true and scaleResolutionDownBy set to 1 assert_equals: expected (number) 1 but got (undefined) undefined
 FAIL addTransceiver(video) should auto-set scaleResolutionDownBy to 1 when some encodings have it, but not all assert_equals: expected (number) 1 but got (undefined) undefined
 FAIL addTransceiver should auto-set scaleResolutionDownBy to powers of 2 (descending) when absent assert_equals: expected (number) 2 but got (undefined) undefined
 PASS addTransceiver with a ridiculous number of encodings should truncate the list
 PASS addTransceiver(audio) with multiple encodings should result in one encoding with no properties other than active
-FAIL addTransceiver(audio) should remove valid scaleResolutionDownBy assert_not_own_property: unexpected property "scaleResolutionDownBy" is found on object
-FAIL addTransceiver(audio) should remove invalid scaleResolutionDownBy promise_test: Unhandled rejection with value: object "RangeError: Failed to execute 'addTransceiver' on 'RTCPeerConnection': Attempted to set RtpParameters scale_resolution_down_by to an invalid value. scale_resolution_down_by must be >= 1.0"
-FAIL setParameters with scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy assert_equals: expected 1 but got 0
-FAIL setParameters with an invalid scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy assert_equals: expected 1 but got 0
+PASS addTransceiver(audio) should remove valid scaleResolutionDownBy
+PASS addTransceiver(audio) should remove invalid scaleResolutionDownBy
+PASS setParameters with scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy
+PASS setParameters with an invalid scaleResolutionDownBy on an audio sender should succeed, but remove the scaleResolutionDownBy
 FAIL addTransceiver with duplicate rid and multiple encodings throws TypeError assert_throws_js: function "() => pc.addTransceiver('video', { sendEncodings: [{rid: "foo"}, {rid: "foo"}] })" did not throw
 FAIL addTransceiver with missing rid and multiple encodings throws TypeError assert_throws_js: function "() => pc.addTransceiver('video', { sendEncodings: [{rid: "foo"}, {}] })" threw object "InvalidAccessError: Failed to execute 'addTransceiver' on 'RTCPeerConnection': RIDs must be provided for either all or none of the send encodings." ("InvalidAccessError") expected instance of function "function TypeError() { [native code] }" ("TypeError")
 FAIL addTransceiver with empty rid throws TypeError assert_throws_js: function "() => pc.addTransceiver('video', { sendEncodings: [{rid: ""}] })" did not throw
@@ -19,14 +19,14 @@
 PASS addTransceiver with rid longer than 255 characters throws TypeError
 PASS addTransceiver with scaleResolutionDownBy < 1 throws RangeError
 PASS sender.getParameters() should return sendEncodings set by addTransceiver()
-FAIL sender.setParameters() with added encodings should reject with InvalidModificationError assert_equals: expected 1 but got 0
+PASS sender.setParameters() with added encodings should reject with InvalidModificationError
 PASS sender.setParameters() with removed encodings should reject with InvalidModificationError
 PASS sender.setParameters() with reordered encodings should reject with InvalidModificationError
 PASS sender.setParameters() with encodings unset should reject with TypeError
 PASS setParameters() with modified encoding.rid field should reject with InvalidModificationError
-FAIL setParameters() with encoding.scaleResolutionDownBy field set to less than 1.0 should reject with RangeError promise_test: Unhandled rejection with value: object "TypeError: Cannot set properties of undefined (setting 'scaleResolutionDownBy')"
-FAIL setParameters() with missing encoding.scaleResolutionDownBy field should succeed, and set the value back to 1 promise_test: Unhandled rejection with value: object "TypeError: Cannot convert undefined or null to object"
-FAIL setParameters() with encoding.scaleResolutionDownBy field set to greater than 1.0 should succeed promise_test: Unhandled rejection with value: object "TypeError: Cannot set properties of undefined (setting 'scaleResolutionDownBy')"
+FAIL setParameters() with encoding.scaleResolutionDownBy field set to less than 1.0 should reject with RangeError promise_rejects_js: function "function() { throw e }" threw object "InvalidStateError: getParameters() needs to be called before setParameters()." ("InvalidStateError") expected instance of function "function RangeError() { [native code] }" ("RangeError")
+FAIL setParameters() with missing encoding.scaleResolutionDownBy field should succeed, and set the value back to 1 assert_equals: expected (number) 1 but got (undefined) undefined
+PASS setParameters() with encoding.scaleResolutionDownBy field set to greater than 1.0 should succeed
 PASS setParameters() with encoding.active false->true should succeed (video) with RTCRtpTransceiverInit
 PASS setParameters() with encoding.active false->true should succeed (video) without RTCRtpTransceiverInit
 PASS setParameters() with encoding.active true->false should succeed (video) with RTCRtpTransceiverInit
diff --git a/third_party/blink/web_tests/external/wpt/webrtc/simulcast/setParameters-encodings.https-expected.txt b/third_party/blink/web_tests/external/wpt/webrtc/simulcast/setParameters-encodings.https-expected.txt
index 04ed2bc7..8b89cfd 100644
--- a/third_party/blink/web_tests/external/wpt/webrtc/simulcast/setParameters-encodings.https-expected.txt
+++ b/third_party/blink/web_tests/external/wpt/webrtc/simulcast/setParameters-encodings.https-expected.txt
@@ -5,14 +5,14 @@
 FAIL sRD(unicast answer) interrupted by setParameters(simulcast) results in keeping the first encoding promise_test: Unhandled rejection with value: object "InvalidStateError: Failed to execute 'setParameters' on 'RTCRtpSender': Failed to set parameters since getParameters() has never been called on this sender"
 FAIL sRD(unicast reoffer) interrupted by setParameters(simulcast) results in keeping the first encoding promise_test: Unhandled rejection with value: object "InvalidStateError: Failed to execute 'setParameters' on 'RTCRtpSender': Failed to set parameters since getParameters() has never been called on this sender"
 FAIL sRD(simulcast answer) interrupted by a setParameters does not result in losing modifications from the setParameters to the encodings that remain promise_test: Unhandled rejection with value: object "InvalidStateError: Failed to execute 'setParameters' on 'RTCRtpSender': Failed to set parameters since getParameters() has never been called on this sender"
-FAIL addTrack, then a unicast setParameters, then sRD(simulcast offer) results in simulcast without the settings from setParameters promise_test: Unhandled rejection with value: object "TypeError: Cannot set properties of undefined (setting 'scaleResolutionDownBy')"
-FAIL addTrack, then sRD(simulcast offer) interrupted by a unicast setParameters results in simulcast without the settings from setParameters promise_test: Unhandled rejection with value: object "TypeError: Cannot set properties of undefined (setting 'scaleResolutionDownBy')"
+FAIL addTrack, then a unicast setParameters, then sRD(simulcast offer) results in simulcast without the settings from setParameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [undefined] length 1
+FAIL addTrack, then sRD(simulcast offer) interrupted by a unicast setParameters results in simulcast without the settings from setParameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [undefined] length 1
 PASS getParameters, then sLD(unicast answer) interrupted by a simulcast setParameters results in unicast
 FAIL Success task for setLocalDescription(answer) clears [[LastReturnedParameters]] assert_unreached: Should have rejected: undefined Reached unreachable code
 FAIL Success task for setRemoteDescription(offer) clears [[LastReturnedParameters]] assert_unreached: Should have rejected: undefined Reached unreachable code
 FAIL Success task for setRemoteDescription(answer) clears [[LastReturnedParameters]] assert_unreached: Should have rejected: undefined Reached unreachable code
-FAIL addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without any previously set parameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [] length 0
+FAIL addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without any previously set parameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [undefined] length 1
 FAIL rollback of a remote offer that disabled a previously negotiated simulcast should restore simulcast along with any previously set parameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got ["foo"] length 1
-FAIL rollback of sRD(simulcast offer) interrupted by setParameters(simulcast) brings us back to having a single encoding without any previously set parameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [] length 0
+FAIL rollback of sRD(simulcast offer) interrupted by setParameters(simulcast) brings us back to having a single encoding without any previously set parameters assert_array_equals: lengths differ, expected array ["foo", "bar"] length 2, got [undefined] length 1
 Harness: the test ran to completion.
 
diff --git a/third_party/blink/web_tests/external/wpt/webxr/events_referenceSpace_reset_inline.https.html.ini b/third_party/blink/web_tests/external/wpt/webxr/events_referenceSpace_reset_inline.https.html.ini
index 00c1d56..642843ad 100644
--- a/third_party/blink/web_tests/external/wpt/webxr/events_referenceSpace_reset_inline.https.html.ini
+++ b/third_party/blink/web_tests/external/wpt/webxr/events_referenceSpace_reset_inline.https.html.ini
@@ -1,7 +1,11 @@
 [events_referenceSpace_reset_inline.https.html]
   expected: TIMEOUT
   [XRSession resetpose from a device properly fires off the right events for non-immersive sessions - webgl]
-    expected: FAIL
+    expected:
+      if product == "chrome": [TIMEOUT, FAIL]
+      FAIL
 
   [XRSession resetpose from a device properly fires off the right events for non-immersive sessions - webgl2]
-    expected: TIMEOUT
+    expected:
+      if product == "chrome": [NOTRUN, TIMEOUT]
+      TIMEOUT
diff --git a/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-listing-expected.txt b/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-listing-expected.txt
index b1f9d4d..6489f80 100644
--- a/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-listing-expected.txt
+++ b/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-listing-expected.txt
@@ -52,6 +52,7 @@
 alignment-baseline: auto
 anchor-name: none
 anchor-scroll: none
+animation-composition: replace
 animation-delay-end: 0s
 animation-delay-start: 0s
 animation-direction: normal
diff --git a/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-without-renderer-listing-expected.txt b/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-without-renderer-listing-expected.txt
index cc3cc5f..05416b1 100644
--- a/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-without-renderer-listing-expected.txt
+++ b/third_party/blink/web_tests/fast/css/getComputedStyle/computed-style-without-renderer-listing-expected.txt
@@ -52,6 +52,7 @@
 alignment-baseline: auto
 anchor-name: none
 anchor-scroll: none
+animation-composition: replace
 animation-delay-end: 0s
 animation-delay-start: 0s
 animation-direction: normal
diff --git a/third_party/blink/web_tests/fast/css/style-enumerate-properties.html b/third_party/blink/web_tests/fast/css/style-enumerate-properties.html
index 66e31e528..270f630 100644
--- a/third_party/blink/web_tests/fast/css/style-enumerate-properties.html
+++ b/third_party/blink/web_tests/fast/css/style-enumerate-properties.html
@@ -43,7 +43,7 @@
                 testFailed("Invalid CSS-mapped property order: '" + p + "' after '" + previous + "'");
                 break;
             }
-            if (++cssPropertyCount <= 160)
+            if (++cssPropertyCount <= 161)
                 previous = p;
             else {
                 if (seenFilter)
diff --git a/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash-expected.txt b/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash-expected.txt
index 24860b3..dc0fb69e 100644
--- a/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash-expected.txt
+++ b/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash-expected.txt
@@ -1,7 +1,7 @@
 Verify that sourcemap sources are mapped event when sourcemap compiled url matches with one of the source urls.
 
 Binding created: {
-       network: http://127.0.0.1:8000/devtools/persistence/resources/sourcemap-name-clash/out.js? [sm]
+       network: http://127.0.0.1:8000/devtools/persistence/resources/sourcemap-name-clash/out.js
     fileSystem: file:///var/www/src/out.js
     exactMatch: true
 }
diff --git a/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash.js b/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash.js
index 2dc80c4..ed7ff17a 100644
--- a/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash.js
+++ b/third_party/blink/web_tests/http/tests/devtools/persistence/automapping-sourcemap-nameclash.js
@@ -14,7 +14,12 @@
   BindingsTestRunner.overrideNetworkModificationTime(
       {'http://127.0.0.1:8000/devtools/persistence/resources/sourcemap-name-clash/out.js': null});
 
-  Promise.all([getResourceContent('out.js'), getResourceContent('out.js? [sm]')]).then(onResourceContents);
+  Promise
+      .all([
+        getResourceContent('out.js', Common.resourceTypes.Script),
+        getResourceContent('out.js', Common.resourceTypes.SourceMapScript),
+      ])
+      .then(onResourceContents);
 
   function onResourceContents(contents) {
     var fs = new BindingsTestRunner.TestFileSystem('/var/www');
@@ -30,10 +35,10 @@
     automappingTest.waitUntilMappingIsStabilized().then(TestRunner.completeTest.bind(TestRunner));
   }
 
-  function getResourceContent(name) {
+  function getResourceContent(name, contentType) {
     var fulfill;
     var promise = new Promise(x => fulfill = x);
-    SourcesTestRunner.waitForScriptSource(name, onSource);
+    SourcesTestRunner.waitForScriptSource(name, onSource, contentType);
     return promise;
 
     function onSource(uiSourceCode) {
diff --git "a/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
index ddb2a07f..6a33129 100644
--- "a/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 343 PASS, 276 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 343 PASS, 278 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
index ddb2a07f..6a33129 100644
--- "a/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 343 PASS, 276 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 343 PASS, 278 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
index b73eb638..85e66b8 100644
--- "a/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 475 PASS, 146 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 475 PASS, 148 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git "a/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
index b73eb638..85e66b8 100644
--- "a/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 475 PASS, 146 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 475 PASS, 148 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
index da4a243..667b13c2 100644
--- "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 342 PASS, 277 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 342 PASS, 279 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
index da4a243..667b13c2 100644
--- "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 342 PASS, 277 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 342 PASS, 279 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
index e08af44..4b4ab20 100644
--- "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 474 PASS, 147 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 474 PASS, 149 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
index e08af44..4b4ab20 100644
--- "a/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/linux/virtual/idna-2008/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 474 PASS, 147 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 474 PASS, 149 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git a/third_party/blink/web_tests/platform/mac-mac10.15/virtual/text-antialias/capitalize-boundaries-expected.png b/third_party/blink/web_tests/platform/mac-mac10.15/virtual/text-antialias/capitalize-boundaries-expected.png
new file mode 100644
index 0000000..58617080
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac-mac10.15/virtual/text-antialias/capitalize-boundaries-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac11-arm64/virtual/text-antialias/capitalize-boundaries-expected.png b/third_party/blink/web_tests/platform/mac-mac11-arm64/virtual/text-antialias/capitalize-boundaries-expected.png
new file mode 100644
index 0000000..58617080
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac-mac11-arm64/virtual/text-antialias/capitalize-boundaries-expected.png
Binary files differ
diff --git a/third_party/blink/web_tests/platform/mac-mac11/virtual/text-antialias/capitalize-boundaries-expected.png b/third_party/blink/web_tests/platform/mac-mac11/virtual/text-antialias/capitalize-boundaries-expected.png
new file mode 100644
index 0000000..58617080
--- /dev/null
+++ b/third_party/blink/web_tests/platform/mac-mac11/virtual/text-antialias/capitalize-boundaries-expected.png
Binary files differ
diff --git "a/third_party/blink/web_tests/platform/mac/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/mac/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
index ddb2a07f..6a33129 100644
--- "a/third_party/blink/web_tests/platform/mac/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/mac/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 343 PASS, 276 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 343 PASS, 278 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
index b73eb638..85e66b8 100644
--- "a/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 475 PASS, 146 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 475 PASS, 148 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git "a/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
index b73eb638..85e66b8 100644
--- "a/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/mac/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 475 PASS, 146 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 475 PASS, 148 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git a/third_party/blink/web_tests/platform/mac/virtual/text-antialias/capitalize-boundaries-expected.png b/third_party/blink/web_tests/platform/mac/virtual/text-antialias/capitalize-boundaries-expected.png
index 58617080..ebce3b5d 100644
--- a/third_party/blink/web_tests/platform/mac/virtual/text-antialias/capitalize-boundaries-expected.png
+++ b/third_party/blink/web_tests/platform/mac/virtual/text-antialias/capitalize-boundaries-expected.png
Binary files differ
diff --git "a/third_party/blink/web_tests/platform/win/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/win/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
index 7649c888b..5dd92be 100644
--- "a/third_party/blink/web_tests/platform/win/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/win/external/wpt/url/a-element-xhtml_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 338 PASS, 281 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 338 PASS, 283 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/win/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/win/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
index 7649c888b..5dd92be 100644
--- "a/third_party/blink/web_tests/platform/win/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/win/external/wpt/url/a-element_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 619 tests; 338 PASS, 281 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 621 tests; 338 PASS, 283 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -272,6 +272,8 @@
 FAIL Parsing: <http://[::1.2.3.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.2.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_unreached: Expected URL to fail parsing Reached unreachable code
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 FAIL Parsing: <#x> against <data:,> assert_equals: href expected "data:,#x" but got "test:test#x"
diff --git "a/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
index a0af15a..e0407ed 100644
--- "a/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any.worker_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 466 PASS, 155 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 466 PASS, 157 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git "a/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt" "b/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
index a0af15a..e0407ed 100644
--- "a/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
+++ "b/third_party/blink/web_tests/platform/win/external/wpt/url/url-constructor.any_exclude=\050file_javascript_mailto\051-expected.txt"
@@ -1,5 +1,5 @@
 This is a testharness.js-based test.
-Found 621 tests; 466 PASS, 155 FAIL, 0 TIMEOUT, 0 NOTRUN.
+Found 623 tests; 466 PASS, 157 FAIL, 0 TIMEOUT, 0 NOTRUN.
 PASS Loading data…
 PASS Parsing: <http://example	.
 org> against <http://example.org/foo/bar>
@@ -306,6 +306,12 @@
 FAIL Parsing: <http://[::1.]> against <http://other.com/> assert_throws_js: function "function() {
           bURL(expected.input, expected.base)
         }" did not throw
+FAIL Parsing: <http://[::%31]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
+FAIL Parsing: <http://%5B::1]> against <http://other.com/> assert_throws_js: function "function() {
+          bURL(expected.input, expected.base)
+        }" did not throw
 PASS Parsing: <http://foo:💩@example.com/bar> against <http://other.com/>
 PASS Parsing: <#> against <test:test>
 PASS Parsing: <#x> against <data:,>
diff --git a/third_party/blink/web_tests/svg/css/getComputedStyle-listing-expected.txt b/third_party/blink/web_tests/svg/css/getComputedStyle-listing-expected.txt
index 35ed31b..24168de 100644
--- a/third_party/blink/web_tests/svg/css/getComputedStyle-listing-expected.txt
+++ b/third_party/blink/web_tests/svg/css/getComputedStyle-listing-expected.txt
@@ -52,6 +52,7 @@
 alignment-baseline: auto
 anchor-name: none
 anchor-scroll: none
+animation-composition: replace
 animation-delay-end: 0s
 animation-delay-start: 0s
 animation-direction: normal
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative-expected.txt b/third_party/blink/web_tests/virtual/stable/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative-expected.txt
similarity index 100%
rename from third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative-expected.txt
rename to third_party/blink/web_tests/virtual/stable/external/wpt/css/css-animations/parsing/animation-composition-computed.tentative-expected.txt
diff --git a/third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative-expected.txt b/third_party/blink/web_tests/virtual/stable/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative-expected.txt
similarity index 100%
rename from third_party/blink/web_tests/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative-expected.txt
rename to third_party/blink/web_tests/virtual/stable/external/wpt/css/css-animations/parsing/animation-composition-valid.tentative-expected.txt
diff --git a/third_party/blink/web_tests/webexposed/css-properties-as-js-properties-expected.txt b/third_party/blink/web_tests/webexposed/css-properties-as-js-properties-expected.txt
index ea8af4f..fc11d68c 100644
--- a/third_party/blink/web_tests/webexposed/css-properties-as-js-properties-expected.txt
+++ b/third_party/blink/web_tests/webexposed/css-properties-as-js-properties-expected.txt
@@ -11,6 +11,7 @@
 anchorName
 anchorScroll
 animation
+animationComposition
 animationDelay
 animationDelayEnd
 animationDelayStart
diff --git a/third_party/blink/web_tests/webexposed/css-property-listing-expected.txt b/third_party/blink/web_tests/webexposed/css-property-listing-expected.txt
index 3c08f45..53264f5 100644
--- a/third_party/blink/web_tests/webexposed/css-property-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/css-property-listing-expected.txt
@@ -59,6 +59,7 @@
     all
     anchor-name
     anchor-scroll
+    animation-composition
     animation-delay-end
     animation-delay-start
     animation-direction
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac-expected.html b/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac-expected.html
new file mode 100644
index 0000000..e0f6443
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac-expected.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="utf-8" />
+<title>CSS Test: MacOS synthesizing glyphs</title>
+
+<body>
+  <p style="font-family: Arial;">&#x002D</p>
+  <p style="font-family: Arial;">&#x002D</p>
+  <p style="font-family: Arial;">&#x002D</p>
+</body>
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html b/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html
new file mode 100644
index 0000000..35807d3
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/css/css-fonts/font-glyph-synthesis-mac.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="utf-8" />
+<title>CSS Test: MacOS synthesizing glyphs</title>
+<link rel="match" href="font-glyph-synthesis-mac-expected.html">
+<meta name="assert"
+  content="This test is Mac specific, it checks cases when hyphen character is being synthesized in Arial font on Mac">
+
+<body>
+  <p style="font-family: Arial;">&#x002D</p>
+  <p style="font-family: Arial;">&#x2010</p>
+  <p style="font-family: Arial;">&#x2011</p>
+</body>
diff --git a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/root-and-nested-element-transition.html.ini b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/root-and-nested-element-transition.html.ini
index 74e3a92b..9767aec 100644
--- a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/root-and-nested-element-transition.html.ini
+++ b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/root-and-nested-element-transition.html.ini
@@ -1,5 +1,4 @@
 [root-and-nested-element-transition.html]
   expected:
-    if (os == "linux") and (version == "Ubuntu 18.04"): ERROR
-    if os == "win": ERROR
+    if processor == "x86": ERROR
     FAIL
diff --git a/tools/mb/mb_config.pyl b/tools/mb/mb_config.pyl
index cc1ecf4..301c730 100644
--- a/tools/mb/mb_config.pyl
+++ b/tools/mb/mb_config.pyl
@@ -24,6 +24,7 @@
       'chromeos-jacuzzi-chrome': 'chromeos_jacuzzi_include_unwind_tables_official_reclient',
       'chromeos-kevin-chrome': 'chromeos_kevin_include_unwind_tables_official_dchecks_reclient',
       'chromeos-octopus-chrome': 'chromeos_octopus_include_unwind_tables_official_dchecks_reclient',
+      'chromeos-octopus-chrome-skylab': 'chromeos_octopus_include_unwind_tables_official_dchecks_skylab',
       'chromeos-reven-chrome': 'chromeos_reven_include_unwind_tables_official_dchecks_reclient',
       'chromeos-trogdor64-chrome-skylab': 'chromeos_trogdor64_include_unwind_tables_official_dchecks_skylab_reclient',
       'lacros-amd64-generic-chrome': 'chromeos_amd64-generic_lacros_official_no_symbols_reclient',
@@ -914,6 +915,7 @@
       'chromeos-kevin-chrome': 'chromeos_kevin_include_unwind_tables_official_dchecks',
       'chromeos-kevin-compile-chrome': 'chromeos_kevin_include_unwind_tables_official_dchecks',
       'chromeos-octopus-chrome': 'chromeos_octopus_include_unwind_tables_official_dchecks',
+      'chromeos-octopus-chrome-skylab': 'chromeos_octopus_include_unwind_tables_official_dchecks_skylab',
       'chromeos-octopus-compile-chrome': 'chromeos_octopus_include_unwind_tables_official_dchecks',
       'chromeos-reven-chrome': 'chromeos_reven_include_unwind_tables_official_dchecks',
       'chromeos-trogdor64-chrome-skylab': 'chromeos_trogdor64_include_unwind_tables_official_dchecks_skylab',
@@ -2238,6 +2240,9 @@
       'chromeos_device_reclient', 'octopus', 'include_unwind_tables', 'official',
       'dcheck_always_on', 'also_build_lacros_chrome_for_architecture_amd64',
     ],
+    'chromeos_octopus_include_unwind_tables_official_dchecks_skylab': [
+      'chromeos_device', 'octopus', 'include_unwind_tables', 'official', 'dcheck_always_on', 'is_skylab',
+    ],
 
     'chromeos_reven_include_unwind_tables_official_dchecks': [
       'chromeos_device', 'reven', 'include_unwind_tables', 'official', 'dcheck_always_on',
diff --git a/tools/mb/mb_config_expectations/chrome.json b/tools/mb/mb_config_expectations/chrome.json
index ef22f2cd..6bd02c2 100644
--- a/tools/mb/mb_config_expectations/chrome.json
+++ b/tools/mb/mb_config_expectations/chrome.json
@@ -137,6 +137,19 @@
       "use_remoteexec": true
     }
   },
+  "chromeos-octopus-chrome-skylab": {
+    "args_file": "//build/args/chromeos/octopus.gni",
+    "gn_args": {
+      "dcheck_always_on": true,
+      "exclude_unwind_tables": false,
+      "is_chrome_branded": true,
+      "is_chromeos_device": true,
+      "is_official_build": true,
+      "is_skylab": true,
+      "ozone_platform_headless": true,
+      "use_goma": true
+    }
+  },
   "chromeos-reven-chrome": {
     "args_file": "//build/args/chromeos/reven-vmtest.gni",
     "gn_args": {
diff --git a/tools/mb/mb_config_expectations/tryserver.chrome.json b/tools/mb/mb_config_expectations/tryserver.chrome.json
index 0d91a95e..9a465f96 100644
--- a/tools/mb/mb_config_expectations/tryserver.chrome.json
+++ b/tools/mb/mb_config_expectations/tryserver.chrome.json
@@ -190,6 +190,19 @@
       "use_goma": true
     }
   },
+  "chromeos-octopus-chrome-skylab": {
+    "args_file": "//build/args/chromeos/octopus.gni",
+    "gn_args": {
+      "dcheck_always_on": true,
+      "exclude_unwind_tables": false,
+      "is_chrome_branded": true,
+      "is_chromeos_device": true,
+      "is_official_build": true,
+      "is_skylab": true,
+      "ozone_platform_headless": true,
+      "use_goma": true
+    }
+  },
   "chromeos-octopus-compile-chrome": {
     "args_file": "//build/args/chromeos/octopus.gni",
     "gn_args": {
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 81b9c79..8ba9568 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -37111,6 +37111,9 @@
 </enum>
 
 <enum name="FcmTokenRevocation">
+  <obsolete>
+    Removed 2023/01 with the removal of the relevant histogram.
+  </obsolete>
   <int value="0" label="Reset grace period"/>
   <int value="1" label="Revoke permission"/>
   <int value="2" label="Grace period is not over"/>
@@ -66430,6 +66433,7 @@
   <int value="740" label="animation-range"/>
   <int value="741" label="animation-range-start"/>
   <int value="742" label="animation-range-end"/>
+  <int value="743" label="animation-composition"/>
 </enum>
 
 <enum name="MappedEditingCommands">
@@ -89162,6 +89166,17 @@
       label="User pasted something on the page and the warning was shown."/>
 </enum>
 
+<enum name="SafeBrowsingHPRTOperationResult">
+  <int value="0" label="Success"/>
+  <int value="1" label="Parse response error"/>
+  <int value="2" label="No cache duration in response"/>
+  <int value="3" label="Incorrect full hash length in response"/>
+  <int value="4" label="Retriable error"/>
+  <int value="5" label="Network error"/>
+  <int value="6" label="HTTP error"/>
+  <int value="7" label="Bug: Reached NOTREACHED code"/>
+</enum>
+
 <enum name="SafeBrowsingMalwareDeepScanningVerdict">
   <int value="0" label="Verdict unspecified"/>
   <int value="1" label="Clean"/>
diff --git a/tools/metrics/histograms/metadata/network/histograms.xml b/tools/metrics/histograms/metadata/network/histograms.xml
index 8e1cb9d4..2f599cf 100644
--- a/tools/metrics/histograms/metadata/network/histograms.xml
+++ b/tools/metrics/histograms/metadata/network/histograms.xml
@@ -96,6 +96,20 @@
   </summary>
 </histogram>
 
+<histogram name="Network.Ash.Cellular.Apn.CustomApns.{CustomApnState}.Count"
+    units="count" expires_after="2023-10-18">
+  <owner>gordonseto@google.com</owner>
+  <owner>cros-connectivity@google.com</owner>
+  <summary>
+    Records the number of {CustomApnState} APNs saved for a cellular network
+    every time a cellular network successfully connects.
+  </summary>
+  <token key="CustomApnState">
+    <variant name="Disabled"/>
+    <variant name="Enabled"/>
+  </token>
+</histogram>
+
 <histogram name="Network.Ash.Cellular.Apn.DisableCustomApn.ApnTypes"
     enum="ApnTypes" expires_after="2023-10-18">
   <owner>gordonseto@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/permissions/histograms.xml b/tools/metrics/histograms/metadata/permissions/histograms.xml
index 4b5ff46..960285a 100644
--- a/tools/metrics/histograms/metadata/permissions/histograms.xml
+++ b/tools/metrics/histograms/metadata/permissions/histograms.xml
@@ -510,6 +510,11 @@
 
 <histogram name="Permissions.FCM.Revocation" enum="FcmTokenRevocation"
     expires_after="2023-10-31">
+  <obsolete>
+    Removed in 2023/01, as it's no longer needed. To track Notification
+    revocation use `Notifications.AppNotificationStatus` and
+    `PushMessaging.DeliveryStatus`.
+  </obsolete>
   <owner>elklm@chromium.org</owner>
   <owner>src/components/permissions/PERMISSIONS_OWNERS</owner>
   <summary>
@@ -520,6 +525,11 @@
 
 <histogram name="Permissions.FCM.Revocation.ResetGracePeriod" units="ms"
     expires_after="2023-10-31">
+  <obsolete>
+    Removed in 2023/01, as it's no longer needed. To track Notification
+    revocation use `Notifications.AppNotificationStatus` and
+    `PushMessaging.DeliveryStatus`.
+  </obsolete>
   <owner>elklm@chromium.org</owner>
   <owner>src/components/permissions/PERMISSIONS_OWNERS</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
index c018968..f87326f 100644
--- a/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
+++ b/tools/metrics/histograms/metadata/safe_browsing/histograms.xml
@@ -826,6 +826,181 @@
   </summary>
 </histogram>
 
+<histogram name="SafeBrowsing.HPRT.AllowlistSizeTooSmall"
+    enum="BooleanUnavailable" expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs whether the size of the high confidence allowlist is too small. It can
+    happen if it is misconfigured on the server-side, or the local file is
+    corrupted on the disk. If too small, the allowlist is considered as
+    unavailable. Logged each time a top frame URL navigation happens for users
+    who have hash-prefix real-time lookups enabled.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.AllStoresAvailable" enum="BooleanAvailable"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs whether the local Safe Browsing stores are available. If not, all URLs
+    are marked as safe during the lookup. Logged each time a top frame URL
+    navigation happens for users who have hash-prefix real-time lookups enabled.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Backoff.State" enum="BooleanEnabled"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs if the hash-prefix real-time lookup service is currently in backoff
+    state due to previous errors. Logged whenever the hash-prefix real-time
+    check is enabled and is run for a particular URL.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Cache.FullHashCount" units="entries"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the number of full hashes in the local hash-prefix real-time lookup
+    cache for this profile. Logged each time cleanup of expired entries is
+    performed on the cache, which happens every 30 minutes. The value represents
+    the count before cleanup and includes expired entries.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Cache.HashPrefixCount" units="entries"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the number of hash prefixes in the local hash-prefix real-time
+    lookup cache for this profile. Logged each time cleanup of expired entries
+    is performed on the cache, which happens every 30 minutes. The value
+    represents the count before cleanup and includes expired entries.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.CacheHit" enum="BooleanCacheHit"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records whether there was a cache hit when a hash prefix was searched for in
+    the local cache for hash-prefix real-time lookups. A cache hit means the
+    prefix was found in the cache (whether or not it has associated full
+    hashes), which allows for that prefix not to be re-requested. This histogram
+    is false in the case of a cache miss for the hash prefix. This is logged for
+    each hash prefix in a lookup when the cache is searched.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.CacheHitAllPrefixes" enum="BooleanCacheHit"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records whether all hash prefixes were found in the hash-prefix real-time
+    lookups local cache. It indicates that as a result of the cache, there was
+    no need to send a request to Safe Browsing servers. This is logged each time
+    a lookup occurs, when the cache is searched.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.GetCache.Time" units="ms"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs the latency between the start of getting a result from the cache and
+    when the cached item is actually obtained for hash-prefix real-time lookups.
+    This is an indicator of the efficiency of retrieving from the cache.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.IsLookupServiceAvailable"
+    enum="BooleanAvailable" expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs whether the hash-prefix real-time lookup service is available when a
+    hash-prefix real-time check is needed. The lookup service is available if it
+    is not nullptr and it is not in backoff mode. Logged when the hash-prefix
+    real-time check is enabled and the URL doesn't match the allowlist.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.LocalMatch.Result"
+    enum="SafeBrowsingAllowlistAsyncMatch" expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs the result of checking the URL against the local Safe Browsing high
+    confidence allowlist. Logged each time a top frame URL navigation happens
+    for users who have hash-prefix real-time lookups enabled.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Network.Result"
+    enum="CombinedHttpResponseAndNetErrorCode" expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Response or error codes from SafeBrowsing hash-prefix real-time lookups.
+    Logged on each resource check for which a lookup request is sent to the
+    server.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Network.Time" units="ms"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs the roundtrip time it took to perform a Safe Browsing hash-prefix
+    real-time lookup. Logged on each resource check for which a lookup request
+    is sent to the server.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.OperationResult"
+    enum="SafeBrowsingHPRTOperationResult" expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Records the return status for hash-prefix real-time lookups. This is logged
+    after network request for the lookup returns.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.Request.CountOfPrefixes" units="prefixes"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    The number of prefixes included in a hash-prefix real-time lookup request
+    sent to the Google Safe Browsing servers. This excludes any prefixes found
+    in the local cache.
+  </summary>
+</histogram>
+
+<histogram name="SafeBrowsing.HPRT.ThreatInfoSize" units="verdicts"
+    expires_after="2023-07-20">
+  <owner>thefrog@chromium.org</owner>
+  <owner>chrome-counter-abuse-alerts@google.com</owner>
+  <summary>
+    Logs the number of unsafe threat type verdicts received in the check
+    response (including cached results) that are relevant to hash-prefix
+    real-time checks. Logged each time a lookup request completes without errors
+    or when the request does not need to be sent because all the results are in
+    the local cache.
+  </summary>
+</histogram>
+
 <histogram name="SafeBrowsing.LocalBinaryUploadRequest.DlpResult"
     enum="BooleanSuccess" expires_after="2023-10-20">
   <owner>rogerta@chromium.org</owner>
@@ -1373,7 +1548,8 @@
     Logs the result of checking the {IsMainframe} URL against the local Safe
     Browsing high confidence allowlist for {UserCategory} real time URL check
     request. Logged each time a top frame URL navigation happens for users who
-    have the real time URL lookups enabled.
+    have the real time URL lookups enabled. It can also be logged for
+    non-mainframe URL hits for EnhancedProtection users.
   </summary>
   <token key="IsMainframe">
     <variant name="Mainframe" summary="mainframe"/>
diff --git a/tools/metrics/histograms/metadata/side_search/histograms.xml b/tools/metrics/histograms/metadata/side_search/histograms.xml
index 973fc384..fb7becf4 100644
--- a/tools/metrics/histograms/metadata/side_search/histograms.xml
+++ b/tools/metrics/histograms/metadata/side_search/histograms.xml
@@ -24,7 +24,7 @@
 
 <histogram
     name="SideSearch.AutoTrigger.NavigationCommittedWithinSideSearchCountPerJourney"
-    units="navigations" expires_after="2023-06-18">
+    units="navigations" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -36,7 +36,7 @@
 </histogram>
 
 <histogram name="SideSearch.AutoTrigger.RedirectionToTabCountPerJourney"
-    units="navigations" expires_after="2023-06-18">
+    units="navigations" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -48,7 +48,7 @@
 </histogram>
 
 <histogram name="SideSearch.AvailabilityChanged"
-    enum="SideSearchAvailabilityChangeType" expires_after="2023-06-18">
+    enum="SideSearchAvailabilityChangeType" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -60,7 +60,7 @@
 </histogram>
 
 <histogram name="SideSearch.CloseAction" enum="SideSearchCloseActionType"
-    expires_after="2023-04-16">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -71,7 +71,7 @@
 </histogram>
 
 <histogram name="SideSearch.LoadCompletedTime" units="ms"
-    expires_after="2023-06-18">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -83,7 +83,7 @@
 </histogram>
 
 <histogram name="SideSearch.LoadDocumentTime" units="ms"
-    expires_after="2023-06-18">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -95,7 +95,7 @@
 </histogram>
 
 <histogram name="SideSearch.Navigation" enum="SideSearchNavigationType"
-    expires_after="2023-06-18">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -107,7 +107,7 @@
 </histogram>
 
 <histogram name="SideSearch.NavigationCommittedWithinSideSearchCountPerJourney"
-    units="navigations" expires_after="2023-02-05">
+    units="navigations" expires_after="2023-12-13">
   <obsolete>
     Removed in M107
   </obsolete>
@@ -123,7 +123,7 @@
 
 <histogram
     name="SideSearch.NavigationCommittedWithinSideSearchCountPerJourney2"
-    units="navigations" expires_after="2023-06-11">
+    units="navigations" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -136,7 +136,7 @@
 
 <histogram
     name="SideSearch.NavigationCommittedWithinSideSearchCountPerJourneyFromMenuOption"
-    units="navigations" expires_after="2023-06-04">
+    units="navigations" expires_after="2023-12-13">
   <owner>pengchaocai@chromium.org</owner>
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
@@ -149,7 +149,7 @@
 </histogram>
 
 <histogram name="SideSearch.OpenAction" enum="SideSearchOpenActionType"
-    expires_after="2023-06-11">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -160,7 +160,7 @@
 </histogram>
 
 <histogram name="SideSearch.PageActionIcon.LabelVisibleWhenToggled"
-    enum="SideSearchPageActionLabelVisibility" expires_after="2023-02-12">
+    enum="SideSearchPageActionLabelVisibility" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -171,7 +171,7 @@
 </histogram>
 
 <histogram name="SideSearch.RedirectionToTabCountPerJourney2"
-    units="navigations" expires_after="2023-02-05">
+    units="navigations" expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <owner>romanarora@chromium.org</owner>
@@ -183,7 +183,7 @@
 </histogram>
 
 <histogram name="SideSearch.RedirectionToTabCountPerJourneyFromMenuOption"
-    units="navigations" expires_after="2023-06-04">
+    units="navigations" expires_after="2023-12-13">
   <owner>pengchaocai@chromium.org</owner>
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
@@ -196,7 +196,7 @@
 </histogram>
 
 <histogram name="SideSearch.SidePanel.TimeShownOpenedVia{OpenAction}"
-    units="ms" expires_after="2023-02-12">
+    units="ms" expires_after="2023-12-13">
   <owner>tluk@chromium.org</owner>
   <owner>yuhengh@chromium.org</owner>
   <summary>
@@ -211,7 +211,7 @@
 </histogram>
 
 <histogram name="SideSearch.TimeSinceSidePanelAvailableToFirstOpen" units="ms"
-    expires_after="2023-06-25">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
@@ -222,7 +222,7 @@
 </histogram>
 
 <histogram name="SideSearch.TimesReturnedBackToSRP" units="navigations"
-    expires_after="2023-06-25">
+    expires_after="2023-12-13">
   <owner>yuhengh@chromium.org</owner>
   <owner>tluk@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/sync/histograms.xml b/tools/metrics/histograms/metadata/sync/histograms.xml
index 96a7752..952c8d8 100644
--- a/tools/metrics/histograms/metadata/sync/histograms.xml
+++ b/tools/metrics/histograms/metadata/sync/histograms.xml
@@ -969,6 +969,8 @@
     <variant name="DecryptionPendingForTooLong"
         summary="Decryption keys were missing for so long that the update was
                  ignored."/>
+    <variant name="DroppedByBridge"
+        summary="The bridge classified the data as bad/invalid."/>
     <variant name="FailedToDecrypt"
         summary="Decryption is not successful (maybe the data is corrupt)."/>
     <variant name="InconsistentClientTag"
diff --git a/tools/rust/update_rust.py b/tools/rust/update_rust.py
index 24e3bd36..71e72e1 100755
--- a/tools/rust/update_rust.py
+++ b/tools/rust/update_rust.py
@@ -75,14 +75,19 @@
 
 
 # Package version built in build_rust.py, which is always built against the
-# latest Clang and never uses the FALLBACK_CLANG_VERSION.
+# current Clang. Typically Clang and Rust revisions are both updated together
+# and this picks the Clang that has just been built.
 def GetPackageVersionForBuild():
     from update import (CLANG_REVISION, CLANG_SUB_REVISION)
-    return GetPackageVersion(f'{CLANG_REVISION}-{CLANG_SUB_REVISION}')
+    return (f'{RUST_REVISION}-{RUST_SUB_REVISION}'
+            '-{CLANG_REVISION}-{CLANG_SUB_REVISION}')
 
 
-# Package version for download, which may differ from GetUploadPackageVersion()
-# if FALLBACK_CLANG_VERSION is set.
+# Package version for download. Ideally this is the latest Clang+Rust roll,
+# which was built successfully and is returned from GetPackageVersionForBuild().
+# However at this time Clang rolls even if Rust fails to build, so we have Rust
+# pinned to the last known successful build with FALLBACK_REVISION. This should
+# go away once we block Clang rolls on Rust also being built.
 def GetDownloadPackageVersion():
     return FALLBACK_REVISION \
         if FALLBACK_REVISION else GetPackageVersionForBuild()
diff --git a/ui/accessibility/platform/ax_platform_node_win.cc b/ui/accessibility/platform/ax_platform_node_win.cc
index ca2443a6..9a20af9d 100644
--- a/ui/accessibility/platform/ax_platform_node_win.cc
+++ b/ui/accessibility/platform/ax_platform_node_win.cc
@@ -5597,6 +5597,7 @@
       guidService == IID_IAccessibleTable2 ||
       guidService == IID_IAccessibleTableCell ||
       guidService == IID_IAccessibleText ||
+      guidService == IID_IAccessibleTextSelectionContainer ||
       guidService == IID_IAccessibleValue) {
     return QueryInterface(riid, object);
   }
diff --git a/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.cc b/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.cc
index 7992fc2..6ba575a 100644
--- a/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.cc
+++ b/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.cc
@@ -10,11 +10,16 @@
 #include "ui/accessibility/platform/inspect/ax_inspect_utils_win.h"
 #include "ui/accessibility/platform/inspect/ax_property_node.h"
 
-#define DEFINE_IA2_QI_ENTRY(ia2_interface)                                \
-  if (interface_name == #ia2_interface) {                                 \
-    Microsoft::WRL::ComPtr<ia2_interface> obj;                            \
-    if (S_OK == ui::IA2QueryInterface<ia2_interface>(target.Get(), &obj)) \
-      return AXOptionalObject(Target(obj));                               \
+#define DEFINE_IA2_QI_ENTRY(ia2_interface)                                    \
+  if (interface_name == #ia2_interface) {                                     \
+    Microsoft::WRL::ComPtr<ia2_interface> obj;                                \
+    HRESULT hr = ui::IA2QueryInterface<ia2_interface>(target.Get(), &obj);    \
+    if (hr == S_OK)                                                           \
+      return AXOptionalObject({obj});                                         \
+    if (hr == E_NOINTERFACE)                                                  \
+      return AXOptionalObject::Error(interface_name + " is not implemented"); \
+    return AXOptionalObject::Error("Unexpected error when querying " +        \
+                                   interface_name);                           \
   }
 
 namespace ui {
@@ -22,7 +27,14 @@
 // static
 std::string AXCallStatementInvokerWin::ToString(
     const AXOptionalObject& optional) {
-  return optional.HasValue() ? optional->ToString() : optional.StateToString();
+  if (optional.HasValue()) {
+    return optional->ToString();
+  }
+
+  if (optional.IsError()) {
+    return "Error:\"" + optional.StateText() + "\"";
+  }
+  return optional.StateToString();
 }
 
 AXCallStatementInvokerWin::AXCallStatementInvokerWin(
@@ -124,6 +136,11 @@
   if (target.Is<IA2TextComPtr>())
     return InvokeForIA2Text(target.As<IA2TextComPtr>(), property_node);
 
+  if (target.Is<IA2TextSelectionContainerComPtr>()) {
+    return InvokeForIA2TextSelectionContainer(
+        target.As<IA2TextSelectionContainerComPtr>(), property_node);
+  }
+
   if (target.Is<IA2ValueComPtr>())
     return InvokeForIA2Value(target.As<IA2ValueComPtr>(), property_node);
 
@@ -238,6 +255,16 @@
   // - [ ] test.getInterface(IAccessibleTableCell).get_columnExtent
 }
 
+AXOptionalObject AXCallStatementInvokerWin::InvokeForIA2TextSelectionContainer(
+    IA2TextSelectionContainerComPtr target,
+    const AXPropertyNode& property_node) const {
+  if (property_node.name_or_value == "selections") {
+    return GetSelections(target);
+  }
+
+  return AXOptionalObject::Error();
+}
+
 AXOptionalObject AXCallStatementInvokerWin::InvokeForIA2Text(
     IA2TextComPtr target,
     const AXPropertyNode& property_node) const {
@@ -320,10 +347,12 @@
   DEFINE_IA2_QI_ENTRY(IAccessibleHypertext)
   DEFINE_IA2_QI_ENTRY(IAccessibleTable)
   DEFINE_IA2_QI_ENTRY(IAccessibleTableCell)
+  DEFINE_IA2_QI_ENTRY(IAccessibleTextSelectionContainer)
   DEFINE_IA2_QI_ENTRY(IAccessibleText)
   DEFINE_IA2_QI_ENTRY(IAccessibleValue)
 
-  return AXOptionalObject::Error();
+  return AXOptionalObject::Error("Unsupported " + interface_name +
+                                 " interface");
 }
 
 AXOptionalObject AXCallStatementInvokerWin::GetIA2Role(IA2ComPtr target) const {
@@ -331,6 +360,7 @@
   if (SUCCEEDED(target->role(&role)))
     return AXOptionalObject(
         Target(base::WideToUTF8(IAccessible2RoleToString(role))));
+
   return AXOptionalObject::Error();
 }
 
@@ -372,6 +402,16 @@
   return AXOptionalObject::Error();
 }
 
+AXOptionalObject AXCallStatementInvokerWin::GetSelections(
+    IA2TextSelectionContainerComPtr target) const {
+  ScopedCoMemArray<IA2TextSelection> selections;
+  if (target->get_selections(selections.Receive(), selections.ReceiveSize()) ==
+      S_OK) {
+    return AXOptionalObject({std::move(selections)});
+  }
+  return AXOptionalObject::Error();
+}
+
 bool AXCallStatementInvokerWin::IsIAccessibleAndNotNull(
     const Target& target) const {
   return target.Is<IAccessibleComPtr>() &&
diff --git a/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.h b/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.h
index 63b4e79..76bca2a 100644
--- a/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.h
+++ b/ui/accessibility/platform/inspect/ax_call_statement_invoker_win.h
@@ -55,6 +55,9 @@
   AXOptionalObject InvokeForIA2TableCell(
       IA2TableCellComPtr target,
       const AXPropertyNode& property_node) const;
+  AXOptionalObject InvokeForIA2TextSelectionContainer(
+      IA2TextSelectionContainerComPtr target,
+      const AXPropertyNode& property_node) const;
   AXOptionalObject InvokeForIA2Text(IA2TextComPtr target,
                                     const AXPropertyNode& property_node) const;
   AXOptionalObject InvokeForIA2Value(IA2ValueComPtr target,
@@ -80,6 +83,10 @@
   // AccessibleTable functionality
   AXOptionalObject GetSelectedColumns(const IA2TableComPtr target) const;
 
+  // IAccessibleSelectionContainer functionality.
+  AXOptionalObject GetSelections(
+      const IA2TextSelectionContainerComPtr target) const;
+
   bool IsIAccessibleAndNotNull(const Target& target) const;
 
   // Map between IAccessible objects and their DOMIds/accessible tree
diff --git a/ui/accessibility/platform/inspect/ax_target_win.cc b/ui/accessibility/platform/inspect/ax_target_win.cc
index 4c595661..019aa8f 100644
--- a/ui/accessibility/platform/inspect/ax_target_win.cc
+++ b/ui/accessibility/platform/inspect/ax_target_win.cc
@@ -37,6 +37,10 @@
   if (Is<IA2TextComPtr>())
     return "IAccessible2TextInterface";
 
+  if (Is<IA2TextSelectionContainerComPtr>()) {
+    return "IA2TextSelectionContainerComPtr";
+  }
+
   if (Is<IA2ValueComPtr>())
     return "IAccessible2ValueInterface";
 
@@ -57,6 +61,26 @@
     return '[' + str + ']';
   }
 
+  if (Is<ScopedCoMemArray<IA2TextSelection>>()) {
+    std::string str;
+    for (const IA2TextSelection& selection :
+         As<ScopedCoMemArray<IA2TextSelection>>()) {
+      if (!str.empty()) {
+        str += ", ";
+      }
+      // TODO(alexs): replace <obj> on something more useful.
+      // It could be a line number the accessible object is placed at in
+      // the stringified accessible tree, potentially including additional
+      // properties making the accessible object identification easier such as
+      // DOM id or accessible role/name, for instance, :3.textbox.
+      str += "{startObj: <obj>, startOffset: " +
+             base::NumberToString(selection.startOffset) +
+             ", endObj: <obj>, endOffset: " +
+             base::NumberToString(selection.endOffset) + "}";
+    }
+    return '[' + str + ']';
+  }
+
   return "Unsupported";
 }
 
diff --git a/ui/accessibility/platform/inspect/ax_target_win.h b/ui/accessibility/platform/inspect/ax_target_win.h
index ab98a6df..f5856bd3 100644
--- a/ui/accessibility/platform/inspect/ax_target_win.h
+++ b/ui/accessibility/platform/inspect/ax_target_win.h
@@ -22,6 +22,8 @@
 using IA2TableComPtr = Microsoft::WRL::ComPtr<IAccessibleTable>;
 using IA2TableCellComPtr = Microsoft::WRL::ComPtr<IAccessibleTableCell>;
 using IA2TextComPtr = Microsoft::WRL::ComPtr<IAccessibleText>;
+using IA2TextSelectionContainerComPtr =
+    Microsoft::WRL::ComPtr<IAccessibleTextSelectionContainer>;
 using IA2ValueComPtr = Microsoft::WRL::ComPtr<IAccessibleValue>;
 
 class COMPONENT_EXPORT(AX_PLATFORM) AXTargetWin final {
@@ -66,8 +68,10 @@
                                     IA2TableComPtr,
                                     IA2TableCellComPtr,
                                     IA2TextComPtr,
+                                    IA2TextSelectionContainerComPtr,
                                     IA2ValueComPtr,
-                                    ScopedCoMemArray<LONG>>;
+                                    ScopedCoMemArray<LONG>,
+                                    ScopedCoMemArray<IA2TextSelection>>;
 
   // Keep the value const to prevent accidental change of the value shared
   // between multiple instances of AXTargetWin.
diff --git a/ui/gl/gl_image.cc b/ui/gl/gl_image.cc
index c0a12ee..a92b637 100644
--- a/ui/gl/gl_image.cc
+++ b/ui/gl/gl_image.cc
@@ -58,10 +58,6 @@
   return Type::NONE;
 }
 
-scoped_refptr<gfx::NativePixmap> GLImage::GetNativePixmap() {
-  return nullptr;
-}
-
 void* GLImage::GetEGLImage() const {
   return nullptr;
 }
diff --git a/ui/gl/gl_image.h b/ui/gl/gl_image.h
index d5f5c6b..ae28aed4 100644
--- a/ui/gl/gl_image.h
+++ b/ui/gl/gl_image.h
@@ -18,7 +18,6 @@
 #include "ui/gfx/geometry/rect_f.h"
 #include "ui/gfx/geometry/size.h"
 #include "ui/gfx/gpu_fence.h"
-#include "ui/gfx/native_pixmap.h"
 #include "ui/gfx/native_widget_types.h"
 #include "ui/gfx/overlay_transform.h"
 #include "ui/gl/gl_export.h"
@@ -114,10 +113,6 @@
   enum class Type { NONE, EGL_STREAM, D3D, PBUFFER };
   virtual Type GetType() const;
 
-  // Returns the NativePixmap backing the GLImage. If not backed by a
-  // NativePixmap, returns null.
-  virtual scoped_refptr<gfx::NativePixmap> GetNativePixmap();
-
   virtual void* GetEGLImage() const;
 
  private:
diff --git a/ui/gl/gl_image_native_pixmap.h b/ui/gl/gl_image_native_pixmap.h
index 7c3fd44..cb8ec9b 100644
--- a/ui/gl/gl_image_native_pixmap.h
+++ b/ui/gl/gl_image_native_pixmap.h
@@ -51,6 +51,9 @@
   // It is aligned with glTexImage{2|3}D's parameter |internalformat|.
   unsigned GetInternalFormat();
 
+  // Returns the NativePixmap backing this instance.
+  scoped_refptr<gfx::NativePixmap> GetNativePixmap();
+
   // Overridden from GLImage:
   gfx::Size GetSize() override;
   void* GetEGLImage() const override;
@@ -58,7 +61,6 @@
   void OnMemoryDump(base::trace_event::ProcessMemoryDump* pmd,
                     uint64_t process_tracing_id,
                     const std::string& dump_name) override;
-  scoped_refptr<gfx::NativePixmap> GetNativePixmap() override;
 
  protected:
   ~GLImageNativePixmap() override;
diff --git a/ui/wm/BUILD.gn b/ui/wm/BUILD.gn
index 02b9482..2450d39 100644
--- a/ui/wm/BUILD.gn
+++ b/ui/wm/BUILD.gn
@@ -28,8 +28,6 @@
     "core/cursor_manager.h",
     "core/cursor_util.cc",
     "core/cursor_util.h",
-    "core/cursors_aura.cc",
-    "core/cursors_aura.h",
     "core/default_activation_client.cc",
     "core/default_activation_client.h",
     "core/default_screen_position_client.cc",
@@ -176,13 +174,17 @@
     "//ui/gfx:gfx_skia",
     "//ui/gfx/animation",
     "//ui/gfx/geometry",
+    "//ui/gfx/geometry:geometry_skia",
     "//ui/gl:test_support",
     "//ui/platform_window",
     "//ui/resources",
     "//ui/wm/public",
   ]
 
-  data_deps = [ "//ui/resources:ui_test_pak_data" ]
+  data_deps = [
+    "//ui/resources:ui_test_pak_data",
+    "//ui/resources:ui_test_pak_data_200_percent",
+  ]
 
   if (is_chromeos_ash) {
     sources += [ "core/ime_util_chromeos_unittest.cc" ]
diff --git a/ui/wm/core/cursor_util.cc b/ui/wm/core/cursor_util.cc
index 0a43a09..c8963ff4 100644
--- a/ui/wm/core/cursor_util.cc
+++ b/ui/wm/core/cursor_util.cc
@@ -9,17 +9,15 @@
 #include "base/ranges/algorithm.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/base/cursor/cursor.h"
+#include "ui/base/cursor/cursor_size.h"
 #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
 #include "ui/base/resource/resource_bundle.h"
 #include "ui/gfx/geometry/point.h"
-#include "ui/gfx/geometry/point_conversions.h"
 #include "ui/gfx/geometry/size.h"
-#include "ui/gfx/geometry/size_conversions.h"
-#include "ui/gfx/geometry/skia_conversions.h"
 #include "ui/gfx/image/image_skia.h"
 #include "ui/gfx/image/image_skia_rep.h"
 #include "ui/gfx/skbitmap_operations.h"
-#include "ui/wm/core/cursors_aura.h"
+#include "ui/resources/grit/ui_resources.h"
 
 namespace wm {
 
@@ -103,6 +101,254 @@
   }
 }
 
+struct HotPoint {
+  int x;
+  int y;
+};
+
+struct CursorData {
+  CursorType id;
+  int resource_id;
+  HotPoint hot_1x;
+  HotPoint hot_2x;
+};
+
+struct CursorSizeData {
+  const ui::CursorSize id;
+  const CursorData* cursors;
+  const int length;
+};
+
+const CursorData kNormalCursors[] = {
+    {CursorType::kNull, IDR_AURA_CURSOR_PTR, {4, 4}, {7, 7}},
+    {CursorType::kPointer, IDR_AURA_CURSOR_PTR, {4, 4}, {7, 7}},
+    {CursorType::kNoDrop, IDR_AURA_CURSOR_NO_DROP, {9, 9}, {18, 18}},
+    {CursorType::kNotAllowed, IDR_AURA_CURSOR_NO_DROP, {9, 9}, {18, 18}},
+    {CursorType::kCopy, IDR_AURA_CURSOR_COPY, {9, 9}, {18, 18}},
+    {CursorType::kHand, IDR_AURA_CURSOR_HAND, {9, 4}, {19, 8}},
+    {CursorType::kMove, IDR_AURA_CURSOR_MOVE, {11, 11}, {23, 23}},
+    {CursorType::kNorthEastResize,
+     IDR_AURA_CURSOR_NORTH_EAST_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kSouthWestResize,
+     IDR_AURA_CURSOR_SOUTH_WEST_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kSouthEastResize,
+     IDR_AURA_CURSOR_SOUTH_EAST_RESIZE,
+     {11, 11},
+     {24, 23}},
+    {CursorType::kNorthWestResize,
+     IDR_AURA_CURSOR_NORTH_WEST_RESIZE,
+     {11, 11},
+     {24, 23}},
+    {CursorType::kNorthResize,
+     IDR_AURA_CURSOR_NORTH_RESIZE,
+     {11, 12},
+     {23, 23}},
+    {CursorType::kSouthResize,
+     IDR_AURA_CURSOR_SOUTH_RESIZE,
+     {11, 12},
+     {23, 23}},
+    {CursorType::kEastResize, IDR_AURA_CURSOR_EAST_RESIZE, {12, 11}, {25, 23}},
+    {CursorType::kWestResize, IDR_AURA_CURSOR_WEST_RESIZE, {12, 11}, {25, 23}},
+    {CursorType::kIBeam, IDR_AURA_CURSOR_IBEAM, {12, 12}, {24, 25}},
+    {CursorType::kAlias, IDR_AURA_CURSOR_ALIAS, {8, 6}, {15, 11}},
+    {CursorType::kCell, IDR_AURA_CURSOR_CELL, {11, 11}, {24, 23}},
+    {CursorType::kContextMenu, IDR_AURA_CURSOR_CONTEXT_MENU, {4, 4}, {8, 9}},
+    {CursorType::kCross, IDR_AURA_CURSOR_CROSSHAIR, {12, 12}, {24, 24}},
+    {CursorType::kHelp, IDR_AURA_CURSOR_HELP, {4, 4}, {8, 9}},
+    {CursorType::kVerticalText,
+     IDR_AURA_CURSOR_XTERM_HORIZ,
+     {12, 11},
+     {26, 23}},
+    {CursorType::kZoomIn, IDR_AURA_CURSOR_ZOOM_IN, {10, 10}, {20, 20}},
+    {CursorType::kZoomOut, IDR_AURA_CURSOR_ZOOM_OUT, {10, 10}, {20, 20}},
+    {CursorType::kRowResize, IDR_AURA_CURSOR_ROW_RESIZE, {11, 12}, {23, 23}},
+    {CursorType::kColumnResize, IDR_AURA_CURSOR_COL_RESIZE, {12, 11}, {25, 23}},
+    {CursorType::kEastWestNoResize,
+     IDR_AURA_CURSOR_EAST_WEST_NO_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kEastWestResize,
+     IDR_AURA_CURSOR_EAST_WEST_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kNorthSouthNoResize,
+     IDR_AURA_CURSOR_NORTH_SOUTH_NO_RESIZE,
+     {11, 12},
+     {23, 23}},
+    {CursorType::kNorthSouthResize,
+     IDR_AURA_CURSOR_NORTH_SOUTH_RESIZE,
+     {11, 12},
+     {23, 23}},
+    {CursorType::kNorthEastSouthWestNoResize,
+     IDR_AURA_CURSOR_NORTH_EAST_SOUTH_WEST_NO_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kNorthEastSouthWestResize,
+     IDR_AURA_CURSOR_NORTH_EAST_SOUTH_WEST_RESIZE,
+     {12, 11},
+     {25, 23}},
+    {CursorType::kNorthWestSouthEastNoResize,
+     IDR_AURA_CURSOR_NORTH_WEST_SOUTH_EAST_NO_RESIZE,
+     {11, 11},
+     {24, 23}},
+    {CursorType::kNorthWestSouthEastResize,
+     IDR_AURA_CURSOR_NORTH_WEST_SOUTH_EAST_RESIZE,
+     {11, 11},
+     {24, 23}},
+    {CursorType::kGrab, IDR_AURA_CURSOR_GRAB, {8, 5}, {16, 10}},
+    {CursorType::kGrabbing, IDR_AURA_CURSOR_GRABBING, {9, 9}, {18, 18}},
+    {CursorType::kWait, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}},
+    {CursorType::kProgress, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}},
+};
+
+const CursorData kLargeCursors[] = {
+    // The 2x hotspots should be double of the 1x, even though the cursors are
+    // shown as same size as 1x (64x64), because in 2x dpi screen, the 1x large
+    // cursor assets (64x64) are internally enlarged to the double size
+    // (128x128)
+    // by ResourceBundleImageSource.
+    {CursorType::kNull, IDR_AURA_CURSOR_BIG_PTR, {10, 10}, {20, 20}},
+    {CursorType::kPointer, IDR_AURA_CURSOR_BIG_PTR, {10, 10}, {20, 20}},
+    {CursorType::kNoDrop, IDR_AURA_CURSOR_BIG_NO_DROP, {10, 10}, {20, 20}},
+    {CursorType::kNotAllowed, IDR_AURA_CURSOR_BIG_NO_DROP, {10, 10}, {20, 20}},
+    {CursorType::kCopy, IDR_AURA_CURSOR_BIG_COPY, {21, 11}, {42, 22}},
+    {CursorType::kHand, IDR_AURA_CURSOR_BIG_HAND, {25, 7}, {50, 14}},
+    {CursorType::kMove, IDR_AURA_CURSOR_BIG_MOVE, {32, 31}, {64, 62}},
+    {CursorType::kNorthEastResize,
+     IDR_AURA_CURSOR_BIG_NORTH_EAST_RESIZE,
+     {31, 28},
+     {62, 56}},
+    {CursorType::kSouthWestResize,
+     IDR_AURA_CURSOR_BIG_SOUTH_WEST_RESIZE,
+     {31, 28},
+     {62, 56}},
+    {CursorType::kSouthEastResize,
+     IDR_AURA_CURSOR_BIG_SOUTH_EAST_RESIZE,
+     {28, 28},
+     {56, 56}},
+    {CursorType::kNorthWestResize,
+     IDR_AURA_CURSOR_BIG_NORTH_WEST_RESIZE,
+     {28, 28},
+     {56, 56}},
+    {CursorType::kNorthResize,
+     IDR_AURA_CURSOR_BIG_NORTH_RESIZE,
+     {29, 32},
+     {58, 64}},
+    {CursorType::kSouthResize,
+     IDR_AURA_CURSOR_BIG_SOUTH_RESIZE,
+     {29, 32},
+     {58, 64}},
+    {CursorType::kEastResize,
+     IDR_AURA_CURSOR_BIG_EAST_RESIZE,
+     {35, 29},
+     {70, 58}},
+    {CursorType::kWestResize,
+     IDR_AURA_CURSOR_BIG_WEST_RESIZE,
+     {35, 29},
+     {70, 58}},
+    {CursorType::kIBeam, IDR_AURA_CURSOR_BIG_IBEAM, {30, 32}, {60, 64}},
+    {CursorType::kAlias, IDR_AURA_CURSOR_BIG_ALIAS, {19, 11}, {38, 22}},
+    {CursorType::kCell, IDR_AURA_CURSOR_BIG_CELL, {30, 30}, {60, 60}},
+    {CursorType::kContextMenu,
+     IDR_AURA_CURSOR_BIG_CONTEXT_MENU,
+     {11, 11},
+     {22, 22}},
+    {CursorType::kCross, IDR_AURA_CURSOR_BIG_CROSSHAIR, {30, 32}, {60, 64}},
+    {CursorType::kHelp, IDR_AURA_CURSOR_BIG_HELP, {10, 11}, {20, 22}},
+    {CursorType::kVerticalText,
+     IDR_AURA_CURSOR_BIG_XTERM_HORIZ,
+     {32, 30},
+     {64, 60}},
+    {CursorType::kZoomIn, IDR_AURA_CURSOR_BIG_ZOOM_IN, {25, 26}, {50, 52}},
+    {CursorType::kZoomOut, IDR_AURA_CURSOR_BIG_ZOOM_OUT, {26, 26}, {52, 52}},
+    {CursorType::kRowResize,
+     IDR_AURA_CURSOR_BIG_ROW_RESIZE,
+     {29, 32},
+     {58, 64}},
+    {CursorType::kColumnResize,
+     IDR_AURA_CURSOR_BIG_COL_RESIZE,
+     {35, 29},
+     {70, 58}},
+    {CursorType::kEastWestNoResize,
+     IDR_AURA_CURSOR_BIG_EAST_WEST_NO_RESIZE,
+     {35, 29},
+     {70, 58}},
+    {CursorType::kEastWestResize,
+     IDR_AURA_CURSOR_BIG_EAST_WEST_RESIZE,
+     {35, 29},
+     {70, 58}},
+    {CursorType::kNorthSouthNoResize,
+     IDR_AURA_CURSOR_BIG_NORTH_SOUTH_NO_RESIZE,
+     {29, 32},
+     {58, 64}},
+    {CursorType::kNorthSouthResize,
+     IDR_AURA_CURSOR_BIG_NORTH_SOUTH_RESIZE,
+     {29, 32},
+     {58, 64}},
+    {CursorType::kNorthEastSouthWestNoResize,
+     IDR_AURA_CURSOR_BIG_NORTH_EAST_SOUTH_WEST_NO_RESIZE,
+     {32, 30},
+     {64, 60}},
+    {CursorType::kNorthEastSouthWestResize,
+     IDR_AURA_CURSOR_BIG_NORTH_EAST_SOUTH_WEST_RESIZE,
+     {32, 30},
+     {64, 60}},
+    {CursorType::kNorthWestSouthEastNoResize,
+     IDR_AURA_CURSOR_BIG_NORTH_WEST_SOUTH_EAST_NO_RESIZE,
+     {32, 31},
+     {64, 62}},
+    {CursorType::kNorthWestSouthEastResize,
+     IDR_AURA_CURSOR_BIG_NORTH_WEST_SOUTH_EAST_RESIZE,
+     {32, 31},
+     {64, 62}},
+    {CursorType::kGrab, IDR_AURA_CURSOR_BIG_GRAB, {21, 11}, {42, 22}},
+    {CursorType::kGrabbing, IDR_AURA_CURSOR_BIG_GRABBING, {20, 12}, {40, 24}},
+    // TODO(https://crbug.com/336867): create IDR_AURA_CURSOR_BIG_THROBBER.
+};
+
+const CursorSizeData kCursorSizes[] = {
+    {ui::CursorSize::kNormal, kNormalCursors, std::size(kNormalCursors)},
+    {ui::CursorSize::kLarge, kLargeCursors, std::size(kLargeCursors)},
+};
+
+const CursorSizeData* GetCursorSizeByType(ui::CursorSize cursor_size) {
+  for (size_t i = 0; i < std::size(kCursorSizes); ++i) {
+    if (kCursorSizes[i].id == cursor_size) {
+      return &kCursorSizes[i];
+    }
+  }
+
+  return nullptr;
+}
+
+bool SearchTable(const CursorData* table,
+                 size_t table_length,
+                 CursorType id,
+                 float scale_factor,
+                 int* resource_id,
+                 gfx::Point* point) {
+  DCHECK_NE(scale_factor, 0);
+
+  bool resource_2x_available =
+      ui::ResourceBundle::GetSharedInstance().GetMaxResourceScaleFactor() ==
+      ui::k200Percent;
+  for (size_t i = 0; i < table_length; ++i) {
+    if (table[i].id == id) {
+      *resource_id = table[i].resource_id;
+      *point = scale_factor == 1.0f || !resource_2x_available
+                   ? gfx::Point(table[i].hot_1x.x, table[i].hot_1x.y)
+                   : gfx::Point(table[i].hot_2x.x, table[i].hot_2x.y);
+      return true;
+    }
+  }
+
+  return false;
+}
+
 }  // namespace
 
 absl::optional<ui::CursorData> GetCursorData(
@@ -194,4 +440,22 @@
   *hotpoint = gfx::ScaleToFlooredPoint(*hotpoint, scale);
 }
 
+bool GetCursorDataFor(ui::CursorSize cursor_size,
+                      CursorType id,
+                      float scale_factor,
+                      int* resource_id,
+                      gfx::Point* point) {
+  const CursorSizeData* cursor_set = GetCursorSizeByType(cursor_size);
+  if (cursor_set && SearchTable(cursor_set->cursors, cursor_set->length, id,
+                                scale_factor, resource_id, point)) {
+    return true;
+  }
+
+  // Falls back to the default cursor set.
+  cursor_set = GetCursorSizeByType(ui::CursorSize::kNormal);
+  DCHECK(cursor_set);
+  return SearchTable(cursor_set->cursors, cursor_set->length, id, scale_factor,
+                     resource_id, point);
+}
+
 }  // namespace wm
diff --git a/ui/wm/core/cursor_util.h b/ui/wm/core/cursor_util.h
index 922641a..9a2c1c0 100644
--- a/ui/wm/core/cursor_util.h
+++ b/ui/wm/core/cursor_util.h
@@ -39,6 +39,17 @@
                                            SkBitmap* bitmap_in_out,
                                            gfx::Point* hotpoint_in_out);
 
+// Returns data about |id|, where id is a cursor constant like
+// ui::mojom::CursorType::kHelp. The IDR will be placed in |resource_id| and
+// the hotspots for the different DPIs will be placed in |hot_1x| and
+// |hot_2x|.  Returns false if |id| is invalid.
+COMPONENT_EXPORT(UI_WM)
+bool GetCursorDataFor(ui::CursorSize cursor_size,
+                      ui::mojom::CursorType id,
+                      float scale_factor,
+                      int* resource_id,
+                      gfx::Point* point);
+
 }  // namespace wm
 
 #endif  // UI_WM_CORE_CURSOR_UTIL_H_
diff --git a/ui/wm/core/cursor_util_unittest.cc b/ui/wm/core/cursor_util_unittest.cc
index c8971d65..c26e25c 100644
--- a/ui/wm/core/cursor_util_unittest.cc
+++ b/ui/wm/core/cursor_util_unittest.cc
@@ -10,8 +10,11 @@
 #include "ui/base/cursor/cursor.h"
 #include "ui/base/cursor/cursor_size.h"
 #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
+#include "ui/base/resource/resource_bundle.h"
+#include "ui/base/resource/resource_scale_factor.h"
 #include "ui/display/display.h"
 #include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/skia_conversions.h"
 
 namespace wm {
 namespace {
@@ -89,6 +92,32 @@
   ASSERT_TRUE(wait_cursor_data);
   EXPECT_GT(wait_cursor_data->bitmaps.size(), 1u);
   EXPECT_FALSE(wait_cursor_data->hotspot.IsOrigin());
+
+  // Test for different scale factors.
+
+  // Data from CursorType::kPointer resources:
+  const auto kSize = gfx::Size(25, 25);
+  const auto kHotspot1x = gfx::Point(4, 4);
+  const auto kHotspot2x = gfx::Point(7, 7);
+
+  bool resource_2x_available =
+      ui::ResourceBundle::GetSharedInstance().GetMaxResourceScaleFactor() ==
+      ui::k200Percent;
+
+  const float kScales[] = {0.8f, 1.0f, 1.3f, 1.5f, 2.0f, 2.5f};
+  for (const auto scale : kScales) {
+    const auto pointer_data = GetCursorData(CursorType::kPointer, kDefaultSize,
+                                            scale, kDefaultRotation);
+    ASSERT_TRUE(pointer_data);
+    ASSERT_EQ(pointer_data->bitmaps.size(), 1u);
+    // TODO(https://crbug.com/1193775): fractional scales are not supported, and
+    // only the bitmap is scaled.
+    EXPECT_EQ(gfx::SkISizeToSize(pointer_data->bitmaps[0].dimensions()),
+              gfx::ScaleToCeiledSize(kSize, scale));  // ImageSkia uses ceil.
+    EXPECT_EQ(pointer_data->hotspot, scale == 1.0f || !resource_2x_available
+                                         ? kHotspot1x
+                                         : kHotspot2x);
+  }
 }
 
 }  // namespace
diff --git a/ui/wm/core/cursors_aura.cc b/ui/wm/core/cursors_aura.cc
deleted file mode 100644
index 0dc6882..0000000
--- a/ui/wm/core/cursors_aura.cc
+++ /dev/null
@@ -1,319 +0,0 @@
-// Copyright 2013 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "ui/wm/core/cursors_aura.h"
-
-#include "ui/base/cursor/cursor_size.h"
-#include "ui/base/cursor/mojom/cursor_type.mojom-shared.h"
-#include "ui/base/resource/resource_bundle.h"
-#include "ui/gfx/geometry/point.h"
-#include "ui/resources/grit/ui_resources.h"
-
-namespace wm {
-
-namespace {
-
-namespace mojom = ::ui::mojom;
-
-struct HotPoint {
-  int x;
-  int y;
-};
-
-struct CursorData {
-  mojom::CursorType id;
-  int resource_id;
-  HotPoint hot_1x;
-  HotPoint hot_2x;
-};
-
-struct CursorSizeData {
-  const ui::CursorSize id;
-  const CursorData* cursors;
-  const int length;
-};
-
-const CursorData kNormalCursors[] = {
-    {mojom::CursorType::kNull, IDR_AURA_CURSOR_PTR, {4, 4}, {7, 7}},
-    {mojom::CursorType::kPointer, IDR_AURA_CURSOR_PTR, {4, 4}, {7, 7}},
-    {mojom::CursorType::kNoDrop, IDR_AURA_CURSOR_NO_DROP, {9, 9}, {18, 18}},
-    {mojom::CursorType::kNotAllowed, IDR_AURA_CURSOR_NO_DROP, {9, 9}, {18, 18}},
-    {mojom::CursorType::kCopy, IDR_AURA_CURSOR_COPY, {9, 9}, {18, 18}},
-    {mojom::CursorType::kHand, IDR_AURA_CURSOR_HAND, {9, 4}, {19, 8}},
-    {mojom::CursorType::kMove, IDR_AURA_CURSOR_MOVE, {11, 11}, {23, 23}},
-    {mojom::CursorType::kNorthEastResize,
-     IDR_AURA_CURSOR_NORTH_EAST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kSouthWestResize,
-     IDR_AURA_CURSOR_SOUTH_WEST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kSouthEastResize,
-     IDR_AURA_CURSOR_SOUTH_EAST_RESIZE,
-     {11, 11},
-     {24, 23}},
-    {mojom::CursorType::kNorthWestResize,
-     IDR_AURA_CURSOR_NORTH_WEST_RESIZE,
-     {11, 11},
-     {24, 23}},
-    {mojom::CursorType::kNorthResize,
-     IDR_AURA_CURSOR_NORTH_RESIZE,
-     {11, 12},
-     {23, 23}},
-    {mojom::CursorType::kSouthResize,
-     IDR_AURA_CURSOR_SOUTH_RESIZE,
-     {11, 12},
-     {23, 23}},
-    {mojom::CursorType::kEastResize,
-     IDR_AURA_CURSOR_EAST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kWestResize,
-     IDR_AURA_CURSOR_WEST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kIBeam, IDR_AURA_CURSOR_IBEAM, {12, 12}, {24, 25}},
-    {mojom::CursorType::kAlias, IDR_AURA_CURSOR_ALIAS, {8, 6}, {15, 11}},
-    {mojom::CursorType::kCell, IDR_AURA_CURSOR_CELL, {11, 11}, {24, 23}},
-    {mojom::CursorType::kContextMenu,
-     IDR_AURA_CURSOR_CONTEXT_MENU,
-     {4, 4},
-     {8, 9}},
-    {mojom::CursorType::kCross, IDR_AURA_CURSOR_CROSSHAIR, {12, 12}, {24, 24}},
-    {mojom::CursorType::kHelp, IDR_AURA_CURSOR_HELP, {4, 4}, {8, 9}},
-    {mojom::CursorType::kVerticalText,
-     IDR_AURA_CURSOR_XTERM_HORIZ,
-     {12, 11},
-     {26, 23}},
-    {mojom::CursorType::kZoomIn, IDR_AURA_CURSOR_ZOOM_IN, {10, 10}, {20, 20}},
-    {mojom::CursorType::kZoomOut, IDR_AURA_CURSOR_ZOOM_OUT, {10, 10}, {20, 20}},
-    {mojom::CursorType::kRowResize,
-     IDR_AURA_CURSOR_ROW_RESIZE,
-     {11, 12},
-     {23, 23}},
-    {mojom::CursorType::kColumnResize,
-     IDR_AURA_CURSOR_COL_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kEastWestNoResize,
-     IDR_AURA_CURSOR_EAST_WEST_NO_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kEastWestResize,
-     IDR_AURA_CURSOR_EAST_WEST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kNorthSouthNoResize,
-     IDR_AURA_CURSOR_NORTH_SOUTH_NO_RESIZE,
-     {11, 12},
-     {23, 23}},
-    {mojom::CursorType::kNorthSouthResize,
-     IDR_AURA_CURSOR_NORTH_SOUTH_RESIZE,
-     {11, 12},
-     {23, 23}},
-    {mojom::CursorType::kNorthEastSouthWestNoResize,
-     IDR_AURA_CURSOR_NORTH_EAST_SOUTH_WEST_NO_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kNorthEastSouthWestResize,
-     IDR_AURA_CURSOR_NORTH_EAST_SOUTH_WEST_RESIZE,
-     {12, 11},
-     {25, 23}},
-    {mojom::CursorType::kNorthWestSouthEastNoResize,
-     IDR_AURA_CURSOR_NORTH_WEST_SOUTH_EAST_NO_RESIZE,
-     {11, 11},
-     {24, 23}},
-    {mojom::CursorType::kNorthWestSouthEastResize,
-     IDR_AURA_CURSOR_NORTH_WEST_SOUTH_EAST_RESIZE,
-     {11, 11},
-     {24, 23}},
-    {mojom::CursorType::kGrab, IDR_AURA_CURSOR_GRAB, {8, 5}, {16, 10}},
-    {mojom::CursorType::kGrabbing, IDR_AURA_CURSOR_GRABBING, {9, 9}, {18, 18}},
-    {mojom::CursorType::kWait, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}},
-    {mojom::CursorType::kProgress, IDR_AURA_CURSOR_THROBBER, {7, 7}, {14, 14}},
-};
-
-const CursorData kLargeCursors[] = {
-    // The 2x hotspots should be double of the 1x, even though the cursors are
-    // shown as same size as 1x (64x64), because in 2x dpi screen, the 1x large
-    // cursor assets (64x64) are internally enlarged to the double size
-    // (128x128)
-    // by ResourceBundleImageSource.
-    {mojom::CursorType::kNull, IDR_AURA_CURSOR_BIG_PTR, {10, 10}, {20, 20}},
-    {mojom::CursorType::kPointer, IDR_AURA_CURSOR_BIG_PTR, {10, 10}, {20, 20}},
-    {mojom::CursorType::kNoDrop,
-     IDR_AURA_CURSOR_BIG_NO_DROP,
-     {10, 10},
-     {20, 20}},
-    {mojom::CursorType::kNotAllowed,
-     IDR_AURA_CURSOR_BIG_NO_DROP,
-     {10, 10},
-     {20, 20}},
-    {mojom::CursorType::kCopy, IDR_AURA_CURSOR_BIG_COPY, {21, 11}, {42, 22}},
-    {mojom::CursorType::kHand, IDR_AURA_CURSOR_BIG_HAND, {25, 7}, {50, 14}},
-    {mojom::CursorType::kMove, IDR_AURA_CURSOR_BIG_MOVE, {32, 31}, {64, 62}},
-    {mojom::CursorType::kNorthEastResize,
-     IDR_AURA_CURSOR_BIG_NORTH_EAST_RESIZE,
-     {31, 28},
-     {62, 56}},
-    {mojom::CursorType::kSouthWestResize,
-     IDR_AURA_CURSOR_BIG_SOUTH_WEST_RESIZE,
-     {31, 28},
-     {62, 56}},
-    {mojom::CursorType::kSouthEastResize,
-     IDR_AURA_CURSOR_BIG_SOUTH_EAST_RESIZE,
-     {28, 28},
-     {56, 56}},
-    {mojom::CursorType::kNorthWestResize,
-     IDR_AURA_CURSOR_BIG_NORTH_WEST_RESIZE,
-     {28, 28},
-     {56, 56}},
-    {mojom::CursorType::kNorthResize,
-     IDR_AURA_CURSOR_BIG_NORTH_RESIZE,
-     {29, 32},
-     {58, 64}},
-    {mojom::CursorType::kSouthResize,
-     IDR_AURA_CURSOR_BIG_SOUTH_RESIZE,
-     {29, 32},
-     {58, 64}},
-    {mojom::CursorType::kEastResize,
-     IDR_AURA_CURSOR_BIG_EAST_RESIZE,
-     {35, 29},
-     {70, 58}},
-    {mojom::CursorType::kWestResize,
-     IDR_AURA_CURSOR_BIG_WEST_RESIZE,
-     {35, 29},
-     {70, 58}},
-    {mojom::CursorType::kIBeam, IDR_AURA_CURSOR_BIG_IBEAM, {30, 32}, {60, 64}},
-    {mojom::CursorType::kAlias, IDR_AURA_CURSOR_BIG_ALIAS, {19, 11}, {38, 22}},
-    {mojom::CursorType::kCell, IDR_AURA_CURSOR_BIG_CELL, {30, 30}, {60, 60}},
-    {mojom::CursorType::kContextMenu,
-     IDR_AURA_CURSOR_BIG_CONTEXT_MENU,
-     {11, 11},
-     {22, 22}},
-    {mojom::CursorType::kCross,
-     IDR_AURA_CURSOR_BIG_CROSSHAIR,
-     {30, 32},
-     {60, 64}},
-    {mojom::CursorType::kHelp, IDR_AURA_CURSOR_BIG_HELP, {10, 11}, {20, 22}},
-    {mojom::CursorType::kVerticalText,
-     IDR_AURA_CURSOR_BIG_XTERM_HORIZ,
-     {32, 30},
-     {64, 60}},
-    {mojom::CursorType::kZoomIn,
-     IDR_AURA_CURSOR_BIG_ZOOM_IN,
-     {25, 26},
-     {50, 52}},
-    {mojom::CursorType::kZoomOut,
-     IDR_AURA_CURSOR_BIG_ZOOM_OUT,
-     {26, 26},
-     {52, 52}},
-    {mojom::CursorType::kRowResize,
-     IDR_AURA_CURSOR_BIG_ROW_RESIZE,
-     {29, 32},
-     {58, 64}},
-    {mojom::CursorType::kColumnResize,
-     IDR_AURA_CURSOR_BIG_COL_RESIZE,
-     {35, 29},
-     {70, 58}},
-    {mojom::CursorType::kEastWestNoResize,
-     IDR_AURA_CURSOR_BIG_EAST_WEST_NO_RESIZE,
-     {35, 29},
-     {70, 58}},
-    {mojom::CursorType::kEastWestResize,
-     IDR_AURA_CURSOR_BIG_EAST_WEST_RESIZE,
-     {35, 29},
-     {70, 58}},
-    {mojom::CursorType::kNorthSouthNoResize,
-     IDR_AURA_CURSOR_BIG_NORTH_SOUTH_NO_RESIZE,
-     {29, 32},
-     {58, 64}},
-    {mojom::CursorType::kNorthSouthResize,
-     IDR_AURA_CURSOR_BIG_NORTH_SOUTH_RESIZE,
-     {29, 32},
-     {58, 64}},
-    {mojom::CursorType::kNorthEastSouthWestNoResize,
-     IDR_AURA_CURSOR_BIG_NORTH_EAST_SOUTH_WEST_NO_RESIZE,
-     {32, 30},
-     {64, 60}},
-    {mojom::CursorType::kNorthEastSouthWestResize,
-     IDR_AURA_CURSOR_BIG_NORTH_EAST_SOUTH_WEST_RESIZE,
-     {32, 30},
-     {64, 60}},
-    {mojom::CursorType::kNorthWestSouthEastNoResize,
-     IDR_AURA_CURSOR_BIG_NORTH_WEST_SOUTH_EAST_NO_RESIZE,
-     {32, 31},
-     {64, 62}},
-    {mojom::CursorType::kNorthWestSouthEastResize,
-     IDR_AURA_CURSOR_BIG_NORTH_WEST_SOUTH_EAST_RESIZE,
-     {32, 31},
-     {64, 62}},
-    {mojom::CursorType::kGrab, IDR_AURA_CURSOR_BIG_GRAB, {21, 11}, {42, 22}},
-    {mojom::CursorType::kGrabbing,
-     IDR_AURA_CURSOR_BIG_GRABBING,
-     {20, 12},
-     {40, 24}},
-    // TODO(https://crbug.com/336867): create IDR_AURA_CURSOR_BIG_THROBBER.
-};
-
-const CursorSizeData kCursorSizes[] = {
-    {ui::CursorSize::kNormal, kNormalCursors, std::size(kNormalCursors)},
-    {ui::CursorSize::kLarge, kLargeCursors, std::size(kLargeCursors)},
-};
-
-const CursorSizeData* GetCursorSizeByType(ui::CursorSize cursor_size) {
-  for (size_t i = 0; i < std::size(kCursorSizes); ++i) {
-    if (kCursorSizes[i].id == cursor_size)
-      return &kCursorSizes[i];
-  }
-
-  return nullptr;
-}
-
-bool SearchTable(const CursorData* table,
-                 size_t table_length,
-                 mojom::CursorType id,
-                 float scale_factor,
-                 int* resource_id,
-                 gfx::Point* point) {
-  DCHECK_NE(scale_factor, 0);
-
-  bool resource_2x_available =
-      ui::ResourceBundle::GetSharedInstance().GetMaxResourceScaleFactor() ==
-      ui::k200Percent;
-  for (size_t i = 0; i < table_length; ++i) {
-    if (table[i].id == id) {
-      *resource_id = table[i].resource_id;
-      *point = scale_factor == 1.0f || !resource_2x_available
-                   ? gfx::Point(table[i].hot_1x.x, table[i].hot_1x.y)
-                   : gfx::Point(table[i].hot_2x.x, table[i].hot_2x.y);
-      return true;
-    }
-  }
-
-  return false;
-}
-
-}  // namespace
-
-bool GetCursorDataFor(ui::CursorSize cursor_size,
-                      mojom::CursorType id,
-                      float scale_factor,
-                      int* resource_id,
-                      gfx::Point* point) {
-  const CursorSizeData* cursor_set = GetCursorSizeByType(cursor_size);
-  if (cursor_set && SearchTable(cursor_set->cursors, cursor_set->length, id,
-                                scale_factor, resource_id, point)) {
-    return true;
-  }
-
-  // Falls back to the default cursor set.
-  cursor_set = GetCursorSizeByType(ui::CursorSize::kNormal);
-  DCHECK(cursor_set);
-  return SearchTable(cursor_set->cursors, cursor_set->length, id, scale_factor,
-                     resource_id, point);
-}
-
-}  // namespace wm
diff --git a/ui/wm/core/cursors_aura.h b/ui/wm/core/cursors_aura.h
deleted file mode 100644
index a40bc4b..0000000
--- a/ui/wm/core/cursors_aura.h
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2013 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef UI_WM_CORE_CURSORS_AURA_H_
-#define UI_WM_CORE_CURSORS_AURA_H_
-
-#include "base/component_export.h"
-#include "ui/base/cursor/mojom/cursor_type.mojom-forward.h"
-
-namespace gfx {
-class Point;
-}
-
-namespace ui {
-enum class CursorSize;
-}
-
-namespace wm {
-
-// Returns data about |id|, where id is a cursor constant like
-// ui::mojom::CursorType::kHelp. The IDR will be placed in |resource_id| and
-// the hotspots for the different DPIs will be placed in |hot_1x| and
-// |hot_2x|.  Returns false if |id| is invalid.
-COMPONENT_EXPORT(UI_WM)
-bool GetCursorDataFor(ui::CursorSize cursor_size,
-                      ui::mojom::CursorType id,
-                      float scale_factor,
-                      int* resource_id,
-                      gfx::Point* point);
-
-}  // namespace wm
-
-#endif  // UI_WM_CORE_CURSORS_AURA_H_
diff --git a/ui/wm/test/run_all_unittests.cc b/ui/wm/test/run_all_unittests.cc
index d4210571..81e77ea5 100644
--- a/ui/wm/test/run_all_unittests.cc
+++ b/ui/wm/test/run_all_unittests.cc
@@ -5,6 +5,7 @@
 #include <memory>
 
 #include "base/compiler_specific.h"
+#include "base/files/file_path.h"
 #include "base/functional/bind.h"
 #include "base/path_service.h"
 #include "base/test/launcher/unit_test_launcher.h"
@@ -14,6 +15,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/aura/env.h"
 #include "ui/base/resource/resource_bundle.h"
+#include "ui/base/resource/resource_scale_factor.h"
 #include "ui/base/ui_base_paths.h"
 #include "ui/gl/test/gl_surface_test_support.h"
 
@@ -34,6 +36,13 @@
     ASSERT_TRUE(base::PathService::Get(ui::UI_TEST_PAK, &ui_test_pak_path));
     ui::ResourceBundle::InitSharedInstanceWithPakPath(ui_test_pak_path);
 
+    if (ui::ResourceBundle::IsScaleFactorSupported(ui::k200Percent)) {
+      base::FilePath ui_test_resources_200 = ui_test_pak_path.DirName().Append(
+          FILE_PATH_LITERAL("ui_test_200_percent.pak"));
+      ui::ResourceBundle::GetSharedInstance().AddDataPackFromPath(
+          ui_test_resources_200, ui::k200Percent);
+    }
+
     env_ = aura::Env::CreateInstance();
 
     base::DiscardableMemoryAllocator::SetInstance(