Low chrome Memory Corruption 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Network
DescriptionMemory corruption in Network
ComponentNetwork
Sub-componentHEv3
Bug ClassMemory Corruption
Tracker513692667
Fix commit34f6bb989a9c (chromium/src) +370/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
net/http/http_stream_pool.cc
modified
while
net/http/http_stream_pool.cc
modified
for
net/http/http_stream_pool.cc
modified
if
net/http/http_stream_pool_attempt_manager.cc
modified
TEST_F
net/http/http_stream_pool_attempt_manager_unittest.cc
modified
BindLambdaForTesting
net/http/http_stream_pool_attempt_manager_unittest.cc
modified

Files Changed

  • net/socket/socket_pool_additional_capacity.h
  • net/http/http_stream_pool_group.cc
  • net/http/http_stream_pool.cc
  • net/http/http_stream_pool_attempt_manager.cc
  • net/http/http_stream_pool.h
  • net/http/http_stream_pool_attempt_manager_unittest.cc
From 34f6bb989a9cbf8f7aa7aa5e6d5107b95813229b Mon Sep 17 00:00:00 2001
From: Kenichi Ishibashi <[email protected]>
Date: Thu, 17 Sep 2026 20:10:38 -0700
Subject: [PATCH] HEv3: Randomize global socket limit in HttpStreamPool

Port SocketPoolAdditionalCapacity and randomized expandability
(kUncapped vs kCapped) into HttpStreamPool to mitigate cross-origin
timing side-channels caused by deterministic socket limit saturation
when HappyEyeballsV3 is enabled.

Bug: 513692667
Change-Id: Ie0607fe73ec0949723b25d2f1500a29b8861dee7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8365045
Commit-Queue: Kenichi Ishibashi <[email protected]>
Reviewed-by: Ari Chivukula <[email protected]>
Reviewed-by: Adam Rice <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1700396}
---
diff --git a/net/http/http_stream_pool.cc b/net/http/http_stream_pool.cc
index 8b8f09b..e6a3e30 100644
--- a/net/http/http_stream_pool.cc
+++ b/net/http/http_stream_pool.cc
@@ -183,7 +183,9 @@
       // limit.
       max_stream_sockets_per_group_(
           std::min(kHttpStreamPoolMaxStreamPerPool.Get(),
-                   kHttpStreamPoolMaxStreamPerGroup.Get())) {
+                   kHttpStreamPoolMaxStreamPerGroup.Get())),
+      additional_capacity_(
+          SocketPoolAdditionalCapacity::Create(max_stream_sockets_per_pool_)) {
   CHECK(http_network_session_);
   if (cleanup_on_ip_address_change) {
     NetworkChangeNotifier::AddIPAddressObserver(this);
@@ -259,7 +261,8 @@
   if (limit_ignoring_job_controller_counts_ > 0) {
     return true;
   }
-  return TotalActiveStreamCount() < max_stream_sockets_per_pool_;
+  return TotalActiveStreamCount() <
+         (max_stream_sockets_per_pool_ + additional_capacity_.capacity());
 }
 
 void HttpStreamPool::IncrementTotalIdleStreamCount() {
@@ -300,7 +303,10 @@
     NOTREACHED() << "handed_out=" << total_handed_out_stream_count_
                  << ", idle=" << total_idle_stream_count_
                  << ", connecting=" << total_connecting_stream_count_
-                 << ", limit=" << max_stream_sockets_per_pool_;
+                 << ", soft_limit=" << max_stream_sockets_per_pool_
+                 << ", hard_limit="
+                 << (max_stream_sockets_per_pool_ +
+                     additional_capacity_.capacity());
   }
   ++total_connecting_stream_count_;
   TRACE_COUNTER("net.stream", "HttpStreamPoolTotalConnectingStreams",
@@ -330,6 +336,7 @@
                                 StreamSocketCloseReason::kIpAddressChanged,
                                 kIpAddressChanged);
   }
