Medium chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Canvas
DescriptionMemory corruption in Canvas
ComponentCanvas
Sub-componentCanvas2D
Bug ClassMemory Corruption
Tracker514422784
Fix commit2514f04ee0b9 (chromium/src) +433/-39
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
origin_clean_
third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc
modified
PLATFORM_EXPORT
third_party/blink/renderer/platform/graphics/image_pattern.h
modified
FakeImageSourceWithOrientation
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
modified
if
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
modified
MATCHER_P2
third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context_test.cc
modified
TEST
third_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.cc
  • third_party/blink/web_tests/external/wpt/css/css-images/image-orientation/image-orientation-none-cross-origin-canvas-pattern.html
  • third_party/blink/renderer/platform/graphics/bitmap_image.cc
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
  • third_party/blink/renderer/platform/graphics/image_pattern.h
  • third_party/blink/renderer/platform/graphics/pattern.h
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.cc
  • third_party/blink/renderer/platform/graphics/bitmap_image_test.cc
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_test.cc
  • third_party/blink/renderer/platform/graphics/image_pattern.cc
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_pattern.h
  • third_party/blink/renderer/platform/graphics/pattern.cc
From 2514f04ee0b98c1e4ad9b7da6cf0888123119a61 Mon Sep 17 00:00:00 2001
From: Andres Ricardo Perez <andresrperez@chromium.org>
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 <jpgravel@chromium.org>
Commit-Queue: Andres Ricardo Perez Rojas <andresrperez@chromium.org>
Auto-Submit: Andres Ricardo Perez Rojas <andresrperez@chromium.org>
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • third_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:

  1. Embed a cross-origin image on a malicious webpage.
  2. Create a 2D canvas and use ctx.drawImage() to draw the image. This renders the oriented version and taints the canvas.
  3. Change ctx.globalCompositeOperation to difference.
  4. Use ctx.createPattern() with the same image and ctx.fillRect() to draw the unoriented pattern over the first image.
  5. 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.
  6. Apply a computationally expensive SVG filter (e.g., feColorMatrix or feComponentTransfer) to the tainted canvas.
  7. 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.

View on issue tracker