diff --git a/net/disk_cache/backend_unittest.cc b/net/disk_cache/backend_unittest.cc index f6f049b2ae23e..b43bd557775e8 100644 --- a/net/disk_cache/backend_unittest.cc +++ b/net/disk_cache/backend_unittest.cc @@ -5272,6 +5272,90 @@ TEST_F(DiskCacheBackendTest, SimpleOpenOrCreateIndexErrorOptimistic) { entry->Close(); } +// POC: SimplePostOperationWaiterTable::OnOperationComplete runs the consumer's +// EntryResultCallback synchronously via RunEntryResultOperationAndCallback +// (simple_backend_impl.cc:145-158), bypassing the PostTask discipline that +// SimpleEntryImpl::PostClientCallback follows. When invoked from the mass-doom +// completion loop at SimpleBackendImpl::DoomEntriesComplete:916-917, a consumer +// that destroys the backend from its EntryResultCallback (an established +// pattern -- see SparseTestCompletionCallback in entry_unittest.cc) causes the +// next loop iteration to read the freed |this->post_doom_waiting_| member. +// +// Under ASAN this test triggers heap-use-after-free. +TEST_F(DiskCacheBackendTest, SimpleMassDoomSyncCallbackDeletesBackend) { + SetBackendToTest(BackendToTest::kSimple); + // net::DISK_CACHE => OPTIMISTIC_OPERATIONS, matching the HTTP cache config. + SetCacheType(net::DISK_CACHE); + InitCache(); + + const std::string kKey1 = "http://a.test/1"; + const std::string kKey2 = "http://a.test/2"; + const uint64_t hash1 = disk_cache::simple_util::GetEntryHashKey(kKey1); + const uint64_t hash2 = disk_cache::simple_util::GetEntryHashKey(kKey2); + + // Step 1: Mass-doom two inactive hashes. This inserts empty vectors into + // post_doom_waiting_[H1] / [H2] and posts DeleteEntrySetFiles to the worker + // pool with reply DoomEntriesComplete (bound via WeakPtr). + std::vector hashes = {hash1, hash2}; + simple_cache_impl_->DoomEntries(&hashes, base::DoNothing()); + + // Step 2: For each hash, queue a DoomEntry retry. Because + // post_doom_waiting_[H] exists, CreateOrFindActiveOrDoomedEntry returns + // nullptr / kPostDoom and DoomEntry queues RunOperationAndCallback onto + // post_doom_waiting_[H]. This makes the vector non-empty so the subsequent + // OpenEntry call is *also* queued rather than early-failing at + // simple_backend_impl.cc:381-392. + net::TestCompletionCallback doom_cb1, doom_cb2; + EXPECT_EQ(net::ERR_IO_PENDING, + cache_->DoomEntry(kKey1, net::HIGHEST, doom_cb1.callback())); + EXPECT_EQ(net::ERR_IO_PENDING, + cache_->DoomEntry(kKey2, net::HIGHEST, doom_cb2.callback())); + + // Step 3: Queue an OpenEntry retry for each hash. post_doom_waiting_[H] is + // now non-empty so OpenEntry queues RunEntryResultOperationAndCallback with + // our |destroy_backend| callback and returns ERR_IO_PENDING. + // + // When DoomEntriesComplete later runs OnOperationComplete(H1): + // closure #1 (DoomEntry retry) creates a fresh SimpleEntryImpl E, calls + // E->DoomEntry -> MarkAsDoomed removes E from active_entries_, + // backend_->OnDoomStart(H1) *re-inserts* an empty post_doom_waiting_[H1], + // returns ERR_IO_PENDING. + // closure #2 (OpenEntry retry) calls SimpleBackendImpl::OpenEntry(K1). + // CreateOrFindActiveOrDoomedEntry finds the re-inserted (empty) + // post_doom_waiting_[H1]; the OPTIMISTIC/empty branch at :381-392 + // synchronously returns EntryResult::MakeError(ERR_FAILED). + // RunEntryResultOperationAndCallback sees net_error()!=ERR_IO_PENDING and + // runs |destroy_backend| SYNCHRONOUSLY on the DoomEntriesComplete stack. + // + // The next loop iteration then reads freed |this->post_doom_waiting_|. + bool callback_ran_sync_on_doom_stack = false; + auto destroy_backend = [&](disk_cache::EntryResult result) { + callback_ran_sync_on_doom_stack = true; + EXPECT_EQ(net::ERR_FAILED, result.net_error()); + if (cache_) { + // Established consumer pattern: destroy the backend from an + // EntryResultCallback (cf. entry_unittest.cc DoomSparseEntry2). + simple_cache_impl_ = nullptr; + cache_.reset(); + } + }; + disk_cache::EntryResult r1 = cache_->OpenEntry( + kKey1, net::HIGHEST, base::BindLambdaForTesting(destroy_backend)); + ASSERT_EQ(net::ERR_IO_PENDING, r1.net_error()); + disk_cache::EntryResult r2 = cache_->OpenEntry( + kKey2, net::HIGHEST, base::BindLambdaForTesting(destroy_backend)); + ASSERT_EQ(net::ERR_IO_PENDING, r2.net_error()); + + // Step 4: Let the worker task complete -> DoomEntriesComplete runs. + // Under ASAN: heap-use-after-free reading SimpleBackendImpl::post_doom_waiting_ + // at simple_backend_impl.cc:917 during iteration for the second hash. + RunUntilIdle(); + + // The callback executed synchronously inside DoomEntriesComplete's frame. + // (Only reached in non-ASAN builds if the freed read happens to survive.) + EXPECT_TRUE(callback_ran_sync_on_doom_stack); +} + TEST_F(DiskCacheBackendTest, SimpleDoomAfterBackendDestruction) { // Test for when validating file headers/footers during close on simple // backend fails. To get the header to be checked on close, there needs to be