CVE Patch Review

CVE-2026-59935: Root-Cause Fix for pypdf Inline Image Infinite Loop

CVE-2026-59935 · GHSA-G867-7843-WF8Q · Updated 2026-07-23 Root-cause

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 PdfReadError makes 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 PdfStreamError to PdfReadError may 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.

Sources