Chrome · WebPackaging
CVE-2026-87639
UAF in WebPackaging
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forservices/network/web_bundle/web_bundle_manager.cc |
modified | |
TEST_Fservices/network/web_bundle/web_bundle_manager_unittest.cc |
modified |
Files Changed
services/network/web_bundle/web_bundle_manager.ccservices/network/web_bundle/web_bundle_manager_unittest.cc
Patch
From e70631df78a1825ace2949e3dc4fa225808d88ff Mon Sep 17 00:00:00 2001 From: Hayato Ito <[email protected]> Date: Thu, 13 Aug 2026 00:24:28 -0700 Subject: [PATCH] Fix reentrant pending_loaders_ UAF in WebBundleManager In WebBundleManager::CreateWebBundleURLLoaderFactory, iterating over pending_loaders_[key] while calling factory->StartLoader(loader) could cause a synchronous failure when factory is created in an error state. The loader's deletion callback synchronously invoked CleanUpWillBeDeletedURLLoader, which erased the map entry from pending_loaders_ while the outer loop was actively iterating over it, causing a heap-use-after-free and container-overflow. To fix this, we detach the vector out of pending_loaders_ and erase the map iterator before calling StartLoader. Bug: 544415098 Change-Id: I854f117a04bdfd8f210e236a217401e4556914bf Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8243029 Reviewed-by: Kenichi Ishibashi <[email protected]> Commit-Queue: Hayato Ito <[email protected]> Cr-Commit-Position: refs/heads/main@{#1678651} --- diff --git a/services/network/web_bundle/web_bundle_manager.cc b/services/network/web_bundle/web_bundle_manager.cc index 44aac0b3..b699459 100644 --- a/services/network/web_bundle/web_bundle_manager.cc +++ b/services/network/web_bundle/web_bundle_manager.cc @@ -87,9 +87,15 @@ // These subresource requests arrived earlier than the request for the bundle. auto it = pending_loaders_.find(key); if (it != pending_loaders_.end()) { - for (auto& loader : it->second) - factory->StartLoader(loader); + // Detach the vector and erase the map entry before invoking StartLoader(), + // because a synchronous failure inside StartLoader() can reentrantly + // call CleanUpWillBeDeletedURLLoader() and modify pending_loaders_. + std::vector<base::WeakPtr<WebBundleURLLoaderFactory::URLLoader>> + pending_loaders = std::move(it->second); pending_loaders_.erase(it); + for (auto& loader : pending_loaders) { + factory->StartLoader(loader); + } } auto weak_factory = factory->GetWeakPtr(); diff --git a/services/network/web_bundle/web_bundle_manager_unittest.cc b/services/network/web_bundle/web_bundle_manager_unittest.cc index ad7a4f7..7a42f07 100644 --- a/services/network/web_bundle/web_bundle_manager_unittest.cc +++ b/services/network/web_bundle/web_bundle_manager_unittest.cc @@ -701,4 +701,79 @@ client->completion_status().error_code); } +// Regression test for crbug.com/544415098. +// +// When a WebBundle request is redirected, the factory is created in an error +// state. In this state, StartLoader(loader1) fails synchronously and deletes +// loader1, which in turn removes loader1 from the pending loaders list. +// +// We queue multiple requests (request1 and request2) to verify that an error +// during request1's processing does not prevent request2 from being handled +// properly. +TEST_F(WebBundleManagerTest, WebBundleURLRedirectionEarlySubresourceRequest) { + WebBundleManager manager; + base::UnguessableToken token = base::UnguessableToken::Create(); + mojo::PendingRemote<mojom::WebBundleHandle> handle_remote; + auto handle = std::make_unique<TestWebBundleHandle>( + handle_remote.InitWithNewPipeAndPassReceiver()); + ResourceRequest::WebBundleTokenParams create_params(GURL(kBundleUrl), token, + std::move(handle_remote)); + + // Subresource requests arrive earlier than the bundle request. + // + // The 1st request. + mojo::Remote<network::mojom::URLLoader> loader1; + auto client1 = std::make_unique<network::TestURLLoaderClient>(); + network::ResourceRequest request1; + request1.url = GURL(kResourceUrl); + request1.method = "GET"; + request1.request_initiator = url::Origin::Create(GURL(kInitiatorUrl)); + ResourceRequest::WebBundleTokenParams subresource_params1( + GURL(kBundleUrl), token, mojo::PendingRemote<mojom::WebBundleHandle>()); + request1.web_bundle_token_params = subresource_params1; + + manager.StartSubresourceRequest( + loader1.BindNewPipeAndPassReceiver(), request1, client1->CreateRemote(), + process_id1, mojo::Remote<mojom::TrustedHeaderClient>()); + + // The 2nd request. + mojo::Remote<network::mojom::URLLoader> loader2; + auto client2 = std::make_unique<network::TestURLLoaderClient>(); + network::ResourceRequest request2; + request2.url = GURL("https://example.com/subresource2.js"); + request2.method = "GET"; + request2.request_initiator = url::Origin::Create(GURL(kInitiatorUrl)); + ResourceRequest::WebBundleTokenParams subresource_params2( + GURL(kBundleUrl), token, mojo::PendingRemote<mojom::WebBundleHandle>()); + request2.web_bundle_token_params = subresource_params2; + + manager.StartSubresourceRequest( + loader2.BindNewPipeAndPassReceiver(), request2, client2->CreateRemote(), + process_id1, mojo::Remote<mojom::TrustedHeaderClient>()); + + // Create a WebBundleURLLoaderFactory where bundle request URL is different + // from WebBundleTokenParams::bundle_url. This triggers an error factory that + // synchronously fails early subresource loaders during StartLoader iteration. + GURL redirected_bundle_url("https://redirected.example.com/bundle.wbn"); + base::WeakPtr<WebBundleURLLoaderFactory> factory = + manager.CreateWebBundleURLLoaderFactory( + redirected_bundle_url, create_params, process_id1, + CrossOriginEmbedderPolicy(), /*coep_reporter=*/nullptr); + + handle->RunUntilBundleError(); + ASSERT_TRUE(handle->last_bundle_error().has_value()); + EXPECT_EQ(handle->last_bundle_error()->first, + mojom::WebBundleErrorType::kWebBundleRedirected); + + client1->RunUntilComplete(); + EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE, + client1->completion_status().error_code); + + // Verify that the second subresource request also completes safely without + // crashing or hanging. + client2->RunUntilComplete(); + EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE, + client2->completion_status().error_code); +} + } // namespace network
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/services/network/web_bundle/web_bundle_manager_unittest.cc b/services/network/web_bundle/web_bundle_manager_unittest.cc
index ad7a4f7..7a42f07 100644
--- a/services/network/web_bundle/web_bundle_manager_unittest.cc
+++ b/services/network/web_bundle/web_bundle_manager_unittest.cc
@@ -701,4 +701,79 @@
client->completion_status().error_code);
}
+// Regression test for crbug.com/544415098.
+//
+// When a WebBundle request is redirected, the factory is created in an error
+// state. In this state, StartLoader(loader1) fails synchronously and deletes
+// loader1, which in turn removes loader1 from the pending loaders list.
+//
+// We queue multiple requests (request1 and request2) to verify that an error
+// during request1's processing does not prevent request2 from being handled
+// properly.
+TEST_F(WebBundleManagerTest, WebBundleURLRedirectionEarlySubresourceRequest) {
+ WebBundleManager manager;
+ base::UnguessableToken token = base::UnguessableToken::Create();
+ mojo::PendingRemote<mojom::WebBundleHandle> handle_remote;
+ auto handle = std::make_unique<TestWebBundleHandle>(
+ handle_remote.InitWithNewPipeAndPassReceiver());
+ ResourceRequest::WebBundleTokenParams create_params(GURL(kBundleUrl), token,
+ std::move(handle_remote));
+
+ // Subresource requests arrive earlier than the bundle request.
+ //
+ // The 1st request.
+ mojo::Remote<network::mojom::URLLoader> loader1;
+ auto client1 = std::make_unique<network::TestURLLoaderClient>();
+ network::ResourceRequest request1;
+ request1.url = GURL(kResourceUrl);
+ request1.method = "GET";
+ request1.request_initiator = url::Origin::Create(GURL(kInitiatorUrl));
+ ResourceRequest::WebBundleTokenParams subresource_params1(
+ GURL(kBundleUrl), token, mojo::PendingRemote<mojom::WebBundleHandle>());
+ request1.web_bundle_token_params = subresource_params1;
+
+ manager.StartSubresourceRequest(
+ loader1.BindNewPipeAndPassReceiver(), request1, client1->CreateRemote(),
+ process_id1, mojo::Remote<mojom::TrustedHeaderClient>());
+
+ // The 2nd request.
+ mojo::Remote<network::mojom::URLLoader> loader2;
+ auto client2 = std::make_unique<network::TestURLLoaderClient>();
+ network::ResourceRequest request2;
+ request2.url = GURL("https://example.com/subresource2.js");
+ request2.method = "GET";
+ request2.request_initiator = url::Origin::Create(GURL(kInitiatorUrl));
+ ResourceRequest::WebBundleTokenParams subresource_params2(
+ GURL(kBundleUrl), token, mojo::PendingRemote<mojom::WebBundleHandle>());
+ request2.web_bundle_token_params = subresource_params2;
+
+ manager.StartSubresourceRequest(
+ loader2.BindNewPipeAndPassReceiver(), request2, client2->CreateRemote(),
+ process_id1, mojo::Remote<mojom::TrustedHeaderClient>());
+
+ // Create a WebBundleURLLoaderFactory where bundle request URL is different
+ // from WebBundleTokenParams::bundle_url. This triggers an error factory that
+ // synchronously fails early subresource loaders during StartLoader iteration.
+ GURL redirected_bundle_url("https://redirected.example.com/bundle.wbn");
+ base::WeakPtr<WebBundleURLLoaderFactory> factory =
+ manager.CreateWebBundleURLLoaderFactory(
+ redirected_bundle_url, create_params, process_id1,
+ CrossOriginEmbedderPolicy(), /*coep_reporter=*/nullptr);
+
+ handle->RunUntilBundleError();
+ ASSERT_TRUE(handle->last_bundle_error().has_value());
+ EXPECT_EQ(handle->last_bundle_error()->first,
+ mojom::WebBundleErrorType::kWebBundleRedirected);
+
+ client1->RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
+ client1->completion_status().error_code);
+
+ // Verify that the second subresource request also completes safely without
+ // crashing or hanging.
+ client2->RunUntilComplete();
+ EXPECT_EQ(net::ERR_INVALID_WEB_BUNDLE,
+ client2->completion_status().error_code);
+}
+
} // namespace network
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