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 <bashi@chromium.org>
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 <bashi@chromium.org>
Reviewed-by: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Adam Rice <ricea@chromium.org>
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