High chrome Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in CacheStorage
DescriptionMemory corruption in CacheStorage
ComponentCacheStorage
Bug ClassMemory Corruption
Tracker548989584
Fix commit8841f4cd2383 (chromium/src) +752/-442
CISA KEVNot listed
CreditedSalvatore Gulizia (nickname: Serotav)
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
CacheStorageCache
content/browser/cache_storage/cache_storage_cache.cc
modified
if
content/browser/cache_storage/cache_storage_cache.cc
modified

Files Changed

  • third_party/blink/renderer/platform/loader/fetch/url_loader/cached_metadata_handler_test.cc
  • services/network/public/cpp/BUILD.gn
  • content/browser/cache_storage/cache_storage_dispatcher_host.cc
  • third_party/blink/common/loader/mime_sniffing_throttle.cc
  • third_party/blink/public/mojom/fetch/fetch_api_response.mojom
  • third_party/blink/public/mojom/loader/code_cache.mojom
  • services/network/public/cpp/cache_storage_side_data_writer_mojom_traits.h
  • third_party/blink/renderer/core/fetch/fetch_response_data.cc
  • third_party/blink/renderer/platform/loader/fetch/persistent_code_cache_host.cc
  • content/browser/cache_storage/cache_storage_cache_unittest.cc
  • content/browser/cache_storage/cache_storage_cache.cc
  • third_party/blink/public/platform/web_url_response.h
  • third_party/blink/renderer/platform/loader/fetch/url_loader/resource_request_sender.cc
  • third_party/blink/public/mojom/BUILD.gn
  • third_party/blink/renderer/platform/loader/fetch/url_loader/worker_main_script_loader.cc
  • third_party/blink/renderer/bindings/core/v8/v8_wasm_response_extensions.cc
  • services/network/public/mojom/cache_storage_side_data_writer.mojom
  • third_party/blink/renderer/platform/loader/fetch/url_loader/cached_metadata_handler.cc
  • content/browser/cache_storage/cache_storage_manager_unittest.cc
  • third_party/blink/renderer/platform/loader/fetch/url_loader/isolated_code_cache_fetcher.cc
  • third_party/blink/renderer/platform/loader/fetch/url_loader/resource_request_sender_unittest.cc
  • third_party/blink/renderer/platform/exported/web_url_response.cc
  • content/renderer/service_worker/service_worker_subresource_loader.h
  • third_party/blink/renderer/platform/loader/fetch/url_loader/cached_metadata_handler.h
  • third_party/blink/renderer/core/fetch/response.cc
  • third_party/blink/public/mojom/cache_storage/cache_storage.mojom
  • content/browser/cache_storage/cache_storage.cc
  • services/network/public/mojom/BUILD.gn
  • content/browser/renderer_host/code_cache_host_impl.cc
  • content/browser/service_worker/service_worker_main_resource_loader.cc
  • content/renderer/service_worker/service_worker_subresource_loader.cc
  • third_party/blink/renderer/platform/loader/fetch/resource_response.cc
  • third_party/blink/renderer/platform/loader/fetch/url_loader/url_loader.cc
  • components/content_settings/renderer/content_settings_agent_impl_browsertest.cc
  • services/network/public/mojom/url_response_head.mojom
  • third_party/blink/renderer/platform/loader/fetch/url_loader/background_url_loader.cc
  • content/browser/renderer_host/code_cache_host_impl.h
  • content/browser/service_worker/service_worker_main_resource_loader.h
  • content/browser/cache_storage/cache_storage_cache.h
  • third_party/blink/renderer/platform/loader/fetch/persistent_code_cache_host.h
  • third_party/blink/renderer/core/fetch/fetch_response_data.h
  • third_party/blink/renderer/modules/cache_storage/cache_test.cc
  • content/common/background_fetch/background_fetch_types.cc
  • third_party/blink/renderer/platform/loader/fetch/resource_response.h
From 8841f4cd23838793f034ebabf6e3417df86d80e3 Mon Sep 17 00:00:00 2001
From: Eriko Kurimoto <elkurin@google.com>
Date: Mon, 05 Oct 2026 02:51:36 -0700
Subject: [PATCH] Plumb CacheStorageSideDataWriter capability remote for code cache

Currently, URLResponseHead and FetchAPIResponse pass cache names as
strings for CacheStorage code-cache metadata feedback. This introduces a
TOCTOU risk and loses storage bucket identity when writing side data.

This change replaces string cache/bucket names with a
pending_remote<network.mojom.CacheStorageSideDataWriter> bound directly
to the CacheStorageCache entry that served the response, invoked
directly by the renderer.

