Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebPackaging
DescriptionUse after free in WebPackaging
ComponentWebPackaging
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker544415098
Fix commite70631df78a1 (chromium/src) +83/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedOpenAI Codex Security (amyb)
Reported2026-08-09 (per vendor advisory)
Fix landed2026-08-13 (public git)
Disclosed2026-09-08
Patch gap26 days — fix public in source before the advisory

Background

WebPackaging / Web Bundles
a Chrome feature that lets a single .wbn file bundle a site’s subresources, served in the sandboxed network service process via WebBundleURLLoaderFactory.
`WebBundleManager`
the network-service object that tracks pending subresource loaders per bundle and, once the bundle’s factory exists, hands them off through factory->StartLoader(loader).
`pending_loaders_`
a map keyed per bundle whose values are std::vector<base::WeakPtr<WebBundleURLLoaderFactory::URLLoader>>, holding early subresource requests that arrived before the bundle request itself.
Reentrancy
a call (StartLoader) that, on its failure path, synchronously calls back into the same object (CleanUpWillBeDeletedURLLoader) and mutates state the caller is still using.

Root Cause Analysis

In WebBundleManager::CreateWebBundleURLLoaderFactory, the pre-patch code looked up pending_loaders_[key] and iterated over the vector in place (for (auto& loader : it->second)), calling factory->StartLoader(loader) on each element before erasing the map entry afterward. When the factory is created in an error state — for example when the resolved bundle URL differs from the requested bundle_url because the bundle load was redirected — StartLoader fails synchronously and destroys the loader, whose deletion callback reentrantly invokes CleanUpWillBeDeletedURLLoader, which calls pending_loaders_.erase on the very entry (and thus the very vector) being iterated. That erase frees/mutates the vector backing store while the outer range-based loop still holds an iterator into it, producing a heap-use-after-free and container-overflow.

The fix moves the vector out of the map (std::move(it->second)) and erases the map entry before the loop, so iteration runs over a stable local pending_loaders copy that reentrant mutation of pending_loaders_ can no longer invalidate.

Key insight
The core mistake is iterating over a container element while a reentrant callback (triggered by the synchronous error path of StartLoader) erases that same element from the container; the fix detaches the vector and erases the map entry up front, decoupling the loop’s lifetime from any mutation of pending_loaders_.

Attack Path

  1. Queue early subresource requests The attacker’s page issues two or more subresource requests carrying web_bundle_token_params for a bundle before the bundle request itself, so they are stored as weak pointers in pending_loaders_[key].
  2. Force an error-state factory The bundle load is redirected so the factory’s resolved URL differs from the token’s bundle_url, causing CreateWebBundleURLLoaderFactory to build a factory that fails loaders synchronously (kWebBundleRedirected / net::ERR_INVALID_WEB_BUNDLE).
  3. Trigger reentrant erase during iteration As the in-place loop calls factory->StartLoader(loader1), the synchronous failure deletes loader1, whose deletion callback calls CleanUpWillBeDeletedURLLoader, which erases the entry from pending_loaders_ while the loop is still iterating it.
  4. Use-after-free on the freed vector The next loop iteration dereferences the now-freed/mutated vector storage, yielding the heap-use-after-free and container-overflow in the network service process.
  5. Escalation Turning this UAF into a controlled read/write primitive or code execution is not demonstrated by the patch and is (inferred).

Impact Assessment

An attacker who controls page content and can serve a redirected Web Bundle gains a use-after-free in the sandboxed network service process, reached remotely with no special privileges beyond loading attacker content. Beyond the crash/container-overflow the patch demonstrates, escalation to a memory-corruption primitive or code execution is (inferred). The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. A specific severity rating are not stated in the provided material and are not asserted here.

Changed Functions

FunctionChangeNotes
for
services/network/web_bundle/web_bundle_manager.cc
modified
TEST_F
services/network/web_bundle/web_bundle_manager_unittest.cc
modified

Files Changed

  • services/network/web_bundle/web_bundle_manager_unittest.cc
  • services/network/web_bundle/web_bundle_manager.cc

Audit Directions

  • Iterate-then-mutate under reentrancy
    Flag range-based loops over a container element (map value, vector) that call out to code which can synchronously delete objects and mutate that same container; detach/copy before iterating.
  • Synchronous error paths of async APIs
    Audit loader/factory Start* methods that can fail synchronously and invoke deletion or cleanup callbacks inline, especially where the caller assumes the callback is deferred.
  • Weak-pointer collections in the network service
    Review other pending_* maps of base::WeakPtr loaders where cleanup callbacks (CleanUp*, deletion handlers) erase entries, since their timing relative to in-progress iteration is easy to get wrong.
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.