High chrome OOB 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassOOB
Tracker574061024
Fix commit0a42f6e64f28 (pdfium) +171/-13
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-27

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: numcomps is the component count and comps is a heap array of that many opj_image_comp_t entries. Consistency between numcomps and the actual comps allocation 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->comps and icc_profile_buf via opj_malloc/opj_calloc. Before the patch it returned void.
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-supplied p_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.

Key insight
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

  1. 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.)
  2. 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’s numcomps differs from the codestream’s component count. Old code returns success with comps == NULL / mismatched numcomps.
  3. Enter tile decode opj_j2k_decode() / opj_j2k_get_tile() proceed (no count guard), and the tile decoder iterates components using codestream indices up to m_private_image->numcomps.
  4. Out-of-bounds access Indexing p_image->comps[compno] beyond its real size (or dereferencing a NULL comps) produces an out-of-bounds read/write of heap opj_image_comp_t entries (and/or use of a freed/partial image).
  5. 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

The JPX decoder is reachable from untrusted documents (PDFs with embedded JPEG 2000 images) processed by PDFium, with no special privilege required. The diff substantiates two concrete sinks: (a) continued use of a partially-initialized/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

FunctionChangeNotes
if
third_party/libopenjpeg/0053-opj_copy_image_header.patch
modified
for
third_party/libopenjpeg/0053-opj_copy_image_header.patch
modified
if
third_party/libopenjpeg/image.c
modified
for
third_party/libopenjpeg/image.c
modified
if
third_party/libopenjpeg/j2k.c
modified

Files Changed

  • third_party/libopenjpeg/image.h
  • third_party/libopenjpeg/0053-opj_copy_image_header.patch
  • third_party/libopenjpeg/j2k.c
  • third_party/libopenjpeg/image.c
  • third_party/libopenjpeg/README.pdfium

Audit Directions

  • void helpers that can fail silently
    Audit OpenJPEG (and other vendored codecs) for void functions performing allocations whose failure is not propagated to callers; these leave inconsistent state the caller trusts.
  • numcomps vs comps invariants
    Review every site that indexes image->comps[...] by a count derived from a different image (codestream vs output vs caller), especially across m_private_image/m_output_image/p_image boundaries.
  • Allocation-failure reachability
    Check whether attacker-controlled sizes can drive opj_malloc/opj_calloc failure deterministically (huge numcomps/dimensions), making the “failed copy then use” path triggerable without true OOM.
  • Other opj_copy_image_header callers
    Confirm all callers now check the OPJ_BOOL result; any missed caller still proceeds on failure.
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) {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.