Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Core
DescriptionUse after free in Core
ComponentCore
Bug ClassUAF
Tracker499206649
Fix commit936b3c6dc1f1 (chromium/src) +10/-32
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/api/processes/processes_api.cc
modified

Files Changed

  • chrome/browser/extensions/api/processes/processes_api.cc
  • chrome/browser/extensions/api/processes/processes_api.h
From 936b3c6dc1f1575f981c3c8f85bd18539bde4595 Mon Sep 17 00:00:00 2001
From: Gabriel Charette <[email protected]>
Date: Fri, 07 Aug 2026 13:56:26 -0700
Subject: [PATCH] [gemini] Fix UAF in chrome.processes.terminate() by querying BrowserChildProcessHost on UI thread

`ProcessesTerminateFunction::Run()` previously posted a task to the IO thread to resolve non-renderer process handles via `BrowserChildProcessHost::FromID()`.

However, `BrowserChildProcessHost::FromID()` iterates `g_child_process_list`, which is strictly owned and mutated on the UI thread. In release builds (where `DCHECK_CURRENTLY_ON(BrowserThread::UI)` is compiled out), this cross-thread traversal raced with UI-thread node deletion in `BrowserChildProcessHostImpl::ForceShutdown()`, causing a heap-use-after-free on the IO thread.

Historical Note:
This asynchronous IO-thread lookup was introduced ~10 years ago (commit 98241839b528) when `BrowserChildProcessHost` and IPC channel handling lived on the IO thread. When child process host management was subsequently migrated to the UI thread, `ProcessesTerminateFunction` was overlooked and remained the sole legacy caller dispatching `FromID()` to the IO thread.

Fix this by invoking `BrowserChildProcessHost::FromID()` directly on the UI thread in `ProcessesTerminateFunction::Run()`, eliminating the IO thread task post.

Verified with the ASAN reproduction extension from crbug.com/499206649.

TAG=agy
CONV=33405ca9-4da2-4d0e-94d6-1a4e6e620582

Fixed: 499206649
Change-Id: Ic5dd79487e10d1e596a42d7b6e87a681870bc42b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8214341
Auto-Submit: Gabriel Charette <[email protected]>
Reviewed-by: Reilly Grant <[email protected]>
Commit-Queue: Reilly Grant <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1675956}
---

diff --git a/chrome/browser/extensions/api/processes/processes_api.cc b/chrome/browser/extensions/api/processes/processes_api.cc
index 785b611..c18f5b7f 100644
--- a/chrome/browser/extensions/api/processes/processes_api.cc
+++ b/chrome/browser/extensions/api/processes/processes_api.cc
@@ -500,39 +500,21 @@
   // Check if it's a renderer.
   auto* render_process_host =
       content::RenderProcessHost::FromID(child_process_host_id_);
-  if (render_process_host)
+  if (render_process_host) {
     return RespondNow(
         TerminateIfAllowed(render_process_host->GetProcess().Handle()));
-
-  // This could be a non-renderer child process like a plugin.
-  // Try to get its handle from the BrowserChildProcessHost on the IO thread.
-  content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult(
-      FROM_HERE,
-      base::BindOnce(&ProcessesTerminateFunction::GetProcessHandleOnIO, this,
-                     child_process_host_id_),
-      base::BindOnce(&ProcessesTerminateFunction::OnProcessHandleOnUI, this));
-
-  // Promise to respond later.
-  return RespondLater();
-}
-
-base::ProcessHandle ProcessesTerminateFunction::GetProcessHandleOnIO(
-    int child_process_host_id) const {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
-
-  auto* host = content::BrowserChildProcessHost::FromID(child_process_host_id);
-  if (host) {
-    return host->GetData().GetProcess().Handle();
   }
 
-  return base::kNullProcessHandle;
-}
+  // Check if it's a non-renderer child process (e.g. utility, GPU).
+  auto* browser_child_process_host =
+      content::BrowserChildProcessHost::FromID(child_process_host_id_);
+  if (browser_child_process_host) {
+    return RespondNow(TerminateIfAllowed(
+        browser_child_process_host->GetData().GetProcess().Handle()));
+  }
 
