Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds write in Skia
DescriptionOut of bounds write in Skia
ComponentSkia
Bug ClassOOB
Tracker538332338
Fix commit36e3c8b0d681 (chromium/src) +31/-4
CISA KEVNot listed
CreditedVu Van Tien (@n0_Be3r)
Disclosed2026-08-06

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
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