TAG=agy
CONV=0ba9eb11-a35f-45aa-941c-58216e07432c

Bug: 548989584
Change-Id: I80860ed642f6635d10fc034703a2865d63b9a251
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8303572
Reviewed-by: Rakina Zata Amni <rakina@chromium.org>
Reviewed-by: Evan Stade <evanstade@microsoft.com>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Commit-Queue: Christian Dullweber <dullweber@chromium.org>
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Auto-Submit: Eriko Kurimoto <elkurin@chromium.org>
Reviewed-by: Christian Dullweber <dullweber@chromium.org>
Reviewed-by: Sam McNally <sammc@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1711121}
---
diff --git a/content/browser/cache_storage/cache_storage.cc b/content/browser/cache_storage/cache_storage.cc
index 632ebca..bbf5826 100644
--- a/content/browser/cache_storage/cache_storage.cc
+++ b/content/browser/cache_storage/cache_storage.cc
@@ -1241,6 +1241,7 @@
   }
 
   cache_index_->FinalizeDoomedCache();
+  impl->InvalidateSideDataWriters();
 
   auto map_iter = cache_map_.find(impl->cache_name());
   CHECK(map_iter != cache_map_.end());
diff --git a/content/browser/cache_storage/cache_storage_cache.cc b/content/browser/cache_storage/cache_storage_cache.cc
index a45e5623..cf8f895 100644
--- a/content/browser/cache_storage/cache_storage_cache.cc
+++ b/content/browser/cache_storage/cache_storage_cache.cc
@@ -44,6 +44,8 @@
 #include "content/browser/cache_storage/cache_storage_trace_utils.h"
 #include "content/common/background_fetch/background_fetch_types.h"
 #include "crypto/hmac.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/receiver.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "net/base/completion_repeating_callback.h"
 #include "net/base/io_buffer.h"
@@ -508,6 +510,7 @@
       mime_type, request_method, /*blob=*/nullptr,
       blink::mojom::ServiceWorkerResponseError::kUnknown, response_time,
       base::UTF16ToUTF8(cache_name),
+      /*cache_storage_side_data_writer=*/mojo::NullRemote(),
       std::vector<std::string>(
           metadata.response().cors_exposed_header_names().begin(),
           metadata.response().cors_exposed_header_names().end()),
@@ -606,6 +609,52 @@
   const int64_t trace_id = 0;
 };
 
+class CacheStorageCache::SideDataWriter
+    : public network::mojom::CacheStorageSideDataWriter {
+ public:
+  SideDataWriter(
+      CacheStorageCache* cache,
+      const GURL& url,
+      base::Time expected_response_time,
+      mojo::PendingReceiver<network::mojom::CacheStorageSideDataWriter>
+          receiver)
+      : handle_(cache->CreateHandle()),
+        url_(url),
+        expected_response_time_(expected_response_time),
+        receiver_(this, std::move(receiver)) {
+    receiver_.set_disconnect_handler(
+        base::BindOnce(&SideDataWriter::OnDisconnect, base::Unretained(this)));
+  }
+
+  SideDataWriter(const SideDataWriter&) = delete;
+  SideDataWriter& operator=(const SideDataWriter&) = delete;
+
+  ~SideDataWriter() override = default;
+
+  const GURL& url() const { return url_; }
+
+  // network::mojom::CacheStorageSideDataWriter:
+  void WriteSideData(mojo_base::BigBuffer data) override {
+    int64_t trace_id = blink::cache_storage::CreateTraceId();
+    TRACE_EVENT("CacheStorage",
+                "CacheStorageCache::SideDataWriter::WriteSideData",
+                perfetto::Flow::Global(trace_id), "url", url_.spec());
+    CacheStorageCache* cache = CacheStorageCache::From(handle_);
+    cache->WriteSideData(std::move(handle_), url_, expected_response_time_,
+                         trace_id, std::move(data));
+  }
+
+ private:
+  void OnDisconnect() {
+    CacheStorageCache::From(handle_)->SideDataWriterDisconnected(this);
+  }
+
+  CacheStorageCacheHandle handle_;
+  const GURL url_;
+  const base::Time expected_response_time_;
+  mojo::Receiver<network::mojom::CacheStorageSideDataWriter> receiver_;
+};
+
 // static
 std::unique_ptr<CacheStorageCache> CacheStorageCache::CreateMemoryCache(
     const storage::BucketLocator& bucket_locator,
@@ -646,6 +695,50 @@
   return base::WrapUnique(cache);
 }
 
