CVE-2026-17862
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/windows_services/elevated_tracing_service/process_watcher.cc |
modified | |
ThreadDelegatechrome/windows_services/elevated_tracing_service/process_watcher.h |
modified |
Files Changed
chrome/windows_services/elevated_tracing_service/process_watcher.ccchrome/windows_services/elevated_tracing_service/process_watcher.hchrome/windows_services/elevated_tracing_service/session_registry.cc
Patch
From 858ee83ea6e4a0ae9c05bd536e23eb9c5c1cac4e Mon Sep 17 00:00:00 2001 From: Greg Thompson <[email protected]> Date: Wed, 10 Jun 2026 09:02:22 -0700 Subject: [PATCH] [etw tracing] Fix shutdown race Destruction of a SystemTracingSession by its last ref being released could race with detection of client process termination by ProcessWatcher. To avoid a data race, ProcessWatcher's destructor must wait for its watch task to complete before allowing destruction to complete. This ensures that the SystemTracingSession instance that owns the watcher (by way of a ScopedSession) is not destroyed while SessionRegistry::OnClientTerminated is handling termination of the client. Fixed: 520426287 Change-Id: Ia6d27379ec566ee1ff7e2454fd9fc9c1810f2ec7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7918051 Reviewed-by: Etienne Pierre-Doray <[email protected]> Commit-Queue: Etienne Pierre-Doray <[email protected]> Cr-Commit-Position: refs/heads/main@{#1644684} --- diff --git a/chrome/windows_services/elevated_tracing_service/process_watcher.cc b/chrome/windows_services/elevated_tracing_service/process_watcher.cc index 38d2dea0..0e35b9ac 100644 --- a/chrome/windows_services/elevated_tracing_service/process_watcher.cc +++ b/chrome/windows_services/elevated_tracing_service/process_watcher.cc @@ -7,6 +7,7 @@ #include <windows.h> #include <array> +#include <functional> #include <utility> #include "base/check.h" @@ -27,11 +28,17 @@ // Waits for either the watched process to terminate or for the shutdown event // to be signaled. In the former case, the `on_terminated` closure is -// run before exiting. +// run before exiting. `completed_event` is signaled when the task is complete. void WatchInThreadPool(base::Process process, base::OnceClosure on_terminated, HANDLE startup_event, - base::win::ScopedHandle shutdown_event) { + base::win::ScopedHandle shutdown_event, + base::win::ScopedHandle completed_event, + base::PlatformThreadId& watch_thread_id) { + // Give the ID of this thread to the ProcessWatcher so that it can avoid + // waiting on itself during destruction. + watch_thread_id = base::PlatformThread::CurrentId(); + // Signal that the task is ready to watch. ::SetEvent(std::exchange(startup_event, nullptr)); @@ -48,6 +55,8 @@ if (result == WAIT_OBJECT_0) { std::move(on_terminated).Run(); } // else the shutdown event was signaled. + + ::SetEvent(completed_event.get()); } } // namespace @@ -66,13 +75,22 @@ /*dwDesiredAccess=*/0, /*bInheritHandle=*/FALSE, DUPLICATE_SAME_ACCESS)); + // Prepare a dup of the completed event for the task to signal. + HANDLE completed_event = nullptr; + CHECK(::DuplicateHandle(::GetCurrentProcess(), completed_event_.handle(), + ::GetCurrentProcess(), &completed_event, + /*dwDesiredAccess=*/0, + /*bInheritHandle=*/FALSE, DUPLICATE_SAME_ACCESS)); + base::ThreadPool::CreateTaskRunner( {base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, base::MayBlock()}) ->PostTask( FROM_HERE, base::BindOnce(&WatchInThreadPool, std::move(process), std::move(on_terminated), startup_event.handle(), - base::win::ScopedHandle(shutdown_event))); + base::win::ScopedHandle(shutdown_event), + base::win::ScopedHandle(completed_event), + std::ref(watch_thread_id_))); // Wait for the watch task to signal that it is ready. startup_event.Wait(); @@ -81,6 +99,13 @@ ProcessWatcher::~ProcessWatcher() { // Signal that the watch task should exit if it is still watching the process. shutdown_event_.Signal(); + + // Wait for the watch task to complete before continuing with destruction, + // unless destruction is taking place on the watch thread itself. This can + // happen if client termination synchronously triggers destruction via COM. + if (base::PlatformThread::CurrentId() != watch_thread_id_) { + completed_event_.Wait(); + } } } // namespace elevated_tracing_service diff --git a/chrome/windows_services/elevated_tracing_service/process_watcher.h b/chrome/windows_services/elevated_tracing_service/process_watcher.h index d31cfa4..49ee70f 100644 --- a/chrome/windows_services/elevated_tracing_service/process_watcher.h +++ b/chrome/windows_services/elevated_tracing_service/process_watcher.h @@ -8,6 +8,7 @@ #include "base/functional/callback_forward.h" #include "base/process/process.h" #include "base/synchronization/waitable_event.h" +#include "base/threading/platform_thread.h" namespace elevated_tracing_service { @@ -22,10 +23,14 @@ ~ProcessWatcher(); private: - class ThreadDelegate; - // An event that is signaled at destruction to cancel the watch. base::WaitableEvent shutdown_event_; + + // An event that is signaled when the watch task has completed. + base::WaitableEvent completed_event_; + + // The TID of the thread servicing the watch task. + base::PlatformThreadId watch_thread_id_ = base::kInvalidThreadId; }; } // namespace elevated_tracing_service diff --git a/chrome/windows_services/elevated_tracing_service/session_registry.cc b/chrome/windows_services/elevated_tracing_service/session_registry.cc index f9610391..bd30ed0 100644 --- a/chrome/windows_services/elevated_tracing_service/session_registry.cc +++ b/chrome/windows_services/elevated_tracing_service/session_registry.cc @@ -6,6 +6,8 @@ #include <objbase.h> +#include <wrl/client.h> + #include <utility> #include "base/check_deref.h" @@ -108,6 +110,9 @@ // still holds the session's IUnknown pointer (meaning that the ScopedSession // has yet to be destroyed), tell COM to force a disconnect. if (IUnknown* unknown = core->release(); unknown != nullptr) { + // Keep a strong reference locally to prevent the object from being + // synchronously destroyed (and deallocated) during CoDisconnectObject. + Microsoft::WRL::ComPtr<IUnknown> keep_alive(unknown); ::CoDisconnectObject(unknown, /*dwReserved=*/0); // This task is handling client termination before the session is destroyed,
Original Bug Report
Potential Use-After-Free in elevated_tracing_service due to SessionRegistry and ProcessWatcher race
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 Windows-only elevated tracing service due to a race condition between COM session destruction and client termination. A thread-pool worker thread can invoke CoDisconnectObject on a dangling pointer to a freed SystemTracingSession object. If exploited, this could allow a local interactive user to escalate privileges to SYSTEM.
Affected files:
chrome/windows_services/elevated_tracing_service/session_registry.ccchrome/windows_services/elevated_tracing_service/process_watcher.ccchrome/windows_services/elevated_tracing_service/system_tracing_session.cc
Estimated timestamp from git blame: 2024-10-09
Description
A potential Use-After-Free (UAF) vulnerability has been identified in the Windows-only elevated_tracing_service within Chromium. The issue lies in how the SessionRegistry coordinates clean session destruction with client process termination.
SessionRegistry::OnClientTerminated (session_registry.cc:106-118) is invoked on a ThreadPool thread when a client process terminates. It calls ::CoDisconnectObject on a raw IUnknown* pointer retrieved from SessionCore via an atomic exchange unknown_.exchange(nullptr) (session_registry.cc:35). However, SessionCore holds this pointer as a raw, weak reference without having called AddRef (session_registry.cc:31), meaning the lifetime of the SystemTracingSession is independent of SessionCore.
Concurrently, if COM drops the last reference to SystemTracingSession (due to client process exit or rundown), the SystemTracingSession destructor runs on an arbitrary MTA RPC thread. The destructor triggers ~ScopedSession, which runs OnSessionDestroyed (session_registry.cc:47).
If the ThreadPool thread wins the atomic exchange, OnSessionDestroyed on the RPC thread retrieves nullptr and returns early without blocking (session_registry.cc:98). The RPC thread then proceeds to destroy ProcessWatcher (session_registry.cc:54). ~ProcessWatcher signals the shutdown event but does not wait/join for the background ThreadPool thread task to finish (process_watcher.cc:81-84). Consequently, the RPC thread deallocates SystemTracingSession immediately. When the preempted ThreadPool thread resumes, it calls CoDisconnectObject(unknown) on the now-freed pointer, initiating a virtual function call on a dangling pointer.
Because cet_shadow_stack (Intel CET) is disabled for this executable target (BUILD.gn:22-23), reclaiming the deallocated session’s heap slot could allow an attacker to hijack control flow.
Potential Attack Steps
- A local low-privilege interactive user calls
CoCreateInstanceto activateCLSID_SystemTracingSessionin the service. - The client calls
AcceptInvitationto register the active session and start the background process-monitoring task (ProcessWatcher). - The attacker terminates their client process to trigger both:
- The kernel process-handle signal (running
OnClientTerminatedon a ThreadPool thread). - The COM fast-rundown stub
Release(destroying the session on an RPC thread).
- The kernel process-handle signal (running
- A race is triggered: if the ThreadPool thread exchanges the pointer and gets preempted before invoking
CoDisconnectObject, the RPC thread will delete the session unhindered. - When the ThreadPool thread resumes, it performs a virtual call (
QueryInterface) on the freedSystemTracingSessionobject. - Grooming/spraying the heap from a secondary COM connection to reclaim the deallocated block could lead to arbitrary code execution as
NT AUTHORITY\SYSTEM.
Note: These are suggested potential steps; our tooling does not currently have the capability to execute code or run a working Proof of Concept (PoC).
Suggested Fix
To remediate this race, ~ProcessWatcher must block and wait for the posted background ThreadPool task to terminate before allowing destruction of the outer class to proceed, or SessionCore should extend the lifetime of the SystemTracingSession object by managing strong references (Microsoft::WRL::ComPtr) rather than un-refcounted raw pointers.
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
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.