High patch-now ~ plausible CVSS 8.8 chrome UAF 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Action tierpatch-now — corruption primitive, file-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker541707261
Fix commitcae4b1b6b086 (pdfium) +14/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedWinD39 - Huynh Dinh Vu
Reported2026-08-02 (per vendor advisory)
Fix landed2026-09-08 (public git)
Disclosed2026-09-17
Patch gap9 days — fix public in source before the advisory

Background

MCC record
an OpenJPEG multiple-component-collection descriptor (opj_mcc_record_t) held in a tile coding parameters’ m_mcc_records array, each entry pointing at MCT transform records used for component decorrelation.
`m_nb_mcc_records` vs `m_nb_max_mcc_records`
m_nb_mcc_records is the count of currently populated MCC records, while m_nb_max_mcc_records is the allocated capacity of the m_mcc_records buffer.
`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-tile l_tcp before building the tile-coder/decoder.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
The single mistake is copying the MCC-records buffer and its capacity while forgetting to copy the companion active count 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

  1. 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.
  2. Trigger per-tile clone During decode, opj_j2k_copy_default_tcp_and_create_tcd() clones the default tcp into each tile’s l_tcp, copying m_mcc_records and m_nb_max_mcc_records but leaving l_tcp->m_nb_mcc_records at its stale/prior value.
  3. Desynchronize count and contents The per-tile record count now disagrees with the copied m_mcc_records array, so the pointer-fixup and later tile-processing loops iterate the array using an incorrect bound.
  4. 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

An attacker who gets the victim to render a malicious JPEG 2000 stream can cause a use-after-free inside PDFium’s bundled OpenJPEG decoder, which runs in Chrome’s sandboxed renderer/PDF process. The advisory classifies this as a use-after-free in PDFium; memory-corruption primitives beyond the crash, and any escape from the sandbox to code execution, are not demonstrated by the patch (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.patch
  • third_party/libopenjpeg/j2k.c
  • third_party/libopenjpeg/README.pdfium

Audit Directions

  • Paired count/capacity/buffer copies
    Wherever 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 in j2k.c (e.g. MCT records) that may be updated inconsistently.
  • Stale-count reuse across tiles
    Audit per-tile setup that reuses or partially reinitializes l_tcp structures, 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 count
    Review loops that re-point interior pointers after a memcpy clone; confirm their iteration bound is derived from a count that provably matches the copied data before any of the source structures can be freed.
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;
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.