MITRE assigned CVE-2026-41254 on 2026-04-17 -- Abhinav Agarwal
On Fri, Apr 17, 2026 at 2:28 PM Abhinav Agarwal <[email protected]> wrote: > > 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
