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

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in Skia
DescriptionOut of bounds write in Skia
ComponentSkia
Bug ClassOOB
Action tierwatch — corruption primitive but gated (local) — 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.
Tracker538332338
Fix commit36e3c8b0d681 (chromium/src) +31/-4
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedVu Van Tien (@n0_Be3r)
Reported2026-07-24 (per vendor advisory)
Fix landed2026-07-27 (public git)
Disclosed2026-08-06
Patch gap10 days — fix public in source before the advisory

Background

`skhdr::AdaptiveGlobalToneMap`
the Skia HDR structure carrying adaptive global tone-mapping metadata, including a nested fHeadroomAdaptiveToneMap with a list of fAlternateImages, each holding a fColorGainFunction.
Mojom `StructTraits::Read`
the deserialization hook in hdr_metadata_mojom_traits.h that reconstructs an skhdr::AdaptiveGlobalToneMap from bytes received over a Mojo IPC message.
`isValid()`
a member predicate on the metadata object that checks structural invariants (e.g., non-empty and bounded fControlPoints, bounded fAlternateImages count, in-range field values).
`fGainCurve.fControlPoints`
the array of control points describing the gain curve, whose size is expected to stay within a fixed bound (the test exercises clear() and resize(33) as invalid extremes).

Root Cause Analysis

The Read method in skia/public/mojom/hdr_metadata_mojom_traits.h deserialized every field of an incoming skhdr::AdaptiveGlobalToneMap — down through ReadHeadroomAdaptiveToneMap and its nested fAlternateImages and fColorGainFunction.fGainCurve.fControlPoints — and returned true without ever checking that the reconstructed object satisfied its own invariants. A compromised or malicious client could therefore hand the browser process metadata that violated the size and range constraints Skia’s tone-mapping code assumes, such as an empty or over-length fControlPoints array or an over-length fAlternateImages list, as the new unit tests demonstrate. Because those bounds were assumed but never enforced at the trust boundary, downstream code that indexes into these arrays would operate on out-of-bounds indices, matching the advisory’s “out of bounds write in Skia.”

The fix inserts an if (!out->isValid()) { return false; } check at the end of Read, so any deserialized metadata that breaks an invariant now causes deserialization to fail and the IPC to be rejected. The accompanying test change (lowering fMin and fComponent from 0.5f/0.6f to 0.0f) confirms the previous test fixture itself carried values that isValid() now considers invalid.

Key insight
The core mistake was trusting IPC-supplied HDR metadata across the process boundary without validating structural invariants after deserialization; the fix closes this by calling isValid() at the end of Read and rejecting the message when the reconstructed object is not internally consistent.

Attack Path

  1. Send crafted metadata over IPC A compromised renderer (or other client) sends a Mojo message carrying a skia::mojom::SkHdrAdaptiveGlobalToneMap whose nested fields violate expected bounds.
  2. Deserialize without validation The unpatched Read in hdr_metadata_mojom_traits.h reconstructs the skhdr::AdaptiveGlobalToneMap and returns true, accepting invalid state such as a cleared or resize(33) fControlPoints array or an oversized fAlternateImages list.
  3. Violate an assumed invariant The object reaches Skia tone-mapping code that assumes the array sizes and value ranges enforced only by isValid(), so those assumptions no longer hold.
  4. Trigger the out-of-bounds write Code indexing into the malformed fControlPoints/fAlternateImages arrays writes past their bounds, producing the OOB write in Skia. (inferred) — the specific sink, offset, and field are not shown in the diff.
  5. Escalate from memory corruption A controlled heap out-of-bounds write may be leveraged toward a read/write primitive or code execution in the affected process. (inferred) — no such primitive or escape is demonstrated by the patch.

Impact Assessment

An attacker who can send this metadata over IPC gains an out-of-bounds write in Skia within the process that deserializes the HDR metadata (the browser/GPU-side consumer of the IPC). The precondition is the ability to submit a crafted SkHdrAdaptiveGlobalToneMap IPC with invalid metadata, e.g., from a compromised renderer. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any escalation to a broader read/write primitive or sandbox escape is (inferred) and not established by the patch itself.

Changed Functions

FunctionChangeNotes
for
skia/public/mojom/test/mojom_traits_unittest.cc
modified

Files Changed

  • skia/public/mojom/hdr_metadata_mojom_traits.h
  • skia/public/mojom/test/mojom_traits_unittest.cc

Audit Directions

  • Unvalidated deserialization
    Review other StructTraits::Read implementations that reconstruct nested container-bearing types and return true without a final isValid()/bounds check.
  • Bound-carrying arrays across IPC
    Audit types like fControlPoints and fAlternateImages whose consumers assume a fixed maximum size, and verify that size is enforced at the trust boundary rather than only where the data is used.
  • Range assumptions on numeric fields
    Look for float/scalar metadata fields (e.g., fMin, fComponent) that downstream Skia code treats as in a valid range, and confirm each is checked after deserialization.
From 36e3c8b0d681b51b59b71238630c97d291639c95 Mon Sep 17 00:00:00 2001
From: Christopher Cameron <ccameron@chromium.org>
Date: Mon, 27 Jul 2026 15:53:34 -0700
Subject: [PATCH] skhdr::AdaptiveGlobalToneMap: Reject invalid metadata

Validate metadata after receiving it. Reject IPCs with invalid metadata.

Update a test that was specifying invalid metadata, add tests of various
types of invalid metadata.

Bug: 538332338
Change-Id: Icdada0edf15d51fce2fe22af88f55e00d1dba65d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8157200
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: ccameron chromium <ccameron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1669050}
---
diff --git a/skia/public/mojom/hdr_metadata_mojom_traits.h b/skia/public/mojom/hdr_metadata_mojom_traits.h
index eb4d4af..875ac1e 100644
--- a/skia/public/mojom/hdr_metadata_mojom_traits.h
+++ b/skia/public/mojom/hdr_metadata_mojom_traits.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef SKIA_PUBLIC_MOJOM_HDR_METADATA_MO_TRAITS_H_
-#define SKIA_PUBLIC_MOJOM_HDR_METADATA_MO_TRAITS_H_
+#ifndef SKIA_PUBLIC_MOJOM_HDR_METADATA_MOJOM_TRAITS_H_
+#define SKIA_PUBLIC_MOJOM_HDR_METADATA_MOJOM_TRAITS_H_
 
 #include <cmath>
 
@@ -265,6 +265,9 @@
     if (!data.ReadHeadroomAdaptiveToneMap(&out->fHeadroomAdaptiveToneMap)) {
       return false;
     }
+    if (!out->isValid()) {
+      return false;
+    }
     return true;
   }
 };
diff --git a/skia/public/mojom/test/mojom_traits_unittest.cc b/skia/public/mojom/test/mojom_traits_unittest.cc
index dc2af641..6fc5019 100644
--- a/skia/public/mojom/test/mojom_traits_unittest.cc
+++ b/skia/public/mojom/test/mojom_traits_unittest.cc
@@ -722,8 +722,8 @@
                           .fGreen = 0.2f,
                           .fBlue = 0.3f,
                           .fMax = 0.4f,
-                          .fMin = 0.5f,
-                          .fComponent = 0.6f,
+                          .fMin = 0.0f,
+                          .fComponent = 0.0f,
                       },
                   .fGainCurve =
                       {
@@ -872,6 +872,30 @@
     EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
                  skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
   }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    bad.fHeadroomAdaptiveToneMap->fAlternateImages[0]
+        .fColorGainFunction.fGainCurve.fControlPoints.clear();
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    bad.fHeadroomAdaptiveToneMap->fAlternateImages[0]
+        .fColorGainFunction.fGainCurve.fControlPoints.resize(33);
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    for (size_t i = 0; i < 4; ++i) {
+      auto alt = bad.fHeadroomAdaptiveToneMap->fAlternateImages.front();
+      alt.fHdrHeadroom += static_cast<float>(i);
+      bad.fHeadroomAdaptiveToneMap->fAlternateImages.push_back(alt);
+    }
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
 }
 
 }  // namespace
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/skia/public/mojom/test/mojom_traits_unittest.cc b/skia/public/mojom/test/mojom_traits_unittest.cc
index dc2af641..6fc5019 100644
--- a/skia/public/mojom/test/mojom_traits_unittest.cc
+++ b/skia/public/mojom/test/mojom_traits_unittest.cc
@@ -722,8 +722,8 @@
                           .fGreen = 0.2f,
                           .fBlue = 0.3f,
                           .fMax = 0.4f,
-                          .fMin = 0.5f,
-                          .fComponent = 0.6f,
+                          .fMin = 0.0f,
+                          .fComponent = 0.0f,
                       },
                   .fGainCurve =
                       {
@@ -872,6 +872,30 @@
     EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
                  skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
   }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    bad.fHeadroomAdaptiveToneMap->fAlternateImages[0]
+        .fColorGainFunction.fGainCurve.fControlPoints.clear();
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    bad.fHeadroomAdaptiveToneMap->fAlternateImages[0]
+        .fColorGainFunction.fGainCurve.fControlPoints.resize(33);
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
+  {
+    skhdr::AdaptiveGlobalToneMap bad = in;
+    for (size_t i = 0; i < 4; ++i) {
+      auto alt = bad.fHeadroomAdaptiveToneMap->fAlternateImages.front();
+      alt.fHdrHeadroom += static_cast<float>(i);
+      bad.fHeadroomAdaptiveToneMap->fAlternateImages.push_back(alt);
+    }
+    EXPECT_FALSE(mojo::test::SerializeAndDeserialize<
+                 skia::mojom::SkHdrAdaptiveGlobalToneMap>(bad, out));
+  }
 }
 
 }  // namespace
