Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Skia
DescriptionOut of bounds read in Skia
ComponentSkia
Bug ClassOOB
Tracker514017820
Fix commit6bdbcfeda12f (chromium/src) +56/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
content/browser/service_host/utility_process_host_browsertest.cc
modified

Files Changed

  • content/browser/service_host/utility_process_host_browsertest.cc
  • content/common/skia_utils.cc
  • content/common/skia_utils.h
  • content/public/test/test_service.mojom
  • content/shell/renderer/shell_content_renderer_client.cc
  • content/shell/utility/shell_content_utility_client.cc
  • content/utility/utility_main.cc
From 6bdbcfeda12fbc9a999974c878c2ec01a36134bc Mon Sep 17 00:00:00 2001
From: Florin Malita <[email protected]>
Date: Mon, 13 Jul 2026 10:50:47 -0700
Subject: [PATCH] [content] Initialize Skia in utility processes

ConfigureSkiaKillSwitches() wires the kForceSkcmsICCParsing and
kForceSkExifCppParsing features to Skia's process-global codec
selectors, but it is only reachable via InitializeSkia() /
InitializeSkiaLite(). Those were called from the browser, renderer, and
GPU process entry points but not from UtilityMain(), so utility-hosted
image consumers (data_decoder, print_compositor,
paint_preview_compositor) ran with Skia's compile-time defaults instead
of the feature-controlled configuration used by every other process
type.

Call InitializeSkiaLite() early in UtilityMain() to match the other
process types. This also gives utility processes the Skia event tracer
and memory-dump provider they were previously missing.

Add a UtilityProcessHostBrowserTest that mirrors the existing
PseudonymizationSaltInitialized check to keep this from regressing.

Bug: 514017820
Change-Id: I8b6be1d519b3efd8548b3fd4ac2888bb411fa124
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8039611
Reviewed-by: Will Harris <[email protected]>
Reviewed-by: Arthur Sonzogni <[email protected]>
Commit-Queue: Florin Malita <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1661228}
---

diff --git a/content/browser/service_host/utility_process_host_browsertest.cc b/content/browser/service_host/utility_process_host_browsertest.cc
index 8128fe0..d8be426 100644
--- a/content/browser/service_host/utility_process_host_browsertest.cc
+++ b/content/browser/service_host/utility_process_host_browsertest.cc
@@ -189,6 +189,13 @@
         base::Unretained(this)));
   }
 
+  void RunSkiaInitializedTest() {
+    CHECK_CURRENTLY_ON(BrowserThread::UI);
+    service_->IsSkiaInitialized(
+        base::BindOnce(&UtilityProcessHostBrowserTest::OnSkiaInitializedChecked,
+                       base::Unretained(this)));
+  }
+
  protected:
   void DoneRunning(base::OnceClosure quit_closure) {
     DCHECK_CURRENTLY_ON(BrowserThread::UI);
@@ -231,6 +238,14 @@
     GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_closure_));
   }
 
+  void OnSkiaInitializedChecked(bool is_initialized) {
+    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+    EXPECT_TRUE(is_initialized)
+        << "Skia should be initialized in the child process";
+    ResetService();
+    GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_closure_));
+  }
+
   mojo::Remote<mojom::TestService> service_;
   base::OnceClosure done_closure_;
   bool expect_crashed_ = false;
@@ -310,6 +325,15 @@
                      base::Unretained(this)));
 }
 
+// Tests that Skia is initialized in utility processes so that image decoding
+// services pick up the same codec configuration as other process types.
+IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest, SkiaInitialized) {
+  RunUtilityProcess(
+      DefaultOptions(),
+      base::BindOnce(&UtilityProcessHostBrowserTest::RunSkiaInitializedTest,
+                     base::Unretained(this)));
+}
+
 #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
 
 // TODO(crbug.com/40253015): Re-enable this test on Android when
diff --git a/content/common/skia_utils.cc b/content/common/skia_utils.cc
index e2a788f..ff3376b 100644
--- a/content/common/skia_utils.cc
+++ b/content/common/skia_utils.cc
@@ -29,9 +29,7 @@
 // allocation that exceeds this limit.
 constexpr size_t kImageCacheSingleAllocationByteLimit = 64 * 1024 * 1024;
 
-}  // namespace
-
-namespace {
+bool g_skia_initialized = false;
 
 void ConfigureSkiaKillSwitches() {
   // Configure the ICC profile parser kill-switch early, before any image
@@ -49,6 +47,8 @@
   // Stable.
   SkExif::ForceSkExif(
       base::FeatureList::IsEnabled(blink::features::kForceSkExifCppParsing));
+
+  g_skia_initialized = true;
 }
 
 }  // namespace
@@ -105,4 +105,8 @@
       kImageCacheSingleAllocationByteLimit);
 }
 
+bool IsSkiaInitializedForTesting() {
+  return g_skia_initialized;
+}
+
 }  // namespace content
