Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Skia
DescriptionOut of bounds read in Skia
ComponentSkia
Sub-componentSkcms
Bug ClassOOB
Tracker530237174
Fix commit01cb6d90ebc2 (skia) +1/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
modules/skcms/skcms.cc
modified

Files Changed

  • modules/skcms/skcms.cc
From 01cb6d90ebc2cbecc8d5fd8f30513f7e9ce8f3b8 Mon Sep 17 00:00:00 2001
From: Jorge Betancourt <[email protected]>
Date: Tue, 08 Sep 2026 10:54:39 -0400
Subject: [PATCH] [skcms] Fix incorrect tag_count bounds check

Bug: b/530237174
Change-Id: I6758cab4bf1976aea046c9ad0e8958609b2a0782
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1353896
Commit-Queue: Jorge Betancourt <[email protected]>
Reviewed-by: Robert Phillips <[email protected]>
---
diff --git a/modules/skcms/skcms.cc b/modules/skcms/skcms.cc
index c23dbc1b..e758590 100644
--- a/modules/skcms/skcms.cc
+++ b/modules/skcms/skcms.cc
@@ -1351,7 +1351,7 @@
 
 void skcms_GetTagByIndex(const skcms_ICCProfile* profile, uint32_t idx, skcms_ICCTag* tag) {
     if (!profile || !profile->buffer || !tag) { return; }
-    if (idx > profile->tag_count) { return; }
+    if (idx >= profile->tag_count) { return; }
     const tag_Layout* tags = get_tag_table(profile);
     tag->signature = read_big_u32(tags[idx].signature);
     tag->size      = read_big_u32(tags[idx].size);
Loading diff…

Original Bug Report

reported by [email protected]

[google/skcms] Off-by-one in skcms_GetTagByIndex allows OOB read past tag table

VulnHunt Finding: Off-by-one in skcms_GetTagByIndex allows OOB read past tag table

  • Target: https://github.com/google/skcms
  • Repo: https://github.com/google/skcms @ 96d9171c94b937a1b5f0293de7309ac16311b722
  • Finding ID: 01
  • Severity: Low
  • CWE / Class: CWE-193
  • Location: skcms.cc:1331
  • Attacker position: local
  • Patch verified: yes

Detailed Report

Off-by-one heap buffer overflow in skcms_GetTagByIndex()

Nutshell

skcms_GetTagByIndex() guards its index with idx > profile->tag_count instead of >=, so idx == tag_count reads a 12-byte tag_Layout one entry past the end of the tag table. Because the tag table is embedded in the attacker-supplied profile buffer, tags[tag_count] lands in attacker-controlled bytes, and the subsequent tag->type = read_big_u32(profile->buffer + attacker_offset) dereferences an attacker-chosen offset. Under ASAN this is a heap-buffer-overflow READ. The idx argument is not attacker-controlled in the browser image-decode path, so severity is Low; the bug is exposed to any caller (e.g. iccdump.c, Skia debug tooling) that iterates 0..tag_count inclusive.

Affected code

  • File: skcms.cc:1329-1336
  • Function: skcms_GetTagByIndex()
  • Version/commit: 96d9171c94b937a1b5f0293de7309ac16311b722 (github.com/google/skcms mirror branch)
void skcms_GetTagByIndex(const skcms_ICCProfile* profile, uint32_t idx, skcms_ICCTag* tag) {
    if (!profile || !profile->buffer || !tag) { return; }
    if (idx > profile->tag_count) { return; }              // BUG: should be >=
    const tag_Layout* tags = get_tag_table(profile);
    tag->signature = read_big_u32(tags[idx].signature);    // OOB read of tags[tag_count]
    tag->size      = read_big_u32(tags[idx].size);
    tag->buf       = read_big_u32(tags[idx].offset) + profile->buffer;
    tag->type      = read_big_u32(tag->buf);               // wild deref: buffer + attacker_u32
}

Root cause

The bounds check uses > where >= is required. Valid indices for a table of tag_count entries are [0, tag_count). The 12 bytes at tags[tag_count] are the first 12 bytes of tag data (or, for a minimally-sized profile, past the end of the malloc’d buffer entirely).

Reachability

  • skcms_GetTagByIndex — skcms.cc:1329 (public API, exported in src/skcms_public.h:266)
  • → get_tag_table(profile) — skcms.cc:439 returns profile->buffer + 132
  • → read_big_u32(tags[idx].signature) — skcms.cc:1333 / skcms.cc:391 reads 4 bytes at buffer + 132 + 12*idx

Impact / risk

  • Attacker position: local / untrusted-file (attacker-supplied .icc bytes) plus a caller passing idx == tag_count
  • Effect: heap OOB read of 4+4 bytes at attacker-chosen offset from profile->buffer; potential info-disclosure or crash
  • Severity (estimate): Low — idx is not attacker-controlled on the browser image-decode path (Skia iterates i < tag_count); requires a caller-side off-by-one to be exploitable

Attack narrative

  1. Attacker supplies an ICC profile that skcms_Parse() accepts (144 bytes suffices).
  2. Victim tooling that enumerates tags with an inclusive upper bound (for (i = 0; i <= p.tag_count; i++)) — as iccdump-style code sometimes does — calls skcms_GetTagByIndex(&p, p.tag_count, &t).
  3. tags[tag_count] is read from attacker bytes (or past the buffer). t.buf = profile->buffer + attacker_u32 is then dereferenced at skcms.cc:1336, reading 4 bytes at an attacker-chosen offset from the profile allocation.

Proof of concept

  • Files: exploit/gen_icc.py, exploit/reproduce.sh, exploit/poc.icc (144 bytes)
  • Success marker: heap-buffer-overflow
  • How to run:
    scripts/docker-harness.sh /usr/local/google/home/avkov/agent/workspace/https-github.com-google-skcms/2026-07-01_145415 run bash /scan/lab/run.sh finding-01 heap-buffer-overflow
    
  • Pre-patch evidence (verification.log):
    VERDICT: CONFIRMED (marker 'heap-buffer-overflow' present)
    ...
    ==5==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60d0000000d0 at pc 0x559920ba373d bp 0x7ffda86c6b00 sp 0x7ffda86c6af8
    READ of size 4 at 0x60d0000000d0 thread T0
        #0 0x559920ba373c in read_big_u32(unsigned char const*) /opt/skcms/skcms.cc:391:5
        #1 0x559920ba373c in skcms_GetTagByIndex /opt/skcms/skcms.cc:1333:22
        #2 0x559920bd1590 in main /opt/skcms/driver.c:68:9
    ...
    0x60d0000000d0 is located 0 bytes to the right of 144-byte region [0x60d000000040,0x60d0000000d0)
    

Patch

  • File: patch.diff
  • Summary: change > to >= at skcms.cc:1331.
    -    if (idx > profile->tag_count) { return; }
    +    if (idx >= profile->tag_count) { return; }
    
  • Post-patch evidence (verification.log, === POST-PATCH === section):
    === POST-PATCH (2026-07-01T18:06:23Z) ===
    VERDICT: UNCONFIRMED (marker 'heap-buffer-overflow' absent)
    + /opt/skcms/driver ./poc.icc tagidx:1
    skcms_Parse len=144 -> true tag_count=1 data_color_space=0x47524159
    tagidx[1] sig=0x00000000 type=0x00000000 size=0 buf=(nil)
    DRIVER_OK
    ...
    --- POST-PATCH functionality ---
    [functest] tagidx:0 on sRGB_Facebook.icc
    tagidx[0] sig=0x64657363 type=0x64657363 size=94 buf=0x61600000017c
    DRIVER_OK
    

Disclosure notes

  • CWE: CWE-193 (Off-by-one Error) → CWE-125 (Out-of-bounds Read)
  • Mitigating factors: idx is caller-supplied, not directly attacker-controlled; the shipped iccdump.c iterates i < tag_count correctly. No known Skia/Chromium production caller passes idx == tag_count.
  • Recommended contact: [email protected] / Buganizer component Skia > Color (internal) or crbug.com Security template. Canonical upstream is skia.googlesource.com/skcms.

Proposed Patch

diff --git a/skcms.cc b/skcms.cc
index 00b19ec..7cc36f0 100644
--- a/skcms.cc
+++ b/skcms.cc
@@ -1328,7 +1328,7 @@ static bool read_cicp(const skcms_ICCTag* tag, skcms_CICP* cicp) {
 
 void skcms_GetTagByIndex(const skcms_ICCProfile* profile, uint32_t idx, skcms_ICCTag* tag) {
     if (!profile || !profile->buffer || !tag) { return; }
-    if (idx > profile->tag_count) { return; }
+    if (idx >= profile->tag_count) { return; }
     const tag_Layout* tags = get_tag_table(profile);
     tag->signature = read_big_u32(tags[idx].signature);
     tag->size      = read_big_u32(tags[idx].size);
View on issue tracker
Links in the report