+mojo::PendingRemote<network::mojom::CacheStorageSideDataWriter>
+CacheStorageCache::CreateSideDataWriter(const GURL& url,
+                                        base::Time expected_response_time) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  if (backend_state_ == BACKEND_CLOSED) {
+    return mojo::NullRemote();
+  }
+  mojo::PendingRemote<network::mojom::CacheStorageSideDataWriter> remote;
+  side_data_writers_.insert(std::make_unique<SideDataWriter>(
+      this, NormalizeCacheUrl(url), expected_response_time,
+      remote.InitWithNewPipeAndPassReceiver()));
+  return remote;
+}
+
+void CacheStorageCache::SideDataWriterDisconnected(SideDataWriter* writer) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  auto it = side_data_writers_.find(writer);
+  if (it != side_data_writers_.end()) {
+    // Extract the node before destroying `SideDataWriter`, as dropping its
+    // `CacheStorageCacheHandle` may synchronously delete `this`.
+    std::ignore = side_data_writers_.extract(it);
+  }
+}
+
+void CacheStorageCache::InvalidateSideDataWriters(std::optional<GURL> url) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  // Extract nodes before destroying `SideDataWriter` instances, as dropping
+  // their `CacheStorageCacheHandle`s may synchronously delete `this`.
+  if (!url.has_value()) {
+    auto writers = std::move(side_data_writers_);
+    return;
+  }
+  GURL normalized_url = NormalizeCacheUrl(*url);
+  std::vector<std::unique_ptr<SideDataWriter>> writers_to_delete;
+  for (auto it = side_data_writers_.begin(); it != side_data_writers_.end();) {
+    if ((*it)->url() == normalized_url) {
+      writers_to_delete.push_back(
+          std::move(side_data_writers_.extract(it++).value()));
+    } else {
+      ++it;
+    }
+  }
+}
+
 base::WeakPtr<CacheStorageCache> CacheStorageCache::AsWeakPtr() {
   return weak_ptr_factory_.GetWeakPtr();
 }
@@ -727,29 +820,28 @@
           scheduler_->WrapCallbackToRunNext(id, std::move(callback))));
 }
 
-void CacheStorageCache::WriteSideData(ErrorCallback callback,
-                                      const GURL& url,
+void CacheStorageCache::WriteSideData(CacheStorageCacheHandle cache_handle,
+                                      GURL url,
                                       base::Time expected_response_time,
                                       int64_t trace_id,
-                                      scoped_refptr<net::IOBuffer> buffer,
-                                      int buf_len) {
+                                      mojo_base::BigBuffer data) {
+  InvalidateSideDataWriters(url);
+
   if (IsClosingOrClosed()) {
-    scheduler_task_runner_->PostTask(
-        FROM_HERE,
-        base::BindOnce(
-            std::move(callback),
-            MakeErrorStorage(ErrorStorageType::kWriteSideDataBackendClosed)));
     return;
   }
 
+  // Copy shared memory to a private heap buffer to avoid TOCTOU issues.
+  data.MakePrivateBytes();
+
   // GetBucketSpaceRemaining is called before entering a scheduled operation
   // since it can call Size, another scheduled operation.
   quota_manager_proxy_->GetBucketSpaceRemaining(
       bucket_locator_, scheduler_task_runner_,
       base::BindOnce(
           &CacheStorageCache::WriteSideDataDidGetBucketSpaceRemaining,
-          weak_ptr_factory_.GetWeakPtr(), std::move(callback), url,
-          expected_response_time, trace_id, buffer, buf_len));
+          weak_ptr_factory_.GetWeakPtr(), std::move(cache_handle),
+          std::move(url), expected_response_time, trace_id, std::move(data)));
 }
 
 void CacheStorageCache::BatchOperation(
@@ -1363,6 +1455,9 @@
           .Run(CacheStorageError::kErrorQueryTooLarge, nullptr);
       return;
     }
