CVE-2026-10915
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/child_process_launcher_helper_ios.mm |
modified |
Files Changed
content/browser/child_process_launcher_helper_ios.mm
Patch
From dcbf07463a2f9e0566e398f8c46b38dd452c28ba Mon Sep 17 00:00:00 2001
From: Dave Tapuska <dtapuska@chromium.org>
Date: Fri, 01 May 2026 09:51:51 -0700
Subject: [PATCH] [ios blink] Fix launch helper
There could be a potential race in determining whether the process
was dead because of multiple threads updating the state object.
Use a std::atomic so it is clear the process terminated.
BUG=497612174
Change-Id: Idd54f2252e94f529a87898b47155af5e4129408b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7810074
Reviewed-by: Justin Cohen <justincohen@google.com>
Commit-Queue: Dave Tapuska <dtapuska@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1623876}
---
diff --git a/content/browser/child_process_launcher_helper_ios.mm b/content/browser/child_process_launcher_helper_ios.mm
index cc35e95..822a8b0 100644
--- a/content/browser/child_process_launcher_helper_ios.mm
+++ b/content/browser/child_process_launcher_helper_ios.mm
@@ -11,6 +11,7 @@
#import <BrowserEngineKit/BrowserEngineKit.h>
+#include <atomic>
#include <list>
#include "base/apple/mach_port_rendezvous_ios.h"
@@ -158,12 +159,18 @@
~ProcessStorage() override { [grant_ invalidate]; }
- void ReleaseProcess() override { process_ = nullptr; }
+ void ReleaseProcess() override {
+ terminated_ = true;
+ process_ = nullptr;
+ }
NSObject* Process() { return process_; }
+ bool IsTerminated() const { return terminated_; }
+
private:
NSObject* process_;
+ std::atomic<bool> terminated_ = false;
[[maybe_unused]] xpc_connection_t ipc_channel_;
id<BEProcessCapabilityGrant> grant_;
};
@@ -464,8 +471,8 @@
ChildProcessTerminationInfo info;
if (!process_storage_) {
info.status = base::TERMINATION_STATUS_LAUNCH_FAILED;
- } else if (static_cast<ProcessStorage*>(process_storage_.get())->Process() ==
- nullptr) {
+ } else if (static_cast<ProcessStorage*>(process_storage_.get())
+ ->IsTerminated()) {
if (exit_code_.has_value()) {
if (exit_code_.value() == RESULT_CODE_NORMAL_EXIT) {
info.status = base::TERMINATION_STATUS_NORMAL_TERMINATION;
Original Bug Report
Potential Use-After-Free in Browser Process via Data Race in ChildProcessLauncherHelper
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A data race in ChildProcessLauncherHelper on iOS allows a potential use-after-free (UAF) of an ARC-managed process object. The UI thread reads the process object without synchronization while a background thread clears it, which under ARC can lead to retaining a deallocated object.
Affected files:
content/browser/child_process_launcher_helper_ios.mmcontent/browser/child_process_launcher_helper.h
Estimated timestamp from git blame: 2024-04-12
Summary
A potential Use-After-Free (UAF) vulnerability exists in the browser process on iOS. It is caused by an unsynchronized cross-thread access to an ARC-managed strong pointer (process_) within the ProcessStorage class. Objective-C ARC pointer reads and writes are not atomic. A race condition between the UI thread reading the pointer and background termination handlers clearing it can cause ARC to retain a deallocated object, leading to a UAF.
Technical Details
In content/browser/child_process_launcher_helper_ios.mm, the ProcessStorage class manages the lifetime of iOS child process objects (BEWebContentProcess, BENetworkingProcess, or BERenderingProcess):
class ProcessStorage : public ProcessStorageBase {
public:
// ...
void ReleaseProcess() override { process_ = nullptr; }
NSObject* Process() { return process_; }
private:
NSObject* process_; // ARC-managed __strong pointer
// ...
};
There are two conflicting paths accessing process_:
-
Writer Path (Background Thread): When a child process terminates, an XPC error handler or interruption handler runs on an arbitrary background libdispatch queue. This handler calls
OnChildProcessTerminatedOnAnyThread, which acquiresg_process_table_lock_and eventually callsReleaseProcess(). This setsprocess_ = nullptr, which ARC translates to anobjc_storeStrongcall that decrements the object’s reference count and deallocates it if the count reaches zero. -
Reader Path (UI Thread): When the UI thread handles a Mojo channel error (e.g., in
RenderProcessHostImpl::OnChannelError), it callsChildProcessLauncherHelper::GetTerminationInfo. This method checksstatic_cast<ProcessStorage*>(process_storage_.get())->Process() == nullptrto determine the process status. Crucially, this read does not acquireg_process_table_lock_or any other synchronization primitive.
The Race and UAF
When the UI thread calls Process(), ARC generates code to load the pointer and typically calls objc_retain (or objc_retainAutoreleasedReturnValue) to ensure the returned object remains valid. If the UI thread loads the pointer value, and then the background thread clears process_ and releases the object before the UI thread can call objc_retain, the object may be deallocated. The UI thread will then call objc_retain on a freed object, dereferencing its isa pointer and causing a Use-After-Free.
This occurs in the highly privileged browser process. Additionally, because the pointer is an Objective-C ARC pointer, it is not protected by MiraclePtr (BackupRefPtr).
Potential Exploitation Steps
Note: These are theoretical steps, as our tooling agent cannot execute code to provide a working PoC.
- An attacker exploits a renderer process to gain arbitrary code execution within the sandbox.
- The attacker performs heap grooming in the browser process via IPC, preparing malicious payloads to replace the
BEWebContentProcessobject in memory. - The attacker deliberately crashes the renderer process or drops the Mojo connection, triggering concurrent termination handling in the browser process: the Mojo channel error on the UI thread and the XPC error on a background thread.
- If the UI thread loads the
process_pointer and is immediately preempted, the background thread may executeReleaseProcess(), dropping the reference count to zero and freeing the object. - The attacker’s groomed data is immediately reallocated into the freed memory space, forging the
isapointer. - The UI thread resumes and calls
objc_retainon the attacker-controlled memory, hijacking the Objective-C method dispatch to redirect control flow to an attacker-controlled ROP chain, achieving arbitrary code execution and a full sandbox escape.
Suggested Fix
Synchronize access to the process_ pointer and exit_code_ within ChildProcessLauncherHelper. One solution is to require g_process_table_lock_ to be held when accessing process_storage_ in GetTerminationInfo, or by using explicit thread-safe primitives (like std::atomic or locks) to manage the Objective-C pointer access.
Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.