Allow the decompression-bomb limit to be scoped per call / per context, not only via the interpreter-global `Image.MAX_IMAGE_PIXELS`
Maintainer antworten meist innerhalb von 1 Tag
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 5/5
- Geschätzter Aufwand
- Über eine Woche
- Anfängerfreundlichkeit
- 35/100
Rechercherichtung
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.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
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.
- Vorherrschende Sprache
- Python
- Sterne
- 13.9k
- Forks
- 2.6k
- Ø Merge
- 1 T. 2 Std.
- Gemergte PRs (30 T.)
- 176
Entwicklungsumgebung
- Kein Dockerfile und keine Docker-Compose-Datei
- Hat eine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus python-pillow/Pillow
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
python-pillow/Pillow#10370 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
Warn that pre-applying arabic_reshaper + python-bidi corrupts text on a Raqm buildEvtl. vergeben @Syamjith-NK hat das vor 63 Tagen übernommen. OffenDocumentation Font Rendering PNG Third Party Framework
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
python-pillow/Pillow#9907 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
Image.putpixel incorrectly labels supported arguments in type annotationsEvtl. vergeben @radarhere hat das vor 191 Tagen übernommen. OffenPNG
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
python-pillow/Pillow#9584 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
`ImageDraw.arc()` segfaults on a tuple subclass whose `__len__` is too largeEvtl. vergeben @Parsiffall1 hat das vor 2 Tagen übernommen. Offen
Schwierigkeit 4/5 1-2 Tage Anfängerfreundlichkeit 58/100
python-pillow/Pillow#10358 · 2 Kommentare ·
Maintainer antworten meist innerhalb von 1 Tag
-
JPEG saved with keep_rgb=True and dpi reopens with incorrect colorsEvtl. vergeben @Muguett-DBY hat das vor 4 Tagen übernommen. Offen🤖-assisted JPEG
Schwierigkeit 4/5 1-2 Tage Anfängerfreundlichkeit 25/100
python-pillow/Pillow#10344 ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in python-pillow/Pillow
Ähnliche Issues
-
predict_file with crop_model duplicates predictions once per imageEvtl. vergeben Ein verknüpfter Pull Request ist offen oder bereits gemergt. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 62/100
weecology/DeepForest#1445 ·
-
agent-ready priority: medium status: ready
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 68/100
melodic-software/claude-code-plugins#11864 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 78/100
PolicyEngine/policyengine-uk#2785 ·
Maintainer antworten meist innerhalb von 2 Tagen
-
correction metadata
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 62/100
acl-org/acl-anthology#10724 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
use-agent-os/agent-os#3739 ·
Maintainer antworten meist innerhalb von 1 Tag