home intel cve-2026-3308-mupdf-pdf-image-integer-overflow-rce
CVE Analysis 2026-03-31 · 8 min read

CVE-2026-3308: Integer Overflow in MuPDF pdf_load_image_imp Enables Heap OOB Write

An integer overflow in MuPDF 1.27.0's pdf_load_image_imp allows a crafted PDF to trigger a heap out-of-bounds write, potentially enabling arbitrary code execution on any platform running the viewer.

#integer-overflow#heap-out-of-bounds#arbitrary-code-execution#pdf-processing#mupdf
Technical mode — for security professionals
▶ Attack flow — CVE-2026-3308 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-3308Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-3308 is a heap-based buffer overwrite in Artifex MuPDF, discovered by researcher Yarden Porat and patched in Debian's LTS track (DLA-4540-1, 1.17.0+ds1-2+deb11u2) on April 21, 2026. The vulnerability lives in pdf-image.c, specifically inside pdf_load_image_imp, where attacker-controlled image dimension fields from a malformed PDF are multiplied together without overflow guards. The resulting undersized heap allocation is then written into, producing an exploitable heap out-of-bounds write primitive. CVSS scores this at 7.8 (HIGH, local/remote vector depending on delivery), and no in-the-wild exploitation has been confirmed at time of writing.

Affected Component

The vulnerable code path is reached any time MuPDF renders an inline or XObject image embedded in a PDF stream. The call chain from document open to crash is:

pdf_load_page()
  -> pdf_process_stream()
    -> pdf_process_image()
      -> pdf_load_image_imp()    <-- integer overflow here
        -> fz_new_pixmap_with_data() / fz_malloc()

Affected versions include MuPDF 1.27.0 and all prior releases sharing the same pdf_load_image_imp logic. Downstream consumers — document converters, mail clients, browser PDF plugins, and CI pipelines invoking mutool — inherit the exposure.

Root Cause Analysis

PDF image dictionaries carry /Width, /Height, and /BitsPerComponent as arbitrary integer objects. pdf_load_image_imp reads these directly from the parsed dictionary and computes a buffer size through a sequence of multiplications. In MuPDF 1.27.0 those multiplications are performed in int before any promotion to size_t, creating a classic signed 32-bit overflow:

/* pdf-image.c — pdf_load_image_imp (MuPDF 1.27.0, simplified for clarity) */
static fz_image *
pdf_load_image_imp(fz_context *ctx, pdf_document *doc,
                   pdf_obj *rdb, pdf_obj *dict,
                   fz_stream *cstm, int forcemask)
{
    int w, h, bpc, n;
    unsigned char *samples;
    size_t stride, total;

    /* All fields are attacker-controlled via the PDF dictionary */
    w   = pdf_dict_get_int(ctx, dict, PDF_NAME(Width));        // e.g. 0x8001
    h   = pdf_dict_get_int(ctx, dict, PDF_NAME(Height));       // e.g. 0x8001
    bpc = pdf_dict_get_int(ctx, dict, PDF_NAME(BitsPerComponent)); // e.g. 8
    n   = pdf_dict_get_int(ctx, dict, PDF_NAME(Colors));       // e.g. 4

    /*
     * BUG: all operands are 'int'; the product (w * h * n) overflows
     * signed 32-bit when w=0x8001, h=0x8001, n=4.
     * 0x8001 * 0x8001 = 0x40010001 — already overflows int.
     * The result wraps to a small positive value, e.g. 0x10001.
     */
    stride = (w * n * bpc + 7) / 8;   // BUG: 'w * n * bpc' computed in int
    total  = stride * h;               // BUG: stride already corrupted; total tiny

    /* Allocates a fatally undersized buffer */
    samples = fz_malloc(ctx, total);   // e.g. allocates 0x10008 bytes

    /*
     * Decoder then writes w*h*n bytes of decompressed image data —
     * up to ~1 GB — into the undersized allocation.
     * Heap out-of-bounds write follows immediately.
     */
    fz_decomp_image_from_stream(ctx, fz_keep_stream(ctx, cstm),
                                 image, cstm != NULL, indexed, 0); // OOB write
    ...
}
Root cause: Attacker-controlled Width, Height, and Colors fields from a PDF image dictionary are multiplied as 32-bit signed integers in pdf_load_image_imp, causing the allocation size to wrap to a small value while the subsequent decode writes the full, untruncated image payload.

