# Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0 carries OSV-2025-219 heap-buffer-overflow write reachable from Image.open().load()

Pillow wheels bundle OpenJPEG 2.5.3/2.5.4 with an unchecked tp_index write in opj_j2k_read_sod, letting a crafted JP2 file reachable via Image.open().load() corrupt the heap.

- Payout: Research
- Severity: high
- Program: Pillow
- Found by: thesecguy45
- Disclosed: 2026-10-22
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-78g4-xqwv-q3g8
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-78g4-xqwv-q3g8
- Tags: area:oss domain:oss impact:dos impact:rce kind:research platform:ghsa sev:high tech:linux tech:python technique:incomplete-fix technique:source-review vuln:dos vuln:memory-corruption

**Target:** Bundled libopenjp2 JPEG 2000 decoder reached through PIL Image.open()/load()
**Root cause:** opj_j2k_read_sod writes tp_index[l_current_tile_part] without null or bounds check, where l_current_tile_part comes from the one-byte TPsot SOT field; a valid TLM marker allocates tp_index sized to TLM entries and TNsot=0 suppresses resizing, so TPsot walks past the allocation.
**Steps:**
1. Build Pillow wheel with ASan-instrumented OpenJPEG 2.5.4 interposed (stock Pillow 12.3.0).
2. Craft a JP2 codestream with a valid TLM marker (so opj_j2k_build_tp_index_from_tlm allocates tp_index sized to TLM entry count) and set `TNsot = 0` in every SOT so opj_j2k_read_sot skips resizing.
3. Set the one-byte `TPsot` field in SOT to a value beyond the TLM entry count.
4. Call `Image.open(p); im.load()` on the file; opj_j2k_read_sod writes `tp_index[l_current_tile_part].end_header/end_pos` out of bounds.
5. Observe ASan: WRITE of size 8 at j2k.c:5086, 8 bytes right of a 24-byte region from opj_j2k_build_tp_index_from_tlm (j2k.c:8939).
**How found:** Noticed the correct guard `if (tp_index && l_current_tile_part < nb_tps)` exists in opj_j2k_add_tlmarker (j2k.c:8459) writing the same array at the same index, but its sibling writer in opj_j2k_read_sod lacks it; traced how a valid TLM marker allocates tp_index and TNsot=0 keeps it from being resized, letting TPsot walk past. Also observed the inversion that OpenJPEG ≤2.5.2 is safe and the bug was introduced in 954c6e3c (2024-06-25), so keeping the dependency current creates exposure.
**Exploitation:** On a stock non-sanitised wheel the overflow performs 245 writes walking to +5856 bytes past a 24-byte allocation, tripping glibc heap-metadata checks (SIGABRT) — reliable DoS of a long-lived worker. With same-size-class objects groomed on both sides, the walk crosses opj_event_mgr_t holding three opj_msg_callback function pointers; opj_event_msg then calls one (event.c:136), giving attacker-chosen RIP (0x4000) and RSI (m_error_data). Shifting the codestream with a COM marker moves RIP (0x16ce → 0x4000). No finished exploit built; remote heap grooming and ASLR defeat not attempted.
**Escalation:** From OOB write to controlled function-pointer call by heap grooming same-size-class objects around the 24-byte tp_index allocation so the walk lands on opj_event_mgr_t callbacks.
**Insight:** When auditing a decoder, diff sibling writers to the same array: a bounds check present in one function (opj_j2k_add_tlmarker) but missing in another (opj_j2k_read_sod) is a strong signal. Also check whether a 'valid' marker (TLM) pre-allocates a structure that a later field (TNsot=0) prevents from being resized, letting an index field (TPsot) walk past it.

## Proof of concept (copied exactly from the original report)

**ASan heap-buffer-overflow write stack trace from Image.open().load()**

```
==766332==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 8
    #0 opj_j2k_read_sod          j2k.c:5086
    #1 opj_j2k_read_tile_header  j2k.c:9912
    #2 opj_read_tile_header      openjpeg.c:575
    #3 j2k_decode_entry          src/libImaging/Jpeg2KDecode.c:793
    #4 ImagingJpeg2KDecode       src/libImaging/Jpeg2KDecode.c:949
    #5 _decode                   src/decode.c:132
located 8 bytes to the right of a 24-byte region allocated by
    opj_j2k_build_tp_index_from_tlm j2k.c:8939
```

