Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Bug ClassUAF
Tracker520202726
Fix commitaf2cf051f9dd (chromium/src) +23/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-16

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/developer_private/developer_private_functions.cc
modified

Files Changed

  • chrome/browser/extensions/api/developer_private/developer_private_functions.cc
From af2cf051f9ddf3eb37ea5b0f675ef7deef8ea4e1 Mon Sep 17 00:00:00 2001
From: Andrea Orru <andreaorru@chromium.org>
Date: Fri, 05 Jun 2026 17:10:49 -0700
Subject: [PATCH] [Extensions] Fix UAF in developerPrivate.updateExtensionConfiguration

Modifying file_access or incognito_access synchronously reloads the
extension, destroying the old Extension object. Subsequent operations in
DeveloperPrivateUpdateExtensionConfigurationFunction::Run() would
dereference the dangling pointer.

This fix:
1. Replaces extension->id() with update.extension_id to avoid
   unnecessary dereferences and prevent UAF during reload helper calls.
2. Re-fetches the extension pointer from ExtensionRegistry after
   potential reload operations and adds defensive null checks.

Fixed: 520202726
Change-Id: I4138c3f1bdd5f857c4c4729ff165f0f3d1d5a062
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7904810
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642683}
---

diff --git a/chrome/browser/extensions/api/developer_private/developer_private_functions.cc b/chrome/browser/extensions/api/developer_private/developer_private_functions.cc
index 9c834fc..046288f 100644
--- a/chrome/browser/extensions/api/developer_private/developer_private_functions.cc
+++ b/chrome/browser/extensions/api/developer_private/developer_private_functions.cc
@@ -632,22 +632,35 @@
   }
 
   if (update.file_access) {
-    util::SetAllowFileAccess(extension->id(), browser_context(),
+    // `util::SetAllowFileAccess()` can synchronously reload the extension,
+    // invalidating the `extension` pointer. We pass `update.extension_id`
+    // to ensure the ID reference stays alive, and re-fetch the pointer after.
+    util::SetAllowFileAccess(update.extension_id, browser_context(),
                              *update.file_access);
+    extension = GetExtensionById(update.extension_id);
+    if (!extension) {
+      return RespondNow(LogNoSuchExtensionFoundAndReturn());
+    }
   }
   if (update.incognito_access) {
-    util::SetIsIncognitoEnabled(extension->id(), browser_context(),
+    // `util::SetIsIncognitoEnabled()` can also synchronously reload the
+    // extension.
+    util::SetIsIncognitoEnabled(update.extension_id, browser_context(),
                                 *update.incognito_access);
+    extension = GetExtensionById(update.extension_id);
+    if (!extension) {
+      return RespondNow(LogNoSuchExtensionFoundAndReturn());
+    }
   }
   if (update.user_scripts_access) {
     ExtensionSystem::Get(browser_context())
         ->user_script_manager()
-        ->SetUserScriptPrefEnabled(extension->id(),
+        ->SetUserScriptPrefEnabled(update.extension_id,
                                    *update.user_scripts_access);
   }
   if (update.error_collection) {
     ErrorConsole::Get(browser_context())
-        ->SetReportingAllForExtension(extension->id(),
+        ->SetReportingAllForExtension(update.extension_id,
                                       *update.error_collection);
   }
   if (update.host_access != developer::HostAccess::kNone) {
@@ -679,31 +692,31 @@
       developer_private::SafetyCheckWarningReason::kNone) {
     ExtensionPrefs::Get(browser_context())
         ->SetIntegerPref(
-            extension->id(), kPrefAcknowledgeSafetyCheckWarningReason,
+            update.extension_id, kPrefAcknowledgeSafetyCheckWarningReason,
             static_cast<int>(update.acknowledge_safety_check_warning_reason));
     DeveloperPrivateEventRouter* event_router =
         DeveloperPrivateAPI::Get(browser_context())
             ->developer_private_event_router();
     if (event_router) {
-      event_router->OnExtensionConfigurationChanged(extension->id());
+      event_router->OnExtensionConfigurationChanged(update.extension_id);
     }
   }
   if (update.show_access_requests_in_toolbar) {
     SitePermissionsHelper(Profile::FromBrowserContext(browser_context()))
         .SetShowAccessRequestsInToolbar(
-            extension->id(), *update.show_access_requests_in_toolbar);
+            update.extension_id, *update.show_access_requests_in_toolbar);
   }
   if (update.pinned_to_toolbar) {
     ToolbarActionsModel* toolbar_actions_model = ToolbarActionsModel::Get(
         Profile::FromBrowserContext(browser_context()));
-    if (!toolbar_actions_model->HasAction(extension->id())) {
+    if (!toolbar_actions_model->HasAction(update.extension_id)) {
       return RespondNow(Error(kCannotSetPinnedWithoutAction));
     }
 
     bool is_action_pinned =
-        toolbar_actions_model->IsActionPinned(extension->id());
+        toolbar_actions_model->IsActionPinned(update.extension_id);
     if (is_action_pinned != *update.pinned_to_toolbar) {
-      toolbar_actions_model->SetActionVisibility(extension->id(),
+      toolbar_actions_model->SetActionVisibility(update.extension_id,
                                                  !is_action_pinned);
     }
   }
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in developerPrivate.updateExtensionConfiguration due to synchronous reload

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the browser process of Chromium within the developerPrivate.updateExtensionConfiguration API. Modifying certain configuration options (like file access or incognito access) synchronously reloads the extension, destroying the heap-allocated Extension object. Subsequent operations in the same execution path then dereference the now-dangling raw pointer.

Affected files:

  • chrome/browser/extensions/api/developer_private/developer_private_functions.cc

Estimated timestamp from git blame: 2015-03-19

Detailed Description

Within DeveloperPrivateUpdateExtensionConfigurationFunction::Run() (chrome/browser/extensions/api/developer_private/developer_private_functions.cc), a raw pointer to the target Extension is obtained and stored on the stack:

const Extension* extension = GetExtensionById(update.extension_id);

No scoped_refptr is taken to keep the Extension object alive during the function’s execution.

When the configuration update contains a change to file_access (line 634) or incognito_access (line 638), the function invokes helper methods that synchronously trigger an extension reload:

  • util::SetAllowFileAccess(...) -> ReloadExtension(...)
  • util::SetIsIncognitoEnabled(...) -> ReloadExtensionIfEnabled(...)

For a packed extension (e.g., a Web Store CRX), the reload flow is entirely synchronous:

  1. ExtensionRegistrar::DoReloadExtension disables the extension, moving the scoped_refptr reference to the disabled_extensions_ set.
  2. ChromeExtensionRegistrarDelegate::LoadExtensionForReload calls InstalledLoader(profile_).Load(...) synchronously using cached in-memory manifest data.
  3. This constructs a brand-new Extension object on the heap and adds it via ExtensionRegistrar::AddExtension.
  4. ReplaceReloadedExtension executes registry_->RemoveDisabled(extension->id()), which drops the final reference to the old Extension object, destroying it synchronously.

When control returns to DeveloperPrivateUpdateExtensionConfigurationFunction::Run(), the stack-local extension pointer is dangling. Any subsequent configuration fields present in the same update request (such as user_scripts_access, error_collection, or host_access) will dereference this freed pointer. Most notably, constructing the ScriptingPermissionsModifier on line 659:

ScriptingPermissionsModifier modifier(browser_context(), extension);

creates a temporary scoped_refptr from the dangling raw pointer. This results in an AddRef() operation on freed memory, followed by a Release() on scope exit, potentially leading to a double-free or arbitrary destructor execution.

Potential Steps to Trigger the Vulnerability

Note: Our tooling does not currently have the capability to execute code or run dynamic proofs of concept. The following steps are theoretical and based on manual analysis of the static code flow.

  1. A victim user must have one of seven deprecated platform apps (such as the Chrome Dev Editor, ID AE27D69DBE571F4B1694F05C89B710C646792231) and any packed extension installed.
  2. An attacker compromises the renderer process hosting the allowlisted developer app.
  3. The compromised renderer dispatches a LocalFrameHost.Request Mojo message calling developerPrivate.updateExtensionConfiguration with user_gesture = true, target extension_id, and both file_access and incognito_access toggles set to values that differ from current settings.
  4. In the browser process, Run() executes, reloads the extension synchronously via the first configuration handler, and frees the old Extension heap allocation.
  5. The execution proceeds to the next handlers within the same task, dereferencing the dangling pointer and triggering the Use-After-Free condition.

Limitations on Exploitation

  1. Task Boundary Constraints: Because the reload and subsequent dereferences occur entirely synchronously within a single UI thread task, the browser does not return to the message loop. Consequently, Mojo message queues cannot be processed in the interim, making it extremely difficult to reliably race and reclaim the freed heap allocation with controlled bytes before the UAF occurs.
  2. Strict Preconditions: The API is restricted to chrome://extensions (WebUI, which is trusted) and seven hard-coded developer platform apps which are deprecated and generally inactive.

Suggested Fix

To prevent the UAF, copy the ExtensionId of the target extension and hold a scoped_refptr<const Extension> on the stack, or re-fetch the pointer from the ExtensionRegistry if any operation that can trigger an extension reload has been executed.

Evaluated with Chrome root at commit: d8b226a3be7c9c1ac9240c09e14698866c82e4ac


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker