Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Updater
DescriptionUse after free in Updater
ComponentUpdater
Bug ClassUAF
Tracker513371963
Fix commitf9b4d807241c (chromium/src) +13/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/updater/net/network_fetcher_mac.mm
modified

Files Changed

  • chrome/updater/net/network_fetcher_mac.mm
From f9b4d807241ceb0fc7ca72b18c73b68f572746a6 Mon Sep 17 00:00:00 2001
From: Noah Rose Ledesma <noahrose@google.com>
Date: Mon, 18 May 2026 19:01:15 -0700
Subject: [PATCH] Ref-count response_code for event logging

The HTTP response code arrives in a different callback from the HTTP
body in network_fetcher_mac.mm. To aggregate these values for event
logging, the response code was stored in a unique pointer bound to the
completion callback with a raw pointer reference written to by the
initial callback.

It is hard to reason about whether a UAF exists in this code; it is
preferable to defend against one by sharing a ref-counted pointer
between the callbacks.

Fixed: 513371963
Change-Id: I02f0769d15d09c318395d1b244b6f9386a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853437
Commit-Queue: Noah Rose Ledesma <noahrose@google.com>
Reviewed-by: Sorin Jianu <sorin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632564}
---

diff --git a/chrome/updater/net/network_fetcher_mac.mm b/chrome/updater/net/network_fetcher_mac.mm
index 9f501b2..5bf3b22 100644
--- a/chrome/updater/net/network_fetcher_mac.mm
+++ b/chrome/updater/net/network_fetcher_mac.mm
@@ -22,6 +22,7 @@
 #include "base/functional/callback.h"
 #include "base/functional/callback_helpers.h"
 #include "base/logging.h"
+#include "base/memory/ref_counted.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/path_service.h"
 #include "base/process/launch.h"
@@ -417,18 +418,21 @@
                           std::move(post_request_complete_callback));
   }
 
-  std::unique_ptr<int> response_code = std::make_unique<int>(0);
+  scoped_refptr<base::RefCountedData<int>> response_code =
+      base::MakeRefCounted<base::RefCountedData<int>>(0);
   return std::make_pair(
       base::BindRepeating(
-          [](int* out_response_code, ResponseStartedCallback callback,
-             int response_code, int64_t content_length) {
-            *out_response_code = response_code;
+          [](scoped_refptr<base::RefCountedData<int>> out_response_code,
+             ResponseStartedCallback callback, int response_code,
+             int64_t content_length) {
+            out_response_code->data = response_code;
             callback.Run(response_code, content_length);
           },
-          response_code.get(), response_started_callback),
+          response_code, response_started_callback),
       base::BindOnce(
           [](scoped_refptr<UpdaterEventLogger> event_logger,
-             base::Time request_start_time, std::unique_ptr<int> response_code,
+             base::Time request_start_time,
+             scoped_refptr<base::RefCountedData<int>> response_code,
              const GURL& url, PostRequestCompleteCallback callback,
              std::optional<std::string> response_body, int net_error,
              const std::string& header_etag,
@@ -443,8 +447,8 @@
                 (base::Time::Now() - request_start_time).InMilliseconds());
             if (net_error > 0) {
               event.set_error_code(net_error);
-            } else if (*response_code < 200 && *response_code > 299) {
-              event.set_error_code(*response_code);
+            } else if (response_code->data < 200 && response_code->data > 299) {
+              event.set_error_code(response_code->data);
             }
             proto::Omaha4Metric metric;
             *metric.mutable_network_event() = std::move(event);
@@ -453,7 +457,7 @@
                                     header_x_cup_server_proof,
                                     header_set_cookie, xheader_retry_after_sec);
           },
-          event_logger, base::Time::Now(), std::move(response_code), url,
+          event_logger, base::Time::Now(), response_code, url,
           std::move(post_request_complete_callback)));
 }
 
Loading diff…

Original Bug Report