Loading diff…

Original Bug Report

reported by ti...@gmail.com

[GPU/Skia] AGTM structured metadata causes an OOB stack write in the GPU process

Brief description

The structured Mojo path accepts an arbitrary number of AGTM alternate_images, while Skia allocates arrays for at most four alternate images plus the baseline. A renderer-authored AGTM object with 64 entries reaches skhdr::AgtmHelpers::ComputeWeighting() during GPU OOP raster and writes beyond both fixed-size arrays.

This is a compromised-renderer simulation. The HTML page contains only a normal P3/PQ AVIF image; the attached renderer-only patch supplies the malformed structured metadata at the paint serialization boundary. I do not claim that unmodified web content can create this object, and I do not claim RCE, instruction-pointer control, or sandbox escape.

Reproduced result

On the attached full Chrome ASan build, the GPU process reached:

skhdr::AgtmHelpers::ComputeWeighting
  third_party/skia/src/codec/SkHdrAgtm.cpp:471:1
skhdr::AgtmHelpers::MakeColorFilter
  third_party/skia/src/codec/SkHdrAgtm.cpp:533:28
skhdr::Metadata::makeToneMapColorFilter
  third_party/skia/src/codec/SkHdrMetadata.cpp:255:12
cc::ToneMapUtil::AddGlobalToneMapFilterToPaint
  cc/paint/tone_map_util.cc:98:40
gpu::raster::RasterDecoderImpl::DoRasterCHROMIUM
  gpu/command_buffer/service/raster_decoder.cc:3067:22

The GPU process reported Received signal 4 ILL_ILLOPN and exited with exit_code=4. The complete raw and symbolized output is attached as asan.log. This ASan build emitted Chromium’s signal stack rather than a conventional ERROR: AddressSanitizer red-zone report; that is stated explicitly in the attachment.

Version and environment

  • Chrome build identifier: 152.0.7951.0
  • Chromium revision: 6e23d2ad7440702de4442bc8e8627e95b2aa3285
  • Skia revision: ca065587acac8432379ae196812dd3ec663129c2
  • OS: Ubuntu 24.04, Linux x86_64, kernel 7.0.0-28-generic
  • Build: ASan component build, is_debug=false, is_asan=true, is_component_build=true, dcheck_always_on=true, symbol_level=1

Reproduction

  1. Check out Chromium revision 6e23d2ad7440702de4442bc8e8627e95b2aa3285.
  2. Apply renderer_simulation.patch and build out/asan/chrome.
  3. Place poc.html beside cosmos1650_yuv444_10bpc_p3pq.avif.
  4. Run the full Chrome binary with a clean temporary profile:
ASAN_SYMBOLIZER_PATH=third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer \
ASAN_OPTIONS=detect_odr_violation=0:abort_on_error=1:symbolize=1 \
out/asan/chrome --user-data-dir=/tmp/agtm-poc-profile \
  --enable-gpu-rasterization file:///ABSOLUTE/PATH/poc.html
  1. Wait for the first image raster task. The GPU process should terminate as shown above. Save stdout/stderr as asan.log.

Before the attached capture, the host was configured for the developer build’s user namespace and verified with:

kernel.apparmor_restrict_unprivileged_userns = 0
unshare -Ur true -> exit 0

The attached asan.log was then produced with no --no-sandbox and no --disable-gpu-sandbox. The outer timeout returned 124 only because the browser remained alive after the GPU process exited; the GPU process itself reported exit_code=4.

No custom harness, CDP, browser test, or unit-test executable is required.

Root cause

skia/public/mojom/hdr_metadata.mojom:65 declares an unbounded alternate_images array. The traits read it without enforcing Skia’s kMaxNumAlternateImages = 4 limit. In third_party/skia/src/codec/SkHdrAgtm.cpp, lines 412 and 416 allocate H[5] and indices[5], while lines 417-429 iterate over the entire vector and write H[N++]/indices[N].

The renderer simulation is guarded by --type=renderer; it changes neither the GPU consumer nor the Skia code. The attached patch is not a proposed fix.

Attachments

Upload these as individual files:

  1. poc.html
  2. cosmos1650_yuv444_10bpc_p3pq.avif
  3. renderer_simulation.patch
  4. repro.md
  5. asan.log

The complete symbolized trace and all additional capture metadata are in asan.log, not only in this report.

Suggested remediation

Enforce the maximum in the structured Mojo boundary and validate AGTM before Skia consumes it. Add regression coverage for 5 and 64 alternate images in the Mojo traits and GPU OOP-raster path.

View on issue tracker