+  ResetExpandability();
 }
 
 void HttpStreamPool::OnSSLConfigChanged(
@@ -379,6 +386,7 @@
     group.second.FlushWithError(error, attempt_cancel_reason,
                                 net_log_close_reason_utf8);
   }
+  ResetExpandability();
 }
 
 void HttpStreamPool::CloseIdleStreams(
@@ -388,6 +396,34 @@
   }
 }
 
+bool HttpStreamPool::ReachedMaxStreamLimit() const {
+  // Enforce hard/soft caps directly to guarantee deterministic behavior when
+  // randomization is disabled (capacity == 0) and for call paths that bypass
+  // UpdateExpandabilityBeforeAllocation().
+  if (TotalActiveStreamCount() >=
+      max_stream_sockets_per_pool_ + additional_capacity_.capacity()) {
+    return true;
+  }
+  if (TotalActiveStreamCount() < max_stream_sockets_per_pool_) {
+    return false;
+  }
+  return expandability_ == SocketPoolExpandability::kCapped;
+}
+
+void HttpStreamPool::UpdateExpandabilityBeforeAllocation() {
+  expandability_ = additional_capacity_.NextExpandabilityBeforeAllocation(
+      expandability_, TotalActiveStreamCount(), max_stream_sockets_per_pool_);
+}
+
+void HttpStreamPool::UpdateExpandabilityAfterRelease() {
+  expandability_ = additional_capacity_.NextExpandabilityAfterRelease(
+      expandability_, TotalActiveStreamCount(), max_stream_sockets_per_pool_);
+}
+
+void HttpStreamPool::ResetExpandability() {
+  expandability_ = SocketPoolExpandability::kUncapped;
+}
+
 bool HttpStreamPool::IsPoolStalled() {
   if (!ReachedMaxStreamLimit()) {
     return false;
@@ -400,7 +436,6 @@
     return;
   }
 
-  // Loop until there is nothing more to do.
   while (true) {
     Group* group = FindHighestStalledGroup();
     if (!group) {
@@ -408,7 +443,17 @@
     }
 
     if (ReachedMaxStreamLimit()) {
-      if (!CloseOneIdleStreamSocket()) {
+      while (total_idle_stream_count_ > 0) {
+        if (!CloseOneIdleStreamSocket()) {
+          return;
+        }
+        // CloseOneIdleStreamSocket() updates expandability after release.
+        // ProcessPendingRequest() will update expandability before allocation.
+        if (!ReachedMaxStreamLimit()) {
+          break;
+        }
+      }
+      if (ReachedMaxStreamLimit()) {
         return;
       }
     }
@@ -498,6 +543,11 @@
   dict.Set("max_socket_count", static_cast<int>(max_stream_sockets_per_pool_));
   dict.Set("max_sockets_per_group",
            static_cast<int>(max_stream_sockets_per_group_));
+  dict.Set("socket_soft_cap", static_cast<int>(max_stream_sockets_per_pool_));
+  dict.Set("additional_capacity", std::string(additional_capacity_));
+  dict.Set("expandability", expandability_ == SocketPoolExpandability::kUncapped
+                                ? "Uncapped"
+                                : "Capped");
 
   base::DictValue group_dicts;
   for (const auto& [key, group] : groups_) {
diff --git a/net/http/http_stream_pool.h b/net/http/http_stream_pool.h
index 2b6749c..9751fe9 100644
--- a/net/http/http_stream_pool.h
+++ b/net/http/http_stream_pool.h
@@ -26,6 +26,7 @@
 #include "net/http/http_stream_pool_request_info.h"
 #include "net/http/http_stream_request.h"
 #include "net/socket/next_proto.h"
+#include "net/socket/socket_pool_additional_capacity.h"
 #include "net/socket/ssl_client_socket.h"
 #include "net/socket/stream_attempt.h"
 #include "net/socket/stream_socket_close_reason.h"
@@ -252,9 +253,23 @@
 
   void CloseIdleStreams(std::string_view net_log_close_reason_utf8);
 
-  bool ReachedMaxStreamLimit() const {
-    return TotalActiveStreamCount() >= max_stream_sockets_per_pool();
-  }
+  bool ReachedMaxStreamLimit() const;
+
+  // Updates `expandability_` before attempting to allocate a stream socket.
+  // Must be called prior to initiating a new connection attempt or before
+  // allocating a socket when the pool might be at or above capacity.
+  void UpdateExpandabilityBeforeAllocation();
+
+  // Updates `expandability_` after releasing or destroying a stream socket, or
+  // cancelling an attempt slot. May transition the pool from capped to
+  // uncapped.
+  void UpdateExpandabilityAfterRelease();
+
+  // Resets `expandability_` to `kUncapped`. Called when all streams and sockets
+  // in the pool are flushed (e.g. on network/IP address changes or pool flush).
+  void ResetExpandability();
+
+  SocketPoolExpandability expandability() const { return expandability_; }
 
   // Return true if there is a request blocked on this pool.
   bool IsPoolStalled();
@@ -338,6 +353,17 @@
   void set_max_stream_sockets_per_pool_for_testing(
       size_t max_stream_sockets_per_pool) {
     max_stream_sockets_per_pool_ = max_stream_sockets_per_pool;
+    additional_capacity_ =
+        SocketPoolAdditionalCapacity::Create(max_stream_sockets_per_pool_);
+    UpdateExpandabilityAfterRelease();
+  }
+
+  void SetAdditionalCapacityForTest(
+      const SocketPoolAdditionalCapacity& additional_capacity) {
+    additional_capacity_ = additional_capacity;
+  }
+  const SocketPoolAdditionalCapacity& AdditionalCapacityForTest() const {
+    return additional_capacity_;
   }
 
   void set_max_stream_sockets_per_group_for_testing(
@@ -409,6 +435,8 @@
 
   size_t max_stream_sockets_per_pool_;
   size_t max_stream_sockets_per_group_;
+  SocketPoolAdditionalCapacity additional_capacity_;
+  SocketPoolExpandability expandability_ = SocketPoolExpandability::kUncapped;
 
   // The total number of active streams this pool handed out across all groups.
   size_t total_handed_out_stream_count_ = 0;
diff --git a/net/http/http_stream_pool_attempt_manager.cc b/net/http/http_stream_pool_attempt_manager.cc
index 53602471..54da44b3 100644
--- a/net/http/http_stream_pool_attempt_manager.cc
+++ b/net/http/http_stream_pool_attempt_manager.cc
@@ -767,10 +767,18 @@
         raw_attempt->ip_endpoint());
   }
 
+  TcpBasedAttemptSlot* slot = raw_attempt->slot();
+  const bool has_other_attempt =
+      (raw_attempt == slot->ipv4_attempt() ? slot->ipv6_attempt() != nullptr
+                                           : slot->ipv4_attempt() != nullptr);
+  const bool slot_extracted = (rv != OK && !has_other_attempt);
   std::unique_ptr<TcpBasedAttempt> tcp_based_attempt =
       ExtractTcpBasedAttempt(raw_attempt, rv);
 
   if (rv != OK) {
+    if (slot_extracted) {
+      pool()->UpdateExpandabilityAfterRelease();
+    }
     HandleTcpBasedAttemptFailure(std::move(tcp_based_attempt), rv);
     return;
   }
@@ -1443,10 +1451,17 @@
   if (reason.has_value()) {
     slot->SetCancelReason(*reason);
   }
+  pool()->UpdateExpandabilityAfterRelease();
 }
 
 bool HttpStreamPool::AttemptManager::IsTcpBasedAttemptReady() {
   CanAttemptResult can_attempt = CanAttemptConnection();
+  if (can_attempt == CanAttemptResult::kAttempt && ShouldRespectLimits()) {
+    pool()->UpdateExpandabilityBeforeAllocation();
+    if (pool()->ReachedMaxStreamLimit()) {
+      can_attempt = CanAttemptResult::kReachedPoolLimit;
+    }
+  }
   // TODO(crbug.com/383606724): Consider removing these trace and net log event
   // once we figure out better endpoint selection algorithm.
   TRACE_EVENT_INSTANT("net.stream", "AttemptManager::IsTcpBasedAttemptReady",
@@ -1491,9 +1506,18 @@
       NotifyPreconnectsComplete(ERR_PRECONNECT_MAX_SOCKET_LIMIT);
       return false;
     case CanAttemptResult::kReachedPoolLimit:
-      // If we can't attempt connection due to the pool's limit, try to close an
-      // idle stream in the pool.
-      if (!pool()->CloseOneIdleStreamSocket()) {
+      while (pool()->TotalIdleStreamCount() > 0) {
+        if (!pool()->CloseOneIdleStreamSocket()) {
+          break;
+        }
+        // Check if a new allocation could succeed after releasing an idle
+        // socket. If not, release more idle sockets and try again.
+        pool()->UpdateExpandabilityBeforeAllocation();
+        if (!pool()->ReachedMaxStreamLimit()) {
+          break;
+        }
+      }
+      if (pool()->ReachedMaxStreamLimit()) {
         // Try to close idle SPDY sessions. SPDY sessions never release the
         // underlying sockets immediately on close, so return false anyway.
         spdy_session_pool()->CloseCurrentIdleSessions("Closing idle sessions");
diff --git a/net/http/http_stream_pool_group.cc b/net/http/http_stream_pool_group.cc
index 3cd8806..d0a97f3d 100644
--- a/net/http/http_stream_pool_group.cc
+++ b/net/http/http_stream_pool_group.cc
@@ -183,6 +183,7 @@
   } else {
     RecordNetLogClosingSocket(*socket, not_reusable_reason);
     socket.reset();
+    pool_->UpdateExpandabilityAfterRelease();
   }
 
   pool_->ProcessPendingRequestsInGroups();
@@ -216,6 +217,7 @@
       RecordNetLogClosingSocket(*it->stream_socket, usable_result.error());
       it = idle_stream_sockets_.erase(it);
       pool_->DecrementTotalIdleStreamCount();
+      pool_->UpdateExpandabilityAfterRelease();
       continue;
     }
     if (it->stream_socket->WasEverUsed()) {
@@ -240,6 +242,8 @@
       std::move(idle_it->stream_socket);
   idle_stream_sockets_.erase(idle_it);
   pool_->DecrementTotalIdleStreamCount();
+  // Don't call UpdateExpandabilityAfterRelease() here because the socket is
+  // immediately handed out, keeping TotalActiveStreamCount() unchanged.
 
   TRACE_EVENT_INSTANT("net.stream", "Group::GetIdleStreamSocket", track_, flow_,
                       "idle_stream_count", idle_stream_sockets_.size());
@@ -264,6 +268,7 @@
                             kExceededSocketLimits);
   idle_stream_sockets_.pop_front();
   pool_->DecrementTotalIdleStreamCount();
+  pool_->UpdateExpandabilityAfterRelease();
   // Use MaybeCompleteLater since MaybeComplete() may delete `this`, and this
   // method could be called while iterating all groups.
   MaybeCompleteLater();
@@ -412,6 +417,7 @@
       RecordNetLogClosingSocket(*it->stream_socket, net_log_close_reason_utf8);
       it = idle_stream_sockets_.erase(it);
       pool_->DecrementTotalIdleStreamCount();
+      pool_->UpdateExpandabilityAfterRelease();
     } else {
       ++it;
     }
diff --git a/net/socket/socket_pool_additional_capacity.h b/net/socket/socket_pool_additional_capacity.h
index 9612c98..bc6d6ce 100644
--- a/net/socket/socket_pool_additional_capacity.h
+++ b/net/socket/socket_pool_additional_capacity.h
@@ -61,6 +61,8 @@
       size_t sockets_in_use,
       size_t socket_soft_cap) const;
 
+  size_t capacity() const { return capacity_; }
+
   explicit operator std::string() const {
     return base::StringPrintf(
         "SocketPoolAdditionalCapacity(base:%e,capacity:%i,minimum:%e,noise:%e)",
diff --git a/net/http/http_stream_pool_attempt_manager_unittest.cc b/net/http/http_stream_pool_attempt_manager_unittest.cc
index b94d5ea..b86bc96 100644
--- a/net/http/http_stream_pool_attempt_manager_unittest.cc
+++ b/net/http/http_stream_pool_attempt_manager_unittest.cc
@@ -65,6 +65,7 @@
 #include "net/quic/quic_context.h"
 #include "net/quic/quic_test_packet_maker.h"
 #include "net/socket/next_proto.h"
+#include "net/socket/socket_pool_additional_capacity.h"
 #include "net/socket/socket_test_util.h"
 #include "net/socket/stream_socket_close_reason.h"
 #include "net/socket/stream_socket_handle.h"
@@ -477,7 +478,9 @@
             base::test::TaskEnvironment::TimeSource::MOCK_TIME,
             {features::kNetworkServicePerPriorityTaskQueues}) {
     FLAGS_quic_enable_http3_grease_randomness = false;
-    AddScopedFeatureList().InitAndEnableFeature(features::kHappyEyeballsV3);
+    AddScopedFeatureList().InitWithFeatures(
+        /*enabled_features=*/{features::kHappyEyeballsV3},
+        /*disabled_features=*/{features::kTcpSocketPoolLimitRandomization});
     InitializeSession();
   }
 
@@ -2074,6 +2077,251 @@
   ASSERT_FALSE(pool().IsPoolStalled());
 }
 
+TEST_F(HttpStreamPoolAttemptManagerTest, ValidateAdditionalCapacity) {
+  pool().set_max_stream_sockets_per_pool_for_testing(256);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateForTest(1.000000e-06, 256,
+                                                  1.000000e-02, 2.000000e-01));
+
+  ValidateAdditionalCapacityForSocketPool(
+      base::BindLambdaForTesting([&]() {
+        pool().UpdateExpandabilityBeforeAllocation();
+        if (pool().expandability() == SocketPoolExpandability::kUncapped) {
+          pool().IncrementTotalHandedOutStreamCount();
+        }
+        return pool().expandability();
+      }),
+      base::DoNothing(), base::BindLambdaForTesting([&]() {
+        pool().DecrementTotalHandedOutStreamCount();
+        pool().UpdateExpandabilityAfterRelease();
+        return pool().expandability();
+      }),
+      base::BindLambdaForTesting(
+          [&]() { return pool().TotalActiveStreamCount(); }));
+
+  while (pool().TotalActiveStreamCount() > 0) {
+    pool().DecrementTotalHandedOutStreamCount();
+  }
+  pool().ResetExpandability();
+}
+
+TEST_F(HttpStreamPoolAttemptManagerTest, ReachedPoolLimitRandomizedWithEmpty) {
+  constexpr size_t kMaxPerGroup = 2;
+  constexpr size_t kMaxPerPool = 2;
+  pool().set_max_stream_sockets_per_group_for_testing(kMaxPerGroup);
+  pool().set_max_stream_sockets_per_pool_for_testing(kMaxPerPool);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateEmpty());
+
+  const HttpStreamKey key_a(
+      url::SchemeHostPort("http", "a.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/net/http/http_stream_pool_attempt_manager_unittest.cc b/net/http/http_stream_pool_attempt_manager_unittest.cc
index b94d5ea..b86bc96 100644
--- a/net/http/http_stream_pool_attempt_manager_unittest.cc
+++ b/net/http/http_stream_pool_attempt_manager_unittest.cc
@@ -65,6 +65,7 @@
 #include "net/quic/quic_context.h"
 #include "net/quic/quic_test_packet_maker.h"
 #include "net/socket/next_proto.h"
+#include "net/socket/socket_pool_additional_capacity.h"
 #include "net/socket/socket_test_util.h"
 #include "net/socket/stream_socket_close_reason.h"
 #include "net/socket/stream_socket_handle.h"
@@ -477,7 +478,9 @@
             base::test::TaskEnvironment::TimeSource::MOCK_TIME,
             {features::kNetworkServicePerPriorityTaskQueues}) {
     FLAGS_quic_enable_http3_grease_randomness = false;
-    AddScopedFeatureList().InitAndEnableFeature(features::kHappyEyeballsV3);
+    AddScopedFeatureList().InitWithFeatures(
+        /*enabled_features=*/{features::kHappyEyeballsV3},
+        /*disabled_features=*/{features::kTcpSocketPoolLimitRandomization});
     InitializeSession();
   }
 
@@ -2074,6 +2077,251 @@
   ASSERT_FALSE(pool().IsPoolStalled());
 }
 
+TEST_F(HttpStreamPoolAttemptManagerTest, ValidateAdditionalCapacity) {
+  pool().set_max_stream_sockets_per_pool_for_testing(256);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateForTest(1.000000e-06, 256,
+                                                  1.000000e-02, 2.000000e-01));
+
+  ValidateAdditionalCapacityForSocketPool(
+      base::BindLambdaForTesting([&]() {
+        pool().UpdateExpandabilityBeforeAllocation();
+        if (pool().expandability() == SocketPoolExpandability::kUncapped) {
+          pool().IncrementTotalHandedOutStreamCount();
+        }
+        return pool().expandability();
+      }),
+      base::DoNothing(), base::BindLambdaForTesting([&]() {
+        pool().DecrementTotalHandedOutStreamCount();
+        pool().UpdateExpandabilityAfterRelease();
+        return pool().expandability();
+      }),
+      base::BindLambdaForTesting(
+          [&]() { return pool().TotalActiveStreamCount(); }));
+
+  while (pool().TotalActiveStreamCount() > 0) {
+    pool().DecrementTotalHandedOutStreamCount();
+  }
+  pool().ResetExpandability();
+}
+
+TEST_F(HttpStreamPoolAttemptManagerTest, ReachedPoolLimitRandomizedWithEmpty) {
+  constexpr size_t kMaxPerGroup = 2;
+  constexpr size_t kMaxPerPool = 2;
+  pool().set_max_stream_sockets_per_group_for_testing(kMaxPerGroup);
+  pool().set_max_stream_sockets_per_pool_for_testing(kMaxPerPool);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateEmpty());
+
+  const HttpStreamKey key_a(
+      url::SchemeHostPort("http", "a.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
+  const HttpStreamKey key_b(
+      url::SchemeHostPort("http", "b.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  EXPECT_FALSE(pool().ReachedMaxStreamLimit());
+
+  Group& group_a = pool().GetOrCreateGroupForTesting(key_a);
+  std::unique_ptr<HttpStream> stream_a = group_a.CreateTextBasedStream(
+      std::make_unique<FakeStreamSocket>(),
+      StreamSocketHandle::SocketReuseType::kUnused,
+      LoadTimingInfo::ConnectTiming());
+
+  // 1 stream in use < soft cap (2): always uncapped.
+  pool().UpdateExpandabilityBeforeAllocation();
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  EXPECT_FALSE(pool().ReachedMaxStreamLimit());
+
+  Group& group_b = pool().GetOrCreateGroupForTesting(key_b);
+  auto fake_socket_b = std::make_unique<FakeStreamSocket>();
+  FakeStreamSocket* fake_socket_b_ptr = fake_socket_b.get();
+  std::unique_ptr<HttpStream> stream_b = group_b.CreateTextBasedStream(
+      std::move(fake_socket_b), StreamSocketHandle::SocketReuseType::kUnused,
+      LoadTimingInfo::ConnectTiming());
+
+  // 2 streams in use == soft cap (2): with CreateEmpty(), deterministically
+  // capped.
+  pool().UpdateExpandabilityBeforeAllocation();
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kCapped);
+  EXPECT_TRUE(pool().ReachedMaxStreamLimit());
+
+  // Releasing stream_b transitions back to uncapped.
+  fake_socket_b_ptr->Disconnect();
+  stream_b.reset();
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  EXPECT_FALSE(pool().ReachedMaxStreamLimit());
+}
+
+TEST_F(HttpStreamPoolAttemptManagerTest,
+       ReachedPoolLimitRandomizedTransitions) {
+  constexpr size_t kMaxPerGroup = 4;
+  constexpr size_t kMaxPerPool = 2;
+  pool().set_max_stream_sockets_per_group_for_testing(kMaxPerGroup);
+  pool().set_max_stream_sockets_per_pool_for_testing(kMaxPerPool);
+  // Base 0.0, capacity 2, minimum 0.5, noise 0.0 ensures 50% probability
+  // for expandability transitions between soft cap (2) and hard cap (4).
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateForTest(0.0, 2, 0.5, 0.0));
+
+  // Below soft cap (1 stream in use), always uncapped.
+  pool().IncrementTotalHandedOutStreamCount();
+  for (size_t i = 0; i < 50; ++i) {
+    pool().UpdateExpandabilityBeforeAllocation();
+    EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  }
+
+  // At soft cap (2 streams in use), NextExpandabilityBeforeAllocation has 50%
+  // chance of transitioning from kUncapped to kCapped.
+  pool().IncrementTotalHandedOutStreamCount();
+  bool saw_uncapped = false;
+  bool saw_capped = false;
+  for (size_t i = 0; i < 1000; ++i) {
+    pool().ResetExpandability();
+    pool().UpdateExpandabilityBeforeAllocation();
+    if (pool().expandability() == SocketPoolExpandability::kCapped) {
+      saw_capped = true;
+    } else {
+      saw_uncapped = true;
+    }
+    if (saw_capped && saw_uncapped) {
+      break;
+    }
+  }
+  EXPECT_TRUE(saw_uncapped);
+  EXPECT_TRUE(saw_capped);
+
+  // At hard cap (4 streams in use), always capped.
+  pool().IncrementTotalHandedOutStreamCount();
+  pool().IncrementTotalHandedOutStreamCount();
+  ASSERT_EQ(pool().TotalActiveStreamCount(), 4u);
+  for (size_t i = 0; i < 50; ++i) {
+    pool().ResetExpandability();
+    pool().UpdateExpandabilityBeforeAllocation();
+    EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kCapped);
+  }
+
+  // For release transitions: with 3 streams in use, starting from kCapped,
+  // NextExpandabilityAfterRelease has 50% chance of transitioning to kUncapped.
+  pool().DecrementTotalHandedOutStreamCount();
+  ASSERT_EQ(pool().TotalActiveStreamCount(), 3u);
+  saw_uncapped = false;
+  saw_capped = false;
+  for (size_t i = 0; i < 1000; ++i) {
+    // Reset to capped: at 4 streams, it's guaranteed capped.
+    pool().IncrementTotalHandedOutStreamCount();
+    pool().UpdateExpandabilityBeforeAllocation();
+    pool().DecrementTotalHandedOutStreamCount();
+    ASSERT_EQ(pool().expandability(), SocketPoolExpandability::kCapped);
+
+    pool().UpdateExpandabilityAfterRelease();
+    if (pool().expandability() == SocketPoolExpandability::kUncapped) {
+      saw_uncapped = true;
+    } else {
+      saw_capped = true;
+    }
+    if (saw_capped && saw_uncapped) {
+      break;
+    }
+  }
+  EXPECT_TRUE(saw_uncapped);
+  EXPECT_TRUE(saw_capped);
+
+  while (pool().TotalActiveStreamCount() > 0) {
+    pool().DecrementTotalHandedOutStreamCount();
+  }
+  pool().ResetExpandability();
+}
+
+TEST_F(HttpStreamPoolAttemptManagerTest, CappedPoolPurgesIdleSockets) {
+  constexpr size_t kMaxPerGroup = 2;
+  constexpr size_t kMaxPerPool = 2;
+  pool().set_max_stream_sockets_per_group_for_testing(kMaxPerGroup);
+  pool().set_max_stream_sockets_per_pool_for_testing(kMaxPerPool);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateForTest(0.0, 2, 0.5, 0.0));
+
+  const HttpStreamKey key_a(
+      url::SchemeHostPort("http", "a.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
+  const HttpStreamKey key_b(
+      url::SchemeHostPort("http", "b.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
+  // Add 2 idle streams in group A.
+  Group& group_a = pool().GetOrCreateGroupForTesting(key_a);
+  for (size_t i = 0; i < kMaxPerGroup; ++i) {
+    group_a.AddIdleStreamSocket(std::make_unique<FakeStreamSocket>());
+  }
+  ASSERT_EQ(group_a.IdleStreamSocketCount(), 2u);
+  ASSERT_EQ(pool().TotalActiveStreamCount(), 2u);
+
+  // Ensure the pool transitions to capped state before allocating in group B.
+  while (pool().expandability() != SocketPoolExpandability::kCapped) {
+    pool().UpdateExpandabilityBeforeAllocation();
+  }
+  ASSERT_TRUE(pool().ReachedMaxStreamLimit());
+  ASSERT_EQ(pool().expandability(), SocketPoolExpandability::kCapped);
+
+  // Request a stream in group B.
+  base::WeakPtr<FakeServiceEndpointRequest> endpoint_request =
+      resolver()->AddFakeRequest();
+  StreamRequester requester(key_b);
+  requester.RequestStream(pool());
+  auto data = std::make_unique<SequencedSocketData>();
+  data->set_connect_data(MockConnect(ASYNC, OK));
+  socket_factory()->AddSocketDataProvider(data.get());
+
+  endpoint_request->add_endpoint(
+      ServiceEndpointBuilder().add_v4("192.0.2.1").endpoint());
+  endpoint_request->CallOnServiceEndpointRequestFinished(OK);
+  requester.WaitForResult();
+
+  // The request in group B should have purged an idle stream in group A and
+  // succeeded.
+  EXPECT_THAT(requester.result(), Optional(IsOk()));
+  EXPECT_EQ(group_a.IdleStreamSocketCount(), 1u);
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  EXPECT_FALSE(pool().ReachedMaxStreamLimit());
+}
+
+TEST_F(HttpStreamPoolAttemptManagerTest, FlushWithErrorResetsExpandability) {
+  constexpr size_t kMaxPerGroup = 2;
+  constexpr size_t kMaxPerPool = 1;
+  pool().set_max_stream_sockets_per_group_for_testing(kMaxPerGroup);
+  pool().set_max_stream_sockets_per_pool_for_testing(kMaxPerPool);
+  pool().SetAdditionalCapacityForTest(
+      SocketPoolAdditionalCapacity::CreateEmpty());
+
+  const HttpStreamKey key(
+      url::SchemeHostPort("http", "a.test", 80), PRIVACY_MODE_DISABLED,
+      SocketTag(), NetworkAnonymizationKey(), SecureDnsPolicy::kAllow,
+      /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle);
+
+  Group& group = pool().GetOrCreateGroupForTesting(key);
+  std::unique_ptr<HttpStream> stream =
+      group.CreateTextBasedStream(std::make_unique<FakeStreamSocket>(),
+                                  StreamSocketHandle::SocketReuseType::kUnused,
+                                  LoadTimingInfo::ConnectTiming());
+
+  pool().UpdateExpandabilityBeforeAllocation();
+  ASSERT_EQ(pool().expandability(), SocketPoolExpandability::kCapped);
+  ASSERT_TRUE(pool().ReachedMaxStreamLimit());
+
+  pool().FlushWithError(ERR_ABORTED, StreamSocketCloseReason::kAbort, "Flush");
+
+  EXPECT_EQ(pool().expandability(), SocketPoolExpandability::kUncapped);
+  stream.reset();
+  EXPECT_FALSE(pool().ReachedMaxStreamLimit());
+}
+
 TEST_F(HttpStreamPoolAttemptManagerTest,
        ReachedPoolLimitHighPriorityGroupFirst) {
   constexpr size_t kMaxPerGroup = 1;
Loading diff…

Original Bug Report

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