Decoded as type
67 62 64 20gbd at 0x0128docs / iccDEV / specifications / html / rev 2
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().
A2B1 at 0x128.gbd and then drifts into overlapping bytes.77 74 70 74 becomes the triangle count.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 ................
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 ................
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.
67 62 64 20gbd at 0x01280x00000011 = 170x0134 - 0x01370x77747074 = 2,004,119,6680x0138 - 0x013b are ASCII wtptINT32_MAX / 3 = 715,827,882, so the old signed path hits UB.+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
The tag table points the second logical record at A2B1 offset 0x0128 with size 0x03dc.
The read path sees gbd at that offset and begins decoding a GamutBoundaryDesc header.
The bytes 77 74 70 74 are treated as m_NumberOfTriangles even though they are visually the next signature, wtpt.
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.
The issue thread shows UBSAN reporting that exact multiplication as a signed integer overflow during iccToXml replay on 2026-02-12.
m_Triangles = new icGamutBoundaryTriangle[m_NumberOfTriangles];
icUInt32Number nNum32 = m_NumberOfTriangles*3;
if (pIO->Read32(m_Triangles, nNum32)!=nNum32)
return false;
m_Triangles = new icGamutBoundaryTriangle[m_NumberOfTriangles];
icUInt32Number nNum32 = (icUInt32Number)m_NumberOfTriangles*3;
if (pIO->Read32(m_Triangles, nNum32)!=nNum32)
return false;
if ((icUInt64Number)m_nPCSChannels * m_NumberOfVertices * sizeof(icFloatNumber) > 134217728ULL) return false; if ((icUInt64Number)m_NumberOfTriangles * sizeof(icGamutBoundaryTriangle) > 134217728ULL) return false;
icInt16Number m_nPCSChannels; icInt16Number m_nDeviceChannels; icUInt16Number m_nPCSChannels; icUInt16Number m_nDeviceChannels;
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.
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.
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.