+    match->response->cache_storage_side_data_writer =
+        CreateSideDataWriter(GURL(blob_entry->disk_cache_entry()->GetKey()),
+                             match->response->response_time);
     if (blob_entry->disk_cache_entry()->GetDataSize(INDEX_RESPONSE_BODY) == 0) {
       QueryCacheOpenNextEntry(std::move(query_cache_context));
       return;
@@ -1570,30 +1665,23 @@
 }
 
 void CacheStorageCache::WriteSideDataDidGetBucketSpaceRemaining(
-    ErrorCallback callback,
+    CacheStorageCacheHandle cache_handle,
     const GURL& url,
     base::Time expected_response_time,
     int64_t trace_id,
-    scoped_refptr<net::IOBuffer> buffer,
-    int buf_len,
+    mojo_base::BigBuffer data,
     storage::QuotaErrorOr<int64_t> space_remaining) {
   TRACE_EVENT("CacheStorage",
               "CacheStorageCache::WriteSideDataDidGetBucketSpaceRemaining",
               perfetto::Flow::Global(trace_id));
 
   if (IsClosingOrClosed()) {
-    scheduler_task_runner_->PostTask(
-        FROM_HERE,
-        base::BindOnce(
-            std::move(callback),
-            MakeErrorStorage(ErrorStorageType::kWriteSideDataBackendClosed)));
     return;
   }
 
-  if (!space_remaining.has_value() || space_remaining.value() < buf_len) {
-    scheduler_task_runner_->PostTask(
-        FROM_HERE, base::BindOnce(std::move(callback),
-                                  CacheStorageError::kErrorQuotaExceeded));
+  if (!space_remaining.has_value() ||
+      !base::IsValueInRangeForNumericType<int64_t>(data.size()) ||
+      space_remaining.value() < static_cast<int64_t>(data.size())) {
     return;
   }
 
@@ -1602,35 +1690,32 @@
       id, CacheStorageSchedulerMode::kExclusive,
       CacheStorageSchedulerOp::kWriteSideData,
       CacheStorageSchedulerPriority::kNormal,
-      base::BindOnce(&CacheStorageCache::WriteSideDataImpl,
-                     weak_ptr_factory_.GetWeakPtr(),
-                     scheduler_->WrapCallbackToRunNext(id, std::move(callback)),
-                     url, expected_response_time, trace_id, buffer, buf_len));
+      base::BindOnce(
+          &CacheStorageCache::WriteSideDataImpl, weak_ptr_factory_.GetWeakPtr(),
+          scheduler_->WrapCallbackToRunNext(
+              id, base::OnceClosure(
+                      base::DoNothingWithBoundArgs(std::move(cache_handle)))),
+          url, expected_response_time, trace_id, std::move(data)));
 }
 
-void CacheStorageCache::WriteSideDataImpl(ErrorCallback callback,
+void CacheStorageCache::WriteSideDataImpl(base::OnceClosure callback,
                                           const GURL& url,
                                           base::Time expected_response_time,
                                           int64_t trace_id,
-                                          scoped_refptr<net::IOBuffer> buffer,
-                                          int buf_len) {
+                                          mojo_base::BigBuffer data) {
   CHECK_NE(BACKEND_UNINITIALIZED, backend_state_, base::NotFatalUntil::M158);
   TRACE_EVENT("CacheStorage", "CacheStorageCache::WriteSideDataImpl",
               perfetto::Flow::Global(trace_id), "url", url.spec());
   if (backend_state_ != BACKEND_OPEN) {
-    std::move(callback).Run(
-        MakeErrorStorage(ErrorStorageType::kWriteSideDataImplBackendClosed));
+    MakeErrorStorage(ErrorStorageType::kWriteSideDataImplBackendClosed);
+    std::move(callback).Run();
     return;
   }
 
-  // Hold the cache alive while performing any operation touching the
-  // disk_cache backend.
-  callback = WrapCallbackWithHandle(std::move(callback));
-
   auto split_callback = base::SplitOnceCallback(
       base::BindOnce(&CacheStorageCache::WriteSideDataDidOpenEntry,
                      weak_ptr_factory_.GetWeakPtr(), std::move(callback),
-                     expected_response_time, trace_id, buffer, buf_len));
+                     expected_response_time, trace_id, std::move(data)));
 
   // Note, the simple disk_cache priority is not important here because we
   // only allow one write operation at a time.  Therefore there will be no
