CVE-2026-106399
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmodules/skcms/skcms.cc |
modified |
Files Changed
modules/skcms/skcms.cc
Patch
From 01cb6d90ebc2cbecc8d5fd8f30513f7e9ce8f3b8 Mon Sep 17 00:00:00 2001
From: Jorge Betancourt <jmbetancourt@google.com>
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 <jmbetancourt@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
---
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);
Original Bug Report
[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/skcmsmirrorbranch)
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 insrc/skcms_public.h:266)- →
get_tag_table(profile)—skcms.cc:439returnsprofile->buffer + 132 - →
read_big_u32(tags[idx].signature)—skcms.cc:1333/skcms.cc:391reads 4 bytes atbuffer + 132 + 12*idx
Impact / risk
- Attacker position: local / untrusted-file (attacker-supplied
.iccbytes) plus a caller passingidx == 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 —
idxis not attacker-controlled on the browser image-decode path (Skia iteratesi < tag_count); requires a caller-side off-by-one to be exploitable
Attack narrative
- Attacker supplies an ICC profile that
skcms_Parse()accepts (144 bytes suffices). - Victim tooling that enumerates tags with an inclusive upper bound (
for (i = 0; i <= p.tag_count; i++)) — asiccdump-style code sometimes does — callsskcms_GetTagByIndex(&p, p.tag_count, &t). tags[tag_count]is read from attacker bytes (or past the buffer).t.buf = profile->buffer + attacker_u32is then dereferenced atskcms.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>=atskcms.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:
idxis caller-supplied, not directly attacker-controlled; the shippediccdump.citeratesi < tag_countcorrectly. No known Skia/Chromium production caller passesidx == tag_count. - Recommended contact: skia-security@google.com / Buganizer component
Skia > Color(internal) or crbug.com Security template. Canonical upstream isskia.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);