Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Bug ClassUAF
Tracker444176961
Fix commit5ec924bc3b4a (chromium/src) +40/-9
CISA KEVNot listed
CreditedHuinian Yang (@vmth6) of Amber Security Lab, OPPO Mobile Telecommunications Corp. Ltd.
Disclosed2026-03-10

Changed Functions

FunctionChangeNotes
if
extensions/renderer/bindings/api_binding_util.cc
modified

Files Changed

  • extensions/renderer/bindings/api_binding_test.cc
  • extensions/renderer/bindings/api_binding_util.cc
  • extensions/renderer/bindings/api_binding_util.h
  • extensions/renderer/bindings/api_bindings_system.cc
  • extensions/renderer/bindings/api_bindings_system.h
  • extensions/renderer/native_extension_bindings_system.cc
From 5ec924bc3b4a65cd06fbb8385d6b2563d60c8197 Mon Sep 17 00:00:00 2001
From: Devlin Cronin <[email protected]>
Date: Mon, 22 Dec 2025 15:19:35 -0800
Subject: [PATCH] [Extensions Bindings] Don't create invalidation data when invalidating

We track whether a given context is valid via an "invalidation data"
that's stored on the context via gin::PerContextData. Today, we lazily
create this data, and consider a context valid if it either has no
invalidation data or the invalidation data indicates the context is
still valid.

Unfortunately, because we consider "no invalidation data" (but still
gin::PerContextData) to be an indication of a valid state, we need to
*create* an invalidation data in InvalidateContext() if one hasn't
already been made. This is a bit weird, since we're adding a new
PerContextData to a context as it's being cleaned up.

Fix this code smell by always instantiating a ContextInvalidationData
entry for each new context. This way, we don't have to create a new
one during the context invalidation flow, and can treat absence of
invalidation data as a signal of an invalid context.

Bug: 444176961
Change-Id: I717e7dd74f3e0ef8b78a36a839174360a3b57a7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7281900
Commit-Queue: Devlin Cronin <[email protected]>
Reviewed-by: Tim <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1562006}
---

diff --git a/extensions/renderer/bindings/api_binding_test.cc b/extensions/renderer/bindings/api_binding_test.cc
index fdba8570..deb0062 100644
--- a/extensions/renderer/bindings/api_binding_test.cc
+++ b/extensions/renderer/bindings/api_binding_test.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 
 #include "base/task/single_thread_task_runner.h"
+#include "extensions/renderer/bindings/api_binding_util.h"
 #include "gin/array_buffer.h"
 #include "gin/public/context_holder.h"
 #include "gin/public/isolate_holder.h"
@@ -44,6 +45,8 @@
   context->Enter();
   main_context_holder_ = std::make_unique<gin::ContextHolder>(isolate());
   main_context_holder_->SetContext(context);
+
+  binding::InitializeContext(context);
 }
 
 void APIBindingTest::TearDown() {
@@ -102,6 +105,7 @@
       v8::Context::New(isolate(), GetV8ExtensionConfiguration());
   holder->SetContext(context);
   additional_context_holders_.push_back(std::move(holder));
+  binding::InitializeContext(context);
   return context;
 }
 
diff --git a/extensions/renderer/bindings/api_binding_util.cc b/extensions/renderer/bindings/api_binding_util.cc
index da66db9..7efcc47 100644
--- a/extensions/renderer/bindings/api_binding_util.cc
+++ b/extensions/renderer/bindings/api_binding_util.cc
@@ -88,13 +88,13 @@
   if (!per_context_data)
     return false;
 
-  auto* invalidation_data =
-      static_cast<ContextInvalidationData*>(per_context_data->GetUserData(
-          ContextInvalidationData::kPerContextDataKey));
-  // The context is valid if we've never created invalidation data for it, or if
-  // we have and it hasn't been marked as invalid.
+  auto* invalidation_data = GetPerContextData<ContextInvalidationData>(
+      context, CreatePerContextData::kDontCreateIfMissing);
+
+  // The context is valid as long as the invalidation data is present and not
+  // marked invalid.
   bool is_context_valid =
-      !invalidation_data || invalidation_data->is_context_valid();
+      invalidation_data && invalidation_data->is_context_valid();
 
   if (is_context_valid) {
     // As long as the context is valid, there should be an associated
@@ -116,11 +116,26 @@
   return false;
 }
 
