CVE-2026-118345
Overview
Background
- OpenJPEG (libopenjpeg)
- The JPEG 2000 codec vendored inside PDFium (
third_party/libopenjpeg) and used to decode JPX image streams embedded in PDFs. PDFium carries local patches (00NN-*.patch) on top of upstream OpenJPEG. - opj_image_t / opj_image_comp_t
- An OpenJPEG image header:
numcompsis the component count andcompsis a heap array of that manyopj_image_comp_tentries. Consistency betweennumcompsand the actualcompsallocation is a core invariant. - opj_copy_image_header()
- Copies header fields (dimensions, components, ICC profile) from a source image to a destination image, allocating
p_image_dest->compsandicc_profile_bufviaopj_malloc/opj_calloc. Before the patch it returnedvoid. - opj_j2k_read_header / opj_j2k_decode / opj_j2k_get_tile / opj_j2k_start_compress
- J2K pipeline entry points that call
opj_copy_image_header()to move header information between the private codestream image (m_private_image), the output image (m_output_image), and the caller-suppliedp_image. - m_private_image / m_output_image
- Internal J2K images; the tile decoder indexes component arrays using the codestream component indices derived from
m_private_image->numcomps.
Root Cause Analysis
The patch, applied as PDFium local patch 0053-opj_copy_image_header.patch, changes opj_copy_image_header() from void to OPJ_BOOL so allocation failures and invalid input are propagated instead of silently swallowed, and it hardens the J2K callers. The advisory labels this a “Use after free in PDFium”; the diff more precisely shows allocation-failure mishandling plus a component-count consistency bug that enables out-of-bounds component indexing — worth noting the label is at best imprecise. Previously, when opj_malloc/opj_calloc for p_image_dest->comps (or icc_profile_buf) failed, the function set the pointer to NULL / numcomps = 0 and simply return;ed, leaving the destination image in an inconsistent partially-initialized state while callers believed the copy succeeded. Callers such as opj_j2k_read_header() then returned OPJ_TRUE, handing back an opj_image_t whose comps could be NULL or whose numcomps no longer matched the codestream — subsequent code dereferences/indexes that array.
The hardening has three parts. (1) opj_copy_image_header() now rejects degenerate input up front (p_image_src->numcomps == 0 || !p_image_src->comps → OPJ_FALSE) and returns OPJ_FALSE on each allocation failure. (2) Every caller checks the return value: opj_j2k_read_header() now calls opj_image_destroy(*p_image); *p_image = NULL; return OPJ_FALSE; on failure (so callers can no longer use a half-built image), and opj_j2k_decode/opj_j2k_get_tile/opj_j2k_start_compress bail out on OPJ_FALSE. (3) Critically, opj_j2k_decode() adds p_image->numcomps != p_j2k->m_private_image->numcomps (and non-NULL comps/m_private_image) to its guard, and opj_j2k_get_tile() adds !p_image->comps || !p_j2k->m_private_image, with the comment “The tile decoder uses the codestream component indices.” Because the tile decoder walks components by codestream index (bounded by m_private_image->numcomps) but writes into the caller’s p_image->comps, a count mismatch (or a NULL comps left behind by a failed copy) lets the decoder index p_image->comps[compno] out of bounds — an OOB read/write — and lets later use operate on a freed/partial image.
opj_copy_image_header() silently discarded opj_malloc failures and never validated component counts, so callers proceeded with an image whose comps/numcomps were inconsistent with the codestream — and the tile decoder then indexed that component array by codestream index, out of bounds.Attack Path
- Deliver JPX An attacker embeds a crafted JPEG 2000 (JPX) stream in a PDF (or otherwise reaches PDFium’s OpenJPEG decoder). (web/PDF reachability inferred from the PDFium/JPX component.)
- Force inconsistent header
The codestream is shaped so that an allocation inside
opj_copy_image_header()fails under memory pressure, or so that the caller-supplied image’snumcompsdiffers from the codestream’s component count. Old code returns success withcomps == NULL/ mismatchednumcomps. - Enter tile decode
opj_j2k_decode()/opj_j2k_get_tile()proceed (no count guard), and the tile decoder iterates components using codestream indices up tom_private_image->numcomps. - Out-of-bounds access
Indexing
p_image->comps[compno]beyond its real size (or dereferencing aNULLcomps) produces an out-of-bounds read/write of heapopj_image_comp_tentries (and/or use of a freed/partial image). - Primitive Heap out-of-bounds access in the JP2/JPX decoder; with a controlled component-count delta this is an attacker-influenced OOB, potentially corrupting adjacent heap state (escalation inferred).
Impact Assessment
NULL-comps image after an unchecked allocation failure, and (b) out-of-bounds component indexing when p_image->numcomps and the codestream component count disagree. The advisory’s “use after free” is plausible for the stale-image path but the code most directly proves input-driven OOB component access; both are memory-corruption-class. Strength is an attacker-influenced heap OOB read/write gated on triggering an allocation failure or supplying a mismatched component count; full control/RCE is inferred, not shown.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/libopenjpeg/0053-opj_copy_image_header.patch |
modified | |
forthird_party/libopenjpeg/0053-opj_copy_image_header.patch |
modified | |
ifthird_party/libopenjpeg/image.c |
modified | |
forthird_party/libopenjpeg/image.c |
modified | |
ifthird_party/libopenjpeg/j2k.c |
modified |
Files Changed
third_party/libopenjpeg/image.hthird_party/libopenjpeg/0053-opj_copy_image_header.patchthird_party/libopenjpeg/j2k.cthird_party/libopenjpeg/image.cthird_party/libopenjpeg/README.pdfium
Audit Directions
- void helpers that can fail silentlyAudit OpenJPEG (and other vendored codecs) for
voidfunctions performing allocations whose failure is not propagated to callers; these leave inconsistent state the caller trusts. - numcomps vs comps invariantsReview every site that indexes
image->comps[...]by a count derived from a different image (codestream vs output vs caller), especially acrossm_private_image/m_output_image/p_imageboundaries. - Allocation-failure reachabilityCheck whether attacker-controlled sizes can drive
opj_malloc/opj_callocfailure deterministically (hugenumcomps/dimensions), making the “failed copy then use” path triggerable without true OOM. - Other opj_copy_image_header callersConfirm all callers now check the
OPJ_BOOLresult; any missed caller still proceeds on failure.
Patch
From 0a42f6e64f28603f3aee118a1c140e9bd0647b71 Mon Sep 17 00:00:00 2001 From: Lei Zhang <[email protected]> Date: Mon, 19 Oct 2026 10:51:37 -0700 Subject: [PATCH] Improve memory allocation failure handling in OpenJPEG Propagate opj_malloc() failure in opj_copy_image_header() to the callers, so they can also fail early instead of continuing after the memory allocation failure. Credit to the bug reporter for providing this patch. Bug: 574061024 Change-Id: Ifb53b384f4c0f0386794a76b1ff46e779e6849b7 Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/161130 Commit-Queue: Lei Zhang <[email protected]> Reviewed-by: Tom Sepez <[email protected]> --- diff --git a/third_party/libopenjpeg/0053-opj_copy_image_header.patch b/third_party/libopenjpeg/0053-opj_copy_image_header.patch new file mode 100644 index 0000000..ee6f802 --- /dev/null +++ b/third_party/libopenjpeg/0053-opj_copy_image_header.patch @@ -0,0 +1,140 @@ +diff --git a/third_party/libopenjpeg/image.c b/third_party/libopenjpeg/image.c +index 017201a..9af8a06 100644 +--- a/third_party/libopenjpeg/image.c ++++ b/third_party/libopenjpeg/image.c +@@ -160,9 +160,10 @@ void opj_image_comp_header_update(opj_image_t * p_image_header, + * @param p_image_src the src image + * @param p_image_dest the dest image + * ++ * @return OPJ_TRUE if successful, OPJ_FALSE otherwise. + */ +-void opj_copy_image_header(const opj_image_t* p_image_src, +- opj_image_t* p_image_dest) ++OPJ_BOOL opj_copy_image_header(const opj_image_t* p_image_src, ++ opj_image_t* p_image_dest) + { + OPJ_UINT32 compno; + +@@ -170,6 +171,10 @@ void opj_copy_image_header(const opj_image_t* p_image_src, + assert(p_image_src != 00); + assert(p_image_dest != 00); + ++ if (p_image_src->numcomps == 0 || !p_image_src->comps) { ++ return OPJ_FALSE; ++ } ++ + p_image_dest->x0 = p_image_src->x0; + p_image_dest->y0 = p_image_src->y0; + p_image_dest->x1 = p_image_src->x1; +@@ -193,7 +198,7 @@ void opj_copy_image_header(const opj_image_t* p_image_src, + if (!p_image_dest->comps) { + p_image_dest->comps = NULL; + p_image_dest->numcomps = 0; +- return; ++ return OPJ_FALSE; + } + + for (compno = 0; compno < p_image_dest->numcomps; compno++) { +@@ -212,7 +217,7 @@ void opj_copy_image_header(const opj_image_t* p_image_src, + if (!p_image_dest->icc_profile_buf) { + p_image_dest->icc_profile_buf = NULL; + p_image_dest->icc_profile_len = 0; +- return; ++ return OPJ_FALSE; + } + memcpy(p_image_dest->icc_profile_buf, + p_image_src->icc_profile_buf, +@@ -221,7 +226,7 @@ void opj_copy_image_header(const opj_image_t* p_image_src, + p_image_dest->icc_profile_buf = NULL; + } + +- return; ++ return OPJ_TRUE; + } + + opj_image_t* OPJ_CALLCONV opj_image_tile_create(OPJ_UINT32 numcmpts, +diff --git a/third_party/libopenjpeg/image.h b/third_party/libopenjpeg/image.h +index bad83c6..95c8d1a 100644 +--- a/third_party/libopenjpeg/image.h ++++ b/third_party/libopenjpeg/image.h +@@ -61,8 +61,8 @@ opj_image_t* opj_image_create0(void); + void opj_image_comp_header_update(opj_image_t * p_image, + const struct opj_cp* p_cp); + +-void opj_copy_image_header(const opj_image_t* p_image_src, +- opj_image_t* p_image_dest); ++OPJ_BOOL opj_copy_image_header(const opj_image_t* p_image_src, ++ opj_image_t* p_image_dest); + + /*@}*/ + +diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c +index 2835adb..6ddbcba 100644 +--- a/third_party/libopenjpeg/j2k.c ++++ b/third_party/libopenjpeg/j2k.c +@@ -8546,7 +8546,11 @@ OPJ_BOOL opj_j2k_read_header(opj_stream_private_t *p_stream, + } + + /* Copy codestream image information to the output image */ +- opj_copy_image_header(p_j2k->m_private_image, *p_image); ++ if (!opj_copy_image_header(p_j2k->m_private_image, *p_image)) { ++ opj_image_destroy(*p_image); ++ *p_image = NULL; ++ return OPJ_FALSE; ++ } + + return OPJ_TRUE; + } +@@ -12415,7 +12419,9 @@ OPJ_BOOL opj_j2k_decode(void * j2k, + opj_event_mgr_t * p_manager) + { + opj_j2k_t* p_j2k = (opj_j2k_t*)j2k; +- if (!p_image) { ++ /* The tile decoder uses the codestream component indices. */ ++ if (!p_image || !p_image->comps || !p_j2k->m_private_image || ++ p_image->numcomps != p_j2k->m_private_image->numcomps) { + return OPJ_FALSE; + } + +@@ -12450,7 +12456,9 @@ OPJ_BOOL opj_j2k_decode(void * j2k, + return OPJ_FALSE; + } + } +- opj_copy_image_header(p_image, p_j2k->m_output_image); ++ if (!opj_copy_image_header(p_image, p_j2k->m_output_image)) { ++ return OPJ_FALSE; ++ } + + /* customization of the decoding */ + if (!opj_j2k_setup_decoding(p_j2k, p_manager)) { +@@ -12479,7 +12487,7 @@ OPJ_BOOL opj_j2k_get_tile(void *j2k, + OPJ_UINT32 l_tile_x, l_tile_y; + opj_image_comp_t* l_img_comp; + +- if (!p_image) { ++ if (!p_image || !p_image->comps || !p_j2k->m_private_image) { + opj_event_msg(p_manager, EVT_ERROR, "We need an image previously created.\n"); + return OPJ_FALSE; + } +@@ -12562,7 +12570,9 @@ OPJ_BOOL opj_j2k_get_tile(void *j2k, + if (!(p_j2k->m_output_image)) { + return OPJ_FALSE; + } +- opj_copy_image_header(p_image, p_j2k->m_output_image); ++ if (!opj_copy_image_header(p_image, p_j2k->m_output_image)) { ++ return OPJ_FALSE; ++ } + + p_j2k->m_specific_param.m_decoder.m_tile_ind_to_dec = (OPJ_INT32)tile_index; + +@@ -12822,7 +12832,9 @@ OPJ_BOOL opj_j2k_start_compress(void *j2k, + opj_event_msg(p_manager, EVT_ERROR, "Failed to allocate image header."); + return OPJ_FALSE; + } +- opj_copy_image_header(p_image, p_j2k->m_private_image); ++ if (!opj_copy_image_header(p_image, p_j2k->m_private_image)) { ++ return OPJ_FALSE; ++ } + + /* TODO_MSD: Find a better way */ + if (p_image->comps) { diff --git a/third_party/libopenjpeg/README.pdfium b/third_party/libopenjpeg/README.pdfium index c0650c6..a7ba86a 100644 --- a/third_party/libopenjpeg/README.pdfium +++ b/third_party/libopenjpeg/README.pdfium @@ -38,6 +38,7 @@ 0050-opj_pi_initialise_encode_overflow.patch: Cherry-pick https://github.com/uclouvain/openjpeg/pull/1619 0051-opj_j2k_copy_default_double_free.patch: Fix a double free. 0052-opj_j2k_copy_default_mcc_records.patch: Fix MCC records count. +0053-opj_copy_image_header.patch: Propagate image header copy failures and validate component counts. Note: diff --git a/third_party/libopenjpeg/image.c b/third_party/libopenjpeg/image.c index 017201a..9af8a06 100644 --- a/third_party/libopenjpeg/image.c +++ b/third_party/libopenjpeg/image.c @@ -160,9 +160,10 @@ * @param p_image_src the src image * @param p_image_dest the dest image * + * @return OPJ_TRUE if successful, OPJ_FALSE otherwise. */ -void opj_copy_image_header(const opj_image_t* p_image_src, - opj_image_t* p_image_dest) +OPJ_BOOL opj_copy_image_header(const opj_image_t* p_image_src, + opj_image_t* p_image_dest) { OPJ_UINT32 compno; @@ -170,6 +171,10 @@ assert(p_image_src != 00); assert(p_image_dest != 00); + if (p_image_src->numcomps == 0 || !p_image_src->comps) { + return OPJ_FALSE; + } + p_image_dest->x0 = p_image_src->x0; p_image_dest->y0 = p_image_src->y0; p_image_dest->x1 = p_image_src->x1; @@ -193,7 +198,7 @@ if (!p_image_dest->comps) { p_image_dest->comps = NULL; p_image_dest->numcomps = 0; - return; + return OPJ_FALSE; } for (compno = 0; compno < p_image_dest->numcomps; compno++) { @@ -212,7 +217,7 @@ if (!p_image_dest->icc_profile_buf) { p_image_dest->icc_profile_buf = NULL; p_image_dest->icc_profile_len = 0; - return; + return OPJ_FALSE; } memcpy(p_image_dest->icc_profile_buf, p_image_src->icc_profile_buf, @@ -221,7 +226,7 @@ p_image_dest->icc_profile_buf = NULL; } - return; + return OPJ_TRUE; } opj_image_t* OPJ_CALLCONV opj_image_tile_create(OPJ_UINT32 numcmpts, diff --git a/third_party/libopenjpeg/image.h b/third_party/libopenjpeg/image.h index bad83c6..95c8d1a 100644 --- a/third_party/libopenjpeg/image.h +++ b/third_party/libopenjpeg/image.h @@ -61,8 +61,8 @@ void opj_image_comp_header_update(opj_image_t * p_image, const struct opj_cp* p_cp); -void opj_copy_image_header(const opj_image_t* p_image_src, - opj_image_t* p_image_dest); +OPJ_BOOL opj_copy_image_header(const opj_image_t* p_image_src, + opj_image_t* p_image_dest); /*@}*/ diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c index 2835adb..6ddbcba 100644 --- a/third_party/libopenjpeg/j2k.c +++ b/third_party/libopenjpeg/j2k.c @@ -8546,7 +8546,11 @@ } /* Copy codestream image information to the output image */ - opj_copy_image_header(p_j2k->m_private_image, *p_image); + if (!opj_copy_image_header(p_j2k->m_private_image, *p_image)) { + opj_image_destroy(*p_image); + *p_image = NULL; + return OPJ_FALSE; + } return OPJ_TRUE; } @@ -12415,7 +12419,9 @@ opj_event_mgr_t * p_manager) { opj_j2k_t* p_j2k = (opj_j2k_t*)j2k; - if (!p_image) { + /* The tile decoder uses the codestream component indices. */ + if (!p_image || !p_image->comps || !p_j2k->m_private_image || + p_image->numcomps != p_j2k->m_private_image->numcomps) { return OPJ_FALSE; } @@ -12450,7 +12456,9 @@ return OPJ_FALSE; } } - opj_copy_image_header(p_image, p_j2k->m_output_image); + if (!opj_copy_image_header(p_image, p_j2k->m_output_image)) { + return OPJ_FALSE; + } /* customization of the decoding */ if (!opj_j2k_setup_decoding(p_j2k, p_manager)) { @@ -12479,7 +12487,7 @@ OPJ_UINT32 l_tile_x, l_tile_y; opj_image_comp_t* l_img_comp; - if (!p_image) { + if (!p_image || !p_image->comps || !p_j2k->m_private_image) { opj_event_msg(p_manager, EVT_ERROR, "We need an image previously created.\n"); return OPJ_FALSE; } @@ -12562,7 +12570,9 @@ if (!(p_j2k->m_output_image)) { return OPJ_FALSE; } - opj_copy_image_header(p_image, p_j2k->m_output_image); + if (!opj_copy_image_header(p_image, p_j2k->m_output_image)) { + return OPJ_FALSE; + } p_j2k->m_specific_param.m_decoder.m_tile_ind_to_dec = (OPJ_INT32)tile_index; @@ -12822,7 +12832,9 @@ opj_event_msg(p_manager, EVT_ERROR, "Failed to allocate image header."); return OPJ_FALSE; } - opj_copy_image_header(p_image, p_j2k->m_private_image); + if (!opj_copy_image_header(p_image, p_j2k->m_private_image)) { + return OPJ_FALSE; + } /* TODO_MSD: Find a better way */ if (p_image->comps) {