High patch-now ✔︎ verified CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in PDFium
DescriptionHeap buffer overflow in PDFium
ComponentPDFium
Bug ClassOOB
Action tierpatch-now — corruption primitive, file-reachable in default config (confirmed) — 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 AIConfirmed
Reviewer notes: none; diff gates the end_header/end_pos writes on tp_index!=NULL && current_tpsno<nb_tps, an OOB write matching advisory's heap overflow. Claim correctly flags README's 'OOB read' mislabel.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker477033835
Fix commit40e5a93503ad (pdfium) +37/-7
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedsoiax
Reported2026-01-19 (per vendor advisory)
Fix landed2026-02-04 (public git)
Disclosed2026-02-18
Patch gap14 days — fix public in source before the advisory

Background

`libopenjpeg` in PDFium
PDFium’s bundled OpenJPEG copy (third_party/libopenjpeg/j2k.c) decodes JPEG 2000 codestreams, such as JPXDecode images embedded in PDFs; local changes are tracked as numbered patch files listed in README.pdfium.
Tile-parts and `SOT`/`SOD` markers
a JPEG 2000 tile can be split into tile-parts; each begins with an SOT marker and its data starts at SOD, which opj_j2k_read_sod handles.
Codestream index (`l_cstr_index`)
per-tile bookkeeping where tile_index[tile].tp_index is an array of tile-part records (with end_header and end_pos) holding nb_tps entries, and current_tpsno is the index of the tile-part currently being parsed.
`opj_j2k_add_tlmarker`
a sibling helper that already guarded its own tp_index access; the commit applies “the same check” here.

Root Cause Analysis

In opj_j2k_read_sod, the code read l_current_tile_part from tile_index[m_current_tile_number].current_tpsno and then wrote end_header and end_pos into tp_index[l_current_tile_part] with no check that tp_index was allocated or that the index was below nb_tps. A malformed codestream whose tile-part numbering exceeds the number of tile-part slots allocated for that tile (or with a null tp_index) therefore caused a heap write past the end of the tp_index array, or a write through a null pointer. The violated invariant is tp_index != NULL && current_tpsno < nb_tps before indexing.

The fix wraps both stores in exactly that condition, mirroring opj_j2k_add_tlmarker, and simply skips the index update otherwise before continuing to the existing opj_j2k_add_tlmarker call. Note that the diff’s removed lines are writes (matching the advisory’s “Heap buffer overflow”), although the new README.pdfium entry describes it as an “out of bounds read”.

Key insight
opj_j2k_read_sod trusted the parser-maintained current_tpsno as a valid index into tp_index; the fix adds the missing null and < nb_tps bounds check before recording tile-part end positions.

Attack Path

  1. Deliver a PDF with a JPEG 2000 image the attacker serves a PDF containing a JPXDecode image stream that PDFium hands to its bundled OpenJPEG decoder.
  2. Craft inconsistent tile-part counts the codestream’s SOT markers are arranged so that a tile’s current_tpsno reaches or exceeds that tile’s nb_tps, or tp_index is left unallocated (exact marker values inferred; not shown in the diff).
  3. Reach `SOD` handling when the SOD marker is processed, opj_j2k_read_sod indexes tp_index[l_current_tile_part] without bounds checking.
  4. Out-of-bounds heap write end_header and end_pos (stream-offset values) are written past the tp_index allocation, corrupting adjacent heap memory (what lies there and any resulting primitive is inferred).

Impact Assessment

The vendor rates this high severity as a heap buffer overflow in PDFium. The diff demonstrates an out-of-bounds write of codestream-offset values beyond the tp_index heap array during JPEG 2000 decoding, which runs in the sandboxed process that renders PDFs (process placement inferred); any control-flow hijack or code execution is (inferred) and would still be confined by the sandbox. Ordinary web content in a stock build can reach it by getting the user to open or navigate to a crafted PDF in Chrome’s built-in viewer, with no special flags shown as required; the advisory text provided does not report in-the-wild exploitation.

Files Changed

  • third_party/libopenjpeg/0047-opj_j2k_read_sod.patch
  • third_party/libopenjpeg/README.pdfium
  • third_party/libopenjpeg/j2k.c