@@ -1643,17 +1728,16 @@
 }
 
 void CacheStorageCache::WriteSideDataDidOpenEntry(
-    ErrorCallback callback,
+    base::OnceClosure callback,
     base::Time expected_response_time,
     int64_t trace_id,
-    scoped_refptr<net::IOBuffer> buffer,
-    int buf_len,
+    mojo_base::BigBuffer data,
     disk_cache::EntryResult result) {
   TRACE_EVENT("CacheStorage", "CacheStorageCache::WriteSideDataDidOpenEntry",
               perfetto::Flow::Global(trace_id));
 
   if (result.net_error() != net::OK) {
-    std::move(callback).Run(CacheStorageError::kErrorNotFound);
+    std::move(callback).Run();
     return;
   }
 
@@ -1667,15 +1751,14 @@
                base::BindOnce(&CacheStorageCache::WriteSideDataDidReadMetaData,
                               weak_ptr_factory_.GetWeakPtr(),
                               std::move(callback), expected_response_time,
-                              trace_id, buffer, buf_len, std::move(entry)));
+                              trace_id, std::move(data), std::move(entry)));
 }
 
 void CacheStorageCache::WriteSideDataDidReadMetaData(
-    ErrorCallback callback,
+    base::OnceClosure callback,
     base::Time expected_response_time,
     int64_t trace_id,
-    scoped_refptr<net::IOBuffer> buffer,
-    int buf_len,
+    mojo_base::BigBuffer data,
     ScopedWritableEntry entry,
     std::unique_ptr<proto::CacheMetadata> headers) {
   TRACE_EVENT("CacheStorage", "CacheStorageCache::WriteSideDataDidReadMetaData",
@@ -1690,6 +1773,9 @@
   // Get a temporary copy of the entry pointer before passing it in base::Bind.
   disk_cache::Entry* temp_entry_ptr = entry.get();
 
+  auto buffer = base::MakeRefCounted<storage::BigIOBuffer>(std::move(data));
+  int buf_len = buffer->size();
+
   // Create a callback that is copyable, even though it can only be called once.
   // BindRepeating() cannot be used directly because |callback|, |entry| and
   // |response| are not copyable.
@@ -1708,7 +1794,7 @@
 }
 
 void CacheStorageCache::WriteSideDataDidWrite(
-    ErrorCallback callback,
+    base::OnceClosure callback,
     ScopedWritableEntry entry,
     int expected_bytes,
     std::unique_ptr<::content::proto::CacheMetadata> metadata,
@@ -1750,12 +1836,13 @@
                         CacheStorageError::kSuccess);
 }
 
