docs / iccDEV / specifications / html / rev 2

Issue #599 as a visual byte-to-bug overlay

The malformed profile is on the left as an annotated xxd wall, and the parser failure is on the right as a compact decode ladder from raw bytes to the signed overflow in CIccTagGamutBoundaryDesc::Read().

Where the bug lives in the file

Header 0x0000 - 0x007f
Tag table 0x0080 - 0x00df
A2B1 owner range 0x0128 - 0x0503
Trigger bytes 0x0138 - 0x013b
ICC header stays visually normal.
The second tag entry points to A2B1 at 0x128.
The payload starts with gbd and then drifts into overlapping bytes.
77 74 70 74 becomes the triangle count.
Window A - header and tag table The second tag advertises A2B1 at 0x0128 with size 0x03dc.
00000000: 00 00 0f 60 00 00 00 00 05 00 00 00 73 70 61 63  ...`........spac
00000010: 52 47 42 20 58 59 5a 20 07 e2 00 08 00 0f 00 0a  RGB XYZ ........
00000020: 00 16 00 12 61 63 73 70 00 00 00 00 00 00 00 00  ....acsp........
00000040: 00 00 00 01 00 00 f3 54 00 01 00 00 00 01 16 cf  .......T........
00000080: 00 00 00 08 64 65 73 63 00 00 00 e4 00 00 00 42  ....desc.......B
00000090: 41 32 42 31 00 00 01 28 00 00 03 dc 42 32 af af  A2B1...(....B2..
000000a0: af af af af af af af af af af af af af af af af  ................
000000b0: af af af af af af af af af af af af af af af af  ................
000000c0: af af af af af af af af af af af af af af af af  ................
Window B - the trigger record gbd begins at 0x0128. The next 20 bytes are enough to explain the overflow.
00000108: 34 00 00 00 01 62 42 8b 71 4e 42 8d 54 95 42 8f  4....bB.qNB.T.B.
00000118: 37 dc 42 91 f5 3f 42 94 b2 b0 42 87 89 db 3b 54  7.B..?B...B...;T
00000128: 67 62 64 20 73 76 63 6e 00 00 00 00 00 00 00 11  gbd svcn........
00000138: 77 74 70 74 00 00 0e d4 00 00 00 14 63 70 72 74  wtpt........cprt
00000148: 00 00 0e e8 00 00 00 76 6d 6c 75 63 00 00 00 00  .......vmluc....
00000158: 00 00 00 ff ff ff ff ff ff ff ff ff ff ff ff ff  ................
Window C - the malformed profile keeps going Later bytes still contain structured ICC data, but the vulnerable read already consumed a hostile count.
000004b0: 94 87 7d 56 47 dd 8a af 7c 51 4a 1d 73 76 63 6e  ..}VG...|QJ.svcn
000004c0: 7f f8 7b f2 50 13 73 55 7b c8 55 73 67 cb 7c 3a  ..{.P.sU{.Usg.|:
000004d0: 5a 5e 5e 37 7d c8 b4 42 e7 d8 93 42 73 63 00 00  Z^^7}..B...Bsc..
000004e0: 00 f0 00 00 00 76 41 32 42 30 00 00 01 68 00 00  .....vA2B0...h..
000004f0: 74 10 41 32 42 31 00 00 75 78 00 00 01 b4 42 32  t.A2B1..ux....B2

Visual takeaway: the overflow is not coming from a random giant integer. The file makes the parser reuse the ASCII bytes for wtpt as the signed m_NumberOfTriangles field inside a gbd read path.

Decoded as type

67 62 64 20
gbd at 0x0128

Decoded as vertices

0x00000011 = 17
Bytes 0x0134 - 0x0137

Decoded as triangles

0x77747074 = 2,004,119,668
Bytes 0x0138 - 0x013b are ASCII wtpt

Historical signed multiply

2,004,119,668 * 3 = 6,012,359,004
INT32_MAX / 3 = 715,827,882, so the old signed path hits UB.

20-byte decode that matters

+0x00 67 62 64 20 Type signature -> gbd
+0x04 73 76 63 6e Reserved field is already dirty: ASCII svcn
+0x08 00 00 nPCSChannels = 0
+0x0a 00 00 nDeviceChannels = 0
+0x0c 00 00 00 11 m_NumberOfVertices = 17
+0x10 77 74 70 74 m_NumberOfTriangles = 0x77747074, because the next tag signature bytes got reinterpreted as a count

Bug ladder

1

The tag table points the second logical record at A2B1 offset 0x0128 with size 0x03dc.

2

The read path sees gbd at that offset and begins decoding a GamutBoundaryDesc header.

3

The bytes 77 74 70 74 are treated as m_NumberOfTriangles even though they are visually the next signature, wtpt.

4

In the older source snapshot, m_NumberOfTriangles is a signed icInt32Number, so m_NumberOfTriangles * 3 is evaluated in signed arithmetic before being stored in icUInt32Number nNum32.

5

The issue thread shows UBSAN reporting that exact multiplication as a signed integer overflow during iccToXml replay on 2026-02-12.

Patch compare

Historical arithmetic in the issue-era mirror

m_Triangles = new icGamutBoundaryTriangle[m_NumberOfTriangles];
icUInt32Number nNum32 = m_NumberOfTriangles*3;
if (pIO->Read32(m_Triangles, nNum32)!=nNum32)
  return false;

Current local iccDEV mirror

m_Triangles = new icGamutBoundaryTriangle[m_NumberOfTriangles];
icUInt32Number nNum32 = (icUInt32Number)m_NumberOfTriangles*3;
if (pIO->Read32(m_Triangles, nNum32)!=nNum32)
  return false;

Issue-thread defense in depth, 2026-02-18

if ((icUInt64Number)m_nPCSChannels * m_NumberOfVertices *
    sizeof(icFloatNumber) > 134217728ULL)
  return false;
if ((icUInt64Number)m_NumberOfTriangles *
    sizeof(icGamutBoundaryTriangle) > 134217728ULL)
  return false;

Issue-thread channel type fix, 2026-03-27

icInt16Number m_nPCSChannels;
icInt16Number m_nDeviceChannels;
icUInt16Number m_nPCSChannels;
icUInt16Number m_nDeviceChannels;

What the bytes caused

The PoC turns a readable signature, wtpt, into a hostile triangle count. The defect is structural: it is a field boundary problem and an arithmetic problem at the same time.

What the current mirror already changed

The local iccDEV mirror already carries the unsigned multiply and unsigned channel fields, so the exact historical SIO path shown in issue #599 is no longer identical here.

What the issue thread still argues for

The 128 MB allocation cap is still useful because the malformed bytes are also good at driving pathological allocation requests even after the signed multiply is removed.