Memory Layout

The following layout is representative of a 64-bit glibc heap after MuPDF opens a crafted single-page PDF. Chunk sizes will vary by allocator and build flags, but the relative adjacency is stable.

HEAP STATE — after fz_malloc(ctx, 0x10008) for image samples:

  0x55a3c0012a00  [ prev_size=0x0000 | size=0x00021 | flags=PREV_INUSE ]
                  [ fz_page struct, 0x20 bytes                          ]

  0x55a3c0012a20  [ prev_size=0x0000 | size=0x10021 | flags=PREV_INUSE ]
                  [ samples buf, 0x10008 bytes  <-- undersized alloc    ]
  0x55a3c0022a40  [ prev_size=0x0000 | size=0x00061 | flags=PREV_INUSE ]
                  [ fz_colorspace *, adjacent victim chunk               ]

HEAP STATE — after fz_decomp_image_from_stream writes ~0x3FFF0000 bytes:

  0x55a3c0012a20  [ samples buf @ 0x55a3c0012a20, 0x10008 bytes ]
                  [ ... 0x10008 bytes of valid image data ...    ]

  0x55a3c0022a28  *** CORRUPTION BEGINS HERE (0x10008 bytes past start) ***
  0x55a3c0022a40  [ fz_colorspace chunk HEADER OVERWRITTEN:
                    prev_size -> attacker data
                    size      -> attacker data (clears PREV_INUSE)      ]
  0x55a3c0022aa0  [ fz_output * — next victim, also overwritten         ]

  ... corruption continues for gigabytes of decode output ...

Exploitation Mechanics

The primitive is a heap OOB write of attacker-controlled bytes (image pixel data, decompressed from a stream the attacker fully controls). The write starts at a predictable offset past the undersized allocation and continues for as long as the decoder runs. This gives strong write-what-where capability, bounded only by the decoder's output length.

EXPLOIT CHAIN (proof-of-concept, no ASLR bypass required for DoS;
               full RCE requires heap shaping):

1. Craft PDF with single-page image XObject:
     /Width 32769 /Height 32769 /ColorSpace /DeviceRGB /BitsPerComponent 8
     Embedded FlateDecode stream that decompresses to exactly
     (0x8001 * 0x8001 * 3) = 0x300060003 bytes of attacker payload.

2. MuPDF parses dictionary; pdf_load_image_imp computes:
     stride = (32769 * 3 * 8 + 7) / 8  -- in 32-bit int
            = (0x60018 + 7) / 8
     BUT w*n*bpc = 32769*3*8 = 786,456 — this stays in range alone,
     however with n=4, bpc=16:
     w*n*bpc = 0x8001*4*16 = 0x200040 ... tuning parameters to hit overflow
     is attacker's only constraint; multiple combinations trigger the wrap.

3. fz_malloc returns undersized buffer of ~0x10008 bytes adjacent to
   a live fz_colorspace or fz_font object on the glibc heap.

4. Decoder writes attacker-controlled pixel data past the allocation,
   overwriting the adjacent chunk's size field, fd/bk pointers (tcache),
   or object vtable pointer.

5. For tcache poisoning path:
     a. Overwrite tcache chunk->next with target address (e.g. __free_hook
        pre-glibc-2.34, or a GOT entry on older distros).
     b. Trigger a subsequent fz_malloc of matching size class.
     c. fz_malloc returns target address as allocation.
     d. Write /bin/sh\0 + execve gadget to returned pointer.
     e. MuPDF calls fz_free on a later object -> __free_hook fires.

6. On platforms without __free_hook (glibc >= 2.34):
     Overwrite fz_colorspace->get_luminance function pointer directly
     (offset +0x48 in struct, hit when rendering grayscale fallback).
     Next call to fz_colorspace_is_gray() invokes controlled pointer.