+void InitializeContext(v8::Local<v8::Context> context) {
+  gin::PerContextData* per_context_data = gin::PerContextData::From(context);
+  CHECK(per_context_data);
+
+  // It would be nice to CHECK() that the invalidation data does *not* yet exist
+  // (since this means we're calling InitializeContext() twice), but a number of
+  // tests do this as part of their setup. It's also fairly harmless.
+  if (per_context_data->GetUserData(
+          ContextInvalidationData::kPerContextDataKey)) {
+    return;
+  }
+
+  per_context_data->SetUserData(ContextInvalidationData::kPerContextDataKey,
+                                std::make_unique<ContextInvalidationData>());
+}
+
 void InvalidateContext(v8::Local<v8::Context> context) {
   ContextInvalidationData* data = GetPerContextData<ContextInvalidationData>(
-      context, CreatePerContextData::kCreateIfMissing);
-  if (!data)
-    return;
+      context, CreatePerContextData::kDontCreateIfMissing);
+  CHECK(data);
 
   data->Invalidate();
 }
diff --git a/extensions/renderer/bindings/api_binding_util.h b/extensions/renderer/bindings/api_binding_util.h
index c3297c8..35bb1ad 100644
--- a/extensions/renderer/bindings/api_binding_util.h
+++ b/extensions/renderer/bindings/api_binding_util.h
@@ -30,6 +30,9 @@
 // Same as above, but throws an exception in the `context` if it is invalid.
 bool IsContextValidOrThrowError(v8::Local<v8::Context> context);
 
+// Initializes the given `context`.
+void InitializeContext(v8::Local<v8::Context> context);
+
 // Marks the given `context` as invalid.
 void InvalidateContext(v8::Local<v8::Context> context);
 
diff --git a/extensions/renderer/bindings/api_bindings_system.cc b/extensions/renderer/bindings/api_bindings_system.cc
index 91816387..e66fedd05 100644
--- a/extensions/renderer/bindings/api_bindings_system.cc
+++ b/extensions/renderer/bindings/api_bindings_system.cc
@@ -155,6 +155,10 @@
   custom_types_[type_name] = std::move(function);
 }
 
+void APIBindingsSystem::DidCreateContext(v8::Local<v8::Context> context) {
+  binding::InitializeContext(context);
+}
+
 void APIBindingsSystem::WillReleaseContext(v8::Local<v8::Context> context) {
   binding::InvalidateContext(context);
   request_handler_.InvalidateContext(context);
diff --git a/extensions/renderer/bindings/api_bindings_system.h b/extensions/renderer/bindings/api_bindings_system.h
index 0e568680..af41434 100644
--- a/extensions/renderer/bindings/api_bindings_system.h
+++ b/extensions/renderer/bindings/api_bindings_system.h
@@ -90,6 +90,9 @@
   void RegisterCustomType(const std::string& type_name,
                           CustomTypeHandler function);
 
+  // Handles any initialization of the context. This should be called before any
+  // APIs or other objects are created.
+  void DidCreateContext(v8::Local<v8::Context> context);
   // Handles any cleanup necessary before releasing the given `context`.
   void WillReleaseContext(v8::Local<v8::Context> context);
 
diff --git a/extensions/renderer/native_extension_bindings_system.cc b/extensions/renderer/native_extension_bindings_system.cc
index 314d4e5d..048bcd38 100644
--- a/extensions/renderer/native_extension_bindings_system.cc
+++ b/extensions/renderer/native_extension_bindings_system.cc
@@ -487,6 +487,8 @@
   DCHECK(per_context_data);
   DCHECK(!per_context_data->GetUserData(kBindingsSystemPerContextKey));
 
+  api_system_.DidCreateContext(v8_context);
+
   auto data = std::make_unique<BindingsSystemPerContextData>(
       weak_factory_.GetWeakPtr());
   per_context_data->SetUserData(kBindingsSystemPerContextKey, std::move(data));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/extensions/renderer/bindings/api_binding_test.cc b/extensions/renderer/bindings/api_binding_test.cc
index fdba8570..deb0062 100644
--- a/extensions/renderer/bindings/api_binding_test.cc
+++ b/extensions/renderer/bindings/api_binding_test.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 
 #include "base/task/single_thread_task_runner.h"
+#include "extensions/renderer/bindings/api_binding_util.h"
 #include "gin/array_buffer.h"
 #include "gin/public/context_holder.h"
 #include "gin/public/isolate_holder.h"
@@ -44,6 +45,8 @@
   context->Enter();
   main_context_holder_ = std::make_unique<gin::ContextHolder>(isolate());
   main_context_holder_->SetContext(context);
+
+  binding::InitializeContext(context);
 }
 
 void APIBindingTest::TearDown() {
@@ -102,6 +105,7 @@
       v8::Context::New(isolate(), GetV8ExtensionConfiguration());
   holder->SetContext(context);
   additional_context_holders_.push_back(std::move(holder));
+  binding::InitializeContext(context);
   return context;
 }
Loading diff…

Original Bug Report

reported by [email protected]

use-after-poison in ContextInvalidationListener

Steps to reproduce the problem

  1. load extension poc.zip. (**disable –no-sandbox **; If it does not crash, try several times.)

