CVE-2026-3919
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifextensions/renderer/bindings/api_binding_util.cc |
modified |
Files Changed
extensions/renderer/bindings/api_binding_test.ccextensions/renderer/bindings/api_binding_util.ccextensions/renderer/bindings/api_binding_util.hextensions/renderer/bindings/api_bindings_system.ccextensions/renderer/bindings/api_bindings_system.hextensions/renderer/native_extension_bindings_system.cc
Patch
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));
Regression Test / PoC
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;
}
Original Bug Report
use-after-poison in ContextInvalidationListener
Steps to reproduce the problem
- 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.
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 \
- https://source.chromium.org/chromium/chromium/src/+/main:extensions/renderer/bindings/api_binding_util.cc;drc=48d6f7175422b2c969c14258f9f8d5b196c28d18;l=84
- https://source.chromium.org/chromium/chromium/src/+/main:extensions/renderer/bindings/api_binding_util.h;drc=48d6f7175422b2c969c14258f9f8d5b196c28d18;l=55