-void CacheStorageCache::WriteSideDataDidWriteMetadata(ErrorCallback callback,
-                                                      ScopedWritableEntry entry,
-                                                      int64_t padding,
-                                                      int64_t side_data_padding,
-                                                      int expected_bytes,
-                                                      int rv) {
+void CacheStorageCache::WriteSideDataDidWriteMetadata(
+    base::OnceClosure callback,
+    ScopedWritableEntry entry,
+    int64_t padding,
+    int64_t side_data_padding,
+    int expected_bytes,
+    int rv) {
   auto result = blink::mojom::CacheStorageError::kSuccess;
   if (rv != expected_bytes) {
     result = MakeErrorStorage(
@@ -1766,7 +1853,7 @@
 }
 
 void CacheStorageCache::WriteSideDataComplete(
-    ErrorCallback callback,
+    base::OnceClosure callback,
     ScopedWritableEntry entry,
     int64_t padding,
     int64_t side_data_padding,
@@ -1778,18 +1865,18 @@
     if (error != CacheStorageError::kErrorNotFound) {
       entry.reset();
       cache_padding_ -= (padding + side_data_padding);
-      UpdateCacheSize(base::BindOnce(std::move(callback), error));
+      UpdateCacheSize(std::move(callback));
       return;
     }
 
     entry.get_deleter()
         .WritingCompleted();  // Since we didn't change the entry.
-    std::move(callback).Run(error);
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/content_settings/renderer/content_settings_agent_impl_browsertest.cc b/components/content_settings/renderer/content_settings_agent_impl_browsertest.cc
index c185dd1d..2fae63c 100644
--- a/components/content_settings/renderer/content_settings_agent_impl_browsertest.cc
+++ b/components/content_settings/renderer/content_settings_agent_impl_browsertest.cc
@@ -240,11 +240,6 @@
     }
     void ClearCodeCacheEntry(blink::mojom::CodeCacheType cache_type,
                              const GURL& url) override {}
-    void DidGenerateCacheableMetadataInCacheStorage(
-        const GURL& url,
-        base::Time expected_response_time,
-        mojo_base::BigBuffer data,
-        const std::string& cache_storage_cache_name) override {}
 
    private:
     mojo::Receiver<blink::mojom::CodeCacheHost> receiver_{this};
diff --git a/content/browser/cache_storage/cache_storage_cache_unittest.cc b/content/browser/cache_storage/cache_storage_cache_unittest.cc
index 0725af7..a9b058c 100644
--- a/content/browser/cache_storage/cache_storage_cache_unittest.cc
+++ b/content/browser/cache_storage/cache_storage_cache_unittest.cc
@@ -687,6 +687,8 @@
 
   void Init() { InitBackend(); }
 
+  using CacheStorageCache::WriteSideData;
+
   base::CheckedNumeric<uint64_t> GetRequiredSafeSpaceForRequest(
       const blink::mojom::FetchAPIRequestPtr& request) {
     return CalculateRequiredSafeSpaceForRequest(request);
@@ -894,6 +896,7 @@
         /*mime_type=*/std::nullopt, net::HttpRequestHeaders::kGetMethod,
         /*blob=*/nullptr, blink::mojom::ServiceWorkerResponseError::kUnknown,
         response_time_, /*cache_storage_cache_name=*/std::string(),
+        /*cache_storage_side_data_writer=*/mojo::NullRemote(),
         /*cors_exposed_header_names=*/std::vector<std::string>(),
         /*side_data_blob=*/nullptr,
         /*side_data_for_cache_put=*/std::nullopt,
@@ -1060,16 +1063,22 @@
                      base::Time expected_response_time,
                      scoped_refptr<net::IOBuffer> buffer,
                      int buf_len) {
+    int old_modified_count =
+        quota_manager_proxy_->notify_bucket_modified_count();
     base::HistogramTester histogram_tester;
-    base::RunLoop run_loop;
     cache_->WriteSideData(
-        base::BindOnce(&CacheStorageCacheTest::ErrorTypeCallback,
-                       base::Unretained(this), base::Unretained(&run_loop)),
-        url, expected_response_time, /* trace_id = */ 0, buffer, buf_len);
-    run_loop.Run();
-    if (callback_error_ == CacheStorageError::kSuccess)
+        cache_->CreateHandle(), url, expected_response_time, /*trace_id=*/0,
+        mojo_base::BigBuffer(base::as_bytes(
+            buffer->first(base::checked_cast<size_t>(buf_len)))));
+    size_t expected_ref_count = cache_->handle_ref_count_ - 1;
+    EXPECT_TRUE(base::test::RunUntil(
+        [&] { return cache_->handle_ref_count_ == expected_ref_count; }));
+    bool succeeded = quota_manager_proxy_->notify_bucket_modified_count() >
+                     old_modified_count;
+    if (succeeded) {
       CheckOpHistograms(histogram_tester, "WriteSideData");
-    return callback_error_ == CacheStorageError::kSuccess;
+    }
+    return succeeded;
   }
 
   int64_t Size() {
@@ -1261,10 +1270,10 @@
     operations.push_back(std::move(operation));
     cache_->BatchOperation(std::move(operations), 0, batch.GetCallback(),
                            base::DoNothing());
-    base::test::TestFuture<CacheStorageError> side_data;
-    cache_->WriteSideData(side_data.GetCallback(), NoBodyUrl(), response_time_,
-                          0, base::MakeRefCounted<net::StringIOBuffer>("data"),
-                          4);
+    cache_->WriteSideData(cache_->CreateHandle(), NoBodyUrl(), response_time_,
+                          /*trace_id=*/0,
+                          mojo_base::BigBuffer(base::as_byte_span("data")));
+    EXPECT_TRUE(cache_->IsUnreferenced());
     base::test::TestFuture<int64_t> size;
     cache_->Size(size.GetCallback());
     base::test::TestFuture<int64_t> size_then_close;
@@ -1273,7 +1282,7 @@
     EXPECT_TRUE(base::test::RunUntil([&] {
       return match.IsReady() && all.IsReady() && keys.IsReady() &&
              entries.IsReady() && put.IsReady() && batch.IsReady() &&
-             side_data.IsReady() && size.IsReady() && size_then_close.IsReady();
+             size.IsReady() && size_then_close.IsReady();
     }));
     EXPECT_FALSE(closed.IsReady());
     EXPECT_FALSE(before.IsReady());
@@ -1285,7 +1294,6 @@
     EXPECT_TRUE(entries.IsReady());
     EXPECT_TRUE(put.IsReady());
     EXPECT_TRUE(batch.IsReady());
-    EXPECT_TRUE(side_data.IsReady());
     EXPECT_TRUE(size.IsReady());
     EXPECT_TRUE(size_then_close.IsReady());
     if (match.IsReady()) {
@@ -1306,9 +1314,6 @@
     if (batch.IsReady()) {
       EXPECT_EQ(CacheStorageError::kErrorStorage, batch.Get()->value);
     }
-    if (side_data.IsReady()) {
-      EXPECT_EQ(CacheStorageError::kErrorStorage, side_data.Get());
-    }
     if (size.IsReady()) {
       EXPECT_EQ(0, size.Get());
     }
@@ -1656,6 +1661,93 @@
   EXPECT_FALSE(Match(body_head_request_));
 }
 
+class TestCacheSizeObserver : public CacheStorageCacheObserver {
+ public:
+  explicit TestCacheSizeObserver(base::OnceClosure closure)
+      : closure_(std::move(closure)) {}
+  void CacheSizeUpdated(const CacheStorageCache* cache) override {
+    if (closure_) {
+      std::move(closure_).Run();
+    }
+  }
+
+ private:
+  base::OnceClosure closure_;
+};
+
+TEST_P(CacheStorageCacheTestP, MatchResponseIncludesSideDataWriterRemote) {
+  base::Time response_time(base::Time::Now());
+  blink::mojom::FetchAPIResponsePtr response = CreateBlobBodyResponse();
+  response->response_time = response_time;
+  EXPECT_TRUE(Put(body_request_, std::move(response)));
+
+  EXPECT_TRUE(Match(body_request_));
+  ASSERT_TRUE(callback_response_);
+  ASSERT_TRUE(callback_response_->cache_storage_side_data_writer.is_valid());
+
+  mojo::Remote<network::mojom::CacheStorageSideDataWriter> writer(
+      std::move(callback_response_->cache_storage_side_data_writer));
+  EXPECT_TRUE(writer.is_connected());
+
+  base::RunLoop size_updated_loop;
+  TestCacheSizeObserver size_observer(size_updated_loop.QuitClosure());
+  cache_->SetObserver(&size_observer);
+
+  const std::string expected_side_data = "SideDataViaMojoRemote";
+  writer->WriteSideData(
+      mojo_base::BigBuffer(base::as_byte_span(expected_side_data)));
+  writer.FlushForTesting();
+
+  // The writer should self-destruct after a single WriteSideData call,
+  // disconnecting its receiver.
+  EXPECT_FALSE(writer.is_connected());
+
+  size_updated_loop.Run();
+  cache_->SetObserver(nullptr);
+
+  EXPECT_TRUE(Match(body_request_));
+  ASSERT_TRUE(callback_response_->blob);
+  mojo::Remote<blink::mojom::Blob> blob(
+      std::move(callback_response_->blob->blob));
+  EXPECT_EQ(expected_side_data, CopySideData(blob.get()));
+}
+
+TEST_P(CacheStorageCacheTestP,
+       MatchResponseSideDataWriterDisconnectedWhenEntryDeleted) {
+  EXPECT_TRUE(Put(no_body_request_, CreateNoBodyResponse()));
+  EXPECT_TRUE(Match(no_body_request_));
+  ASSERT_TRUE(callback_response_);
+  ASSERT_TRUE(callback_response_->cache_storage_side_data_writer.is_valid());
+
+  mojo::Remote<network::mojom::CacheStorageSideDataWriter> writer(
+      std::move(callback_response_->cache_storage_side_data_writer));
+  EXPECT_TRUE(writer.is_connected());
+
+  EXPECT_TRUE(Delete(no_body_request_));
+  writer.FlushForTesting();
+
+  EXPECT_FALSE(writer.is_connected());
+}
+
+TEST_P(CacheStorageCacheTestP,
+       MatchResponseSideDataWriterDisconnectedWhenCacheClosed) {
+  EXPECT_TRUE(Put(no_body_request_, CreateNoBodyResponse()));
+  EXPECT_TRUE(Match(no_body_request_));
+  ASSERT_TRUE(callback_response_);
+  ASSERT_TRUE(callback_response_->cache_storage_side_data_writer.is_valid());
+
+  mojo::Remote<network::mojom::CacheStorageSideDataWriter> writer(
+      std::move(callback_response_->cache_storage_side_data_writer));
+  EXPECT_TRUE(writer.is_connected());
+
+  base::RunLoop loop;
+  cache_->Close(loop.QuitClosure());
+  loop.Run();
+  writer.FlushForTesting();
+
+  EXPECT_FALSE(writer.is_connected());
+}
+
 TEST_P(CacheStorageCacheTestP, MatchAll_Empty) {
   std::vector<blink::mojom::FetchAPIResponsePtr> responses;
   EXPECT_TRUE(MatchAll(&responses));
@@ -2268,7 +2360,6 @@
   std::ranges::fill(buffer->span(), 0);
   EXPECT_FALSE(
       WriteSideData(no_body_request_->url, response_time, buffer, kSize));
-  EXPECT_EQ(CacheStorageError::kErrorQuotaExceeded, callback_error_);
   ASSERT_TRUE(Delete(no_body_request_));
 }
 
@@ -2304,7 +2395,6 @@
   std::ranges::fill(buffer->span(), 0);
   EXPECT_FALSE(WriteSideData(no_body_request_->url,
                              response_time + base::Seconds(1), buffer, kSize));
-  EXPECT_EQ(CacheStorageError::kErrorNotFound, callback_error_);
   ASSERT_TRUE(Delete(no_body_request_));
 }
 
@@ -2314,7 +2404,6 @@
   std::ranges::fill(buffer->span(), 0);
   EXPECT_FALSE(WriteSideData(GURL("http://www.example.com/not_exist"),
                              base::Time::Now(), buffer, kSize));
-  EXPECT_EQ(CacheStorageError::kErrorNotFound, callback_error_);
 }
 
 TEST_P(CacheStorageCacheTestP, ReadSideDataShortRead) {
@@ -2788,9 +2877,10 @@
   operations.push_back(std::move(operation));
   cache_->BatchOperation(std::move(operations), 0, batch.GetCallback(),
                          base::DoNothing());
-  base::test::TestFuture<CacheStorageError> side_data;
-  cache_->WriteSideData(side_data.GetCallback(), BodyUrl(), response_time_, 0,
-                        base::MakeRefCounted<net::StringIOBuffer>("data"), 4);
+  cache_->WriteSideData(cache_->CreateHandle(), BodyUrl(), response_time_,
+                        /*trace_id=*/0,
+                        mojo_base::BigBuffer(base::as_byte_span("data")));
+  EXPECT_FALSE(cache_->IsUnreferenced());
 
   base::test::TestFuture<void> closed;
   cache_->Close(base::BindLambdaForTesting([&] {
@@ -2798,16 +2888,13 @@
     closed.SetValue();
   }));
   EXPECT_TRUE(base::test::RunUntil(
-      [&] { return batch.IsReady() && side_data.IsReady(); }));
+      [&] { return batch.IsReady() && cache_->IsUnreferenced(); }));
   EXPECT_FALSE(closed.IsReady());
   EXPECT_TRUE(batch.IsReady());
