Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Updater
DescriptionUse after free in Updater
ComponentUpdater
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker550181232
Fix commit83b0a752f1e7 (chromium/src) +6/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
Crediteda45hif
Reported2026-08-21 (per vendor advisory)
Fix landed2026-08-21 (public git)
Disclosed2026-09-22
Patch gap32 days — fix public in source before the advisory

Background

`DataPipeDrainer`
A helper that asynchronously reads bytes out of a Mojo data pipe and forwards them to a client via callbacks such as OnDataAvailable and OnDataComplete.
`drainer_`
The std::unique_ptr<DataPipeDrainer> member owned by the fetcher’s callback object that keeps the drainer alive while data is being pulled from the pipe.
`base::SequencedTaskRunner`
An interface for posting tasks that run one at a time on a specific sequence, used here to defer work until after the current callback has returned.
`base::DoNothingWithBoundArgs`
A helper that produces a no-op callback which merely owns the arguments bound into it, so those objects are destroyed only when the posted task runs.

Root Cause Analysis

The macOS network fetcher’s callback object receives completion and error notifications directly from its own DataPipeDrainer while that drainer is executing inside DataPipeDrainer::ReadData(). In both OnDataComplete() and ClosePipeAndSignalMojoError(), the original code called drainer_.reset(), which synchronously destroyed the DataPipeDrainer object whose stack frame in ReadData() was still active and about to touch its own members. This violated the invariant that an object must not be freed while one of its methods is still on the call stack, producing a use-after-free when control returned into DataPipeDrainer::ReadData().

The fix preserves the intent to release the drainer but defers the destruction by moving drainer_ into a base::DoNothingWithBoundArgs callback posted to base::SequencedTaskRunner::GetCurrentDefault(), so the object stays alive through the rest of the callback and ReadData() and is only destroyed later on the same sequence. Because the deferred task runs after the current call stack has fully unwound, no live DataPipeDrainer method frame can reference freed memory.

Key insight
The single mistake is destroying the DataPipeDrainer synchronously via drainer_.reset() from inside a callback that the drainer itself invoked while still running in DataPipeDrainer::ReadData(); the fix defers the destruction to a posted sequenced task so the drainer outlives the callback that frees it.

Attack Path

  1. Trigger a fetch The updater performs a network fetch on macOS, whose DataPipeDrainer begins draining response bytes from the Mojo data pipe inside DataPipeDrainer::ReadData().
  2. Drive a completion or error callback The drainer, still executing ReadData(), invokes the client’s OnDataComplete() on end-of-data, or a pipe error routes into ClosePipeAndSignalMojoError().
  3. Synchronous self-destruction The callback calls drainer_.reset(), freeing the very DataPipeDrainer instance whose ReadData() frame is still on the stack.
  4. Use-after-free on return Control unwinds back into DataPipeDrainer::ReadData(), which dereferences its now-freed members, causing the use-after-free.
  5. Exploitation via crafted response An attacker able to influence the fetched response stream or pipe closure timing shapes the freed-memory access into a potential memory-corruption primitive in the updater.

Impact Assessment

An attacker who can influence the update download stream gains a use-after-free inside the Chrome Updater’s macOS network fetcher, which may lead to memory corruption and potential code execution in the updater process. The updater is a privileged, security-sensitive component, so corruption there is high value. The precondition is triggering a network fetch whose completion or error path invokes the drainer’s callback while DataPipeDrainer::ReadData() is still executing.

Changed Functions

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

Files Changed

  • chrome/updater/net/network_fetcher_mac.mm

Audit Directions

  • Synchronous self-owned deletion in callbacks
    Search for unique_ptr::reset() or delete of a member object performed inside a callback that the same object invoked, where the object’s method may still be on the stack.
  • Client callbacks from streaming/draining helpers
    Review DataPipeDrainer and similar pipe/stream readers whose OnData* completion or error callbacks destroy the reader that is mid-ReadData() or equivalent loop.
  • Deferred-destruction consistency
    Audit other platform network fetchers and updater code for the same pattern to ensure destruction is deferred via base::SequencedTaskRunner posting rather than executed synchronously during a callback.