reported by vm...@google.com

macOS Google Updater: Potential root LPE via Mojo heap-use-after-free write

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 heap use-after-free (UAF) write exists in the macOS Google Updater due to mismanaged object lifetimes between two Mojo-driven callbacks. A local attacker controlling the low-privileged child process can trigger this write in the root updater process by invoking Mojo methods out of order. This vulnerability could facilitate a privilege escalation from a standard user to root.

Affected files:

  • chrome/updater/net/network_fetcher_mac.mm
  • chrome/updater/net/fetcher_callback_adapter.cc

Estimated timestamp from git blame: 2025-06-18

Summary

A potential heap use-after-free (UAF) write vulnerability in the macOS Google Updater may allow a local attacker to escalate privileges from a standard user to root. The issue resides in the network fetcher logic where a heap-allocated integer’s lifetime is mismanaged across two callbacks exposed via Mojo to a child process. By invoking these callbacks out of order, a compromised child process could write an attacker-controlled 32-bit value to a freed heap location in the parent root process.

Root Cause Analysis

In chrome/updater/net/network_fetcher_mac.mm, the functions WrapDownloadToFileCallbacksWithEventLogging and WrapPostRequestCallbacksWithEventLogging create a pair of callbacks for network events.

Both functions follow a similar pattern (e.g., line 471):

  1. A heap-allocated integer is created: std::unique_ptr<int> response_code = std::make_unique<int>(0);.
  2. A RepeatingCallback (response_started_callback) is created, capturing a raw pointer to this integer: base::BindRepeating(..., response_code.get(), ...).
  3. A OnceCallback (download_to_file_complete_callback) is created, taking ownership of the integer: base::BindOnce(..., std::move(response_code), ...).

These callbacks are used by the FileDownloadObserverImpl or PostRequestObserverImpl (in chrome/updater/net/fetcher_callback_adapter.cc) to handle Mojo IPC messages from a child net-worker process. While the child process is spawned with dropped privileges (console user), the parent process runs as root.

Potential Attack Vector

An attacker who can control the net-worker child process (running as a standard user) can trigger the UAF write in the root process as follows:

  1. The attacker invokes the OnDownloadComplete (or OnRequestComplete) Mojo method on the observer interface.
  2. In the root process, the OnceCallback executes. When the callback’s bound state is subsequently destroyed, the unique_ptr<int> is deleted, freeing the heap memory.
  3. The attacker then invokes the OnResponseStarted Mojo method, providing a controlled 32-bit integer as the status code.
  4. The root process executes the RepeatingCallback, which dereferences its dangling raw pointer and writes the attacker’s value to the freed memory: *out_response_code = response_code;.

Because the RepeatingCallback can be invoked multiple times, an attacker could use this primitive to corrupt the heap and achieve arbitrary code execution as root. Furthermore, current analysis suggests that PartitionAlloc with MiraclePtr (BackupRefPtr) protection is not active for this specific binary on macOS, making the UAF write more likely to be exploitable.

Suggested Potential Trigger Steps

  1. On a macOS system with Google Updater installed, identify the scenario triggering an out-of-process network fetch (e.g., forcing an update check).
  2. Hijack or manipulate the environment of the net-worker child process spawned by the root updater.
  3. From the child process, use the Mojo handle for the observer interface to call OnDownloadComplete with arbitrary values.
  4. Immediately follow with a call to OnResponseStarted, providing the desired 32-bit value for the http_status_code argument.
  5. Monitor for memory corruption or a crash in the parent updater process.

Ensure that the shared state (response_code) remains valid as long as any callback holding a reference to it exists. This can be achieved by using a scoped_refptr to manage the lifetime of the integer or a small struct containing the integer, rather than a unique_ptr with raw pointer capture. Alternatively, the architecture of the observer implementations could be updated to enforce strict state transitions, preventing the execution of OnResponseStarted after completion.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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.

View on issue tracker