oss-sec mailing list archives
lcms2 <= 2.18 CubeSize() integer overflow: stock Ubuntu 24.04 Poppler / evince-thumbnailer / OpenJDK crashers (different triggers), no CVE
From: Abhinav Agarwal <abhinavagarwal1996 () gmail com>
Date: Fri, 17 Apr 2026 14:28:14 -0700
A 992-byte PDF crashes a bunch of stock Ubuntu 24.04 consumers: evince-thumbnailer, Poppler (pdftoppm / pdftocairo / pdfimages), the cups-filters PDF-to-raster print filter, Okular, and GIMP's PDF plug-in all segfault inside liblcms2. OpenJDK 21 on Ubuntu crashes too, and Windows Temurin 21.0.9 crashes in its bundled lcms.dll (3/3 independent runs). There's also a coarse seed- correlated heap-read primitive on Linux glibc with ASLR off - a real CWE-200 channel, though not a generic arbitrary read. Upstream fixed it on master in February/March but hasn't cut a release, no advisory, no CVE. The GHSA I filed was closed without a reply. Looking for a CVE and for distro attention. Full write-up: https://abhinavagarwal07.github.io/posts/lcms2-cubesize-overflow/ Reachability (Ubuntu 24.04 LTS stock, liblcms2-2 2.14-2build1; Windows Server 2022 with Temurin 21.0.9) -------------------------------------------------------------------------- SEGV, no local code changes: * tumblerd (D-Bus auto-activated thumbnail service). tumblerd is the freedesktop thumbnail daemon that ships as the default on Xfce and is available on GNOME as a fallback; its bundled tumbler-poppler-thumbnailer.so plugin loads libpoppler + liblcms2 directly into the daemon process. A single `dbus-send` "Queue" call with the PDF's URI is enough: tumblerd was not running beforehand and wasn't on $PATH, but D-Bus auto-activated the service on the Queue call, the service pulled the PDF, and the daemon SIGSEGV'd in liblcms2.so.2.0.14. Reproduced 4/4, with kernel `segfault ... in liblcms2.so.2.0.14` and apport records. This is the same D-Bus call that a file manager issues when a directory is opened, so the real-world shape is "open a folder containing the PDF, the system's thumbnail daemon dies." Direct evince-thumbnailer CLI (`evince-thumbnailer -s 200 poc.pdf out.jpg`) crashes the same way (SEGV at liblcms2.so.2.0.14+0xb503, Eval4Inputs+643, cmsintrp.c:909). SHA256 (poc_iccbased_5ch.pdf): 5c328a4362185c6dca2d6cae13c74ed456889798220f3f16e840449648121b55 * Poppler: pdftoppm, pdftocairo, pdfimages -list. Same 992-byte PDF with a 1x1 image XObject using /ColorSpace [/ICCBased 5 0 R]. Poppler warns on N>4 and does not abort; goes on to call cmsCreateTransform(). Same crash site. * Okular 4:23.08.5 (xvfb-run). SEGV via okularGenerator_poppler.so -> libpoppler-qt5 -> lcms2. Kernel: `Okular::PixmapG[PID]: segfault ... in liblcms2.so.2.0.14[0xb503]`, Eval4Inputs+643. Core file + gdb backtrace captured. * cups-filters pdftoraster 2.0.0-0ubuntu4.1. This is the CUPS PDF-to-raster filter. It lives at /usr/lib/cups/filter/pdftoraster rather than on $PATH, so `which pdftoraster` misses it - invoke it the way CUPS does: `/usr/lib/cups/filter/pdftoraster 1 root "" 1 "" < poc.pdf`. Kernel: `pdftoraster[PID]: segfault ... in liblcms2.so.2.0.14[0xb503]`. Core + gdb backtrace captured in the primary-evidence bundle. * GIMP 2.10.36-3 file-pdf-load plug-in (under xvfb-run, headless batch mode). The plug-in subprocess SIGSEGVs. GIMP installs its own signal handler, so the usual kernel dmesg line doesn't appear, but strace catches SIGSEGV{si_code=SEGV_ACCERR} at fault time, and the frame-by-frame proof comes from running the same PDF through evince-thumbnailer under gdb - identical poppler + lcms2 library chain. * LibreOffice import: inconsistent enough that I'd treat it as a secondary target rather than cite it as confirmed. On the authoritative fresh-VM run under script(1), LO rejected the PDF at the load stage before ever calling into lcms2. On a separate VM earlier, xpdfimport crashed with a matching dmesg line. Both outcomes reproduce; I can't point at a single reliable command that crashes LO the way the other rows do. * Flask+Docker PDF thumbnailer spawning pdftoppm returns HTTP 500 (exit_code:-11) per upload. Same shape as any Poppler-backed webmail preview, DMS thumbnailer, or CI artifact renderer. * OpenJDK 21 on Ubuntu. ICC_Profile.getInstance() + ICC_ColorSpace.toRGB(). SEGV in system liblcms2.so.2. Confirmed with both the 18 MB 7CLR profile and a 4,819-byte 5CLR variant (JdkPoc5.java, input array sized via getNumComponents()). * OpenJDK 21 Temurin 21.0.9 on Windows Server 2022. EXCEPTION_ACCESS_VIOLATION in lcms.dll+0x9fd2, 86-304 ms, 3/3 runs. Reproduced on two independent Azure VM instances. Windows JDK bundles lcms.dll (not system-linked); Azure WindowsServer:2022-datacenter-azure-edition images ship Temurin 21.0.9 pre-installed. * transicc -l (lcms2's own bundled utility). 4,819-byte device-link profile. SEGV, exit 139. * Python ctypes, Rust lcms2 crate 5.6. Direct calls to cmsCreateTransform with TYPE_CMYK5_8. SEGV. Paths that did not reproduce in my tests: Ghostscript, ImageMagick, tificc, jpgicc, Pillow ImageCms, libvips 8.15, Inkscape, Node.js @kittl/little-cms. See the write-up for per-consumer detail. Bug (one paragraph) ------------------- src/cmslut.c:461, function CubeSize(). Check-after-multiply on a uint32 accumulator: `rv *= dim` wraps silently before the guard `rv > UINT_MAX / dim` runs. Crafted CLUT dims where the product exceeds 2^32 but wraps to a small value (e.g. [61,7,161,245,255] wraps to 1,529 from a true product of ~4.3e9) pass every guard. cmsStageAllocCLut16bitGranular() undersizes the CLUT buffer (~9 KB instead of ~10 GB of nodes); the interpolator's opta[] strides are computed from the real dims and index past Tab.T[] during transform construction (OptimizeByResampling -> cmsStageSampleCLut16bit) or during cmsDoTransform. CWE-190 causes CWE-125. Fix status ---------- File: src/cmslut.c Affects: all released versions through lcms2 2.18 Fixed on master (unreleased), no CVE, no advisory: https://github.com/mm2/Little-CMS/commit/da6110b (widen rv to uint64) https://github.com/mm2/Little-CMS/commit/e0641b1 (guard before multiply) Affected -------- Any distro shipping lcms2 <= 2.18: Ubuntu 24.04 LTS liblcms2-2 2.14-2build1 (validated) Debian bookworm liblcms2-2 2.16-2 Fedora lcms2 2.16 Alpine edge lcms2 2.17-r0 Homebrew little-cms2 2.18 (validated) JDK-bundled lcms: Temurin 21.0.9 on Windows confirmed vulnerable via its bundled lcms.dll (3/3 runs, EXCEPTION_ACCESS_VIOLATION in lcms.dll+0x9fd2). On Ubuntu 24.04, OpenJDK 21 uses the SYSTEM liblcms2.so.2, so patching liblcms2-2 fixes both the JDK and Poppler paths on that platform. Other mainstream JDK distributions (Oracle, Corretto, Zulu, Microsoft OpenJDK) commonly bundle their own lcms2 source tree; patch status is per-vendor. Minimal C reproducer (stock Ubuntu 24.04) ----------------------------------------- sudo apt install liblcms2-dev gcc cat > poc.c <<'EOF' #include <stdio.h> #include <stdlib.h> #include <string.h> #include <lcms2.h> static void be32(unsigned char*p,unsigned v){p[0]=v>>24;p[1]=v>>16;p[2]=v>>8;p[3]=v;} int main(void){ const int N = 4587, tag = 32+36+20+N, tot = 128+4+12+tag; unsigned char *b = calloc(1,tot); be32(b,tot); b[8]=4; b[9]=0x30; memcpy(b+12,"scnr",4); memcpy(b+16,"5CLR",4); memcpy(b+20,"Lab ",4); memcpy(b+36,"acsp",4); be32(b+68,63190); be32(b+72,65536); be32(b+76,54061); be32(b+128,1); memcpy(b+132,"A2B0",4); be32(b+136,144); be32(b+140,tag); unsigned char *t = b+144; memcpy(t,"mAB ",4); t[8]=5; t[9]=3; be32(t+12,32); be32(t+24,68); for (int i=0;i<3;i++) memcpy(t+32+i*12,"curv",4); unsigned char g[] = {61,7,161,245,255}; memcpy(t+68,g,5); t[68+16]=1; cmsHPROFILE h = cmsOpenProfileFromMem(b,tot); cmsHPROFILE s = cmsCreate_sRGBProfile(); cmsCreateTransform(h, TYPE_CMYK5_8, s, TYPE_RGB_8, 0, 0); // SEGV return 0; } EOF # ASAN (clean OOB frame): gcc -fsanitize=address -g -o poc poc.c -llcms2 -lm && ./poc # Without ASAN (matches production behavior): gcc -o poc_plain poc.c -llcms2 -lm && ./poc_plain; echo "exit=$?" # exit=139 (SIGSEGV) Python, Rust, Java, PDF, and device-link variants build equivalently. CVSS 3.1 -------- Availability only (UI:R): AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H = 6.5 (Medium) Availability only, server-side renderer (UI:N): AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5 (High) With demonstrated info disclosure, UI:R: AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:H = 8.1 (High) Same, UI:N (any headless Poppler-backed render worker fits this): AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H = 9.1 (Critical) I don't have a write primitive. I looked at PatchLUT in cmsopt.c:632 as a possible mirror of the read side, but the math doesn't reach a signed-int wrap. Information disclosure (CWE-200) -------------------------------- Coarse but real. On Ubuntu 24.04 with glibc's default allocator and ASLR off (setarch -R), the first output byte from cmsDoTransform tracks a pre-run heap-seed byte for specific inputs - so this is a seed-correlated leak of memory below the CLUT allocation. It isn't an arbitrary-heap-read: the first output byte isn't a raw heap byte, it's LinearInterp'd through the sRGB pipeline, so bytes come back with some blur. The reliable window on the 5CLR profile is axis 3's [-365 KB, -1.5 KB] offsets below the CLUT allocation. Two small tricks in cmsintrp.c make this work: * EVAL_FNS(N,NM) short-circuits the far-corner read when Input[i] == 0xFFFFU. With 8-bit input that's byte 0xFF, so setting 4 of 5 axes to 0xFF collapses the usual 2^5=32 corner reads down to 2. * opta[NM] is uint32; opta[NM] * k0 is computed as uint32 and wraps mod 2^32. The wrapped value then goes into int K0, and anything above 2^31 reinterprets as a large negative int. So LutTable + K0 ends up reading below the CLUT allocation, in heap we've just sprayed. Axis 3 (opta[1] = 765) gives offsets of -1.5 KB to -365 KB, which a 260 MB malloc-spray covers comfortably. Evidence (16 sampled seed bytes spanning 0x00..0xFF: 0x00, 0x11, 0x22, ..., 0xEE, 0xFF): seed=0xAA axis=3 in=0xd9 out=aa3b53 (byte[0] = seed) seed=0xAA axis=3 in=0xf5 out=add800 (byte[0] ~ seed) seed=0xCC axis=3 in=0xd9 out=e32b45 (byte[0] tracks seed) seed=0xCC axis=3 in=0xf5 out=ebe300 (byte[0] tracks seed) seed=0xAA axis=3 in=0xea out=005f91 (control, in-bounds) seed=0xCC axis=3 in=0xea out=005f91 (same) Control input (0xea, in-bounds) produces byte-identical output across all 16 seeds; OOB inputs (0xd9, 0xf4, 0xf5) produce outputs whose first byte tracks the heap seed. POC (`infoleak_linux_v3.c`) and the 16-seed-sweep logs on request. Caveats: ASLR must be off and glibc's default allocator is assumed. Axis 3 is the reliable surface; axes 0-2 fall too far out of bounds without MAP_FIXED reservations or multi-GB sprays. The primitive is seed-correlated, not arbitrary-read. Timeline -------- 2010-10 CubeSize() check-after-multiply pattern introduced. 2026-02-19 Fix 1: da6110b. 2026-03-12 Fix 2: e0641b1. 2026-04-13 GHSA-4xp6-rcgg-m9qq filed (private advisory). 2026-04-14 MITRE CVE request filed (CVE Request 2025002). Submitted with the evidence that existed at the time. 2026-04-16 Asked the maintainer on the GHSA whether he'd triage, told him I'd publish otherwise. 2026-04-17 GHSA closed without engagement. Public disclosure References ---------- Vulnerable source (lcms2 2.18): https://github.com/mm2/Little-CMS/blob/lcms2.18/src/cmslut.c#L461 Prior same-codebase CVEs: CVE-2018-16435, CVE-2016-10165. CWEs: CWE-190, CWE-125. Write-up + per-consumer evidence: https://abhinavagarwal07.github.io/posts/lcms2-cubesize-overflow/ -- Abhinav Agarwal
Current thread:
- lcms2 <= 2.18 CubeSize() integer overflow: stock Ubuntu 24.04 Poppler / evince-thumbnailer / OpenJDK crashers (different triggers), no CVE Abhinav Agarwal (Apr 17)