-  EXPECT_TRUE(side_data.IsReady());
+  EXPECT_TRUE(cache_->IsUnreferenced());
   if (batch.IsReady()) {
     EXPECT_EQ(CacheStorageError::kErrorStorage, batch.Get()->value);
   }
-  if (side_data.IsReady()) {
-    EXPECT_EQ(CacheStorageError::kErrorStorage, side_data.Get());
-  }
 
   std::move(unblock).Run();
   ASSERT_TRUE(closed.Wait());
@@ -3057,15 +3144,10 @@
   delayable_backend->set_delay_open_entry(true);
 
   const std::string expected_side_data = "SideDataSample";
-  scoped_refptr<net::IOBuffer> buffer =
-      base::MakeRefCounted<net::StringIOBuffer>(expected_side_data);
 
-  std::unique_ptr<base::RunLoop> loop(new base::RunLoop());
   cache_->WriteSideData(
-      base::BindOnce(&CacheStorageCacheTest::ErrorTypeCallback,
-                     base::Unretained(this), base::Unretained(loop.get())),
-      BodyUrl(), response_time, /* trace_id = */ 0, buffer,
-      expected_side_data.length());
+      cache_->CreateHandle(), BodyUrl(), response_time, /*trace_id=*/0,
+      mojo_base::BigBuffer(base::as_byte_span(expected_side_data)));
 
   // Blocks on opening the cache entry.
   base::RunLoop().RunUntilIdle();