Problem Description

In the process of UnloadExtension when extensions::APIBindingsSystem::WillReleaseContext(v8::Local<v8::Context>) triggers context invalidation. During context invalidation, a ContextInvalidationListener attempts to access its base::OnceClosure callback after the callback has already been destroyed, resulting in a use-after-poison error when checking the callback’s validity in OnInvalidated(). This indicates a lifetime management issue where the callback object is destroyed before its associated ContextInvalidationListener, possibly due to incorrect destruction ordering or missing cleanup mechanisms during context release.

1 2

fix: Ensure ContextInvalidationListener unregisters itself or uses weak pointers to handle callback invalidation safely.

Summary

use-after-poison in ContextInvalidationListener

Custom Questions

Crash state:

=================================================================
==3775476==ERROR: AddressSanitizer: use-after-poison on address 0x7eac00559a58 at pc 0x615e5703021b bp 0x7ffe2d0cbbb0 sp 0x7ffe2d0cbba8
READ of size 8 at 0x7eac00559a58 thread T0 (chrome)
    #0 0x615e5703021a in operator bool base/memory/scoped_refptr.h:319:43
    #1 0x615e5703021a in is_null base/functional/callback_internal.h:140:34
    #2 0x615e5703021a in operator bool base/functional/callback_internal.h:141:44
    #3 0x615e5703021a in operator bool base/functional/callback.h:110:45
    #4 0x615e5703021a in OnInvalidated extensions/renderer/bindings/api_binding_util.cc:167:3
    #5 0x615e5703021a in extensions::binding::ContextInvalidationData::Invalidate() extensions/renderer/bindings/api_binding_util.cc:84:14
    #6 0x615e57036bb7 in extensions::APIBindingsSystem::WillReleaseContext(v8::Local<v8::Context>) extensions/renderer/bindings/api_bindings_system.cc:159:3
    #7 0x615e57138587 in extensions::NativeExtensionBindingsSystem::WillReleaseScriptContext(extensions::ScriptContext*) extensions/renderer/native_extension_bindings_system.cc:525:15
    #8 0x615e570c3ebf in Invoke<void (extensions::NativeExtensionBindingsSystem::*)(extensions::ScriptContext *), extensions::NativeExtensionBindingsSystem *, extensions::ScriptContext *> base/functional/bind_internal.h:730:12
    #9 0x615e570c3ebf in MakeItSo<void (extensions::NativeExtensionBindingsSystem::*const &)(extensions::ScriptContext *), const std::__Cr::tuple<base::internal::UnretainedWrapper<extensions::NativeExtensionBindingsSystem, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0> > &, extensions::ScriptContext *> base/functional/bind_internal.h:922:12
    #10 0x615e570c3ebf in RunImpl<void (extensions::NativeExtensionBindingsSystem::*const &)(extensions::ScriptContext *), const std::__Cr::tuple<base::internal::UnretainedWrapper<extensions::NativeExtensionBindingsSystem, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0> > &, 0UL> base/functional/bind_internal.h:1059:14
    #11 0x615e570c3ebf in base::internal::Invoker<base::internal::FunctorTraits<void (extensions::NativeExtensionBindingsSystem::* const&)(extensions::ScriptContext*), extensions::NativeExtensionBindingsSystem*>, base::internal::BindState<true, true, false, void (extensions::NativeExtensionBindingsSystem::*)(extensions::ScriptContext*), base::internal::UnretainedWrapper<extensions::NativeExtensionBindingsSystem, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0>>, void (extensions::ScriptContext*)>::Run(base::internal::BindStateBase*, extensions::ScriptContext*) base/functional/bind_internal.h:979:12
    #12 0x615e57175949 in base::RepeatingCallback<void (extensions::ScriptContext*)>::Run(extensions::ScriptContext*) const & base/functional/callback.h:343:12
    #13 0x615e5717520b in ExecuteCallbackWithContext extensions/renderer/script_context_set.cc:202:14
    #14 0x615e5717520b in extensions::ScriptContextSet::ForEach(extensions::mojom::HostID const&, content::RenderFrame*, base::RepeatingCallback<void (extensions::ScriptContext*)> const&) extensions/renderer/script_context_set.cc:175:11
    #15 0x615e570b59a6 in extensions::Dispatcher::UnloadExtension(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>> const&) extensions/renderer/dispatcher.cc:1185:24
    #16 0x615e4e2f1377 in extensions::mojom::RendererStubDispatch::Accept(extensions::mojom::Renderer*, mojo::Message*) gen/extensions/common/mojom/renderer.mojom.cc:2779:13
    #17 0x615e58c0e8a2 in mojo::InterfaceEndpointClient::HandleValidatedMessage(mojo::Message*) mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:1059:54
    #18 0x615e58c38fbb in mojo::MessageDispatcher::Accept(mojo::Message*) mojo/public/cpp/bindings/lib/message_dispatcher.cc:43:19
    #19 0x615e58c15d88 in mojo::InterfaceEndpointClient::HandleIncomingMessage(mojo::Message*) mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:731:20
    #20 0x615e5db96a4f in IPC::ChannelAssociatedGroupController::AcceptOnEndpointThread(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification) ipc/ipc_mojo_bootstrap.cc:1202:24
    #21 0x615e5db9978c in Invoke<void (IPC::ChannelAssociatedGroupController::*)(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification), scoped_refptr<IPC::ChannelAssociatedGroupController>, mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification> base/functional/bind_internal.h:730:12
    #22 0x615e5db9978c in MakeItSo<void (IPC::ChannelAssociatedGroupController::*)(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification), std::__Cr::tuple<scoped_refptr<IPC::ChannelAssociatedGroupController>, mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification> > base/functional/bind_internal.h:922:12
    #23 0x615e5db9978c in RunImpl<void (IPC::ChannelAssociatedGroupController::*)(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification), std::__Cr::tuple<scoped_refptr<IPC::ChannelAssociatedGroupController>, mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification>, 0UL, 1UL, 2UL> base/functional/bind_internal.h:1059:14
    #24 0x615e5db9978c in base::internal::Invoker<base::internal::FunctorTraits<void (IPC::ChannelAssociatedGroupController::*&&)(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification), IPC::ChannelAssociatedGroupController*&&, mojo::Message&&, IPC::(anonymous namespace)::ScopedUrgentMessageNotification&&>, base::internal::BindState<true, true, false, void (IPC::ChannelAssociatedGroupController::*)(mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification), scoped_refptr<IPC::ChannelAssociatedGroupController>, mojo::Message, IPC::(anonymous namespace)::ScopedUrgentMessageNotification>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:972:12
    #25 0x615e3a86dbe9 in base::OnceCallback<void ()>::Run() && base/functional/callback.h:155:12
    #26 0x615e58f1a527 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/task/common/task_annotator.cc:207:34
    #27 0x615e58fd8838 in RunTask<(lambda at ../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:474:11)> base/task/common/task_annotator.h:104:5
    #28 0x615e58fd8838 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:472:23
    #29 0x615e58fd6756 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:346:40
    #30 0x615e58fd978a in non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc
    #31 0x615e58d60e8b in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
    #32 0x615e58fdacad in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:647:12
    #33 0x615e58e61f2b in base::RunLoop::Run(base::Location const&) base/run_loop.cc:134:14
    #34 0x615e676787af in content::RendererMain(content::MainFunctionParams) content/renderer/renderer_main.cc:355:16
    #35 0x615e540becda in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:669:14
    #36 0x615e540c0e4d in content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>> const&, content::MainFunctionParams, content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:772:12
    #37 0x615e540c468b in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1129:10
    #38 0x615e540bc3a4 in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:346:36
    #39 0x615e540bc93c in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:359:10
    #40 0x615e3a157258 in ChromeMain chrome/app/chrome_main.cc:228:12
    #41 0x704becc2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #42 0x704becc2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #43 0x615e3a07a029 in _start (/home/xx/xx/src/chrome/chromium/src/out/asan/chrome+0x27d2d029) (BuildId: 2120e3b077da9166)

