CVE-2026-10993
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifrust/icc/FFI.cpp |
modified | |
forrust/icc/FFI.cpp |
modified | |
fortests/RustIccTest.cpp |
modified |
Files Changed
rust/icc/FFI.cpprust/icc/FFI.rssrc/codec/SkCodecColorProfileRust.cpptests/RustIccTest.cpp
Patch
From f4834c75d3ec17a87ab16a1340dc8e910f244072 Mon Sep 17 00:00:00 2001 From: Sergio Gonzalez Martin <[email protected]> Date: Wed, 22 Apr 2026 13:12:25 +0000 Subject: [PATCH] [rust icc] Reject unsupported A2B/B2A channel counts and grid dimensions Fixes: - Check ToSkcmsIccProfile return; return nullptr on failure. - Validate channel counts (1-4) in both FFI.rs and FFI.cpp. - Validate grid_points[i] >= 2 for every active CLUT dimension. - Fix Lut arm to populate grid_points for all active input channels. - Move FFI.cpp writes to output struct after validation (don't write input_channels=64 before returning false). Bug: 503958940 Bug: 504160794 Bug: 504103236 Change-Id: Ic034ec283807e665d50d6377a1757dee4451574b Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1215636 Reviewed-by: Florin Malita <[email protected]> Commit-Queue: Florin Malita <[email protected]> Reviewed-by: Kaylee Lubick <[email protected]> --- diff --git a/rust/icc/FFI.cpp b/rust/icc/FFI.cpp index 1b1de4d..935fdf2 100644 --- a/rust/icc/FFI.cpp +++ b/rust/icc/FFI.cpp @@ -65,11 +65,11 @@ memset(out_skcms, 0, sizeof(skcms_A2B)); // Input curves: If input_channels is non-zero, ensure we have enough curves + if (rust_a2b.input_channels > 4) { + return false; + } out_skcms->input_channels = rust_a2b.input_channels; if (rust_a2b.input_channels > 0) { - if (rust_a2b.input_channels < 1 || rust_a2b.input_channels > 4) { - return false; - } // Only validate curve count if input_channels is specified if (!rust_a2b.input_curves.empty() && rust_a2b.input_channels > rust_a2b.input_curves.size()) { return false; @@ -89,6 +89,13 @@ } memcpy(out_skcms->grid_points, rust_a2b.grid_points.data(), 4); if (!rust_a2b.grid_data.empty()) { + // Each active CLUT dimension must have >= 2 grid points, matching the + // constraint enforced by skcms_Parse (crbug.com/504103236). + for (uint32_t i = 0; i < out_skcms->input_channels; i++) { + if (out_skcms->grid_points[i] < 2) { + return false; + } + } if (rust_a2b.is_16bit_grid) { out_skcms->grid_16 = rust_a2b.grid_data.data(); } else { @@ -129,11 +136,11 @@ } // Output curves: If output_channels is non-zero, ensure we have enough curves + if (rust_a2b.output_channels > 4) { + return false; + } out_skcms->output_channels = rust_a2b.output_channels; if (rust_a2b.output_channels > 0) { - if (rust_a2b.output_channels > 4) { - return false; - } // Only validate curve count if output_channels is specified if (!rust_a2b.output_curves.empty() && rust_a2b.output_channels > rust_a2b.output_curves.size()) { return false; @@ -203,6 +210,12 @@ } memcpy(out_skcms->grid_points, rust_b2a.grid_points.data(), 4); if (!rust_b2a.grid_data.empty()) { + // Each active CLUT dimension must have >= 2 grid points (crbug.com/504103236). + for (uint32_t i = 0; i < rust_b2a.output_channels; i++) { + if (out_skcms->grid_points[i] < 2) { + return false; + } + } if (rust_b2a.is_16bit_grid) { out_skcms->grid_16 = rust_b2a.grid_data.data(); } else { diff --git a/rust/icc/FFI.rs b/rust/icc/FFI.rs index 0a1cc7a..46d4b14 100644 --- a/rust/icc/FFI.rs +++ b/rust/icc/FFI.rs @@ -535,6 +535,18 @@ match lut { LutWarehouse::Multidimensional(mdt) => { + // ICC.1:2022 §7.2.6 defines colour spaces up to 15 channels + // (nCLR), but skcms_A2B/B2A structs use fixed-size arrays of + // 4 elements for input_curves[] and grid_points[], so we can + // only represent device spaces with 1-4 channels (up to CMYK). + // Reject anything outside that range (crbug.com/504160794). + if mdt.num_input_channels > 4 + || mdt.num_output_channels == 0 + || mdt.num_output_channels > 4 + { + return None; + } + let input_curves: Vec<ffi::Curve> = mdt.a_curves.iter().filter_map(convert_to_curve).collect(); @@ -566,6 +578,17 @@ return None; } + // ICC.1:2022 §10.14/§10.15: each active CLUT dimension must + // have at least 2 grid points. A zero would cause skcms clut() + // to underflow when computing grid_points[i] - 1 (crbug.com/504103236). + if !grid_data.is_empty() { + for i in 0..mdt.num_input_channels.min(4) as usize { + if grid_points[i] < 2 { + return None; + } + } + } + // If there is no CLUT, input and output channels must match // and we set input_channels to 0 to signal "skip this stage" let (final_input_channels, final_input_curves) = if grid_data.is_empty() { @@ -598,6 +621,14 @@ // Legacy Lut8Type/Lut16Type (mft1/mft2 tags) // Similar structure to Multidimensional, but uses uniform grid size + // Same channel-count constraint as Multidimensional above. + if ldt.num_input_channels > 4 + || ldt.num_output_channels == 0 + || ldt.num_output_channels > 4 + { + return None; + } + let input_curves: Vec<ffi::Curve> = { let curve_data = lut_store_to_u16(&ldt.input_table); split_table_to_curves( @@ -610,11 +641,16 @@ let (grid_data, is_16bit_grid) = convert_grid_data(&ldt.clut_table); let grid_size = ldt.num_clut_grid_points; - let grid_points: [u8; 4] = match ldt.num_input_channels { - 3 => [grid_size, grid_size, grid_size, 0], - 4 => [grid_size, grid_size, grid_size, grid_size], - _ => [grid_size, 0, 0, 0], // 1D or 2D case - }; + let mut grid_points = [0u8; 4]; + for i in 0..ldt.num_input_channels.min(4) as usize { + grid_points[i] = grid_size; + } + + // Legacy lut8/lut16 types always have a CLUT. Each active + // dimension must have >= 2 grid points (crbug.com/504103236). + if grid_size < 2 { + return None; + } let mut matrix = matrix3d_to_ffi(&ldt.matrix); // Legacy LUT matrix is typically applied post-CLUT, so bias is zero diff --git a/src/codec/SkCodecColorProfileRust.cpp b/src/codec/SkCodecColorProfileRust.cpp index 66707db..7398595 100644 --- a/src/codec/SkCodecColorProfileRust.cpp +++ b/src/codec/SkCodecColorProfileRust.cpp @@ -26,7 +26,9 @@ new rust_icc::IccProfile(std::move(rust_profile))); skcms_ICCProfile profile; - rust_icc::ToSkcmsIccProfile(*retained, &profile); + if (!rust_icc::ToSkcmsIccProfile(*retained, &profile)) { + return nullptr; + } auto result = std::unique_ptr<ColorProfile>( new ColorProfile(profile, std::move(data))); result->fRetainedData = retained; diff --git a/tests/RustIccTest.cpp b/tests/RustIccTest.cpp index b68efe0..d74422d 100644 --- a/tests/RustIccTest.cpp +++ b/tests/RustIccTest.cpp @@ -13,6 +13,7 @@ #include "tests/Test.h" #include "tools/Resources.h" +#include <array> #include <cmath> #include <cstring> @@ -440,13 +441,15 @@ rust_profile.a2b.matrix.vals[2][1] = 0.1192f; rust_profile.a2b.matrix.vals[2][2] = 0.9505f; - // Set up minimal grid + // Set up minimal grid (2x2x2 = 8 points, 1 byte per output = 8 bytes) rust::Vec<uint8_t> grid_data; - grid_data.push_back(0x80); + for (int i = 0; i < 8; i++) { + grid_data.push_back(0x80); + }
Original Bug Report
Heap-buffer-overflow in skcms::select_curve_ops via a PNG iCCP mAB tag on the default Rust ICC path
Report description
Heap-buffer-overflow in skcms::select_curve_ops via a PNG iCCP mAB tag on the default Rust ICC path
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src/
The problem
Please describe the technical details of the vulnerability
Summary
A PNG iCCP chunk carrying an mAB (A-to-B multidimensional LUT) tag with input_channels=64 and output_channels=3 is accepted by the default Rust ICC parser because the channel check in moxcms::Reader::read_lut_abm_type (reader.rs) reads if in_channels > 4 && out_channels > 4 { return Ok(None); }, where the intent is ||.
The Rust FFI in FFI.rs forwards A2B.input_channels = 64 to C++. rust_icc::ToSkcmsA2B in FFI.cpp returns false because the value is out of range, but SkCodecs::MakeICCProfileWithRust in SkCodecColorProfileRust.cpp throws that return value away, so the incomplete skcms_A2B is installed anyway. When SkColorSpace::Make runs skcms_ApproximatelyEqualProfiles during PNG decode, it reaches add_curve_ops(A2B.input_curves, 64) in skcms.cc, and select_curve_ops iterates its loop 64 times. At the first iteration it reads curves[63].table_entries, which sits 2016 bytes from the start of the four-element input_curves array and 1176 bytes past the end of the 1000-byte ColorProfile heap allocation.
A plain <img src=evil.png> triggers this on dev/beta Chromium with no JavaScript, no user gesture, and no feature flag.
Choosing other values for input_channels shifts the read offset: each +1 moves it by 32 bytes (sizeof(skcms_Curve)). Setting input_channels=5 reaches curves[4].table_entries inside the skcms_A2B struct and also reaches kOps[4] inside select_curve_op, which AddressSanitizer reports as a global-buffer-overflow 4 bytes past a 96-byte static array.
Tested build
chromium/srcrevision736c900b2d459d998a4ea2da291d98fbfce75ca6
Steps to reproduce
- Place
poc.htmlandevil.pngin a directory and serve it locally on port 7200, e.g.python3 -m http.server 7200. - Launch the ASAN Chromium build against a fresh user-data directory:
The renderer aborts and AddressSanitizer prints a symbolizedASAN_OPTIONS=symbolize=1:external_symbolizer_path=<asan-dir>/llvm-symbolizer <asan-dir>/chrome --user-data-dir=/tmp/p1 --no-sandbox --headless=new --disable-gpu --virtual-time-budget=10000 http://127.0.0.1:7200/poc.htmlheap-buffer-overflowreport identifyingselect_curve_opsinskcms.ccas the site of the OOB read andSkCodecs::MakeICCProfileWithRustas the origin of the 1000-byte allocation.
ASAN stack trace
[37715:37715:0419/074644.093283:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.Properties.GetAll: object_path= /org/freedesktop/UPower/devices/DisplayDevice: org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.UPower was not provided by any .service files
=================================================================
==37825==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6e4cb3062e00 at pc 0x590cacc40e57 bp 0x7fff6648bb20 sp 0x7fff6648bb18
READ of size 4 at 0x6e4cb3062e00 thread T0 (chrome)
#0 0x590cacc40e56 in select_curve_ops(skcms_Curve const*, int, OpAndArg*) third_party/skia/modules/skcms/skcms.cc:2545:27
#1 0x590cacc3fbef in skcms_Transform::$_2::operator()(skcms_Curve const*, int) const third_party/skia/modules/skcms/skcms.cc:2758:22
#2 0x590cacc393d3 in skcms_Transform third_party/skia/modules/skcms/skcms.cc:2877:22
#3 0x590cacc37f4d in skcms_ApproximatelyEqualProfiles third_party/skia/modules/skcms/skcms.cc:1782:10
#4 0x590cac808b19 in SkColorSpace::Make(skcms_ICCProfile const&) third_party/skia/src/core/SkColorSpace.cpp:345:16
#5 0x590cc6187779 in SkEncodedInfo::makeImageInfo() const third_party/skia/src/codec/SkEncodedInfo.cpp:19:46
#6 0x590cd501653f in blink::SkiaImageDecoderBase::OnSetData(scoped_refptr<blink::SegmentReader>) third_party/skia/include/codec/SkCodec.h:233:55
#7 0x590cd4b7af09 in blink::ImageDecoder::SetData(scoped_refptr<blink::SegmentReader>, bool) third_party/blink/renderer/platform/image-decoders/image_decoder.h:286:5
#8 0x590cd4fd4a54 in blink::ImageDecoder::CreateByMimeType(blink::String, scoped_refptr<blink::SegmentReader>, bool, blink::ImageDecoder::AlphaOption, blink::ImageDecoder::HighBitDepthDecodingOption, blink::ColorBehavior, cc::AuxImage, unsigned long, SkISize const&, blink::ImageDecoder::AnimationOption) third_party/blink/renderer/platform/image-decoders/image_decoder.cc:353:14
#9 0x590cd4fd3787 in blink::ImageDecoder::Create(scoped_refptr<blink::SegmentReader>, bool, blink::ImageDecoder::AlphaOption, blink::ImageDecoder::HighBitDepthDecodingOption, blink::ColorBehavior, cc::AuxImage, unsigned long, SkISize const&, blink::ImageDecoder::AnimationOption) third_party/blink/renderer/platform/image-decoders/image_decoder.cc:290:10
#10 0x590cd4b73ee0 in blink::DeferredImageDecoder::Create(scoped_refptr<blink::SharedBuffer>, bool, blink::ImageDecoder::AlphaOption, blink::ColorBehavior) third_party/blink/renderer/platform/image-decoders/image_decoder.h:230:12
#11 0x590cd4a88cd0 in blink::BitmapImage::SetData(scoped_refptr<blink::SharedBuffer>, bool) third_party/blink/renderer/platform/graphics/bitmap_image.cc:240:14
#12 0x590cd3645930 in blink::ImageResourceContent::UpdateImage(scoped_refptr<blink::SharedBuffer>, blink::ResourceStatus, blink::ImageResourceContent::UpdateImageOption, bool, bool) third_party/blink/renderer/core/loader/resource/image_resource_content.cc:514:35
#13 0x590cd3636047 in blink::ImageResource::UpdateImage(scoped_refptr<blink::SharedBuffer>, blink::ImageResourceContent::UpdateImageOption, bool) third_party/blink/renderer/core/loader/resource/image_resource.cc:677:31
#14 0x590cd3636d8a in blink::ImageResource::AppendData(std::__Cr::variant<blink::SegmentedBuffer, base::span<char const, 18446744073709551615ul, char const*>>) third_party/blink/renderer/core/loader/resource/image_resource.cc:460:7
#15 0x590cbc81c4ee in blink::ResourceLoader::DidReceiveDataImpl(std::__Cr::variant<blink::SegmentedBuffer, base::span<char const, 18446744073709551615ul, char const*>>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:1067:14
#16 0x590cbc81ed15 in non-virtual thunk to blink::ResourceLoader::DidReceiveData(base::span<char const, 18446744073709551615ul, char const*>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:1041:3
#17 0x590cbc85928b in blink::ResponseBodyLoader::OnStateChange() third_party/blink/renderer/platform/loader/fetch/response_body_loader.cc:433:12
#18 0x590cbc80abda in blink::ResourceLoader::DidStartLoadingResponseBodyInternal(blink::BytesConsumer&) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:346:28
#19 0x590cbc81713b in blink::ResourceLoader::DidReceiveResponse(blink::WebURLResponse const&, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:787:3
#20 0x590cbc8859ae in blink::BackgroundURLLoader::Context::OnReceivedResponse(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int) third_party/blink/renderer/platform/loader/fetch/url_loader/background_url_loader.cc:492:14
#21 0x590cbc885efe in void base::internal::DecayedFunctorTraits<void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&>::Invoke<void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>, mojo::StructPtr<network::mojom::URLResponseHead>, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, std::__Cr::optional<mojo_base::BigBuffer>, int>(void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&, int&&) base/functional/bind_internal.h:740:12
#22 0x590cbc885c4a in base::internal::Invoker<base::internal::FunctorTraits<void (blink::BackgroundURLLoader::Context::*&&)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&>, base::internal::BindState<true, true, false, void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>, mojo::StructPtr<network::mojom::URLResponseHead>, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, std::__Cr::optional<mojo_base::BigBuffer>>, void (int)>::RunOnce(base::internal::BindStateBase*, int) base/functional/bind_internal.h:932:12
#23 0x590cb7faafd3 in base::internal::Invoker<base::internal::FunctorTraits<base::OnceCallback<void (int)>&&, int&&>, base::internal::BindState<false, true, true, base::OnceCallback<void (int)>, int>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/callback.h:155:12
#24 0x590cbc882fc7 in blink::BackgroundURLLoader::Context::RunTasksOnMainThread() base/functional/callback.h:155:12
#25 0x590cbc87b670 in base::internal::Invoker<base::internal::FunctorTraits<void (blink::BackgroundURLLoader::Context::*&&)(), scoped_refptr<blink::BackgroundURLLoader::Context>&&>, base::internal::BindState<true, true, false, void (blink::BackgroundURLLoader::Context::*)(), scoped_refptr<blink::BackgroundURLLoader::Context>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
#26 0x590cc3f837e3 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
#27 0x590cc3ff48ec in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:112:5
#28 0x590cc3ff378a in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:336:40
#29 0x590cc3e4147f in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
#30 0x590cc3ff5fd4 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:640:12
#31 0x590cc3efdb20 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
#32 0x590cd05a218a in content::RendererMain(content::MainFunctionParams) content/renderer/renderer_main.cc:337:16
#33 0x590cbff692ff in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:664:14
#34 0x590cbff6a637 in content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string<char, std::__Cr::char_traits<char>, std::__Cr::allocator<char>> const&, content::MainFunctionParams, content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:771:12
#35 0x590cbff6d348 in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1152:10
#36 0x590cbff66d01 in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:356:36
#37 0x590cbff672fc in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:369:10
#38 0x590cabbda2a9 in ChromeMain chrome/app/chrome_main.cc:194:12
#39 0x70bcb4a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#40 0x70bcb4a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#41 0x590cabaff029 in _start (/home/ubuntu/workspaces/chromium/asan-chrome/chrome+0x10fdf029) (BuildId: 250795ae951b3a3b)
0x6e4cb3062e00 is located 1176 bytes after 1000-byte region [0x6e4cb3062580,0x6e4cb3062968)
allocated by thread T0 (chrome) here:
#0 0x590cabbd859d in operator new(unsigned long) (/home/ubuntu/workspaces/chromium/asan-chrome/chrome+0x110b859d) (BuildId: 250795ae951b3a3b)
#1 0x590cacc1bfa0 in SkCodecs::MakeICCProfileWithRust(sk_sp<SkData const>) third_party/skia/src/codec/SkCodecColorProfileRust.cpp:31:25
#2 0x590cc6186afc in SkCodecs::ColorProfile::MakeICCProfile(sk_sp<SkData const>) third_party/skia/src/codec/SkCodecColorProfile.cpp:83:12
#3 0x590cc61a5a9c in SkPngRustCodec::MakeFromStream(std::__Cr::unique_ptr<SkStream, std::__Cr::default_delete<SkStream>>, SkCodec::Result*) third_party/skia/src/codec/SkPngRustCodec.cpp:168:17
#4 0x590cc61a5299 in SkPngRustDecoder::Decode(std::__Cr::unique_ptr<SkStream, std::__Cr::default_delete<SkStream>>, SkCodec::Result*, void*) third_party/skia/src/codec/SkPngRustDecoder.cpp:34:12
#5 0x590cd501646f in blink::SkiaImageDecoderBase::OnSetData(scoped_refptr<blink::SegmentReader>) third_party/blink/renderer/platform/image-decoders/skia/skia_image_decoder_base.cc:86:14
#6 0x590cd4b7af09 in blink::ImageDecoder::SetData(scoped_refptr<blink::SegmentReader>, bool) third_party/blink/renderer/platform/image-decoders/image_decoder.h:286:5
#7 0x590cd4fd4a54 in blink::ImageDecoder::CreateByMimeType(blink::String, scoped_refptr<blink::SegmentReader>, bool, blink::ImageDecoder::AlphaOption, blink::ImageDecoder::HighBitDepthDecodingOption, blink::ColorBehavior, cc::AuxImage, unsigned long, SkISize const&, blink::ImageDecoder::AnimationOption) third_party/blink/renderer/platform/image-decoders/image_decoder.cc:353:14
#8 0x590cd4fd3787 in blink::ImageDecoder::Create(scoped_refptr<blink::SegmentReader>, bool, blink::ImageDecoder::AlphaOption, blink::ImageDecoder::HighBitDepthDecodingOption, blink::ColorBehavior, cc::AuxImage, unsigned long, SkISize const&, blink::ImageDecoder::AnimationOption) third_party/blink/renderer/platform/image-decoders/image_decoder.cc:290:10
#9 0x590cd4b73ee0 in blink::DeferredImageDecoder::Create(scoped_refptr<blink::SharedBuffer>, bool, blink::ImageDecoder::AlphaOption, blink::ColorBehavior) third_party/blink/renderer/platform/image-decoders/image_decoder.h:230:12
#10 0x590cd4a88cd0 in blink::BitmapImage::SetData(scoped_refptr<blink::SharedBuffer>, bool) third_party/blink/renderer/platform/graphics/bitmap_image.cc:240:14
#11 0x590cd3645930 in blink::ImageResourceContent::UpdateImage(scoped_refptr<blink::SharedBuffer>, blink::ResourceStatus, blink::ImageResourceContent::UpdateImageOption, bool, bool) third_party/blink/renderer/core/loader/resource/image_resource_content.cc:514:35
#12 0x590cd3636047 in blink::ImageResource::UpdateImage(scoped_refptr<blink::SharedBuffer>, blink::ImageResourceContent::UpdateImageOption, bool) third_party/blink/renderer/core/loader/resource/image_resource.cc:677:31
#13 0x590cd3636d8a in blink::ImageResource::AppendData(std::__Cr::variant<blink::SegmentedBuffer, base::span<char const, 18446744073709551615ul, char const*>>) third_party/blink/renderer/core/loader/resource/image_resource.cc:460:7
#14 0x590cbc81c4ee in blink::ResourceLoader::DidReceiveDataImpl(std::__Cr::variant<blink::SegmentedBuffer, base::span<char const, 18446744073709551615ul, char const*>>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:1067:14
#15 0x590cbc81ed15 in non-virtual thunk to blink::ResourceLoader::DidReceiveData(base::span<char const, 18446744073709551615ul, char const*>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:1041:3
#16 0x590cbc85928b in blink::ResponseBodyLoader::OnStateChange() third_party/blink/renderer/platform/loader/fetch/response_body_loader.cc:433:12
#17 0x590cbc80abda in blink::ResourceLoader::DidStartLoadingResponseBodyInternal(blink::BytesConsumer&) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:346:28
#18 0x590cbc81713b in blink::ResourceLoader::DidReceiveResponse(blink::WebURLResponse const&, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>) third_party/blink/renderer/platform/loader/fetch/resource_loader.cc:787:3
#19 0x590cbc8859ae in blink::BackgroundURLLoader::Context::OnReceivedResponse(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int) third_party/blink/renderer/platform/loader/fetch/url_loader/background_url_loader.cc:492:14
#20 0x590cbc885efe in void base::internal::DecayedFunctorTraits<void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&>::Invoke<void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>, mojo::StructPtr<network::mojom::URLResponseHead>, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, std::__Cr::optional<mojo_base::BigBuffer>, int>(void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&, int&&) base/functional/bind_internal.h:740:12
#21 0x590cbc885c4a in base::internal::Invoker<base::internal::FunctorTraits<void (blink::BackgroundURLLoader::Context::*&&)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>&&, mojo::StructPtr<network::mojom::URLResponseHead>&&, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>&&, std::__Cr::optional<mojo_base::BigBuffer>&&>, base::internal::BindState<true, true, false, void (blink::BackgroundURLLoader::Context::*)(mojo::StructPtr<network::mojom::URLResponseHead>, std::__Cr::variant<mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, blink::SegmentedBuffer>, std::__Cr::optional<mojo_base::BigBuffer>, int), scoped_refptr<blink::BackgroundURLLoader::Context>, mojo::StructPtr<network::mojom::URLResponseHead>, mojo::ScopedHandleBase<mojo::DataPipeConsumerHandle>, std::__Cr::optional<mojo_base::BigBuffer>>, void (int)>::RunOnce(base::internal::BindStateBase*, int) base/functional/bind_internal.h:932:12
#22 0x590cb7faafd3 in base::internal::Invoker<base::internal::FunctorTraits<base::OnceCallback<void (int)>&&, int&&>, base::internal::BindState<false, true, true, base::OnceCallback<void (int)>, int>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/callback.h:155:12
#23 0x590cbc882fc7 in blink::BackgroundURLLoader::Context::RunTasksOnMainThread() base/functional/callback.h:155:12
#24 0x590cbc87b670 in base::internal::Invoker<base::internal::FunctorTraits<void (blink::BackgroundURLLoader::Context::*&&)(), scoped_refptr<blink::BackgroundURLLoader::Context>&&>, base::internal::BindState<true, true, false, void (blink::BackgroundURLLoader::Context::*)(), scoped_refptr<blink::BackgroundURLLoader::Context>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
#25 0x590cc3f837e3 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
#26 0x590cc3ff48ec in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:112:5
#27 0x590cc3ff378a in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:336:40
#28 0x590cc3e4147f in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
#29 0x590cc3ff5fd4 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:640:12
SUMMARY: AddressSanitizer: heap-buffer-overflow third_party/skia/modules/skcms/skcms.cc:2545:27 in select_curve_ops(skcms_Curve const*, int, OpAndArg*)
Shadow bytes around the buggy address:
0x6e4cb3062b80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062c00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x6e4cb3062e00:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062e80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3062f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3063000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x6e4cb3063080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==37825==ADDITIONAL INFO
==37825==Note: Please include this section with the ASan report.
Task trace:
#0 0x590cbc8820f8 in blink::BackgroundURLLoader::Context::PostTaskToMainThread(blink::CrossThreadOnceFunction<void ()>) third_party/blink/renderer/platform/loader/fetch/url_loader/background_url_loader.cc:422:52
#1 0x590cc4c8e1d3 in mojo::SimpleWatcher::Context::Notify(unsigned int, MojoHandleSignalsState, unsigned int) mojo/public/cpp/system/simple_watcher.cc:103:13
Command line: `/proc/self/exe --type=renderer --crashpad-handler-pid=37718 --enable-crash-reporter=, --noerrdialogs --user-data-dir=/tmp/p1 --change-stack-guard-on-fork=enable --no-sandbox --file-url-path-alias=/gen=/home/ubuntu/workspaces/chromium/asan-chrome/gen --ozone-platform=headless --disable-gpu-compositing --lang=en-US --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=5 --time-ticks-at-unix-epoch=-1776554627983056 --launch-time-ticks=4975995880 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,18058394970892133764,3878336514317533622,262144 --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,7079416843415264851,3999492147164696732,4 --trace-process-track-uuid=3190708990997080739`
==37825==END OF ADDITIONAL INFO
==37825==ABORTING
Root cause
Four defects on one reachable path; fixing any one closes the bug.
-
The channel check in
moxcms::Reader::read_lut_abm_type(reader.rs) usesif in_channels > 4 && out_channels > 4 { return Ok(None); }. The operator should be||. For(in=64, out=3)the expression istrue && false, so the check passes. -
The
LutWarehouse::Multidimensionalarm in FFI.rs copiesmdt.num_input_channelsintoA2B.input_channelsand truncatesmdt.grid_points[..4]into a fixed four-element array without rejecting out-of-range channel counts. -
rust_icc::ToSkcmsA2Bin FFI.cpp does rejectinput_channels > 4and returnsfalse, but it writes the partially-filledskcms_A2Bstruct first. Only four of the 64 declaredinput_curvesslots end up populated. -
SkCodecs::MakeICCProfileWithRustin SkCodecColorProfileRust.cpp ignores theboolreturned byrust_icc::ToSkcmsIccProfile, so the half-populated profile flows through and gets installed on theSkColorSpace.
With the profile installed, SkColorSpace::Make runs skcms_ApproximatelyEqualProfiles, which runs skcms_Transform over 84 probe pixels. That path calls add_curve_ops(A2B.input_curves, A2B.input_channels) with the attacker-controlled channel count. add_curve_ops in skcms.cc declares a four-element stack buffer OpAndArg oa[4], asserts numChannels <= 4 (the assertion is removed in release builds), and calls select_curve_ops(curves, 64, oa). The loop reads curves[index].table_entries for index from 63 down to 0. The first read is 63 slots past &A2B::input_curves[0], which is past the end of the 1000-byte ColorProfile allocation that the renderer made in SkCodecs::MakeICCProfileWithRust.
Related report
A separate report of mine (https://issues.chromium.org/issues/504103236) targets a different validator (read_lut_a_to_b_type, handling the legacy mft1/mft2 tags) and a different sink (clut() via sample_clut_16). A patch to that validator does not close this report because mAB/mBA tags take a different parser path (read_lut_abm_type). Defects 3 and 4 above are common to both reports, so a patch on either of those two defects would close both as a side effect.
Suggested fix
In moxcms::Reader::read_lut_a_to_b_type and read_lut_abm_type, replace the current channel checks with a plain range test, for example:
if !(1..=4).contains(&in_channels) || !(1..=4).contains(&out_channels) {
return Err(CmsError::InvalidProfile);
}
Separately, have SkCodecs::MakeICCProfileWithRust check the return of rust_icc::ToSkcmsIccProfile and fail the decode when it returns false. The upstream moxcms repository is https://github.com/awxkee/moxcms.
Attachments
poc.htmlone-line HTML that loadsevil.png.evil.png620-byte PNG with the craftediCCPmAB tag (input_channels=64,output_channels=3).make_icc_poc.pydeterministic generator.python3 make_icc_poc.py Nemits the variant forinput_channels=N(default 64;N=5reproduces thekOps[4]global OOB).
Impact analysis
Security impact
Any origin serving <img src=evil.png> reaches this path during Blink’s image decode. The same decode flow is used by <img>, <picture>, CSS background-image, SVG url(), createImageBitmap, canvas drawImage, and worker ImageDecoder. There is no CORS prompt, no user gesture, and no feature flag to enable.
The Rust ICC parser is the dev/beta default: SK_CODEC_COLOR_PROFILE_PARSE_WITH_RUST is listed in the defines array of config("skia_config") in skia/BUILD.gn, and blink::features::kForceSkcmsICCParsing is declared FEATURE_DISABLED_BY_DEFAULT in features.cc.
The input_channels byte of the mAB tag controls the OOB distance, so the attacker can aim the 4-byte read at different offsets past the ColorProfile allocation by varying that single byte.
The cause
What version of Chrome have you found the security issue in?
Chromium 149.0.7795.0 Dev ASAN
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
M. Fauzan Wijaya (Gh05t666nero)
- http://127.0.0.1:7200/poc.html
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src/
- https://chromium.googlesource.com/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6
- https://github.com/awxkee/moxcms
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:skia/BUILD.gn
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/blink/common/features.cc
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/rust/chromium_crates_io/vendor/moxcms-v0_8/src/reader.rs
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/skia/modules/skcms/skcms.cc
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/skia/rust/icc/FFI.cpp
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/skia/rust/icc/FFI.rs
- https://source.chromium.org/chromium/chromium/src/+/736c900b2d459d998a4ea2da291d98fbfce75ca6:third_party/skia/src/codec/SkCodecColorProfileRust.cpp