Patch Analysis

The fix, backported by Emilio Pozuelo Monfort into Debian LTS as 1.17.0+ds1-2+deb11u2, promotes all dimension operands to size_t before multiplication and adds explicit overflow checks using MuPDF's existing fz_add_int_overflow/fz_mul_size_t helpers (the upstream fix in 1.27.x uses similar guards).

/* BEFORE (vulnerable — MuPDF 1.27.0): */
stride = (w * n * bpc + 7) / 8;   // int * int * int — wraps silently
total  = stride * h;               // further truncated product

samples = fz_malloc(ctx, total);   // undersized allocation


/* AFTER (patched): */
size_t sw, sn, sbpc, sh;
sw   = (size_t)w;
sn   = (size_t)n;
sbpc = (size_t)bpc;
sh   = (size_t)h;

/* fz_mul_size_t throws fz_error on overflow — longjmp out safely */
stride = fz_mul_size_t(ctx, fz_mul_size_t(ctx, sw, sn), sbpc);
stride = (stride + 7) / 8;
total  = fz_mul_size_t(ctx, stride, sh);

if (total > FZ_MAX_IMAGE_BYTES)     // hard cap: 1 << 30
    fz_throw(ctx, FZ_ERROR_LIMIT, "image too large");

samples = fz_malloc(ctx, total);   // allocation now matches decode output

The fz_mul_size_t helper itself performs a compile-time-selected overflow check — either __builtin_mul_overflow on GCC/Clang, or an explicit division check on MSVC — and calls fz_throw on overflow, unwinding via MuPDF's fz_try/fz_catch exception mechanism rather than crashing. The additional FZ_MAX_IMAGE_BYTES cap prevents legitimate but absurdly large images from exhausting memory even when dimensions individually fit in size_t.

Detection and Indicators

Crashes manifest as SIGSEGV or SIGABRT (heap corruption detected by glibc) inside fz_decomp_image_from_stream or a subsequent allocator call. A sanitizer-instrumented build produces output similar to:

==12483==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60f000022a28
WRITE of size 4 at 0x60f000022a28 thread T0
    #0 fz_decomp_image_from_stream  pdf-image.c:412
    #1 pdf_load_image_imp           pdf-image.c:601
    #2 pdf_load_image               pdf-image.c:688
    #3 pdf_process_image            pdf-page.c:1021
SUMMARY: AddressSanitizer: heap-buffer-overflow pdf-image.c:412

Shadow bytes around the buggy address:
  0x0c1e7ffffd90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c1e7ffffd a0: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 07
  =>0x0c1e7ffffd b0:[fa]fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd

For detection in production environments: monitor mutool or embedding processes for abnormal RSS growth (decoding a crafted image drives RSS toward several gigabytes before the OOB write triggers an allocator abort). A triggering PDF will contain an image dictionary where /Width * /Height * /Colors * /BitsPerComponent overflows 32-bit signed arithmetic — grep extracted PDF object streams for dimension values exceeding 0x7fff in combination.

Remediation

  • Debian 11 (bullseye) LTS: Upgrade to mupdf 1.17.0+ds1-2+deb11u2 immediately. apt-get install --only-upgrade mupdf.
  • Upstream MuPDF: Verify you are running a build from the post-1.27.0 patch series that includes the fz_mul_size_t overflow guards in pdf-image.c. Build with -DHAVE_ASAN and run the Artifex regression suite against the PoC dimensions.
  • Embedders (libmupdf consumers): If you wrap MuPDF in a service, enforce per-process memory limits via setrlimit(RLIMIT_AS, ...) and run the renderer in a sandboxed subprocess with seccomp or equivalent. This converts RCE to DoS in the unpatched case.
  • Static analysis: Add a Semgrep rule matching int-typed multiplication of PDF dictionary integer fields before fz_malloc/malloc calls. The pattern pdf_dict_get_int(...) * pdf_dict_get_int(...) without intervening size_t cast is the signature of this bug class across the MuPDF codebase.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →