Chrome · Network › HEv3
CVE-2026-113167
Memory Corruption in Network (HEv3)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/http/http_stream_pool.cc |
modified | |
whilenet/http/http_stream_pool.cc |
modified | |
fornet/http/http_stream_pool.cc |
modified | |
ifnet/http/http_stream_pool_attempt_manager.cc |
modified | |
TEST_Fnet/http/http_stream_pool_attempt_manager_unittest.cc |
modified | |
BindLambdaForTestingnet/http/http_stream_pool_attempt_manager_unittest.cc |
modified |
Files Changed
net/socket/socket_pool_additional_capacity.hnet/http/http_stream_pool_group.ccnet/http/http_stream_pool.ccnet/http/http_stream_pool_attempt_manager.ccnet/http/http_stream_pool.hnet/http/http_stream_pool_attempt_manager_unittest.cc
Patch
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.
References
On This Page