diff --git a/content/common/skia_utils.h b/content/common/skia_utils.h
index 0585d5fa..c22bb78 100644
--- a/content/common/skia_utils.h
+++ b/content/common/skia_utils.h
@@ -5,6 +5,8 @@
 #ifndef CONTENT_COMMON_SKIA_UTILS_H_
 #define CONTENT_COMMON_SKIA_UTILS_H_
 
+#include "content/common/content_export.h"
+
 namespace content {
 
 // Full Skia initialization for processes that do heavy Skia work (renderer,
@@ -17,6 +19,10 @@
 // memory dump providers.
 void InitializeSkiaLite();
 
+// Returns whether one of the InitializeSkia* functions above has run in this
+// process. Exposed for tests that need to verify per-process initialization.
+CONTENT_EXPORT bool IsSkiaInitializedForTesting();
+
 }  // namespace content
 
 #endif  // CONTENT_COMMON_SKIA_UTILS_H_
diff --git a/content/public/test/test_service.mojom b/content/public/test/test_service.mojom
index 7e689c3..b54f6087 100644
--- a/content/public/test/test_service.mojom
+++ b/content/public/test/test_service.mojom
@@ -61,6 +61,9 @@
   // Returns the pseudonymization salt value directly.
   GetPseudonymizationSalt() => (uint32 salt);
 
+  // Returns whether Skia has been initialized in this process.
+  IsSkiaInitialized() => (bool is_initialized);
+
   // Returns whether the pseudonymization salt has been initialized.
   IsPseudonymizationSaltInitialized() => (bool is_initialized);
 
diff --git a/content/shell/renderer/shell_content_renderer_client.cc b/content/shell/renderer/shell_content_renderer_client.cc
index e4ee7e3..a3735c1 100644
--- a/content/shell/renderer/shell_content_renderer_client.cc
+++ b/content/shell/renderer/shell_content_renderer_client.cc
@@ -24,6 +24,7 @@
 #include "components/surface_embed/renderer/create_plugin.h"
 #include "components/web_cache/renderer/web_cache_impl.h"
 #include "content/common/pseudonymization_salt.h"
+#include "content/common/skia_utils.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/common/pseudonymization_util.h"
 #include "content/public/common/web_identity.h"
@@ -160,6 +161,10 @@
     std::move(callback).Run(content::IsSaltInitialized());
   }
 
+  void IsSkiaInitialized(IsSkiaInitializedCallback callback) override {
+    std::move(callback).Run(IsSkiaInitializedForTesting());
+  }
+
   void PassWriteableFile(base::File file,
                          PassWriteableFileCallback callback) override {
     std::move(callback).Run();
diff --git a/content/shell/utility/shell_content_utility_client.cc b/content/shell/utility/shell_content_utility_client.cc
index 2d67c98..e56b2ab3 100644
--- a/content/shell/utility/shell_content_utility_client.cc
+++ b/content/shell/utility/shell_content_utility_client.cc
@@ -27,6 +27,7 @@
 #include "build/build_config.h"
 #include "components/services/storage/test_api/test_api.h"
 #include "content/common/pseudonymization_salt.h"
+#include "content/common/skia_utils.h"
 #include "content/public/child/child_thread.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/common/pseudonymization_util.h"
@@ -151,6 +152,10 @@
     std::move(callback).Run(content::IsSaltInitialized());
   }
 
+  void IsSkiaInitialized(IsSkiaInitializedCallback callback) override {
+    std::move(callback).Run(IsSkiaInitializedForTesting());
+  }
+
   void PassWriteableFile(base::File file,
                          PassWriteableFileCallback callback) override {
     std::move(callback).Run();
diff --git a/content/utility/utility_main.cc b/content/utility/utility_main.cc
index 14a49383..3eb18f88 100644
--- a/content/utility/utility_main.cc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/service_host/utility_process_host_browsertest.cc b/content/browser/service_host/utility_process_host_browsertest.cc
index 8128fe0..d8be426 100644
--- a/content/browser/service_host/utility_process_host_browsertest.cc
+++ b/content/browser/service_host/utility_process_host_browsertest.cc
@@ -189,6 +189,13 @@
         base::Unretained(this)));
   }
 
+  void RunSkiaInitializedTest() {
+    CHECK_CURRENTLY_ON(BrowserThread::UI);
+    service_->IsSkiaInitialized(
+        base::BindOnce(&UtilityProcessHostBrowserTest::OnSkiaInitializedChecked,
+                       base::Unretained(this)));
+  }
+
  protected:
   void DoneRunning(base::OnceClosure quit_closure) {
     DCHECK_CURRENTLY_ON(BrowserThread::UI);
@@ -231,6 +238,14 @@
     GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_closure_));
   }
 
+  void OnSkiaInitializedChecked(bool is_initialized) {
+    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+    EXPECT_TRUE(is_initialized)
+        << "Skia should be initialized in the child process";
+    ResetService();
+    GetUIThreadTaskRunner({})->PostTask(FROM_HERE, std::move(done_closure_));
+  }
+
   mojo::Remote<mojom::TestService> service_;
   base::OnceClosure done_closure_;
   bool expect_crashed_ = false;
