Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Bug ClassUAF
Tracker513508128
Fix commitc5ba62b5c799 (chromium/src) +15/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
OffscreenCloseDocumentFunction
extensions/browser/api/offscreen/offscreen_api.h
modified

Files Changed

  • extensions/browser/api/offscreen/offscreen_api.cc
  • extensions/browser/api/offscreen/offscreen_api.h
From c5ba62b5c799b4d0caafe376fa1bc5760259636f Mon Sep 17 00:00:00 2001
From: Devlin Cronin <rdevlin.cronin@chromium.org>
Date: Fri, 15 May 2026 13:09:48 -0700
Subject: [PATCH] [Extensions] Fix lifetime management in OffscreenCreateDocumentFunction

There's a race where, if a task for NotifyPageFailedToLoad is pending,
both OnBrowserContextShutdown() and OnExtensionHostDestroyed() could
potentially be called, even though only one of these should be.

Fix this by using a WeakPtr in the NotifyPageFailedToLoad posted task,
and also add a CHECK() in OnBrowserContextShutdown() to ensure
OnExtensionHostDestroyed() (and SendResponseToExtension()) hasn't run.

No test is added for this because it would require extremely precise
timing of having a browser context shut down while the
NotifyPageFailedToLoad() task is pending in the queue. This is
coercible, but the test would then be unrealistic.

Bug: 513508128
Change-Id: Iae2b24ad101fd2cde835c6f9d49ada9e82d5b69b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853081
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1631500}
---

diff --git a/extensions/browser/api/offscreen/offscreen_api.cc b/extensions/browser/api/offscreen/offscreen_api.cc
index 35f6eccc..580375d 100644
--- a/extensions/browser/api/offscreen/offscreen_api.cc
+++ b/extensions/browser/api/offscreen/offscreen_api.cc
@@ -126,6 +126,11 @@
 }
 
 void OffscreenCreateDocumentFunction::OnBrowserContextShutdown() {
+  // This should never trigger if we've responded to the extension -- only one
+  // of OnBrowserContextShutdown() or SendResponseToExtension() should run,
+  // since they each Release() the reference from Run().
+  CHECK(!did_respond());
+
   // Release dangling lifetime pointers and bail. No point in responding now;
   // the context is shutting down. Reset `host_observer_` first to allay any
   // re-entrancy concerns about the host being destructed at this point.
@@ -151,10 +156,16 @@
     // ExtensionHost finished its first load. `NotifyPageFailedToLoad()` will
     // delete the extension host, which isn't allowed in the middle of observer
     // iteration.
+    // NOTE: We use a WeakPtr here (and *not* a ref count). We've already added
+    // (exactly) one reference to ensure we respond to the extension function,
+    // either in OnBrowserContextShutdown() or SendResponseToExtension(). Adding
+    // another reference here would potentially allow for both of those paths
+    // to trigger, causing two releases. If that happened and this didn't use a
+    // WeakPtr, the task would run with a deleted function.
     base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
         FROM_HERE,
         base::BindOnce(&OffscreenCreateDocumentFunction::NotifyPageFailedToLoad,
-                       this));
+                       weak_factory_.GetWeakPtr()));
     return;
   }
 
diff --git a/extensions/browser/api/offscreen/offscreen_api.h b/extensions/browser/api/offscreen/offscreen_api.h
index f047294..6e93dda 100644
--- a/extensions/browser/api/offscreen/offscreen_api.h
+++ b/extensions/browser/api/offscreen/offscreen_api.h
@@ -5,6 +5,7 @@
 #ifndef EXTENSIONS_BROWSER_API_OFFSCREEN_OFFSCREEN_API_H_
 #define EXTENSIONS_BROWSER_API_OFFSCREEN_OFFSCREEN_API_H_
 
+#include "base/memory/weak_ptr.h"
 #include "base/scoped_observation.h"
 #include "extensions/browser/extension_function.h"
 #include "extensions/browser/extension_function_histogram_value.h"
@@ -50,6 +51,8 @@
   // Observes the newly-created document to wait for it to be ready.
   base::ScopedObservation<ExtensionHost, ExtensionHostObserver> host_observer_{
       this};