-void ProcessesTerminateFunction::OnProcessHandleOnUI(
-    base::ProcessHandle handle) {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-
-  Respond(TerminateIfAllowed(handle));
+  return RespondNow(Error(errors::kProcessNotFound,
+                          base::NumberToString(child_process_host_id_)));
 }
 
 ExtensionFunction::ResponseValue
diff --git a/chrome/browser/extensions/api/processes/processes_api.h b/chrome/browser/extensions/api/processes/processes_api.h
index 0939467..8d535b1 100644
--- a/chrome/browser/extensions/api/processes/processes_api.h
+++ b/chrome/browser/extensions/api/processes/processes_api.h
@@ -146,10 +146,6 @@
  private:
   ~ProcessesTerminateFunction() override = default;
 
-  // Functions to get the process handle on the IO thread and post it back to
-  // the UI thread from processing.
-  base::ProcessHandle GetProcessHandleOnIO(int child_process_host_id) const;
-  void OnProcessHandleOnUI(base::ProcessHandle handle);
 
   // Terminates the process with `handle` if it's valid and is allowed to be
   // terminated. Returns the response value of this extension function to be
Loading diff…

Original Bug Report

reported by [email protected]

Cross-thread use-after-free in browser process via chrome.processes.terminate()

Cross-thread use-after-free in browser process via chrome.processes.terminate()

Summary

The chrome.processes.terminate() extension API dispatches BrowserChildProcessHost::FromID() to the IO thread, but this function and the global g_child_process_list it iterates are exclusively owned by the UI thread with no synchronization. The only guards are DCHECK_CURRENTLY_ON(BrowserThread::UI) assertions, which are compiled out in release builds. When an extension rapidly calls terminate() on non-renderer child process IDs, the IO thread iterates the list concurrently with the UI thread destroying hosts and removing list nodes, producing a heap-use-after-free in the browser process. ASAN confirms the crash is not protected by MiraclePtr. Affects all desktop platforms (Linux, macOS, Windows). Requires the processes extension permission, currently gated to dev channel.

Bisect

Introducing Commit: 98241839b528f945fabb03f38fb1ea6a3e837f09

Root Cause

ProcessesTerminateFunction::Run() handles two categories of process IDs. For renderer processes it resolves the handle synchronously on the UI thread via RenderProcessHost::FromID(). For non-renderer child processes (utility, GPU, etc.) it posts GetProcessHandleOnIO to the IO thread:

// chrome/browser/extensions/api/processes/processes_api.cc
content::GetIOThreadTaskRunner({})->PostTaskAndReplyWithResult(
    FROM_HERE,
    base::BindOnce(&ProcessesTerminateFunction::GetProcessHandleOnIO, this,
                   child_process_host_id_),
    base::BindOnce(&ProcessesTerminateFunction::OnProcessHandleOnUI, this));

GetProcessHandleOnIO then calls BrowserChildProcessHost::FromID(), which iterates g_child_process_list:

// content/browser/browser_child_process_host_impl.cc
BrowserChildProcessHost* BrowserChildProcessHost::FromID(int child_process_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  BrowserChildProcessHostImpl::BrowserChildProcessList* process_list =
      g_child_process_list.Pointer();
  for (BrowserChildProcessHostImpl* host : *process_list) {
    if (host->GetData().id == child_process_id)
      return host;
  }
  return nullptr;
}

The DCHECK_CURRENTLY_ON(BrowserThread::UI) assertion is the only thing preventing cross-thread access here, and it vanishes in release builds. The list type is std::list<raw_ptr<BrowserChildProcessHostImpl, CtnExperimental>>, a plain doubly-linked list with no mutex, no lock, and no sequence checker protecting it.

The race unfolds as follows. When an extension fires many terminate() calls targeting non-renderer child processes, each call queues a GetProcessHandleOnIO task on the IO thread. The first successful termination kills a child process (e.g. GPU). The process exit triggers an IPC disconnect notification that propagates through the UI thread: OnChildDisconnected invokes the host delegate’s destructor, which calls ForceShutdown():

