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 <thestig@chromium.org>
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 <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
---
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) {