Open
fix(cve): 修复 CVE-2026-96638 在 python-pillow 中的漏洞 #322
fix(cve): 修复 CVE-2026-96638 在 python-pillow 中的漏洞 #322
Open
infra_teamAdd on 8 days ago
Total: 2 files changed+97-1
@@ -0,0 +1,92 @@
1+From ef5b931d8b32676f8145b6ffda969fbc246ad6d4 Mon Sep 17 00:00:00 2001
2+From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
3+Date: Thu, 17 Sep 2026 21:49:45 +1000
4+Subject: [PATCH] Do not decode more BMP data than necessary (#10015)
5+ 
6+Backport of the upstream fix for CVE-2026-96638 / GHSA-24rc-8hj2-v98c
7+(fixed in Pillow 13.0.0), adapted to Pillow 10.3.0.
8+ 
9+BmpRleDecoder.decode() appended delta-escape padding and absolute-mode
10+runs to the output buffer without checking the remaining dest_length,
11+so a small crafted BMP (RLE8/RLE4) could expand into gigabytes of
12+zero-filled data (CWE-400/CWE-770). The fix caps every append at the
13+remaining destination length and is the combination of two upstream
14+commits, applied here in dependency order:
15+ 
16+ 9af332ef (#9824) "Do not read any further than necessary" caps
17+ absolute-mode runs: data_count = min(dest_length - len(data),
18+ byte[0]), rle4 reads data_count // 2 bytes, non-rle4 reads
19+ data_count bytes, and the short-read break also stops once the
20+ destination buffer is full.
21+ https://github.com/python-pillow/Pillow/commit/9af332ef2b27bc54fcf34f6c84de21d27d8c0065
22+ 
23+ e7794835 (#10015, referenced by NVD as the fix commit) caps delta
24+ escapes: data += b"\x00" * min(dest_length - len(data), right + up *
25+ self.state.xsize).
26+ https://github.com/python-pillow/Pillow/commit/e7794835e323ff2691d9b3a974cade1a44301b9a
27+ 
28+Upstream-commit: https://github.com/python-pillow/Pillow/commit/e7794835e323ff2691d9b3a974cade1a44301b9a
29+ 
30+Adaptation to 10.3.0 (both hunks rewritten by hand; neither upstream
31+pre-image matches 10.3.0, and the interdependent #9443/#9710 changes
32+are not part of this CVE's scope and are not ported):
33+- delta branch: the capped append matches e7794835's post-image
34+ verbatim; the surrounding 10.3.0 lines (including the pre-#9443
35+ "right, up = self.fd.read(2)") are kept unchanged.
36+- absolute branch: 10.3.0 has no data_count and its rle4 loop appends
37+ 2 pixels per byte unconditionally, so data_count is introduced at
38+ the branch entry and rle4 uses byte_count = data_count // 2, which
39+ is verbatim-equivalent to the original byte[0] // 2 whenever no
40+ truncation happens (13.0.0's (data_count + 1) // 2 and odd-pixel
41+ guard belong to #9710 and are intentionally not ported); the
42+ non-rle4 branch uses byte_count = data_count, and the break
43+ condition is extended with "or len(data) == dest_length" as in
44+ #9824.
45+ 
46+Verified on 10.3.0: crafted delta-escape and absolute-mode streams that
47+previously overshot dest_length (25755 vs 10000, 255 vs 100, 254 vs
48+100) now stop exactly at dest_length, while non-truncating streams
49+decode byte-identically to the pristine decoder.
50+ 
51+Signed-off-by: infra_team <zhaiwenjie1@huawei.com>
52+---
53+ src/PIL/BmpImagePlugin.py | 11 +++++++----
54+ 1 file changed, 7 insertions(+), 4 deletions(-)
55+ 
56+diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
57+index 9947f43..6ee5ff3 100644
58+--- a/src/PIL/BmpImagePlugin.py
59++++ b/src/PIL/BmpImagePlugin.py
60+@@ -329,22 +329,25 @@ class BmpRleDecoder(ImageFile.PyDecoder):
61+ if len(bytes_read) < 2:
62+ break
63+ right, up = self.fd.read(2)
64+- data += b"\x00" * (right + up * self.state.xsize)
65++ data += b"\x00" * min(
66++ dest_length - len(data), right + up * self.state.xsize
67++ )
68+ x = len(data) % self.state.xsize
69+ else:
70+ # absolute mode
71++ data_count = min(dest_length - len(data), byte[0])
72+ if rle4:
73+ # 2 pixels per byte
74+- byte_count = byte[0] // 2
75++ byte_count = data_count // 2
76+ bytes_read = self.fd.read(byte_count)
77+ for byte_read in bytes_read:
78+ data += o8(byte_read >> 4)
79+ data += o8(byte_read & 0x0F)
80+ else:
81+- byte_count = byte[0]
82++ byte_count = data_count
83+ bytes_read = self.fd.read(byte_count)
84+ data += bytes_read
85+- if len(bytes_read) < byte_count:
86++ if len(bytes_read) < byte_count or len(data) == dest_length:
87+ break
88+ x += byte[0]
89+
90+--
91+2.39.5
92+ 
@@ -4,7 +4,7 @@
4 4
5Name: python-pillow5Name: python-pillow
6Version: 10.3.06Version: 10.3.0
7-Release: 147+Release: 15
8Summary: Python image processing library 8Summary: Python image processing library
9License: MIT9License: MIT
10URL: http://python-pillow.github.io/10URL: http://python-pillow.github.io/
@@ -27,6 +27,7 @@ Patch14: backport-CVE-2026-59205.patch
27Patch15: backport-CVE-2026-59199.patch27Patch15: backport-CVE-2026-59199.patch
28Patch16: backport-CVE-2026-59198.patch28Patch16: backport-CVE-2026-59198.patch
29Patch17: backport-CVE-2026-112395.patch29Patch17: backport-CVE-2026-112395.patch
30+Patch18: backport-CVE-2026-96638.patch
30 31 
31BuildRequires: freetype-devel ghostscript lcms2-devel libimagequant-devel libjpeg-devel libtiff-devel32BuildRequires: freetype-devel ghostscript lcms2-devel libimagequant-devel libjpeg-devel libtiff-devel
32BuildRequires: libwebp-devel openjpeg2-devel tk-devel zlib-devel python3-cffi python3-devel python3-numpy python3-olefile33BuildRequires: libwebp-devel openjpeg2-devel tk-devel zlib-devel python3-cffi python3-devel python3-numpy python3-olefile
@@ -165,6 +166,9 @@ pytest --ignore=_build.python2 --ignore=_build.python3 --ignore=_build.pypy3 -v
165%{python3_sitearch}/PIL/__pycache__/ImageQt*166%{python3_sitearch}/PIL/__pycache__/ImageQt*
166 167 
167%changelog168%changelog
169+* Sat Oct 31 2026 infra_team <zhaiwenjie1@huawei.com> - 10.3.0-15
170+- Fix CVE-2026-96638: cap BmpRleDecoder delta escapes and absolute-mode runs at dest_length (#9824, #10015)
171+ 
168* Tue Oct 20 2026 infra_team <zhaiwenjie1@huawei.com> - 10.3.0-14172* Tue Oct 20 2026 infra_team <zhaiwenjie1@huawei.com> - 10.3.0-14
169- Fix CVE-2026-112395: Reject TIFFs whose unpacker row is wider than libtiff decoded row (#10201)173- Fix CVE-2026-112395: Reject TIFFs whose unpacker row is wider than libtiff decoded row (#10201)
170 174