CVE-2026-11230
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forextensions/renderer/user_script_set.cc |
modified | |
UserScriptSetTestextensions/renderer/user_script_set_unittest.cc |
modified | |
UserScriptSetTestextensions/renderer/user_script_set_unittest.cc |
modified | |
TEST_Fextensions/renderer/user_script_set_unittest.cc |
modified |
Files Changed
extensions/renderer/BUILD.gnextensions/renderer/user_script_set.ccextensions/renderer/user_script_set_unittest.cc
Patch
From fe455e2957c87f430dacae82947e947aeb62c55e Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Wed, 29 Apr 2026 15:19:42 -0700 Subject: [PATCH] [extensions] Fix Use-After-Free in UserScriptSet::UpdateUserScripts When updating extension scripts, the existing shared memory mapping was replaced (and thus unmapped) before the script objects were cleared. If mapping the new region failed, the renderer would be left with dangling pointers to the unmapped memory. This CL fixes this by clearing the scripts immediately after mapping is attempted, and ensuring that any early return (due to mapping failure or invalid data) does not leave the renderer in a corrupted state. Fixed: 493225428 Change-Id: Ia4226d70b7a0826534a1c1e572a57364aabb3efe Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7801635 Reviewed-by: Devlin Cronin <[email protected]> Commit-Queue: Andrew Paseltiner <[email protected]> Cr-Commit-Position: refs/heads/main@{#1622768} --- diff --git a/extensions/renderer/BUILD.gn b/extensions/renderer/BUILD.gn index 0965f9e..3957b417 100644 --- a/extensions/renderer/BUILD.gn +++ b/extensions/renderer/BUILD.gn @@ -416,6 +416,7 @@ "scoped_web_frame.h", "script_context_set_unittest.cc", "storage_area_unittest.cc", + "user_script_set_unittest.cc", "utils_unittest.cc", ] diff --git a/extensions/renderer/user_script_set.cc b/extensions/renderer/user_script_set.cc index 6d5a5465..fcec9315 100644 --- a/extensions/renderer/user_script_set.cc +++ b/extensions/renderer/user_script_set.cc @@ -156,10 +156,17 @@ bool only_inject_incognito = ExtensionsRendererClient::Get()->IsIncognitoProcess(); + // Clear out the references in `scripts_` and `script_sources_`. These + // internally depend on the contents of `shared_memory_mapping_`, so we + // ensure these references are removed before releasing the memory. + scripts_.clear(); + script_sources_.clear(); + // Create the shared memory mapping. shared_memory_mapping_ = shared_memory.Map(); - if (!shared_memory.IsValid()) + if (!shared_memory_mapping_.IsValid()) { return false; + } // First get the size of the memory block. const base::Pickle::Header* pickle_header = @@ -186,8 +193,6 @@ // scripts so that we don't add OOM noise to crash reports. CHECK_LT(num_scripts, kNumScriptsArbitraryMax); - scripts_.clear(); - script_sources_.clear(); scripts_.reserve(num_scripts); for (uint32_t i = 0; i < num_scripts; ++i) { std::unique_ptr<UserScript> script(new UserScript()); diff --git a/extensions/renderer/user_script_set_unittest.cc b/extensions/renderer/user_script_set_unittest.cc new file mode 100644 index 0000000..c31b1c5 --- /dev/null +++ b/extensions/renderer/user_script_set_unittest.cc @@ -0,0 +1,68 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "extensions/renderer/user_script_set.h" + +#include "base/containers/span.h" +#include "base/memory/read_only_shared_memory_region.h" +#include "base/pickle.h" +#include "extensions/common/mojom/host_id.mojom.h" +#include "extensions/common/user_script.h" +#include "extensions/renderer/extensions_renderer_client.h" +#include "extensions/renderer/test_extensions_renderer_client.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace extensions { + +class UserScriptSetTest : public testing::Test { + public: + UserScriptSetTest() { + ExtensionsRendererClient::Set(&extensions_renderer_client_); + } + ~UserScriptSetTest() override { ExtensionsRendererClient::Set(nullptr); } + + protected: + TestExtensionsRendererClient extensions_renderer_client_; +}; + +// Regression test for crbug.com/493225428. +TEST_F(UserScriptSetTest, UpdateUserScripts_FailureClearsScripts) { + mojom::HostID host_id(mojom::HostID::HostType::kExtensions, "extension_id"); + UserScriptSet user_script_set(host_id); + + // 1. Create a valid shared memory region with one script. + base::Pickle pickle; + pickle.WriteUInt32(1); // num_scripts + + UserScript script; + script.set_id("script_id"); + script.Pickle(&pickle); + + // No JS or CSS scripts for simplicity. + + base::MappedReadOnlyRegion mapped_region = + base::ReadOnlySharedMemoryRegion::Create(pickle.size()); + ASSERT_TRUE(mapped_region.IsValid()); + + mapped_region.mapping.GetMemoryAsSpan<uint8_t>() + .first(pickle.size()) + .copy_from(pickle.AsBytes()); + + // 2. Update with valid scripts. + + EXPECT_TRUE( + user_script_set.UpdateUserScripts(std::move(mapped_region.region))); + EXPECT_TRUE(user_script_set.HasScripts()); + + // 3. Attempt to update with an INVALID shared memory region. + // This should return false and clear the scripts. + EXPECT_FALSE( + user_script_set.UpdateUserScripts(base::ReadOnlySharedMemoryRegion())); + + // 4. Verify that HasScripts() is false if update failed. + EXPECT_FALSE(user_script_set.HasScripts()) + << "Scripts should be cleared after a failed update!"; +} + +} // namespace extensions
Regression Test / PoC
diff --git a/extensions/renderer/user_script_set_unittest.cc b/extensions/renderer/user_script_set_unittest.cc
new file mode 100644
index 0000000..c31b1c5
--- /dev/null
+++ b/extensions/renderer/user_script_set_unittest.cc
@@ -0,0 +1,68 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "extensions/renderer/user_script_set.h"
+
+#include "base/containers/span.h"
+#include "base/memory/read_only_shared_memory_region.h"
+#include "base/pickle.h"
+#include "extensions/common/mojom/host_id.mojom.h"
+#include "extensions/common/user_script.h"
+#include "extensions/renderer/extensions_renderer_client.h"
+#include "extensions/renderer/test_extensions_renderer_client.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace extensions {
+
+class UserScriptSetTest : public testing::Test {
+ public:
+ UserScriptSetTest() {
+ ExtensionsRendererClient::Set(&extensions_renderer_client_);
+ }
+ ~UserScriptSetTest() override { ExtensionsRendererClient::Set(nullptr); }
+
+ protected:
+ TestExtensionsRendererClient extensions_renderer_client_;
+};
+
+// Regression test for crbug.com/493225428.
+TEST_F(UserScriptSetTest, UpdateUserScripts_FailureClearsScripts) {
+ mojom::HostID host_id(mojom::HostID::HostType::kExtensions, "extension_id");
+ UserScriptSet user_script_set(host_id);
+
+ // 1. Create a valid shared memory region with one script.
+ base::Pickle pickle;
+ pickle.WriteUInt32(1); // num_scripts
+
+ UserScript script;
+ script.set_id("script_id");
+ script.Pickle(&pickle);
+
+ // No JS or CSS scripts for simplicity.
+
+ base::MappedReadOnlyRegion mapped_region =
+ base::ReadOnlySharedMemoryRegion::Create(pickle.size());
+ ASSERT_TRUE(mapped_region.IsValid());
+
+ mapped_region.mapping.GetMemoryAsSpan<uint8_t>()
+ .first(pickle.size())
+ .copy_from(pickle.AsBytes());
+
+ // 2. Update with valid scripts.
+
+ EXPECT_TRUE(
+ user_script_set.UpdateUserScripts(std::move(mapped_region.region)));
+ EXPECT_TRUE(user_script_set.HasScripts());
+
+ // 3. Attempt to update with an INVALID shared memory region.
+ // This should return false and clear the scripts.
+ EXPECT_FALSE(
+ user_script_set.UpdateUserScripts(base::ReadOnlySharedMemoryRegion()));
+
+ // 4. Verify that HasScripts() is false if update failed.
+ EXPECT_FALSE(user_script_set.HasScripts())
+ << "Scripts should be cleared after a failed update!";
+}
+
+} // namespace extensions
Original Bug Report
Potential Use-After-Free in UserScriptSet::UpdateUserScripts via shared memory mapping failure
Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.
Overview: A Use-After-Free vulnerability exists in UserScriptSet::UpdateUserScripts when updating extension scripts. If mapping the new shared memory region fails (e.g., due to address space exhaustion), the old mapping is destroyed but the scripts_ vector is not cleared, leaving dangling pointers to the unmapped memory. A malicious web page could potentially reclaim this memory and execute arbitrary JavaScript in the extension’s privileged Isolated World.
Affected files:
extensions/renderer/user_script_set.cc
Estimated timestamp from git blame: 2026-02-10
Description
A Use-After-Free (UAF) vulnerability exists in extensions/renderer/user_script_set.cc. In the UserScriptSet::UpdateUserScripts method, the existing shared memory mapping (shared_memory_mapping_) is replaced with a new mapping before the existing UserScript objects (stored in scripts_) are cleared.
If the attempt to map the new shared memory region fails, the renderer process is left in a corrupted state with dangling pointers that can be exploited by a malicious web page to escalate privileges into the extension’s Isolated World.
Root Cause
When the browser sends an IPC message to update extension scripts, UserScriptSet::UpdateUserScripts receives a base::ReadOnlySharedMemoryRegion (shared_memory). At line 160, the method attempts to map this region:
160: shared_memory_mapping_ = shared_memory.Map();
161: if (!shared_memory.IsValid())
162: return false;
The assignment to shared_memory_mapping_ invokes the move-assignment operator of base::SharedMemoryMapping. This operator immediately unmaps the existing memory pages, returning them to the operating system.
If the shared_memory.Map() call fails (e.g., due to address space exhaustion or hitting OS mapping limits like vm.max_map_count), it returns an invalid mapping. Because shared_memory.IsValid() checks the validity of the region handle (which is valid), execution continues.
At lines 165-168, the code attempts to read the pickle header:
165: const base::Pickle::Header* pickle_header =
166: shared_memory_mapping_.GetMemoryAs<base::Pickle::Header>();
167: if (!pickle_header)
168: return false;
Because the mapping failed and is invalid, GetMemoryAs safely returns nullptr. This causes the condition if (!pickle_header) to evaluate to true, resulting in an early return.
Crucially, this early return bypasses the cleanup of the scripts_ vector at lines 189-190:
189: scripts_.clear();
190: script_sources_.clear();
As a result, the scripts_ vector retains the UserScript objects from the previous update. These objects store std::string_view members pointing directly into the virtual memory address range that was unmapped by the move-assignment operator at line 160.
Exploitation and Impact
While the original report suggested this could be exploited by an already “compromised renderer,” Chromium’s security model dictates that an attacker with RCE in a renderer already possesses full control over that process’s isolated worlds. Therefore, a post-compromise UAF is not a security boundary bypass.
However, a severe security boundary bypass exists if an uncompromised, malicious web page triggers this flaw:
- Trigger the Mapping Failure: A malicious web page aggressively consumes the renderer’s virtual address space (e.g., creating numerous
ArrayBufferobjects) or exhausts the OS mapping limits. This is particularly feasible on 32-bit platforms (like 32-bit Android) where the 3GB address space can be exhausted without triggering PartitionAlloc’s OOM killer (base::TerminateBecauseOutOfMemory). - Await an Update: The attacker waits for an extension script update (e.g., an auto-update or dynamic script registration) that triggers the
UpdateUserScriptsIPC. At that exact moment, theMap()call fails, unmapping the old extension scripts but leaving thescripts_vector populated with dangling pointers. - Reclaim Memory: The attacker immediately allocates a large
ArrayBufferin JavaScript to predictably reclaim the recently freed virtual address space. They populate thisArrayBufferwith a malicious JavaScript payload. - Execute Payload: The attacker triggers a DOM event or iframe navigation that matches the injection criteria for the extension’s content scripts. The browser instructs the renderer to inject the script. The renderer iterates over the stale
UserScriptobjects, reads the danglingstd::string_viewpointers (now pointing to the attacker’s ArrayBuffer), and executes the malicious payload within the extension’s Isolated World.
Executing arbitrary JavaScript within an extension’s Isolated World from a normal web origin is a Privilege Escalation (S1 Severity), as the isolated world possesses elevated privileges (e.g., bypassing CORS for the extension’s declared permissions).
Mitigation
The scripts_ and script_sources_ vectors must be cleared at the beginning of UpdateUserScripts, or at the very least, before the existing shared_memory_mapping_ is replaced. Alternatively, the new mapping should be validated in a temporary variable before it replaces the member variable, ensuring that the renderer state is not corrupted if the Map() operation fails.
Note: This analysis identifies a potential theoretical exploit path. As an AI agent, Flapjack cannot execute code to provide a functional proof-of-concept.
Evaluated with Chrome root at commit: 43c4f3945742db6f06efbaf9a77b90f34a720277
Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.