CVE-2026-93382
Overview
Background
- MCC record
- an OpenJPEG multiple-component-collection descriptor (
opj_mcc_record_t) held in a tile coding parameters’m_mcc_recordsarray, each entry pointing at MCT transform records used for component decorrelation. - `m_nb_mcc_records` vs `m_nb_max_mcc_records`
m_nb_mcc_recordsis the count of currently populated MCC records, whilem_nb_max_mcc_recordsis the allocated capacity of them_mcc_recordsbuffer.- `opj_j2k_copy_default_tcp_and_create_tcd()`
- the JPEG 2000 decode-setup routine that clones the default (main-header) tile coding parameters (
l_default_tcp) into each per-tilel_tcpbefore building the tile-coder/decoder.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: The use-after-free chain is speculative: the diff only adds copying of the MCC record count and does not show how a stale count leads to a freed record being used.
When cloning the default tile coding parameters into a per-tile l_tcp, the function memcpys the m_mcc_records buffer and copies the capacity field m_nb_max_mcc_records, but the pre-patch code never copied the active count m_nb_mcc_records. As a result the destination l_tcp->m_nb_mcc_records retained whatever value it held before (stale from an earlier tile or default-initialized), so the record count no longer described the actual set of records just copied into l_tcp->m_mcc_records. The loop that follows re-points each record’s internal MCT pointers by walking the records, and downstream tile processing and teardown iterate m_mcc_records according to m_nb_mcc_records; a count inconsistent with the freshly copied array leaves records either unvisited during pointer fixup or iterated beyond what was validly initialized. That mismatch produces MCC records whose interior pointers still reference the default (or a prior tile’s) structures, which are later freed, yielding a use-after-free when the per-tile records are dereferenced or destroyed (inferred).
The fix copies m_nb_mcc_records from l_default_tcp in lockstep with the buffer and its capacity, restoring the invariant that the count matches the copied contents.
m_nb_mcc_records, leaving count and contents desynchronized; the fix adds the one missing assignment so the per-tile count always reflects the records actually cloned.Attack Path
- Deliver a crafted JPEG 2000 image An attacker serves a PDF (or image) containing an OpenJPEG-encoded stream whose main header defines MCC records in the default tile coding parameters.
- Trigger per-tile clone
During decode,
opj_j2k_copy_default_tcp_and_create_tcd()clones the defaulttcpinto each tile’sl_tcp, copyingm_mcc_recordsandm_nb_max_mcc_recordsbut leavingl_tcp->m_nb_mcc_recordsat its stale/prior value. - Desynchronize count and contents
The per-tile record count now disagrees with the copied
m_mcc_recordsarray, so the pointer-fixup and later tile-processing loops iterate the array using an incorrect bound. - Use after free Records left with un-fixed interior pointers into the default/prior structures (or iterated past valid data) are dereferenced or freed after those backing structures are released, causing a use-after-free in the decoder (inferred).
Impact Assessment
(inferred). Preconditions are only that the target opens/renders attacker-controlled JPEG 2000 content with MCC records defined in the default tile parameters.Files Changed
third_party/libopenjpeg/0052-opj_j2k_copy_default_mcc_records.patchthird_party/libopenjpeg/j2k.cthird_party/libopenjpeg/README.pdfium
Audit Directions
- Paired count/capacity/buffer copiesWherever a struct is cloned by copying a data buffer plus its capacity field, verify the corresponding active count field is also copied; look for other
m_nb_*/m_nb_max_*pairs inj2k.c(e.g. MCT records) that may be updated inconsistently. - Stale-count reuse across tilesAudit per-tile setup that reuses or partially reinitializes
l_tcpstructures, checking that every count field describing a just-copied array is explicitly set rather than inherited from a previous tile or default init. - Fixup loops bounded by a copied countReview loops that re-point interior pointers after a
memcpyclone; confirm their iteration bound is derived from a count that provably matches the copied data before any of the source structures can be freed.
Patch
From cae4b1b6b08628f43d3dcfe8d722cbea8f9064b0 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 08 Sep 2026 13:35:07 -0700
Subject: [PATCH] Copy MCC record count in OpenJPEG tile setup
In opj_j2k_copy_default_tcp_and_create_tcd(), copy m_nb_mcc_records
from the default tile coding parameters along with m_nb_max_mcc_records.
TAG=agy
CONV=a534277b-a66b-488e-ab0a-8833cb8e63ea
Bug: 541707261
Change-Id: Ic9b58719dd662e067751e141782d629e93149bc6
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/157011
Commit-Queue: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
---
diff --git a/third_party/libopenjpeg/0052-opj_j2k_copy_default_mcc_records.patch b/third_party/libopenjpeg/0052-opj_j2k_copy_default_mcc_records.patch
new file mode 100644
index 0000000..ce6b829
--- /dev/null
+++ b/third_party/libopenjpeg/0052-opj_j2k_copy_default_mcc_records.patch
@@ -0,0 +1,12 @@
+diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c
+index 1fb83f8b5f..2835adbbb4 100644
+--- a/third_party/libopenjpeg/j2k.c
++++ b/third_party/libopenjpeg/j2k.c
+@@ -9306,6 +9306,7 @@ static OPJ_BOOL opj_j2k_copy_default_tcp_and_create_tcd(opj_j2k_t * p_j2k,
+ }
+ memcpy(l_tcp->m_mcc_records, l_default_tcp->m_mcc_records, l_mcc_records_size);
+ l_tcp->m_nb_max_mcc_records = l_default_tcp->m_nb_max_mcc_records;
++ l_tcp->m_nb_mcc_records = l_default_tcp->m_nb_mcc_records;
+
+ /* Copy the mcc record data from dflt_tile_cp to the current tile*/
+ l_src_mcc_rec = l_default_tcp->m_mcc_records;
diff --git a/third_party/libopenjpeg/README.pdfium b/third_party/libopenjpeg/README.pdfium
index 2823e66..c0650c6 100644
--- a/third_party/libopenjpeg/README.pdfium
+++ b/third_party/libopenjpeg/README.pdfium
@@ -37,6 +37,7 @@
0049-dwt_neon_5-3_idwt.patch: Cherry-pick https://github.com/uclouvain/openjpeg/pull/1630
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.
Note:
diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c
index 1fb83f8..2835adb 100644
--- a/third_party/libopenjpeg/j2k.c
+++ b/third_party/libopenjpeg/j2k.c
@@ -9306,6 +9306,7 @@
}
memcpy(l_tcp->m_mcc_records, l_default_tcp->m_mcc_records, l_mcc_records_size);
l_tcp->m_nb_max_mcc_records = l_default_tcp->m_nb_max_mcc_records;
+ l_tcp->m_nb_mcc_records = l_default_tcp->m_nb_mcc_records;
/* Copy the mcc record data from dflt_tile_cp to the current tile*/
l_src_mcc_rec = l_default_tcp->m_mcc_records;