High watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Viz
DescriptionUse after free in Viz
ComponentViz
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.
Tracker497846428
Fix commit4002a66778d2 (chromium/src) +5/-3
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-30 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-04-15
Patch gap14 days — fix public in source before the advisory

Background

Viz / `CopyOutputResult`
Viz is Chrome’s compositing service; CopyOutputResult carries the pixels produced by a copy-output request, serialized across processes via Mojo.
Mojo `StructTraits` deserialization
The Read method in bitmap_in_shared_memory_mojom_traits.cc reconstructs an SkBitmap from a shared-memory region sent by a (possibly untrusted) sender.
`SkBitmap::installPixels` + release proc
Skia’s installPixels(info, pixels, rowBytes, releaseProc, context) adopts an external pixel buffer and, per Skia’s contract, always invokes releaseProc(pixels, context) to free it—even when installPixels itself returns false.
`std::unique_ptr::get()` vs `release()`
get() returns the raw pointer while the unique_ptr retains ownership (and will still free on destruction); release() returns the raw pointer and relinquishes ownership so the unique_ptr no longer frees it.

Root Cause Analysis

In the mojom Read/deserialization routine in services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc, the pre-patch code called sk_bitmap->installPixels(image_info, mapping_ptr->memory(), data.row_bytes(), &DeleteSharedMemoryMapping, mapping_ptr.get()) and only called mapping_ptr.release() after a successful return. The violated invariant is Skia’s documented guarantee (cited in the commit via SkBitmap.h) that installPixels always runs the supplied release proc, including on the failure path. On failure, installPixels therefore invoked DeleteSharedMemoryMapping(mapping_ptr.get()), freeing the shared-memory mapping, but because mapping_ptr still owned the mapping (the release() was skipped on the failure branch, which return falses), the std::unique_ptr destructor freed the same mapping a second time when it went out of scope—a double free.

The fix captures the pointer in bitmap_memory and passes mapping_ptr.release() as the release-proc context, transferring ownership to Skia unconditionally so that whether installPixels succeeds or fails, the mapping is freed exactly once by DeleteSharedMemoryMapping and never again by the unique_ptr. The early return false; after installPixels fails is unchanged in effect; only the ownership transfer moved from after the call to into the call’s argument.

Key insight
The core mistake was passing a still-owned pointer (mapping_ptr.get()) to installPixels and releasing ownership only on success, so the failure path—where Skia still runs the release proc—freed the mapping twice; the fix hands Skia mapping_ptr.release() up front so ownership transfers regardless of outcome.

Attack Path

  1. Send a crafted `CopyOutputResult` A compromised or malicious sender transmits a bitmap-in-shared-memory message to the process running this Viz deserialization code.
  2. Reach `installPixels` The Read routine maps the shared memory and calls SkBitmap::installPixels with mapping_ptr.get() as the release context while mapping_ptr still owns the mapping.
  3. Force the failure path The attacker supplies parameters that make installPixels return false (e.g. inconsistent image info/row bytes), so Skia invokes DeleteSharedMemoryMapping on the mapping and Read takes return false;.
  4. Double free The still-owning mapping_ptr is destroyed as the scope exits and frees the already-freed mapping a second time, corrupting allocator state (double free, as the commit title states).
  5. Exploitation Grooming the heap so the freed region is reallocated between the two frees could turn the double free into controlled corruption (inferred; not shown by the diff).

Impact Assessment

An attacker able to deliver a crafted serialized bitmap to this Viz deserialization path gains a double free of a shared-memory mapping in the process performing the deserialization, with the precondition of driving the message to the failure branch of installPixels. The vendor advisory rates this high severity and classifies it as a use after free in Viz; no exploited-in-the-wild status is stated. Escalation to memory corruption or code execution is (inferred) and not demonstrated by the diff, which shows only the double-free fix.

Files Changed

  • services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc

Audit Directions

  • Ownership vs. unconditional-callback APIs
    Audit calls to APIs that “always” invoke a release/free callback (Skia installPixels, similar adopt-buffer APIs); ensure the owning smart pointer is release()d before or into the call, not only on the success path.
  • Mojo `StructTraits::Read` resource handling
    Review other *_mojom_traits.cc deserializers that hand raw pointers from unique_ptr/ScopedHandle into third-party sinks, checking every early-return false path for leaked or double-freed resources.
  • Partial ownership transfer on error paths
    Look for the pattern of passing ptr.get() to a sink and calling ptr.release() only afterward; the gap between the two is where double-free/UAF arises when the sink takes ownership or fails.
From 4002a66778d2aef40fb7d72c580d7bbc10115ea8 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <vasilyt@chromium.org>
Date: Wed, 01 Apr 2026 11:19:52 -0700
Subject: [PATCH] Fix potential double free in deserializing CopyOutputResults

Skia always runs release proc of the SkBitmap::installPixels [1].

[1] https://source.chromium.org/chromium/chromium/src/+/main:third_party/skia/include/core/SkBitmap.h;drc=405f385dce2db578ff3b2301686d231ee8f0b042;l=586

Bug: 497846428
Change-Id: I4d3fd2e676fa7aa74e1022cbd2c9d9db8970a90c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718978
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Cr-Commit-Position: refs/heads/main@{#1608675}
---

diff --git a/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc b/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
index 59fbc85e..9d6bdd42 100644
--- a/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
+++ b/services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
@@ -111,12 +111,14 @@
     return false;
   }
 