Audit Directions

  • Parser-state indices into codestream-index arrays
    audit every use of current_tpsno, current_nb_tps, nb_tps and maxmarknum in j2k.c (e.g. SOT handling, TLM/PLT parsing) for array indexing that relies on header-declared counts without a matching bounds check.
  • Inconsistent guards between sibling helpers
    when one helper (like opj_j2k_add_tlmarker) has a bounds check and a neighbour touching the same structure does not, treat the unguarded one as suspect and diff against upstream OpenJPEG fixes.
  • Vendored-patch hygiene
    review other third-party decoders in PDFium (libtiff, libpng, jbig2) for upstream bounds fixes not yet carried as local patches, and verify patch descriptions match the actual bug class.
From 40e5a93503adcdd4b9b3fdc085a31a58b7b0f2dd Mon Sep 17 00:00:00 2001
From: Tom Sepez <[email protected]>
Date: Wed, 04 Feb 2026 10:59:25 -0800
Subject: [PATCH] Fix indexing in opj_j2k_read_sod()

Apply same check as in opj_j2k_add_tlmarker().

Bug: 477033835
Change-Id: I18def8cdd683cb1e8ed9359914e286de9837d382
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/142390
Auto-Submit: Tom Sepez <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
Commit-Queue: Lei Zhang <[email protected]>
Commit-Queue: Tom Sepez <[email protected]>
---

diff --git a/third_party/libopenjpeg/0047-opj_j2k_read_sod.patch b/third_party/libopenjpeg/0047-opj_j2k_read_sod.patch
new file mode 100644
index 0000000..93259a5
--- /dev/null
+++ b/third_party/libopenjpeg/0047-opj_j2k_read_sod.patch
@@ -0,0 +1,27 @@
+diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c
+index 5e9d75076..fc5ce62df 100644
+--- a/third_party/libopenjpeg/j2k.c
++++ b/third_party/libopenjpeg/j2k.c
+@@ -5082,13 +5082,15 @@ static OPJ_BOOL opj_j2k_read_sod(opj_j2k_t *p_j2k,
+ 
+         OPJ_UINT32 l_current_tile_part =
+             l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
+-        l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
+-            =
+-                l_current_pos;
+-        l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
+-            =
+-                l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
+-
++        if (l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index &&
++            l_current_tile_part < l_cstr_index->tile_index[p_j2k->m_current_tile_number].nb_tps) {
++          l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
++              =
++              l_current_pos;
++          l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
++              =
++              l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
++        }
+         if (OPJ_FALSE == opj_j2k_add_tlmarker(p_j2k->m_current_tile_number,
+                                               l_cstr_index,
+                                               J2K_MS_SOD,
diff --git a/third_party/libopenjpeg/README.pdfium b/third_party/libopenjpeg/README.pdfium
index 289f19a..905c640 100644
--- a/third_party/libopenjpeg/README.pdfium
+++ b/third_party/libopenjpeg/README.pdfium
@@ -33,6 +33,7 @@
 0039-opj_mqc_renorme.patch: Remove unused opj_mqc_renorme().
 0041-remove_opj_clock.patch: Remove unused opj_clock.h include.
 0046-func-ptr-mixup.patch: Prevent mixing up function pointer types.
+0047-opj_j2k_read_sod.patch: Fix out of bounds read.
 
 Note:
 
diff --git a/third_party/libopenjpeg/j2k.c b/third_party/libopenjpeg/j2k.c
index 5e9d750..fc5ce62 100644
--- a/third_party/libopenjpeg/j2k.c
+++ b/third_party/libopenjpeg/j2k.c
@@ -5082,13 +5082,15 @@
 
         OPJ_UINT32 l_current_tile_part =
             l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
-        l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
-            =
-                l_current_pos;
-        l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
-            =
-                l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
-
+        if (l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index &&
+            l_current_tile_part < l_cstr_index->tile_index[p_j2k->m_current_tile_number].nb_tps) {
+          l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
+              =
+              l_current_pos;
+          l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
+              =
+              l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
+        }
         if (OPJ_FALSE == opj_j2k_add_tlmarker(p_j2k->m_current_tile_number,
                                               l_cstr_index,
                                               J2K_MS_SOD,
Loading diff…

Original Bug Report

reported by [email protected]

PDFium heap-buffer-overflow at opj_j2k_read_sod

VULNERABILITY DETAILS

A heap buffer overflow vulnerability exists in OpenJPEG’s TLM (Tile Length Marker) decoding functionality. The vulnerability allows an attacker to write out-of-bounds to heap memory when processing a specially crafted JPEG 2000 file. The root cause is a missing bounds check in opj_j2k_read_sod() where the current_tpsno (current tile-part number) value derived from SOT markers is used to index the tp_index array without validating it against the allocated size (nb_tps).

REPRODUCTION CASE

Attached testcase uses multiple tiles, and more SOT fields, to crash even without ASAN. Pdfium_test ASAN log:

==48964==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x1235a21aa800 at pc 0x7ff74b7de0a0 bp 0x0057daefe200 sp 0x0057daefe248
WRITE of size 8 at 0x1235a21aa800 thread T0
    #0 0x7ff74b7de09f in opj_j2k_read_sod C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:5086
    #1 0x7ff74b7de09f in opj_j2k_read_tile_header C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:9923:19
    #2 0x7ff74b7f61f7 in opj_j2k_decode_tiles C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:12108:19
    #3 0x7ff74b7e4d56 in opj_j2k_exec C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:9190
    #4 0x7ff74b7e4d56 in opj_j2k_decode C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:12455:11
    #5 0x7ff74b70f36e in fxcodec::CJPX_Decoder::StartDecode(void) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fxcodec\jpx\cjpx_decoder.cpp:519:11
    #6 0x7ff74b934e72 in CPDF_DIB::LoadJpxBitmap(unsigned char) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:597:17
    #7 0x7ff74b92e7e2 in CPDF_DIB::CreateDecoder(unsigned char) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:452:22
    #8 0x7ff74b9310f6 in CPDF_DIB::StartLoadDIBBase(bool, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:207:31
    #9 0x7ff74b966e48 in CPDF_PageImageCache::Entry::StartGetCachedBitmap(class CPDF_PageImageCache *, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_pageimagecache.cpp:283:60
    #10 0x7ff74b966690 in CPDF_PageImageCache::StartGetCachedBitmap(class fxcrt::RetainPtr<class CPDF_Image>, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_pageimagecache.cpp:191:53
    #11 0x7ff74b9574f8 in CPDF_ImageLoader::Start(class CPDF_ImageObject const *, class CPDF_PageImageCache *, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_imageloader.cpp:35:31
    #12 0x7ff74b9b270e in CPDF_ImageRenderer::StartLoadDIBBase(void) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_imagerenderer.cpp:73:17
    #13 0x7ff74b9b77d0 in CPDF_ImageRenderer::Start(class CPDF_ImageObject *, class CFX_Matrix const &, bool) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_imagerenderer.cpp:202:7
    #14 0x7ff74b9d09d1 in CPDF_RenderStatus::ContinueSingleObject(class CPDF_PageObject *, class CFX_Matrix const &, class PauseIndicatorIface *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_renderstatus.cpp:286:25
    #15 0x7ff74b9bc09a in CPDF_ProgressiveRenderer::Continue(class PauseIndicatorIface *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_progressiverenderer.cpp:95:29
    #16 0x7ff74ad0ae5c in `anonymous namespace'::RenderPageImpl C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\cpdfsdk_renderpage.cpp:87:23
    #17 0x7ff74ad0b22d in CPDFSDK_RenderPageWithContext(class CPDF_PageRenderContext *, class CPDF_Page *, int, int, int, int, int, int, struct FPDF_COLORSCHEME_ const *, bool, class CPDFSDK_PauseAdapter *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\cpdfsdk_renderpage.cpp:117:3
    #18 0x7ff74ad23008 in FPDF_RenderPageBitmapWithColorScheme_Start C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\fpdf_progressive.cpp:83:3
    #19 0x7ff74acae42a in `anonymous namespace'::ProgressiveBitmapPageRenderer::Start C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1137:9
    #20 0x7ff74aca5254 in `anonymous namespace'::PdfProcessor::ProcessPage C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1607
    #21 0x7ff74aca5254 in `anonymous namespace'::Processor::ProcessPdf C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1789
    #22 0x7ff74aca5254 in main C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:2117:17
    #23 0x7ff750dcb22f in invoke_main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
    #24 0x7ff750dcb22f in __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
    #25 0x7ff99d2ce8d6  (C:\WINDOWS\System32\KERNEL32.DLL+0x18002e8d6)
    #26 0x7ff99dd6c53b  (C:\WINDOWS\SYSTEM32\ntdll.dll+0x18008c53b)

0x1235a21aa800 is located 8 bytes after 24-byte region [0x1235a21aa7e0,0x1235a21aa7f8)
allocated by thread T0 here:
    #0 0x7ff8e583ca26  (F:\fuzz\chromium-146.0.7637.0-win64-asan\clang_rt.asan_dynamic-x86_64.dll+0x18004ca26)
    #1 0x7ff7509f494d in calloc C:\b\s\w\ir\cache\builder\src\third_party\llvm\compiler-rt\lib\asan\asan_malloc_win_thunk.cpp:82
    #2 0x7ff74b7e9a35 in opj_j2k_build_tp_index_from_tlm C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:8948:57
    #3 0x7ff74b7e7b33 in opj_j2k_read_header_procedure C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:9161:5
    #4 0x7ff74b7da366 in opj_j2k_exec C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:9190
    #5 0x7ff74b7da366 in opj_j2k_read_header C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:8535:11
    #6 0x7ff74b70c620 in fxcodec::CJPX_Decoder::Init(class pdfium::span<unsigned char const, -1, unsigned char const *>, unsigned char, bool) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fxcodec\jpx\cjpx_decoder.cpp:503:8
    #7 0x7ff74b70c002 in fxcodec::CJPX_Decoder::Create(class pdfium::span<unsigned char const, -1, unsigned char const *>, enum fxcodec::CJPX_Decoder::ColorSpaceOption, unsigned char, bool) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fxcodec\jpx\cjpx_decoder.cpp:438:17
    #8 0x7ff74b934e1b in CPDF_DIB::LoadJpxBitmap(unsigned char) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:587:7
    #9 0x7ff74b92e7e2 in CPDF_DIB::CreateDecoder(unsigned char) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:452:22
    #10 0x7ff74b9310f6 in CPDF_DIB::StartLoadDIBBase(bool, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_dib.cpp:207:31
    #11 0x7ff74b966e48 in CPDF_PageImageCache::Entry::StartGetCachedBitmap(class CPDF_PageImageCache *, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_pageimagecache.cpp:283:60
    #12 0x7ff74b966690 in CPDF_PageImageCache::StartGetCachedBitmap(class fxcrt::RetainPtr<class CPDF_Image>, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_pageimagecache.cpp:191:53
    #13 0x7ff74b9574f8 in CPDF_ImageLoader::Start(class CPDF_ImageObject const *, class CPDF_PageImageCache *, class CPDF_Dictionary const *, class CPDF_Dictionary const *, bool, enum CPDF_ColorSpace::Family, bool, class CFX_STemplate<int> const &) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\page\cpdf_imageloader.cpp:35:31
    #14 0x7ff74b9b270e in CPDF_ImageRenderer::StartLoadDIBBase(void) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_imagerenderer.cpp:73:17
    #15 0x7ff74b9b77d0 in CPDF_ImageRenderer::Start(class CPDF_ImageObject *, class CFX_Matrix const &, bool) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_imagerenderer.cpp:202:7
    #16 0x7ff74b9d09d1 in CPDF_RenderStatus::ContinueSingleObject(class CPDF_PageObject *, class CFX_Matrix const &, class PauseIndicatorIface *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_renderstatus.cpp:286:25
    #17 0x7ff74b9bc09a in CPDF_ProgressiveRenderer::Continue(class PauseIndicatorIface *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\core\fpdfapi\render\cpdf_progressiverenderer.cpp:95:29
    #18 0x7ff74ad0ae5c in `anonymous namespace'::RenderPageImpl C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\cpdfsdk_renderpage.cpp:87:23
    #19 0x7ff74ad0b22d in CPDFSDK_RenderPageWithContext(class CPDF_PageRenderContext *, class CPDF_Page *, int, int, int, int, int, int, struct FPDF_COLORSCHEME_ const *, bool, class CPDFSDK_PauseAdapter *) C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\cpdfsdk_renderpage.cpp:117:3
    #20 0x7ff74ad23008 in FPDF_RenderPageBitmapWithColorScheme_Start C:\b\s\w\ir\cache\builder\src\third_party\pdfium\fpdfsdk\fpdf_progressive.cpp:83:3
    #21 0x7ff74acae42a in `anonymous namespace'::ProgressiveBitmapPageRenderer::Start C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1137:9
    #22 0x7ff74aca5254 in `anonymous namespace'::PdfProcessor::ProcessPage C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1607
    #23 0x7ff74aca5254 in `anonymous namespace'::Processor::ProcessPdf C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:1789
    #24 0x7ff74aca5254 in main C:\b\s\w\ir\cache\builder\src\third_party\pdfium\testing\pdfium_test.cc:2117:17
    #25 0x7ff750dcb22f in invoke_main D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
    #26 0x7ff750dcb22f in __scrt_common_main_seh D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
    #27 0x7ff99d2ce8d6  (C:\WINDOWS\System32\KERNEL32.DLL+0x18002e8d6)
    #28 0x7ff99dd6c53b  (C:\WINDOWS\SYSTEM32\ntdll.dll+0x18008c53b)

SUMMARY: AddressSanitizer: heap-buffer-overflow C:\b\s\w\ir\cache\builder\src\third_party\pdfium\third_party\libopenjpeg\j2k.c:5086 in opj_j2k_read_sod
Shadow bytes around the buggy address:
  0x1235a21aa580: fd fd fa fa fd fd fd fd fa fa fd fd fd fd fa fa
  0x1235a21aa600: fd fd fd fd fa fa 00 00 00 00 fa fa 00 00 00 00
  0x1235a21aa680: fa fa fd fd fd fd fa fa fd fd fd fd fa fa fd fd
  0x1235a21aa700: fd fd fa fa 00 00 00 00 fa fa fd fd fd fa fa fa
  0x1235a21aa780: 00 00 00 00 fa fa 00 00 00 fa fa fa 00 00 00 fa
=>0x1235a21aa800:[fa]fa 00 00 00 fa fa fa fa fa fa fa fa fa fa fa
  0x1235a21aa880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1235a21aa900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1235a21aa980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1235a21aaa00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x1235a21aaa80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb

Root Cause Analysis

Overview

The vulnerability stems from a mismatch between:

  1. The tp_index array size, which is allocated based on TLM (Tile Length Marker) entries
  2. The current_tpsno value, which is set from SOT (Start Of Tile-part) marker’s TPsot field

When a maliciously crafted file provides inconsistent TLM and SOT data, the decoder can write beyond the allocated tp_index buffer.

Step 1: TLM Allocates tp_index Based on Tile-Part Count

In j2k.c opj_j2k_build_tp_index_from_tlm() (lines 8914-8946), the tp_index array is allocated based on the number of tile-part entries found in TLM markers:

// Lines 8914-8919: First pass counts tile-parts per tile
for (i = 0; i < l_tlm->m_entries_count; ++i) {
    OPJ_UINT32 l_tile_index_no = l_tlm->m_tile_part_infos[i].m_tile_index;
    assert(l_tile_index_no < p_j2k->cstr_index->nb_of_tiles);
    p_j2k->cstr_index->tile_index[l_tile_index_no].tileno = l_tile_index_no;
    ++p_j2k->cstr_index->tile_index[l_tile_index_no].current_nb_tps;
}

// Lines 8938-8946: Allocate tp_index based on count
if (!l_tile_index->tp_index) {
    l_tile_index->tp_index = (opj_tp_index_t *) opj_calloc(
                                 l_tile_index->current_nb_tps, sizeof(opj_tp_index_t));
    if (! l_tile_index->tp_index) {
        opj_event_msg(p_manager, EVT_ERROR,
                      "opj_j2k_build_tp_index_from_tlm(): tile index allocation failed\n");
        goto error;
    }
}

If TLM says tile 0 has 2 tile-parts, then tp_index[2] is allocated with valid indices 0 and 1.

Step 2: SOT Sets current_tpsno Without Sufficient Validation

In opj_j2k_read_sot() (lines 4659-4660), the current_tpsno is set directly from the SOT marker’s TPsot field:

// Lines 4659-4660: current_tpsno set UNCONDITIONALLY from SOT
p_j2k->cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno =
    l_current_part;

Step 3: Inadequate TLM Invalidation Check

The check that should invalidate TLM when SOT data conflicts (lines 4662-4668) only compares TNsot (total number of tile-parts) against TLM data, NOT TPsot (current tile-part number):

// Lines 4662-4668: Only checks TNsot, NOT TPsot
if (!p_j2k->m_specific_param.m_decoder.m_tlm.m_is_invalid &&
        l_num_parts >   // ← l_num_parts is TNsot
        p_j2k->cstr_index->tile_index[p_j2k->m_current_tile_number].nb_tps) {
    opj_event_msg(p_manager, EVT_WARNING,
                  "SOT marker for tile %u declares more tile-parts than found in TLM marker.",
                  p_j2k->m_current_tile_number);
    p_j2k->m_specific_param.m_decoder.m_tlm.m_is_invalid = OPJ_TRUE;
}

When TNsot = 0 (unknown), this check passes even if TPsot >= nb_tps.

Step 4: Vulnerable Access in opj_j2k_read_sod

In opj_j2k_read_sod() (lines 5083-5090), the current_tpsno value is used to index tp_index without any bounds check:

// Lines 5083-5090: VULNERABLE - No bounds check before array access
OPJ_UINT32 l_current_tile_part =
    l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
    =
        l_current_pos;
l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
    =
        l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;

Compare this to the protected code in opj_j2k_add_tlmarker() (lines 8462-8465):

// Lines 8462-8465: PROTECTED - Has bounds check
if (cstr_index->tile_index[tileno].tp_index &&
        l_current_tile_part < cstr_index->tile_index[tileno].nb_tps) {  // ← Bounds check
    cstr_index->tile_index[tileno].tp_index[l_current_tile_part].start_pos = pos;
}

Memory Corruption Details

The overflow writes OPJ_OFF_T values to heap memory:

typedef struct opj_tp_index {
    OPJ_OFF_T start_pos;    // 8 bytes
    OPJ_OFF_T end_header;   // 8 bytes - WRITTEN BY VULNERABLE CODE
    OPJ_OFF_T end_pos;      // 8 bytes - WRITTEN BY VULNERABLE CODE
} opj_tp_index_t;           // Total: 24 bytes per entry

Attacker-controlled values:

  • end_header = current stream position (partially controlled via file layout)
  • end_pos = current stream position + SOT length (attacker-controlled)

Fix : Add Bounds Check in opj_j2k_read_sod

diff --git a/src/lib/openjp2/j2k.c b/src/lib/openjp2/j2k.c
index a2014c8..c4304c7 100644
--- a/src/lib/openjp2/j2k.c
+++ b/src/lib/openjp2/j2k.c
@@ -5082,12 +5082,15 @@ static OPJ_BOOL opj_j2k_read_sod(opj_j2k_t *p_j2k,

         OPJ_UINT32 l_current_tile_part =
             l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
+        if (l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index &&
+            l_current_tile_part < l_cstr_index->tile_index[p_j2k->m_current_tile_number].nb_tps) {
         l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_header
             =
                 l_current_pos;
         l_cstr_index->tile_index[p_j2k->m_current_tile_number].tp_index[l_current_tile_part].end_pos
             =
                 l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;
+            }

         if (OPJ_FALSE == opj_j2k_add_tlmarker(p_j2k->m_current_tile_number,
                                               l_cstr_index,

CREDIT INFORMATION

Reporter credit: soiax

View on issue tracker