Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

Allow the decompression-bomb limit to be scoped per call / per context, not only via the interpreter-global `Image.MAX_IMAGE_PIXELS`

已关闭
#10,362 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
35/100
Issue 类型
功能
描述清晰度
描述清楚
活跃度
活跃
技术栈
python
领域
backend, security

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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.

主要语言
Python
星标
13.9k
派生
2.6k
平均合并
1 天 2 小时
30 天内合并 PR
176

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

python-pillow/Pillow 的其他 Issue

查看 python-pillow/Pillow 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。