CVE-2026-112992
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
origin_clean_third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc |
modified | |
PLATFORM_EXPORTthird_party/blink/renderer/platform/graphics/image_pattern.h |
modified | |
FakeImageSourceWithOrientationthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc |
modified | |
ifthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc |
modified | |
MATCHER_P2third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc |
modified | |
TESTthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.ccthird_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-pattern.htmlthird_party/blink/renderer/platform/graphics/bitmap_image.ccthird_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.ccthird_party/blink/renderer/platform/graphics/image_pattern.hthird_party/blink/renderer/platform/graphics/pattern.hthird_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.ccthird_party/blink/renderer/platform/graphics/bitmap_image_test.ccthird_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.ccthird_party/blink/renderer/platform/graphics/image_pattern.ccthird_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.hthird_party/blink/renderer/platform/graphics/pattern.cc
Patch
From 2514f04ee0b98c1e4ad9b7da6cf0888123119a61 Mon Sep 17 00:00:00 2001 From: Andres Ricardo Perez <[email protected]> Date: Wed, 23 Sep 2026 09:24:39 -0700 Subject: [PATCH] [Canvas2D] Respect image orientation in createPattern() Ensure `Canvas2DRecorderContext::createPattern` respects image orientation when creating pattern fills. Previously, the resolved orientation setting was not forwarded to `CanvasPattern` and `ImagePattern`, which caused pattern shaders to render raw, unoriented tiles and led to inconsistencies with `drawImage`. Pass the resolved `RespectImageOrientationEnum` through `CanvasPattern` and `Pattern` into `ImagePattern`, reorienting the tile image when orientation must be respected and the source image has non-default orientation. Also preserve orientation metadata in multi-frame `BitmapImage::ImageForDefaultFrame()`. The WPT test matches its equivalent for drawImage(): https://crsrc.org/c/third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas.html Bug: 514422784 Change-Id: I4e4586efe50669331f2cf9308b79000dfb94376e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8399726 Reviewed-by: Jean-Philippe Gravel <[email protected]> Commit-Queue: Andres Ricardo Perez Rojas <[email protected]> Auto-Submit: Andres Ricardo Perez Rojas <[email protected]> Cr-Commit-Position: refs/heads/main@{#1703515} --- diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc index 1d6240bf..efd11d7 100644 --- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc +++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc @@ -2456,6 +2456,9 @@ return nullptr; } + RespectImageOrientationEnum respect_orientation = + RespectImageOrientationInternal(image_source); + SourceImageStatus status = kInvalidSourceImageStatus; gfx::SizeF default_object_size(Width(), Height()); @@ -2468,9 +2471,7 @@ case kZeroSizeCanvasSourceImageStatus: exception_state.ThrowDOMException( DOMExceptionCode::kInvalidStateError, - image_source - ->ElementSize(default_object_size, - RespectImageOrientationInternal(image_source)) + image_source->ElementSize(default_object_size, respect_orientation) .width() ? "The canvas height is 0." : "The canvas width is 0."); @@ -2503,7 +2504,8 @@ bool origin_clean = !WouldTaintCanvasOrigin(image_source); return MakeGarbageCollected<CanvasPattern>(std::move(image_for_rendering), - repeat_mode, origin_clean); + repeat_mode, origin_clean, + respect_orientation); } namespace { diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc index 76aaefcf..2498178 100644 --- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc +++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc @@ -25,13 +25,15 @@ #include "third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h" -#include "base/compiler_specific.h" +#include <utility> + #include "base/memory/scoped_refptr.h" #include "third_party/blink/renderer/core/geometry/dom_matrix_read_only.h" #include "third_party/blink/renderer/platform/bindings/exception_code.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/blink/renderer/platform/graphics/image.h" +#include "third_party/blink/renderer/platform/graphics/image_orientation.h" #include "third_party/blink/renderer/platform/graphics/pattern.h" #include "third_party/blink/renderer/platform/heap/visitor.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" @@ -65,8 +67,11 @@ CanvasPattern::CanvasPattern(scoped_refptr<Image> image, Pattern::RepeatMode repeat, - bool origin_clean) - : pattern_(Pattern::CreateImagePattern(image, repeat)), + bool origin_clean, + RespectImageOrientationEnum respect_orientation) + : pattern_(Pattern::CreateImagePattern(std::move(image), + repeat, + respect_orientation)), origin_clean_(origin_clean) {} void CanvasPattern::setTransform(DOMMatrix2DInit* transform, diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h index e809ac1a..58b36fc 100644 --- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h +++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h @@ -31,6 +31,7 @@ #include "base/memory/scoped_refptr.h" #include "third_party/blink/renderer/modules/modules_export.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h" +#include "third_party/blink/renderer/platform/graphics/image_orientation.h" #include "third_party/blink/renderer/platform/graphics/pattern.h" #include "third_party/blink/renderer/platform/transforms/affine_transform.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" @@ -48,7 +49,10 @@ static Pattern::RepeatMode ParseRepetitionType(const String&, ExceptionState&); - CanvasPattern(scoped_refptr<Image>, Pattern::RepeatMode, bool origin_clean); + CanvasPattern(scoped_refptr<Image>, + Pattern::RepeatMode, + bool origin_clean, + RespectImageOrientationEnum respect_orientation); Pattern* GetPattern() const { return pattern_.get(); } const AffineTransform& GetTransform() const { return pattern_transform_; } diff --git a/third_party/blink/renderer/platform/graphics/bitmap_image.cc b/third_party/blink/renderer/platform/graphics/bitmap_image.cc index 9bd02a9..26c9834 100644 --- a/third_party/blink/renderer/platform/graphics/bitmap_image.cc +++ b/third_party/blink/renderer/platform/graphics/bitmap_image.cc @@ -608,7 +608,7 @@ .set_repetition_count(kAnimationNone) .TakePaintImage(); } - return StaticBitmapImage::Create(std::move(paint_image)); + return StaticBitmapImage::Create(std::move(paint_image), Orientation()); } return Image::ImageForDefaultFrame(); diff --git a/third_party/blink/renderer/platform/graphics/image_pattern.cc b/third_party/blink/renderer/platform/graphics/image_pattern.cc index b6b8897..6f17b816 100644 --- a/third_party/blink/renderer/platform/graphics/image_pattern.cc +++ b/third_party/blink/renderer/platform/graphics/image_pattern.cc @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/platform/graphics/image_pattern.h" +#include "base/check.h" #include "base/memory/ptr_util.h" #include "third_party/blink/renderer/platform/graphics/image.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_shader.h" @@ -11,13 +12,28 @@ namespace blink { -std::unique_ptr<ImagePattern> ImagePattern::Create(scoped_refptr<Image> image, - RepeatMode repeat_mode) { - return base::WrapUnique(new ImagePattern(std::move(image), repeat_mode)); +std::unique_ptr<ImagePattern> ImagePattern::Create( + scoped_refptr<Image> image, + RepeatMode repeat_mode, + RespectImageOrientationEnum respect_orientation) { + return base::WrapUnique( + new ImagePattern(std::move(image), repeat_mode, respect_orientation)); } -ImagePattern::ImagePattern(scoped_refptr<Image> image, RepeatMode repeat_mode) - : Pattern(repeat_mode), tile_image_(image->PaintImageForCurrentFrame()) {} +ImagePattern::ImagePattern(scoped_refptr<Image> image, + RepeatMode repeat_mode, + RespectImageOrientationEnum respect_orientation) + : Pattern(repeat_mode) { + CHECK(image); + PaintImage paint_image = image->PaintImageForCurrentFrame(); + if (paint_image && respect_orientation == kRespectImageOrientation && + !image->HasDefaultOrientation()) { + tile_image_ = + Image::ResizeAndOrientImage(paint_image, image->Orientation()); + } else { + tile_image_ = std::move(paint_image); + } +} sk_sp<PaintShader> ImagePattern::CreateShader( const SkMatrix& local_matrix) const { diff --git a/third_party/blink/renderer/platform/graphics/image_pattern.h b/third_party/blink/renderer/platform/graphics/image_pattern.h index 004099c..e464b497 100644 --- a/third_party/blink/renderer/platform/graphics/image_pattern.h +++ b/third_party/blink/renderer/platform/graphics/image_pattern.h @@ -5,6 +5,7 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_IMAGE_PATTERN_H_ #define THIRD_PARTY_BLINK_RENDERER_PLATFORM_GRAPHICS_IMAGE_PATTERN_H_ +#include "third_party/blink/renderer/platform/graphics/image_orientation.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_image.h" #include "third_party/blink/renderer/platform/graphics/pattern.h" @@ -14,7 +15,10 @@ class PLATFORM_EXPORT ImagePattern final : public Pattern { public: - static std::unique_ptr<ImagePattern> Create(scoped_refptr<Image>, RepeatMode); + static std::unique_ptr<ImagePattern> Create( + scoped_refptr<Image>, + RepeatMode, + RespectImageOrientationEnum respect_orientation); bool IsTextureBacked() const override; @@ -22,7 +26,7 @@ sk_sp<PaintShader> CreateShader(const SkMatrix&) const override; private: - ImagePattern(scoped_refptr<Image>, RepeatMode); + ImagePattern(scoped_refptr<Image>, RepeatMode, RespectImageOrientationEnum); PaintImage tile_image_; }; diff --git a/third_party/blink/renderer/platform/graphics/pattern.cc b/third_party/blink/renderer/platform/graphics/pattern.cc index b7359c4..ab13d78 100644 --- a/third_party/blink/renderer/platform/graphics/pattern.cc +++ b/third_party/blink/renderer/platform/graphics/pattern.cc @@ -38,8 +38,10 @@ std::unique_ptr<Pattern> Pattern::CreateImagePattern( scoped_refptr<Image> tile_image, - RepeatMode repeat_mode) { - return ImagePattern::Create(std::move(tile_image), repeat_mode); + RepeatMode repeat_mode, + RespectImageOrientationEnum respect_orientation) { + return ImagePattern::Create(std::move(tile_image), repeat_mode, + respect_orientation); } std::unique_ptr<Pattern> Pattern::CreatePaintRecordPattern( diff --git a/third_party/blink/renderer/platform/graphics/pattern.h b/third_party/blink/renderer/platform/graphics/pattern.h index acb42c53..30894d45 100644 --- a/third_party/blink/renderer/platform/graphics/pattern.h +++ b/third_party/blink/renderer/platform/graphics/pattern.h @@ -32,6 +32,7 @@ #include <memory> #include "third_party/blink/renderer/platform/graphics/image.h" +#include "third_party/blink/renderer/platform/graphics/image_orientation.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_record.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_shader.h" #include "third_party/blink/renderer/platform/platform_export.h" @@ -53,7 +54,8 @@ static std::unique_ptr<Pattern> CreateImagePattern( scoped_refptr<Image>, - RepeatMode = kRepeatModeXY); + RepeatMode repeat_mode, + RespectImageOrientationEnum respect_orientation); static std::unique_ptr<Pattern> CreatePaintRecordPattern( PaintRecord, const gfx::RectF& record_bounds, diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc index 01d9627..269c6fb 100644 --- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc +++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc @@ -18,6 +18,7 @@ #include "cc/paint/paint_shader.h" #include "cc/paint/refcounted_buffer.h" #include "cc/test/paint_op_matchers.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_begin_layer_options.h" @@ -43,25 +44,33 @@ #include "third_party/blink/renderer/platform/geometry/length.h" #include "third_party/blink/renderer/platform/graphics/draw_looper_builder.h" #include "third_party/blink/renderer/platform/graphics/image_orientation.h" +#include "third_party/blink/renderer/platform/graphics/image_orientation_enum.h" #include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h" #include "third_party/blink/renderer/platform/graphics/paint/paint_filter.h" #include "third_party/blink/renderer/platform/graphics/pattern.h" +#include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/heap/member.h" #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/task_environment.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkBlendMode.h" #include "third_party/skia/include/core/SkClipOp.h" #include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkImageInfo.h" #include "third_party/skia/include/core/SkM44.h" #include "third_party/skia/include/core/SkMatrix.h" #include "third_party/skia/include/core/SkPath.h" #include "third_party/skia/include/core/SkPoint.h" #include "third_party/skia/include/core/SkRect.h" #include "third_party/skia/include/core/SkRefCnt.h" +#include "third_party/skia/include/core/SkSize.h" #include "third_party/skia/include/core/SkTileMode.h" #include "ui/gfx/geometry/rect.h" +#include "ui/gfx/geometry/size.h" +#include "ui/gfx/geometry/size_f.h" namespace blink { @@ -128,7 +137,10 @@ int Height() const override { return 300; } RespectImageOrientationEnum RespectImageOrientation() const override { - return kRespectImageOrientation; + return respect_image_orientation_; + } + void SetRespectImageOrientation(RespectImageOrientationEnum respect) { + respect_image_orientation_ = respect; } Color GetCurrentColor() const override { return Color::kBlack; } @@ -193,6 +205,8 @@ Member<ExecutionContext> execution_context_; bool restore_matrix_enabled_ = true; + RespectImageOrientationEnum respect_image_orientation_ = + kRespectImageOrientation; MemoryManagedPaintRecorder recorder_; Member<HTMLCanvasElement> host_canvas_element_; }; @@ -235,7 +249,8 @@ auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope); auto* pattern = MakeGarbageCollected<CanvasPattern>( - Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true); + Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true, + kRespectImageOrientation); context->setFillStyle(scope.GetIsolate(), pattern->ToV8(scope.GetScriptState()), @@ -252,6 +267,142 @@ PaintOpEq<DrawRectOp>(SkRect::MakeXYWH(1, 1, 5, 5), flags))); } +class FakeImageSourceWithOrientation : public CanvasImageSource { + public: + FakeImageSourceWithOrientation(gfx::Size size, + ImageOrientation orientation, + bool would_taint_origin) + : would_taint_origin_(would_taint_origin) { + SkBitmap bitmap; + bitmap.allocN32Pixels(size.width(), size.height()); + bitmap.eraseColor(SkColors::kRed); + sk_sp<SkImage> sk_image = SkImages::RasterFromBitmap(bitmap); + image_ = UnacceleratedStaticBitmapImage::Create(sk_image, orientation); + } + + scoped_refptr<Image> GetSourceImageForCanvas(SourceImageStatus* status, + const gfx::SizeF&) override { + if (status) { + *status = kNormalSourceImageStatus; + } + return image_; + } + + bool WouldTaintOrigin() const override { return would_taint_origin_; } + gfx::SizeF ElementSize( + const gfx::SizeF& default_object_size, + const RespectImageOrientationEnum respect_orientation) const override { + return gfx::SizeF(image_->Size(respect_orientation)); + } + bool IsOpaque() const override { return true; } + bool IsAccelerated() const override { return false; } + + ~FakeImageSourceWithOrientation() override = default; + + private: + const bool would_taint_origin_; + scoped_refptr<Image> image_; +}; + +MATCHER_P2(IsDrawRectWithImageShaderSize, width, height, "") { + if (arg.GetType() != cc::PaintOpType::kDrawRect) { + *result_listener << "PaintOp is not a DrawRectOp"; + return false; + } + const cc::PaintShader* shader = + static_cast<const cc::DrawRectOp&>(arg).flags.getShader(); + if (!shader) { + *result_listener << "DrawRectOp has no shader"; + return false; + } + if (shader->shader_type() != cc::PaintShader::Type::kImage) { + *result_listener << "DrawRectOp shader is not an image shader"; + return false; + } + SkISize dimensions = shader->paint_image().GetSkImageInfo().dimensions(); + if (!dimensions.equals(width, height)) { + *result_listener << "actual width == " << dimensions.width() + << ", actual height == " << dimensions.height(); + return false; + } + return true; +} + +// Tests pattern creation respecting image orientation. +TEST(Canvas2DRecorderContextCompositingTests, PatternImageOrientationRespect) { + test::TaskEnvironment task_environment; + V8TestingScope scope; + auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope); ... (file diff truncated)
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
index 01d9627..269c6fb 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
@@ -18,6 +18,7 @@
#include "cc/paint/paint_shader.h"
#include "cc/paint/refcounted_buffer.h"
#include "cc/test/paint_op_matchers.h"
+#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_begin_layer_options.h"
@@ -43,25 +44,33 @@
#include "third_party/blink/renderer/platform/geometry/length.h"
#include "third_party/blink/renderer/platform/graphics/draw_looper_builder.h"
#include "third_party/blink/renderer/platform/graphics/image_orientation.h"
+#include "third_party/blink/renderer/platform/graphics/image_orientation_enum.h"
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_filter.h"
#include "third_party/blink/renderer/platform/graphics/pattern.h"
+#include "third_party/blink/renderer/platform/graphics/unaccelerated_static_bitmap_image.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/member.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
+#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkClipOp.h"
#include "third_party/skia/include/core/SkColor.h"
+#include "third_party/skia/include/core/SkImage.h"
+#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkM44.h"
#include "third_party/skia/include/core/SkMatrix.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
+#include "third_party/skia/include/core/SkSize.h"
#include "third_party/skia/include/core/SkTileMode.h"
#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/gfx/geometry/size_f.h"
namespace blink {
@@ -128,7 +137,10 @@
int Height() const override { return 300; }
RespectImageOrientationEnum RespectImageOrientation() const override {
- return kRespectImageOrientation;
+ return respect_image_orientation_;
+ }
+ void SetRespectImageOrientation(RespectImageOrientationEnum respect) {
+ respect_image_orientation_ = respect;
}
Color GetCurrentColor() const override { return Color::kBlack; }
@@ -193,6 +205,8 @@
Member<ExecutionContext> execution_context_;
bool restore_matrix_enabled_ = true;
+ RespectImageOrientationEnum respect_image_orientation_ =
+ kRespectImageOrientation;
MemoryManagedPaintRecorder recorder_;
Member<HTMLCanvasElement> host_canvas_element_;
};
@@ -235,7 +249,8 @@
auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
auto* pattern = MakeGarbageCollected<CanvasPattern>(
- Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true);
+ Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true,
+ kRespectImageOrientation);
context->setFillStyle(scope.GetIsolate(),
pattern->ToV8(scope.GetScriptState()),
@@ -252,6 +267,142 @@
PaintOpEq<DrawRectOp>(SkRect::MakeXYWH(1, 1, 5, 5), flags)));
}
+class FakeImageSourceWithOrientation : public CanvasImageSource {
+ public:
+ FakeImageSourceWithOrientation(gfx::Size size,
+ ImageOrientation orientation,
+ bool would_taint_origin)
+ : would_taint_origin_(would_taint_origin) {
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(size.width(), size.height());
+ bitmap.eraseColor(SkColors::kRed);
+ sk_sp<SkImage> sk_image = SkImages::RasterFromBitmap(bitmap);
+ image_ = UnacceleratedStaticBitmapImage::Create(sk_image, orientation);
+ }
+
+ scoped_refptr<Image> GetSourceImageForCanvas(SourceImageStatus* status,
+ const gfx::SizeF&) override {
+ if (status) {
+ *status = kNormalSourceImageStatus;
+ }
+ return image_;
+ }
+
+ bool WouldTaintOrigin() const override { return would_taint_origin_; }
+ gfx::SizeF ElementSize(
+ const gfx::SizeF& default_object_size,
+ const RespectImageOrientationEnum respect_orientation) const override {
+ return gfx::SizeF(image_->Size(respect_orientation));
+ }
+ bool IsOpaque() const override { return true; }
+ bool IsAccelerated() const override { return false; }
+
+ ~FakeImageSourceWithOrientation() override = default;
+
+ private:
+ const bool would_taint_origin_;
+ scoped_refptr<Image> image_;
+};
+
+MATCHER_P2(IsDrawRectWithImageShaderSize, width, height, "") {
+ if (arg.GetType() != cc::PaintOpType::kDrawRect) {
+ *result_listener << "PaintOp is not a DrawRectOp";
+ return false;
+ }
+ const cc::PaintShader* shader =
+ static_cast<const cc::DrawRectOp&>(arg).flags.getShader();
+ if (!shader) {
+ *result_listener << "DrawRectOp has no shader";
+ return false;
+ }
+ if (shader->shader_type() != cc::PaintShader::Type::kImage) {
+ *result_listener << "DrawRectOp shader is not an image shader";
+ return false;
+ }
+ SkISize dimensions = shader->paint_image().GetSkImageInfo().dimensions();
+ if (!dimensions.equals(width, height)) {
+ *result_listener << "actual width == " << dimensions.width()
+ << ", actual height == " << dimensions.height();
+ return false;
+ }
+ return true;
+}
+
+// Tests pattern creation respecting image orientation.
+TEST(Canvas2DRecorderContextCompositingTests, PatternImageOrientationRespect) {
+ test::TaskEnvironment task_environment;
+ V8TestingScope scope;
+ auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
+ NonThrowableExceptionState exception_state;
+
+ context->SetRespectImageOrientation(kRespectImageOrientation);
+ FakeImageSourceWithOrientation image_source(
+ gfx::Size(20, 10), ImageOrientationEnum::kOriginRightTop,
+ /*would_taint_origin=*/false);
+ CanvasPattern* pattern =
+ context->createPattern(&image_source, "repeat", exception_state);
+ ASSERT_NE(pattern, nullptr);
+
+ context->setFillStyle(scope.GetIsolate(),
+ pattern->ToV8(scope.GetScriptState()),
+ scope.GetExceptionState());
+ context->fillRect(0, 0, 10, 10);
+
+ EXPECT_THAT(context->FlushRecorder(),
+ RecordedOpsAre(IsDrawRectWithImageShaderSize(10, 20)));
+}
+
+// Tests pattern creation ignoring image orientation when not respecting it on
+// same-origin.
+TEST(Canvas2DRecorderContextCompositingTests, PatternImageOrientationIgnore) {
+ test::TaskEnvironment task_environment;
+ V8TestingScope scope;
+ auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
+ NonThrowableExceptionState exception_state;
+
+ context->SetRespectImageOrientation(kDoNotRespectImageOrientation);
+ FakeImageSourceWithOrientation image_source(
+ gfx::Size(20, 10), ImageOrientationEnum::kOriginRightTop,
+ /*would_taint_origin=*/false);
+ CanvasPattern* pattern =
+ context->createPattern(&image_source, "repeat", exception_state);
+ ASSERT_NE(pattern, nullptr);
+
+ context->setFillStyle(scope.GetIsolate(),
+ pattern->ToV8(scope.GetScriptState()),
+ scope.GetExceptionState());
+ context->fillRect(0, 0, 10, 10);
+
+ EXPECT_THAT(context->FlushRecorder(),
+ RecordedOpsAre(IsDrawRectWithImageShaderSize(20, 10)));
+}
+
+// Tests pattern creation with a source that taints origin enforcing
+// orientation.
+TEST(Canvas2DRecorderContextCompositingTests,
+ PatternImageOrientationWithTaintedOrigin) {
+ test::TaskEnvironment task_environment;
+ V8TestingScope scope;
+ auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
+ NonThrowableExceptionState exception_state;
+
+ context->SetRespectImageOrientation(kDoNotRespectImageOrientation);
+ FakeImageSourceWithOrientation image_source(
+ gfx::Size(20, 10), ImageOrientationEnum::kOriginRightTop,
+ /*would_taint_origin=*/true);
+ CanvasPattern* pattern =
+ context->createPattern(&image_source, "repeat", exception_state);
+ ASSERT_NE(pattern, nullptr);
+
+ context->setFillStyle(scope.GetIsolate(),
+ pattern->ToV8(scope.GetScriptState()),
+ scope.GetExceptionState());
+ context->fillRect(0, 0, 10, 10);
+
+ EXPECT_THAT(context->FlushRecorder(),
+ RecordedOpsAre(IsDrawRectWithImageShaderSize(10, 20)));
+}
+
// Tests a plain drawImage.
TEST(Canvas2DRecorderContextCompositingTests, DrawImage) {
test::TaskEnvironment task_environment;
@@ -917,7 +1068,8 @@
auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
auto* pattern = MakeGarbageCollected<CanvasPattern>(
- Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true);
+ Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true,
+ kRespectImageOrientation);
context->setShadowBlur(2);
context->setShadowOffsetX(2);
@@ -979,7 +1131,8 @@
auto* context = MakeGarbageCollected<Test2DRecordingContext>(scope);
auto* pattern = MakeGarbageCollected<CanvasPattern>(
- Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true);
+ Image::NullImage(), Pattern::kRepeatModeXY, /*origin_clean=*/true,
+ kRespectImageOrientation);
context->setShadowBlur(2);
context->setShadowOffsetX(2);
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
index 8bd47323..9ae8bd58 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
@@ -35,6 +35,7 @@
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_image.h"
#include "cc/paint/paint_op.h"
+#include "cc/paint/paint_shader.h"
#include "cc/test/paint_op_matchers.h"
#include "components/viz/common/resources/release_callback.h"
#include "components/viz/common/resources/shared_image_format.h"
@@ -98,6 +99,7 @@
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/canvas/canvas2d/base_rendering_context_2d.h"
#include "third_party/blink/renderer/modules/canvas/canvas2d/canvas_gradient.h"
+#include "third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h"
#include "third_party/blink/renderer/modules/canvas/canvas2d/canvas_style_test_utils.h"
#include "third_party/blink/renderer/modules/webcodecs/video_frame.h"
#include "third_party/blink/renderer/modules/webcodecs/video_frame_handle.h"
@@ -112,8 +114,10 @@
#include "third_party/blink/renderer/platform/graphics/graphics_types_3d.h"
#include "third_party/blink/renderer/platform/graphics/image.h"
#include "third_party/blink/renderer/platform/graphics/image_orientation.h"
+#include "third_party/blink/renderer/platform/graphics/image_orientation_enum.h"
#include "third_party/blink/renderer/platform/graphics/memory_managed_paint_recorder.h"
#include "third_party/blink/renderer/platform/graphics/opacity_mode.h"
+#include "third_party/blink/renderer/platform/graphics/pattern.h"
#include "third_party/blink/renderer/platform/graphics/skia/skia_utils.h"
#include "third_party/blink/renderer/platform/graphics/test/gpu_compositing_test_platform.h"
#include "third_party/blink/renderer/platform/graphics/test/gpu_test_utils.h"
@@ -145,7 +149,9 @@
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorType.h"
#include "third_party/skia/include/core/SkImage.h"
+#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkM44.h"
+#include "third_party/skia/include/core/SkMatrix.h"
#include "third_party/skia/include/core/SkPixmap.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
@@ -215,14 +221,25 @@
class FakeImageSource : public CanvasImageSource {
public:
- FakeImageSource(gfx::Size, BitmapOpacity);
+ struct Options {
+ gfx::Size size;
+ BitmapOpacity opacity = kOpaqueBitmap;
+ ImageOrientation orientation = ImageOrientationEnum::kDefault;
+ bool would_taint_origin = false;
+ };
+
+ explicit FakeImageSource(Options options);
scoped_refptr<Image> GetSourceImageForCanvas(SourceImageStatus*,
const gfx::SizeF&) override;
- bool WouldTaintOrigin() const override { return false; }
- gfx::SizeF ElementSize(const gfx::SizeF&,
... (truncated)
Original Bug Report
EXIF-orientation privacy clamp bypass via createPattern
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The createPattern API in CanvasRenderingContext2D fails to apply the EXIF orientation privacy clamp to cross-origin images, unlike drawImage. This discrepancy allows a malicious website to render both oriented and unoriented versions of an image, potentially leaking private EXIF orientation metadata via rendering side-channels.
Affected files:
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.ccthird_party/blink/renderer/platform/graphics/image_pattern.cc
Estimated timestamp from git blame: 2020-10-21
Chromium implements an EXIF-orientation privacy clamp that forces cross-origin images (which taint the canvas) to always render with their EXIF orientation applied. This ensures a consistent state and prevents attackers from probing private EXIF metadata through layout or rendering differences.
However, there is a discrepancy in how this clamp is enforced between drawImage and createPattern.
Technical Details
In third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc, drawImage evaluates RespectImageOrientationInternal(image_source). It passes this flag down via ImageDrawOptions, ensuring the underlying rendering pipeline (e.g., StaticBitmapImage::DrawHelper) applies the correct geometric transform matrix before drawing the image.
Conversely, Canvas2DRecorderContext::createPattern calls RespectImageOrientationInternal(), but only uses the return value to format a DOMException error string if the image dimensions are zero. The CanvasPattern object is constructed using the raw, unoriented PaintImage obtained via image->PaintImageForCurrentFrame() in ImagePattern::ImagePattern.
When this pattern is subsequently used in a fill operation, ImagePattern::CreateShader generates an SkShader using the raw image data and only applies the user-defined patternTransform. No EXIF orientation matrix is applied, allowing the cross-origin image to be rendered in its unoriented state, bypassing the privacy clamp.
Potential Exploitation Steps
Note: The following steps are potential mechanisms identified via code analysis; our tooling agent does not yet have the capability to run and verify a live Proof of Concept.
An attacker could potentially exploit this discrepancy to leak the EXIF orientation of cross-origin images:
- Embed a cross-origin image on a malicious webpage.
- Create a 2D canvas and use
ctx.drawImage()to draw the image. This renders the oriented version and taints the canvas. - Change
ctx.globalCompositeOperationtodifference. - Use
ctx.createPattern()with the same image andctx.fillRect()to draw the unoriented pattern over the first image. - If the image has a non-default EXIF orientation, the mismatch leaves colored pixels on the canvas. If the orientation is default, the images align perfectly, leaving the canvas fully transparent/black.
- Apply a computationally expensive SVG filter (e.g.,
feColorMatrixorfeComponentTransfer) to the tainted canvas. - Measure the rasterization timing using
requestAnimationFrame. The performance difference between processing empty/black pixels versus colored pixels serves as an oracle, leaking the private EXIF orientation state.
Suggested Fix
Update Canvas2DRecorderContext::createPattern to properly utilize the result of RespectImageOrientationInternal(image_source). If the orientation must be respected, the CanvasPattern (or its underlying ImagePattern) should be provided with the appropriate orientation transform matrix. ImagePattern::CreateShader should then pre-multiply this orientation transform into the local_matrix before generating the SkShader.
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.