-  if (!sk_bitmap->installPixels(image_info, mapping_ptr->memory(),
+  // Skia guarantees that it will call release proc, so we pass release()'ed
+  // pointer into it.
+  void* bitmap_memory = mapping_ptr->memory();
+  if (!sk_bitmap->installPixels(image_info, bitmap_memory,
                                 data.row_bytes(), &DeleteSharedMemoryMapping,
-                                mapping_ptr.get())) {
+                                mapping_ptr.release())) {
     return false;
   }
-  mapping_ptr.release();
   return true;
 }
 
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Double-Free in BitmapInSharedMemory Deserialization via SkBitmap::installPixels

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A potential double-free vulnerability exists in the browser process when deserializing viz::mojom::BitmapInSharedMemory from an untrusted process. If SkBitmap::installPixels fails, it synchronously invokes a release callback that frees a shared memory mapping, which is then freed a second time by a std::unique_ptr when the function returns early.

Affected files:

  • services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc

Estimated timestamp from git blame: 2020-04-29

Description

A double-free vulnerability exists in services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc within the StructTraits<viz::mojom::BitmapInSharedMemoryDataView, SkBitmap>::Read method. This method is used by the browser process to deserialize SkBitmap data stored in shared memory, typically received from a potentially compromised GPU or Renderer process via IPCs such as viz.mojom.CopyOutputResultSender.SendResult.

In the Read method, a base::WritableSharedMemoryMapping object is allocated on the heap and managed by a std::unique_ptr named mapping_ptr. This mapping is passed to sk_bitmap->installPixels along with a release callback, DeleteSharedMemoryMapping, and the raw pointer mapping_ptr.get() as the context:

// services/viz/public/cpp/compositing/bitmap_in_shared_memory_mojom_traits.cc
auto mapping_ptr =
    std::make_unique<base::WritableSharedMemoryMapping>(region_opt->Map());
// ...
if (!sk_bitmap->installPixels(image_info, mapping_ptr->memory(),
                              data.row_bytes(), &DeleteSharedMemoryMapping,
                              mapping_ptr.get())) {
  return false;
}
mapping_ptr.release();
return true;

The DeleteSharedMemoryMapping function is responsible for deleting the mapping object:

void DeleteSharedMemoryMapping(void* not_used, void* context) {
  delete static_cast<base::WritableSharedMemoryMapping*>(context);
}

If sk_bitmap->installPixels fails (e.g., due to an invalid row_bytes value that wasn’t caught by earlier checks), it internally invokes the provided releaseProc (DeleteSharedMemoryMapping), which performs the first delete of the mapping object. The Read method then returns false without calling mapping_ptr.release(). Consequently, when the mapping_ptr std::unique_ptr goes out of scope, its destructor attempts to delete the same pointer a second time, resulting in a synchronous double-free on the browser process heap.

Potential Attack Vector

(Note: These are suggested steps based on code analysis; our tooling agent does not yet have the ability to run code to verify an exploit.)

  1. An attacker compromises the GPU process.
  2. The browser process requests a screenshot (e.g., via tab thumbnail capture or DevTools) using a CopyOutputRequest.
  3. The compromised GPU process responds by sending a malicious viz::mojom::CopyOutputResult over IPC, containing a crafted BitmapInSharedMemory struct.
  4. The attacker crafts the BitmapInSharedMemory with:
    • image_info.width = 1
    • image_info.height = 1
    • image_info.colorType = kAlpha_8_SkColorType
    • row_bytes = 0x80000000 (2147483648)
    • pixels = a valid shared memory region of at least 1 byte.
  5. During deserialization in the browser process:
    • The check image_info.validRowBytes(0x80000000) passes because 0x80000000 >= minRowBytes (which is 1) and alignment is correct.
    • The check mapping_ptr->size() < image_info.computeByteSize(data.row_bytes()) passes because computeByteSize multiplies row_bytes by height - 1 (which is 0), resulting in a required size of 1 byte.
    • sk_bitmap->installPixels is called.
  6. Inside Skia’s SkBitmap::installPixels, it calls SkBitmap::setInfo. This function checks if row_bytes fits in a signed 32-bit integer: SkTFitsIn<int32_t>(rowBytes). Since 0x80000000 > INT32_MAX (0x7FFFFFFF), this check fails, causing setInfo to return false.
  7. SkBitmap::installPixels handles the failure by calling the releaseProc (DeleteSharedMemoryMapping), performing the first free of the WritableSharedMemoryMapping object.
  8. installPixels returns false to StructTraits::Read.
  9. StructTraits::Read takes the error path return false;. The std::unique_ptr mapping_ptr goes out of scope and its destructor performs the second free of the exact same object.
  10. This corrupts the PartitionAlloc freelist in the Browser process, providing a powerful primitive for arbitrary Remote Code Execution (RCE) and sandbox escape.

Suggested Fix

Release the std::unique_ptr before calling installPixels. Since installPixels guarantees that the releaseProc will be called even if it fails, ownership of the mapping should be transferred to Skia before the call.

  auto* mapping_raw = mapping_ptr.release();
  if (!sk_bitmap->installPixels(image_info, mapping_raw->memory(),
                                data.row_bytes(), &DeleteSharedMemoryMapping,
                                mapping_raw)) {
    return false;
  }
  return true;

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.

View on issue tracker