Allow the decompression-bomb limit to be scoped per call / per context, not only via the interpreter-global `Image.MAX_IMAGE_PIXELS`
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 35/100
Direzione di ricerca
Start with _decompression_bomb_check() in src/PIL/Image.py, then inspect the Image type stub and Tests/test_decompression_bomb.py. The issue proposes a scoped pixel limit with the global limit as the default; verify that strict and permissive concurrent jobs retain their own limits and existing global behavior remains compatible. Add the proposed tests, documentation, and release note.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What did you do?
Ran two jobs with different decompression-bomb limits in one Python process — the situation OCRmyPDF documents in its v17.11.0 release notes (https://github.com/ocrmypdf/OCRmyPDF/releases/tag/v17.11.0):
"Known limitation of concurrent in-process jobs: they must use the same
max_image_mpixels. Pillow's decompression-bomb limit is interpreter-global and the last job to set it wins. Use separate processes to run jobs with differing configurations."
OCRmyPDF's own workaround is to tell users to run separate processes. The underlying problem is in Pillow: PIL.Image.MAX_IMAGE_PIXELS is a single module global, _decompression_bomb_check() reads it at whatever moment each format plugin happens to run the check, and Image.open() (fp, mode, formats) offers no way to scope a limit to one call, one image, or one job.
Minimal reproduction (stock Pillow 12.3.0). A strict job (1M px limit, untrusted uploads) and a permissive job (20M px limit, its own large scans) each set the global once at start-up, then open the same 9M px image:
import io, threading, warnings
from PIL import Image
buf = io.BytesIO()
Image.new("RGB", (3000, 3000)).save(buf, format="PNG") # 9,000,000 pixels
data = buf.getvalue()
def outcome():
with warnings.catch_warnings():
warnings.simplefilter("error")
try:
with Image.open(io.BytesIO(data)) as im:
im.load()
return "OPENED"
except Image.DecompressionBombError:
return "REJECTED"
barrier = threading.Barrier(3)
results = {}
def job(name, limit):
Image.MAX_IMAGE_PIXELS = limit # set once, at job start-up
barrier.wait() # both jobs have "configured" themselves
results[name] = outcome()
t1 = threading.Thread(target=job, args=("permissive", 20_000_000))
t2 = threading.Thread(target=job, args=("strict", 1_000_000))
t1.start(); t2.start(); barrier.wait(); t1.join(); t2.join()
print(results)
What did you expect to happen?
The permissive job opens its image and the strict job rejects the same image: each job's limit applies to its own work.
What actually happened?
Whichever job set the global last governs both jobs. With the strict job setting last, the result is {'permissive': 'REJECTED', 'strict': 'REJECTED'} — the permissive job's legitimate image fails. Reverse the set order and it is {'permissive': 'OPENED', 'strict': 'OPENED'} — the strict job's protection silently disappears; nothing in its code changed and nothing warns it. The same interference happens without threads: any library that raises or lowers the global between another component's start-up and its next open() silently rewrites that component's policy. Re-setting the global immediately before each open() does not fix it either, because another thread can set it again between the set and Pillow's check, and checks also run at seek/load time for some formats.
Related history I found before filing: #9029 asked for the limit to be read from an environment variable and was closed after a set-it-at-start-up workaround was suggested — that still leaves a single process-wide value. #4939 made the BMP limit configurable in terms of the same global. Neither gives concurrent in-process components independent limits.
Proposal
Add a scoped form of the limit that leaves the global as the default. The least invasive shape appears to be a context manager backed by contextvars, e.g.:
with Image.limit_pixels(1_000_000):
im = Image.open(untrusted_upload)
_decompression_bomb_check() is the single enforcement point — every format plugin calls it — so it would consult the scoped value when one is active and fall back to MAX_IMAGE_PIXELS otherwise: one function changes, no plugin call sites change. contextvars gives the semantics the use case needs for free: each thread runs in its own context, so one job's scope cannot leak into another's; async tasks copy the context at creation; scopes nest and restore on exit. A scoped value of None would disable the checks inside the scope, matching the global's existing convention.
Alternative considered: a keyword on Image.open(), e.g. Image.open(fp, max_pixels=...). It reads naturally for the open-time check, but open() is lazy — plugins also run the check while seeking (e.g. GIF frames) and loading, where a per-open value would have to be stored on the image and passed from ~30 plugin call sites; it also would not cover the font-file checks that run outside any opened image. The context manager covers all of these through the one check function. If maintainers prefer the keyword shape anyway, the same fallback logic applies.
Compatibility: with no scope active, behaviour is byte-for-byte what it is today, including the warning threshold (limit), the error threshold (2× limit), and MAX_IMAGE_PIXELS = None disabling checks. Code that sets the global keeps working unchanged.
If the maintainers are open to this, I am willing to implement it: I have a working prototype (a ~40-line change to src/PIL/Image.py, plus the Image.pyi stub) in which the strict/permissive scenario above passes with each job in its own scope, and I would add tests in Tests/test_decompression_bomb.py, documentation, and a release note per the contributing guidelines.
What are your OS, Python and Pillow versions?
- OS: Linux
- Python: 3.12
- Pillow: 12.3.0
Disclosure: this contribution was written by Árbol, an AI assistant working under the direction of the account holder, who set the acceptance criteria and verified the results before submission.
- Lingua principale
- Python
- Stelle
- 13.9k
- Fork
- 2.6k
- Merge medio
- 1g 5h
- PR unite (30g)
- 179
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di python-pillow/Pillow
-
Converting F images to I turns large values, inf and NaN into -2147483648Forse già presa @usmanmateen l’ha presa 2 giorni fa. ApertaConversion
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
python-pillow/Pillow#10372 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Sun raster files with RLE encoding and odd-width rows decode shiftedForse già presa @PerryLink l’ha presa 1 giorno fa. Aperta🤖-assisted
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
python-pillow/Pillow#10370 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Warn that pre-applying arabic_reshaper + python-bidi corrupts text on a Raqm buildForse già presa @Syamjith-NK l’ha presa 65 giorni fa. ApertaDocumentation Font Rendering PNG Third Party Framework
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
python-pillow/Pillow#9907 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Image.putpixel incorrectly labels supported arguments in type annotationsForse già presa @radarhere l’ha presa 193 giorni fa. ApertaPNG
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
python-pillow/Pillow#9584 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
`ImageDraw.arc()` segfaults on a tuple subclass whose `__len__` is too largeForse già presa @Parsiffall1 l’ha presa 4 giorni fa. Aperta
Difficoltà 4/5 1-2 giorni Idoneità per principianti 58/100
python-pillow/Pillow#10358 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di python-pillow/Pillow
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
aws/sagemaker-python-sdk#6554 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
huggingface/diffusers#15170 ·
I maintainer di solito rispondono entro 1 giorno
-
[BUG] LLM conflict check ignores MNEMOSYNE_LLM_MAX_TOKENS, so reasoning models return empty repliesAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
mnemosyne-oss/mnemosyne#1325 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement meshtastic-2.8
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
pskillen/meshflow-api#459 ·