CVE-2026-19173
Overview
Background
- `skhdr::AdaptiveGlobalToneMap`
- the Skia HDR structure carrying adaptive global tone-mapping metadata, including a nested
fHeadroomAdaptiveToneMapwith a list offAlternateImages, each holding afColorGainFunction. - Mojom `StructTraits::Read`
- the deserialization hook in
hdr_metadata_mojom_traits.hthat reconstructs anskhdr::AdaptiveGlobalToneMapfrom 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, boundedfAlternateImagescount, 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()andresize(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.
isValid() at the end of Read and rejecting the message when the reconstructed object is not internally consistent.Attack Path
- Send crafted metadata over IPC
A compromised renderer (or other client) sends a Mojo message carrying a
skia::mojom::SkHdrAdaptiveGlobalToneMapwhose nested fields violate expected bounds. - Deserialize without validation
The unpatched
Readinhdr_metadata_mojom_traits.hreconstructs theskhdr::AdaptiveGlobalToneMapand returnstrue, accepting invalid state such as a cleared orresize(33)fControlPointsarray or an oversizedfAlternateImageslist. - 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. - Trigger the out-of-bounds write
Code indexing into the malformed
fControlPoints/fAlternateImagesarrays writes past their bounds, producing the OOB write in Skia.(inferred)— the specific sink, offset, and field are not shown in the diff. - 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
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
| Function | Change | Notes |
|---|---|---|
forskia/public/mojom/test/mojom_traits_unittest.cc |
modified |
Files Changed
skia/public/mojom/hdr_metadata_mojom_traits.hskia/public/mojom/test/mojom_traits_unittest.cc
Audit Directions
- Unvalidated deserializationReview other
StructTraits::Readimplementations that reconstruct nested container-bearing types andreturn truewithout a finalisValid()/bounds check. - Bound-carrying arrays across IPCAudit types like
fControlPointsandfAlternateImageswhose 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 fieldsLook 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.
Patch
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
Regression Test / PoC
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
Original Bug Report
[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
- Check out Chromium revision
6e23d2ad7440702de4442bc8e8627e95b2aa3285. - Apply
renderer_simulation.patchand buildout/asan/chrome. - Place
poc.htmlbesidecosmos1650_yuv444_10bpc_p3pq.avif. - 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
- 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:
poc.htmlcosmos1650_yuv444_10bpc_p3pq.avifrenderer_simulation.patchrepro.mdasan.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.