From 83b0a752f1e716896861d24dc0c9e76e8e1da2fb Mon Sep 17 00:00:00 2001
From: Noah Rose Ledesma <noahrose@google.com>
Date: Fri, 21 Aug 2026 09:45:10 -0700
Subject: [PATCH] Defer DataPipeDrainer destruction in fetcher

Destroying the DataPipeDrainer during its client's callback can lead to
a UAF. Defer its deletion by posting it to the sequence.

Fixed: 550181232
Change-Id: If98ea8ee09fb2ca13468fc13f1e301d26a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8276952
Reviewed-by: Sorin Jianu <sorin@chromium.org>
Commit-Queue: Noah Rose Ledesma <noahrose@google.com>
Cr-Commit-Position: refs/heads/main@{#1684008}
---
diff --git a/chrome/updater/net/network_fetcher_mac.mm b/chrome/updater/net/network_fetcher_mac.mm
index 26473718..cd2993c 100644
--- a/chrome/updater/net/network_fetcher_mac.mm
+++ b/chrome/updater/net/network_fetcher_mac.mm
@@ -596,14 +596,18 @@
 
   void OnDataComplete() override {
     DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-    drainer_.reset();
+    // Defer resetting the drainer to avoid UAF in DataPipeDrainer::ReadData().
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::DoNothingWithBoundArgs(std::move(drainer_)));
     MaybeSignalComplete();
   }
 
  private:
   void ClosePipeAndSignalMojoError() {
     DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-    drainer_.reset();
+    // Defer resetting the drainer to avoid UAF in DataPipeDrainer::ReadData().
+    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::DoNothingWithBoundArgs(std::move(drainer_)));
     file_.Close();
     if (completion_callback_) {
       base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
Loading diff…

Original Bug Report

reported by un...@gmail.com

Heap-use-after-free in macOS Updater fallback stream reader on destination write failure


Report description

Heap-use-after-free in macOS Updater fallback stream reader on destination write failure


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules


The problem

Please describe the technical details of the vulnerability

Summary

The macOS Chromium Updater synchronously destroys an active mojo::DataPipeDrainer from inside its OnDataAvailable() callback when a destination-file write fails. DataPipeDrainer::ReadData() then resumes after the callback and dereferences the freed drainer’s source_ member.

I reproduced this through independent manual runs and a clean packaged replay using the installed ASAN updater, its production out-of-process fallback network worker, a valid CRX download, and a disposable filesystem that reaches ENOSPC. The preserved run reports a heap-use-after- free in the installed updater parent.

Tested revision: 34a482b3cd98d6d599f43091babae366fb9c3d8e

Tested updater: 154.0.8012.0, macOS arm64 ASAN

Also affected: Stable 152.0.7977.54

Reproduction

From the root of a Chromium checkout on macOS arm64:

gn gen out/cand048-asan --args='is_asan=true is_debug=false dcheck_always_on=true symbol_level=1 blink_symbol_level=1 v8_symbol_level=2 use_remoteexec=false use_siso=false angle_enable_metal=false'
autoninja -C out/cand048-asan ChromiumUpdater_test.app ChromiumUpdaterUtil
../security-research/chromium-vrp-nday-audit-2026-08-21/reports/updater-stream-reader-drainer-uaf-522314940/poc/run_product_asan_poc.sh

The script:

  • creates and mounts a disposable 256 MiB HFS+ image;
  • installs the ASAN Chromium updater at user scope;
  • registers a benign test app through the installed ksadmin IPC path;
  • starts the installed update service with the image as its temp directory;
  • forces the production primary network fetches to fail so the updater launches its real --net-worker fallback;
  • fills the volume only after the production download directory exists; and
  • streams Chromium’s valid updater qualification CRX after the volume reaches zero free blocks.

Expected final output:

PASS: installed updater reproduced heap-use-after-free
client_exit=1 (IPC loss is expected after ASAN abort)

The cleanup trap restores any pre-existing override file, detaches and removes the disposable image, deletes the test registration, and uninstalls the test updater.

ASAN result

==47546==ERROR: AddressSanitizer: heap-use-after-free
READ of size 8 at 0x611000014688 thread T5
    #0 mojo::DataPipeDrainer::ReadData()

freed by thread T5 here:
    #1 DownloadStreamReader::ClosePipeAndSignalMojoError()
    #2 DownloadStreamReader::OnDataAvailable(...)
    #3 mojo::DataPipeDrainer::ReadData()

0x611000014688 is located 8 bytes inside of 248-byte region

The independent volume watcher records:

ENOSPC armed: ... remaining=0 write_errno=28

Offline symbolization of the binary offsets gives:

mojo::DataPipeDrainer::ReadData()
  mojo/public/cpp/system/data_pipe_drainer.cc:36
DownloadStreamReader::ClosePipeAndSignalMojoError()
  chrome/updater/net/network_fetcher_mac.mm:606
DownloadStreamReader::OnDataAvailable(...)
  chrome/updater/net/network_fetcher_mac.mm:589

Root cause

On write failure, OnDataAvailable() calls:

void ClosePipeAndSignalMojoError() {
  drainer_.reset();
  file_.Close();
  ...
}

But the executing drainer still has code after the client callback:

client_->OnDataAvailable(buffer);
source_->EndReadData(buffer.size());

The reset frees the current DataPipeDrainer; the following source_ access is the ASAN-reported read from freed memory.

Regression history

The bug was introduced by commit 3fe58100b42fb607e3c62a5cd010ce44d9fa22c1 (2026-06-23), which implemented streamed downloads from the out-of-process fetcher as the fix for issue 522314940. The vulnerable code remains in Stable 152.0.7977.54 and current main.

The original issue is CVE-2026-17654. This is a separate lifetime bug in the replacement design, although I expect high incomplete-fix/grouping risk.

Direct-upload attachments

I am uploading every attachment directly rather than as a ZIP. The “PoC” and “Logs” headings are categories only; none of the uploaded filenames has a folder prefix.

Direct-upload attachments

PoC

Filename SHA-256
run_product_asan_poc.sh 29034c63a1237d3c9a4f9a64cd55e8dba162eeff603da5b19c619383f2925b53
local_omaha_enospc_server.py 9fcee95feef8e8d4d0b0ed6f2b078c5c355afbaf55bdbef28ca51dbe3404d12d
enospc_vnode_watcher.c 7b1b00e169a77b0e7d596a8743119d4b58849b6a1f32a86bde21e483d011d517

Logs

Filename SHA-256
updater-product-asan-replay-20260821-132344-47365.47546 abfb686c4ff0c569aed5da580eed921f551626526c0f71c3c09f8b78f44ff917
product-symbolization.txt d9c813638b3a84da09f3cd8687ad5fc2fee1d8d42fb31d514fe114e7898cbde1
product-enospc-watcher-replay-20260821-132344-47365.log e2c9b8ae926a37ed47d2c10e155563b9f6c75522990a870cb31e4aa5a8459e81
product-server-replay-20260821-132344-47365.log 2ee4d5462a3bd1f28d4e212ffd74f1a8b93597b196cee179364bf9628b9202c8
product-updater-client-replay-20260821-132344-47365.log f80e6cbeeefffdd4d6949820de9406bb2386dc3141523232a166abfad6494667
product-updater-service-replay-20260821-132344-47365.log 078f2adac35ffef9a1f40ac1506e98feaf6c727db99079d79d13a55d457cbcf6

Impact analysis

Security impact and scope

This is demonstrated product memory corruption in a non-renderer process, not a unit-test-only lifetime pattern. The trigger requires a destination write failure; the PoC uses a bounded disposable volume and a valid update payload.

The preserved run is user scope. The same class is used by the system-scope updater parent, where the parent is privileged and the fallback network worker is unprivileged.


The cause

What version of Chrome have you found the security issue in?

152.0.7977.54

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption (in a non-sandboxed process)

How would you like to be publicly acknowledged for your report?

a45hif

View on issue tracker