Chrome · Cache › HttpCache
CVE-2026-118352
Memory Corruption in Cache (HttpCache)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/http/http_transaction_test_util.cc |
modified | |
ifnet/http/http_cache_unittest.cc |
modified | |
TEST_Fnet/http/http_cache_unittest.cc |
modified |
Files Changed
net/http/http_transaction_test_util.ccnet/http/http_cache_unittest.ccnet/http/http_transaction_test_util.hnet/http/http_cache.ccnet/http/http_cache_transaction.h
Patch
From ef5adf65e8dcd6202bbb4d17f91dd1774ff14446 Mon Sep 17 00:00:00 2001
From: Maks Orlovich <morlovich@chromium.org>
Date: Thu, 15 Oct 2026 18:43:59 -0700
Subject: [PATCH] HttpCache: prevent lock timeout overtaking explicit restart
...by cancelling it when queuing the restart. This can happen if our
thread is super-overloaded (which is worrisome)
Bug: 569674221
Change-Id: Iaa3f12540534ca58c2446102d6b79e3566a557c1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8578804
Commit-Queue: Maks Orlovich <morlovich@chromium.org>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1720263}
---
diff --git a/net/http/http_cache.cc b/net/http/http_cache.cc
index 13ef0046..fb45d1f 100644
--- a/net/http/http_cache.cc
+++ b/net/http/http_cache.cc
@@ -1437,6 +1437,13 @@
// for the transaction to not be found in this entry.
for (HttpCache::Transaction* transaction : entry->add_to_entry_queue()) {
transaction->ResetCachePendingState();
+
+ // Cancel the timeout timer. Under extreme load that can end up firing
+ // before the task we post below, and our tasks expects us to be removed
+ // from the entry queue, which we might no longer be the case with the
+ // reversal.
+ transaction->CancelEntryLockTimeout();
+
TaskRunner(transaction->priority())
->PostTask(FROM_HERE, base::BindOnce(transaction->cache_io_callback(),
ERR_CACHE_RACE));
diff --git a/net/http/http_cache_transaction.h b/net/http/http_cache_transaction.h
index b4375716..bfd85145 100644
--- a/net/http/http_cache_transaction.h
+++ b/net/http/http_cache_transaction.h
@@ -182,6 +182,10 @@
// added to an entry and the entry.
void ResetCachePendingState() { cache_pending_ = false; }
+ void CancelEntryLockTimeout() {
+ entry_lock_waiting_since_ = base::TimeTicks();
+ }
+
RequestPriority priority() const { return priority_; }
PartialData* partial() { return partial_.get(); }
bool is_truncated() { return truncated_; }
diff --git a/net/http/http_transaction_test_util.cc b/net/http/http_transaction_test_util.cc
index 646dde5..88a416c 100644
--- a/net/http/http_transaction_test_util.cc
+++ b/net/http/http_transaction_test_util.cc
@@ -389,6 +389,10 @@
const MockTransaction* t = FindMockTransaction(current_request_.url);
DCHECK(t);
+ if (test_mode_ & TEST_MODE_STUCK_READ) {
+ return ERR_IO_PENDING;
+ }
+
CHECK(!done_reading_called_);
reading_ = true;
@@ -517,8 +521,17 @@
if (t->start_handler) {
DCHECK_EQ(start_return_code, OK)
<< "set either start_return_code or start_handler, not both";
- start_return_code = t->start_handler.Run(¤t_request_);
+ start_return_code = t->start_handler.Run(
+ ¤t_request_, base::BindOnce(&MockNetworkTransaction::OnIOComplete,
+ weak_factory_.GetWeakPtr()));
+ if (start_return_code == ERR_IO_PENDING) {
+ // The `start_handler` is responsible for resuming us.
+ callback_ = std::move(callback);
+ next_state_ = State::CREATE_STREAM;
+ return ERR_IO_PENDING;
+ }
}
+
if (OK != start_return_code) {
if (test_mode_ & TEST_MODE_SYNC_NET_START) {
return start_return_code;
diff --git a/net/http/http_transaction_test_util.h b/net/http/http_transaction_test_util.h
index 3ce1625..c830d6c 100644
--- a/net/http/http_transaction_test_util.h
+++ b/net/http/http_transaction_test_util.h
@@ -52,14 +52,15 @@
enum {
TEST_MODE_NORMAL = 0,
TEST_MODE_SYNC_NET_START = 1 << 0,
- TEST_MODE_SYNC_NET_READ = 1 << 1,
+ TEST_MODE_SYNC_NET_READ = 1 << 1,
TEST_MODE_SYNC_CACHE_START = 1 << 2,
- TEST_MODE_SYNC_CACHE_READ = 1 << 3,
- TEST_MODE_SYNC_CACHE_WRITE = 1 << 4,
+ TEST_MODE_SYNC_CACHE_READ = 1 << 3,
+ TEST_MODE_SYNC_CACHE_WRITE = 1 << 4,
TEST_MODE_SYNC_ALL = (TEST_MODE_SYNC_NET_START | TEST_MODE_SYNC_NET_READ |
TEST_MODE_SYNC_CACHE_START | TEST_MODE_SYNC_CACHE_READ |
TEST_MODE_SYNC_CACHE_WRITE),
- TEST_MODE_SLOW_READ = 1 << 5
+ TEST_MODE_SLOW_READ = 1 << 5,
+ TEST_MODE_STUCK_READ = 1 << 6
};
using MockTransactionReadHandler = base::RepeatingCallback<
@@ -71,7 +72,8 @@
std::string* response_headers,
std::string* response_data)>;
using MockTransactionStartHandler =
- base::RepeatingCallback<Error(const HttpRequestInfo* request)>;
+ base::RepeatingCallback<Error(const HttpRequestInfo* request,
+ CompletionOnceCallback callback)>;
// Default TransportInfo suitable for most MockTransactions.
// Describes a direct connection to (127.0.0.1, 80).
diff --git a/net/http/http_cache_unittest.cc b/net/http/http_cache_unittest.cc
index 0943611c..d15b408 100644
--- a/net/http/http_cache_unittest.cc
+++ b/net/http/http_cache_unittest.cc
@@ -10821,7 +10821,8 @@
transaction.request_headers = EXTRA_HEADER;
transaction.start_handler =
- base::BindLambdaForTesting([&](const HttpRequestInfo* request) -> Error {
+ base::BindLambdaForTesting([&](const HttpRequestInfo* request,
+ CompletionOnceCallback callback) -> Error {
std::optional<std::string_view> range_header =
request->extra_headers.GetHeaderView(HttpRequestHeaders::kRange);
if (!range_header) {
@@ -17493,6 +17494,78 @@
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
};
+// Test for cache timeout triggering at strange time when a transaction
+// already had a restart queued since saved entry's validation failed.
+//
+// See crbug.com/569674221
+TEST_F(HttpCacheTestWithMockTime, LockTimeoutRace) {
+ MockHttpCache cache;
+
+ ScopedMockTransaction transaction(kSimpleGET_Transaction);
+ transaction.response_headers = "Cache-Control: max-age=0;\n ETag: 'a'\n";
+ transaction.test_mode = TEST_MODE_STUCK_READ;
+
+ // Request 1: gets stuck reading data.
+ MockHttpRequest request1(transaction);
+ std::unique_ptr<HttpTransaction> trans_stuck_in_data_phase;
+ {
+ TestCompletionCallback callback;
+ trans_stuck_in_data_phase = cache.CreateTransaction();
+
+ int rv = callback.GetResult(trans_stuck_in_data_phase->Start(
+ &request1, callback.callback(), NetLogWithSource()));
+ ASSERT_EQ(rv, OK);
+
+ TestCompletionCallback read_callback;
+ auto buf = base::MakeRefCounted<IOBufferWithSize>(256);
+ rv = trans_stuck_in_data_phase->Read(buf.get(), 256,
+ read_callback.callback());
+ ASSERT_EQ(rv, ERR_IO_PENDING);
+ }
+
+ // Request 2: paused in sending request; will try (and fail) validation
+ // once resumed.
+ base::RunLoop run_loop;
+ CompletionOnceCallback concurrent1_callback;
+ transaction.start_handler =
+ base::BindLambdaForTesting([&](const HttpRequestInfo* request,
+ CompletionOnceCallback callback) -> Error {
+ concurrent1_callback = std::move(callback);
+ run_loop.Quit();
+ return ERR_IO_PENDING;
+ });
+
+ MockHttpRequest request2(transaction);
+ std::unique_ptr<HttpTransaction> trans_concurrent_1;
+
+ TestCompletionCallback callback2;
+ trans_concurrent_1 = cache.CreateTransaction();
+
+ int rv = trans_concurrent_1->Start(&request2, callback2.callback(),
+ NetLogWithSource());
+ ASSERT_EQ(rv, ERR_IO_PENDING);
+
+ run_loop.Run();
+
+ // Request 3: to same URL; we resume request 2 once it returns from
+ // Start when it should be trying to be added to the entry.
+ MockHttpRequest request3(transaction);
+ std::unique_ptr<HttpTransaction> trans_concurrent_2;
+
+ TestCompletionCallback callback3;
+ trans_concurrent_2 = cache.CreateTransaction();
+
+ rv = trans_concurrent_2->Start(&request3, callback3.callback(),
+ NetLogWithSource());
+ ASSERT_EQ(rv, ERR_IO_PENDING);
+ std::move(concurrent1_callback).Run(net::OK);
+
+ // Advance past the timeline for cache lock, so it can beat the
+ // restart.
+ AdvanceClock(base::Seconds(30));
+ EXPECT_EQ(OK, callback2.WaitForResult());
+}
+
TEST_F(HttpCacheTestWithMockTime, InvalidationFilterTimeBoundaries) {
AddScopedFeatureList().InitAndEnableFeature(
net::features::kLogicalClearHttpCache);
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