CVE-2026-59935: Root-Cause Fix for pypdf Inline Image Infinite Loop
Summary
The patch for CVE-2026-59935 appears to address the parser livelock at the correct failure boundary: inline image decoders now explicitly detect zero-byte progress at EOF and raise PdfReadError instead of continuing to scan for terminators indefinitely. The accompanying tests add timeout guards and malformed ASCIIHex/ASCII85 cases that directly exercise the reported denial-of-service condition.
Analysis
Vulnerability
CVE-2026-59935 describes a denial-of-service issue in pypdf before 6.14.2 where a crafted PDF can drive parsing into an infinite loop with 100% CPU usage. The affected area is inline image extraction for ASCII85 and ASCIIHex encoded data, specifically malformed streams that never present a valid terminator before EOF. The patch references in the upstream pull request and commit show that the parser previously continued scanning after reads that made no forward progress, which is the classic precondition for a livelock in stream parsers handling attacker-controlled input.
The source-grounded symptom is that inline image decoding attempted to keep searching for the EI end marker or encoded-stream terminator even when the underlying stream had already reached end-of-file. In the vulnerable code, the read path only checked whether the combined buffered data was empty, not whether the latest read returned zero bytes. That distinction matters because a leading non-whitespace byte can keep data_buffered non-empty while subsequent stream.read(BUFFER_SIZE) calls return b'', allowing the loop to continue without consuming new input. See the upstream patch discussion and code changes at PR #3892 and commit 5a33a46, plus the advisory GHSA-G867-7843-WF8Q.
Vulnerable pattern:
data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)
if not data_buffered:
raise PdfReadError("Unexpected end of stream")
# parser may continue although stream.read(...) returned b''Patch
The core fix is in pypdf/generic/_image_inline.py. The patched code captures the latest read into a dedicated variable and treats a zero-byte read as an immediate EOF condition:
data_buffered = read_non_whitespace(stream) + (read_bytes := stream.read(BUFFER_SIZE))
if not data_buffered or not read_bytes:
raise PdfReadError("Unexpected end of stream.")This is the important semantic change. The parser no longer relies solely on aggregate buffer emptiness; it now checks for lack of forward progress on the underlying stream. That directly breaks the infinite-loop condition for malformed ASCII85 and ASCIIHex inline images. The patch also normalizes several error paths to raise PdfReadError with punctuated messages such as Unexpected end of stream. and EI stream not found., including a related change in pypdf/generic/_data_structures.py from PdfStreamError to PdfReadError. Those exception changes improve consistency but are secondary to the DoS fix itself.
The regression coverage is materially improved. New tests in tests/generic/test_image_inline.py explicitly exercise early EOF for both ASCIIHex and ASCII85 decoders, and timeout markers are added to ensure the failure mode is bounded rather than hanging indefinitely. Existing tests were updated to expect PdfReadError and the normalized message format. These changes are visible in both the pull request and commit references: PR #3892, commit 5a33a46.
Review
Pros
- The fix targets the actual parser-progress invariant: if a read returns zero bytes, the decoder aborts instead of continuing to search for terminators.
- The change is narrow and low-risk. It does not redesign inline image parsing; it adds explicit EOF detection at the point where the loop previously lost liveness.
- Regression tests are aligned with the reported exploit shape: malformed ASCIIHex and ASCII85 inline image data ending prematurely.
@pytest.mark.timeout(5)is a strong addition for this class of bug because it verifies liveness properties, not just exception types.- Exception normalization to
PdfReadErrormakes malformed-input handling more consistent across parser layers.
Cons
- The provided diff snippets are partial, so the exact loop structure is not fully visible; review confidence is based on the changed guard condition and tests rather than a full control-flow audit.
- The patch appears focused on ASCII85 and ASCIIHex inline image extraction. The sources do not demonstrate a broader audit of other stream-decoding loops for the same zero-progress pattern.
- Changing
PdfStreamErrortoPdfReadErrormay be observable to downstream consumers that catch specific exception classes, although this is likely acceptable for a security fix.
Verdict
Root-cause.
The patch addresses the root cause of the denial-of-service: failure to terminate parsing when the decoder reaches EOF without making progress. By checking read_bytes directly, the parser now distinguishes between residual buffered state and actual stream advancement, which is the key condition that previously enabled an infinite loop. The new malformed-input tests and timeout assertions provide credible evidence that the reported CPU-exhaustion path is closed. Based on the available sources, this is a technically sound and source-supported fix for CVE-2026-59935.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- Badtar.py
Closest Python hands-on defensive lab to this CVE’s theme. CVE-2026-59935 is a parser-driven denial of service caused by crafted file input leading to resource exhaustion; Badtar.py maps well because it focuses on defending against malicious archive/file content that can trigger uncontrolled resource consumption (CWE-400).
- Energy.py
Useful defensive follow-on lab for denial-of-service mitigation patterns. While not PDF-parser specific, it teaches mitigation for uncontrolled consumption and missing throttling controls (CWE-770/CWE-400), which is relevant when hardening services that process attacker-supplied documents.
- ImageBoard.py
Good supporting lab because the vulnerable pypdf path is triggered via uploaded crafted files. This lab reinforces defensive handling of user-supplied files at the upload boundary, helping reduce exposure before malicious parser input reaches backend libraries.