+
+  base::WeakPtrFactory<OffscreenCreateDocumentFunction> weak_factory_{this};
 };
 
 class OffscreenCloseDocumentFunction : public ExtensionFunction,
Loading diff…

Original Bug Report

reported by vm...@google.com

Use-After-Free in OffscreenCreateDocumentFunction during BrowserContext shutdown

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 reference counting underflow in OffscreenCreateDocumentFunction can lead to a Use-After-Free in the browser process during profile shutdown. This is caused by multiple paths triggering an unconditional Release() of a manually managed reference. If an asynchronous task is pending, the object may be destroyed prematurely, leading to a crash or memory corruption when the task executes.

Affected files:

  • extensions/browser/api/offscreen/offscreen_api.cc
  • extensions/browser/extension_function.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Description

A potential Use-After-Free (UAF) vulnerability exists in the browser process within the OffscreenCreateDocumentFunction class. The class manually manages its lifetime using AddRef() and Release() to handle asynchronous responses. However, during BrowserContext (profile) shutdown, two independent code paths can trigger Release() back-to-back without verifying if the reference has already been consumed. This leads to a reference count underflow, potentially deleting the object while an asynchronous task still holds a reference to it.

Technical Details

In OffscreenCreateDocumentFunction::Run(), a manual reference is added to ensure the object stays alive until it can respond to the extension or the context is shut down:

// extensions/browser/api/offscreen/offscreen_api.cc
host_observer_.Observe(offscreen_document);
AddRef();
return RespondLater();

This reference is intended to be balanced by a single Release() call in either OnBrowserContextShutdown() or SendResponseToExtension(). However, both methods call Release() unconditionally:

void OffscreenCreateDocumentFunction::OnBrowserContextShutdown() {
  host_observer_.Reset();
  Release();  // Balanced in Run().
}

void OffscreenCreateDocumentFunction::SendResponseToExtension(ResponseValue response_value) {
  host_observer_.Reset();
  Respond(std::move(response_value));
  Release();  // Balanced in Run().
}

During profile teardown (e.g., closing an incognito window), OffscreenDocumentManager::Shutdown() is executed, which destroys the OffscreenDocumentHost. This triggers OnExtensionHostDestroyed(), which calls SendResponseToExtension() and performs the first Release(). Subsequently, the ExtensionFunction shutdown notifier triggers OnBrowserContextShutdown(), performing a second Release().

Use-After-Free Mechanism

The vulnerability is potentially reachable if a navigation error occurs in the offscreen document. In OnExtensionHostDidStopFirstLoad, if an error page is detected, a task is posted to the UI thread:

base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
    FROM_HERE,
    base::BindOnce(&OffscreenCreateDocumentFunction::NotifyPageFailedToLoad,
                   this));

base::BindOnce stores a scoped_refptr to this in the task state, incrementing the refcount. If profile teardown occurs while this task is pending, the double Release() will decrement the refcount twice, causing the object to be deleted despite the pending task. When the task eventually executes NotifyPageFailedToLoad(), it dereferences the freed this pointer.

Potential Impact

This issue resides in the unsandboxed browser process. A compromised renderer or a malicious extension with the offscreen permission could potentially trigger this UAF to cause a browser crash or achieve memory corruption, which may facilitate a sandbox escape.

Potential Reproduction Steps

  1. Install a Manifest V3 extension with the offscreen permission and split incognito mode enabled.
  2. From an incognito context, call chrome.offscreen.createDocument() with a URL that results in a navigation error (e.g., a non-existent extension resource).
  3. Immediately close the incognito profile (e.g., close the last incognito window).
  4. If the profile teardown occurs while the NotifyPageFailedToLoad task is pending in the UI thread queue, the object may be over-released and freed, causing a UAF upon task execution.

Suggested Fix

The OffscreenCreateDocumentFunction should ensure that the manual reference is released exactly once. This can be achieved by checking if the function has already responded or by using a boolean flag to track the manual reference’s state. Ideally, the class should avoid manual reference counting in favor of safer patterns like base::WeakPtr or relying solely on scoped_refptr management through the task runner and dispatcher.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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