// content/browser/browser_child_process_host_impl.cc
void BrowserChildProcessHostImpl::ForceShutdown() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  g_child_process_list.Get().remove(this);
  child_process_host_->ForceShutdown();
}

This std::list::remove unlinks and frees the list node on the UI thread. Meanwhile, the IO thread is still processing queued FromID() calls, iterating the same list by following next pointers through heap-allocated std::list nodes. When it dereferences a node that the UI thread has already freed, ASAN detects a heap-use-after-free.

The freed 24-byte region is a std::list node containing the raw_ptr<BrowserChildProcessHostImpl> element plus the prev/next pointers. ASAN’s additional information confirms “MiraclePtr Status: NOT PROTECTED” because the use-after-free is on the list node itself, not on a raw_ptr-managed object.

A contrasting example exists within the same class. BrowserChildProcessHostIterator (used by code that legitimately needs to enumerate child processes) enforces DCHECK_CURRENTLY_ON(BrowserThread::UI) and is only called from UI-thread code paths. The processes.terminate() handler is the sole caller that incorrectly dispatches FromID() to the IO thread.

Reproduce

Tested at commit f7d73bbd27f24 on Linux x86_64 with the out/asan-release build configuration.

The PoC is a Manifest V3 extension that enumerates non-renderer child processes via chrome.processes.getProcessInfo() and fires concurrent terminate() calls to race the IO-thread lookup against UI-thread host destruction. The directory doubles as a loadable unpacked extension:

issue_processes_terminate_race_uaf/
├── manifest.json      # MV3 extension manifest
├── background.js      # PoC service worker

A 5ms sleep is inserted in BrowserChildProcessHost::FromID() to widen the race window for reliable reproduction. Apply patch.diff and rebuild:

cd ~/chromium/src
git apply issue_processes_terminate_race_uaf/patch.diff
autoninja -C out/asan-release chrome

Load the extension directory and launch:

ASAN_OPTIONS=detect_odr_violation=0 \
  xvfb-run -a out/asan-release/chrome \
  --disable-gpu \
  --enable-experimental-extension-apis \
  --load-extension=issue_processes_terminate_race_uaf \
  --user-data-dir=/tmp/poc-$(date +%s) \
  --no-first-run \
  about:blank

The browser process crashes within seconds with a heap-use-after-free in BrowserChildProcessHost::FromID() on the IO thread, while the UI thread frees a list node via ForceShutdown().

==1272169==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b73c90c1d48 at pc 0x7f443311242c bp 0x7b43b7044f70 sp 0x7b43b7044f68
READ of size 8 at 0x7b73c90c1d48 thread T10 (Chrome_IOThread)
    #0 content::BrowserChildProcessHost::FromID(int) content/browser/browser_child_process_host_impl.cc:142
    #1 extensions::ProcessesTerminateFunction::GetProcessHandleOnIO(int) chrome/browser/extensions/api/processes/processes_api.cc:524

freed by thread T0 (chrome) here:
    #0 operator delete(void*, unsigned long)
    #1 std::__Cr::list<...>::remove(...) gen/third_party/libc++/src/include/__new/allocate.h:63
    #2 content::BrowserChildProcessHostImpl::ForceShutdown() content/browser/browser_child_process_host_impl.cc:268
    #3 content::GpuProcessHost::ValidateHost(content::GpuProcessHost*) content/browser/gpu/gpu_process_host.cc:1249

previously allocated by thread T0 (chrome) here:
    #0 operator new(unsigned long)
    #1 content::BrowserChildProcessHostImpl::BrowserChildProcessHostImpl(...)

SUMMARY: AddressSanitizer: heap-use-after-free content/browser/browser_child_process_host_impl.cc:142 in content::BrowserChildProcessHost::FromID(int)

MiraclePtr Status: NOT PROTECTED

Full ASAN log is in asan.log.

Credit

Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.

View on issue tracker