@@ -310,6 +325,15 @@
                      base::Unretained(this)));
 }
 
+// Tests that Skia is initialized in utility processes so that image decoding
+// services pick up the same codec configuration as other process types.
+IN_PROC_BROWSER_TEST_F(UtilityProcessHostBrowserTest, SkiaInitialized) {
+  RunUtilityProcess(
+      DefaultOptions(),
+      base::BindOnce(&UtilityProcessHostBrowserTest::RunSkiaInitializedTest,
+                     base::Unretained(this)));
+}
+
 #if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC)
 
 // TODO(crbug.com/40253015): Re-enable this test on Android when
diff --git a/content/public/test/test_service.mojom b/content/public/test/test_service.mojom
index 7e689c3..b54f6087 100644
--- a/content/public/test/test_service.mojom
+++ b/content/public/test/test_service.mojom
@@ -61,6 +61,9 @@
   // Returns the pseudonymization salt value directly.
   GetPseudonymizationSalt() => (uint32 salt);
 
+  // Returns whether Skia has been initialized in this process.
+  IsSkiaInitialized() => (bool is_initialized);
+
   // Returns whether the pseudonymization salt has been initialized.
   IsPseudonymizationSaltInitialized() => (bool is_initialized);
Loading diff…

Original Bug Report

reported by [email protected]

Out of bounds read in SkTiff::ImageFileDirectory::getEntryValuesGeneric

Project Fortify, 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: Rust-based EXIF and ICC parsers, intended to mitigate C++ memory safety risks in Skia, are never enabled in Chromium Utility processes due to missing initialization. This causes security-sensitive services like the image decoder to use legacy C++ parsers when processing untrusted metadata, bypassing intended hardening. This regression leaves the data_decoder service exposed to potential vulnerabilities in the C++ TIFF and ICC implementations.

Affected files:

  • third_party/skia/src/codec/SkExif.cpp
  • third_party/skia/src/codec/SkCodecColorProfile.cpp
  • content/common/skia_utils.cc
  • content/utility/utility_main.cc
  • services/data_decoder/image_decoder_impl.cc
  • third_party/skia/src/codec/SkTiffUtility.cpp

Estimated timestamp from git blame: 2026-04-10

Description

Chromium has introduced Rust-based EXIF and ICC profile parsers in Skia to eliminate memory safety vulnerabilities when handling hostile image metadata. These parsers are gated by process-global configuration flags in Skia that default to the legacy C++ path. In the Browser, Renderer, and GPU processes, these flags are explicitly reconfigured at startup via content::InitializeSkia() or content::InitializeSkiaLite() to enable the safer Rust paths.

However, a review of the startup sequence indicates that Utility processes never call these initialization functions. As a result, every Utility process—including those hosting the data_decoder service—fails to configure these security features and remains defaulted to the legacy C++ parsers.

Technical Details

In third_party/skia/src/codec/SkExif.cpp, the configuration flag defaults to C++:

static bool gForceSkExifCpp = true;

Similarly, in third_party/skia/src/codec/SkCodecColorProfile.cpp, ICC profile parsing defaults to C++ skcms:

static bool gForceSkcmsForICCProfiles = true;

These flags are updated by content::ConfigureSkiaKillSwitches() based on the state of the kForceSkExifCppParsing and kForceSkcmsICCParsing features. While this configuration is reached in other process types, content/utility/utility_main.cc lacks a call to InitializeSkia() or InitializeSkiaLite(). Consequently, when the data_decoder service processes a JPEG for a favicon or notification icon, it uses the C++ SkTiff::ImageFileDirectory and skcms parsers instead of the Rust equivalents.

Potential Impact

This is a significant mitigation bypass. The primary purpose of the data_decoder service is to safely parse untrusted data in a sandboxed process. By failing to enable the Rust parsers, this service remains vulnerable to undiscovered or future bugs in the legacy C++ TIFF and ICC implementations.

For example, the C++ SkTiff::ImageFileDirectory::getEntryValuesGeneric implementation contains logic that could potentially be used for out-of-bounds reads if called with specific metadata counts/types, as it uses a fixed 4-byte stride for calculations that may involve 2-byte values.

Potential Steps to Trigger

  1. An attacker crafts an image containing malformed EXIF metadata or a crafted ICC profile.
  2. The image is processed by a feature that utilizes the data_decoder service (e.g., retrieving a favicon or processing a WebAPK icon).
  3. The Utility process, lacking the Rust parser initialization, invokes the legacy C++ parser on the hostile data.
  4. The attacker leverages a vulnerability in the C++ parser to achieve memory corruption within the sandboxed Utility process.

Suggested Fix

  1. Call content::InitializeSkiaLite() from content/utility/utility_main.cc to ensure Utility processes configure Skia kill-switches and features correctly.
  2. In Skia, consider inverting the static defaults for gForceSkExifCpp and gForceSkcmsForICCProfiles to false so that the system fails closed (to the safer Rust path) if initialization is missed.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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