Address 0x7eac00559a58 is a wild pointer inside of access range of size 0x000000000008.
SUMMARY: AddressSanitizer: use-after-poison base/memory/scoped_refptr.h:319:43 in operator bool
Shadow bytes around the buggy address:
  0x7eac00559780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7eac00559800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7eac00559880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7eac00559900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7eac00559980: 00 00 00 00 00 00 00 00 00 00 00 00 00 f7 f7 f7
=>0x7eac00559a00: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7[f7]f7 f7 f7 f7
  0x7eac00559a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7eac00559b00: 00 00 00 00 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
  0x7eac00559b80: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
  0x7eac00559c00: f7 f7 f7 f7 f7 f7 00 00 00 00 00 00 00 00 00 00
  0x7eac00559c80: 00 00 00 00 00 00 00 00 00 00 f7 f7 f7 f7 f7 f7
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb

NOTE: the stack trace above identifies the code that *accessed* the poisoned memory.
To identify the code that *poisoned* the memory, try the experimental setting ASAN_OPTIONS=poison_history_size=<size>.

==3775476==ADDITIONAL INFO

==3775476==Note: Please include this section with the ASan report.
Task trace:
    #0 0x615e5db8084c in IPC::ChannelAssociatedGroupController::Accept(mojo::Message*) ipc/ipc_mojo_bootstrap.cc:1141:13

Additional Data

Category: Security
Chrome Channel: Not sure
Regression: N/A \

View on issue tracker