@@ -3076,10 +3158,7 @@
 
   // Allow the operation to continue.
   EXPECT_TRUE(delayable_backend->OpenEntryContinue());
-  loop->Run();
-
-  // The operation should succeed.
-  EXPECT_EQ(CacheStorageError::kSuccess, callback_error_);
+  EXPECT_TRUE(base::test::RunUntil([&] { return cache_->IsUnreferenced(); }));
 }
 
 TEST_P(CacheStorageCacheTestP, SelfRefsDuringBatchOperation) {
diff --git a/content/browser/cache_storage/cache_storage_manager_unittest.cc b/content/browser/cache_storage/cache_storage_manager_unittest.cc
index df8f5d93..4c0263f 100644
--- a/content/browser/cache_storage/cache_storage_manager_unittest.cc
+++ b/content/browser/cache_storage/cache_storage_manager_unittest.cc
@@ -688,6 +688,7 @@
         net::HttpRequestHeaders::kGetMethod, std::move(blob),
         blink::mojom::ServiceWorkerResponseError::kUnknown, base::Time(),
         /*cache_storage_cache_name=*/std::string(),
+        /*cache_storage_side_data_writer=*/mojo::NullRemote(),
         /*cors_exposed_header_names=*/std::vector<std::string>(),
... (truncated)
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.