---
title: "lopdf 0.42.0 still overflows its stack on a chain of /Length references"
slug: lopdf-length-chain-stack-overflow
draft: false
date: "2026-11-12"
datetime: "2026-11-12T18:52:53Z"
updated: "2026-11-12"
description: "RUSTSEC-2026-0187 calls 0.42.0 patched after adding a nesting-depth limit. The limit covers arrays and dictionaries; a stream whose /Length is another stream recurses without it and still aborts."
abstract: >-
  RUSTSEC-2026-0187 reports unbounded recursion in lopdf’s PDF object parser and calls 0.42.0 patched: commit c755394 adds MAX_NESTING_DEPTH = 100 and threads it through the array, dictionary and direct-object paths. A stream dictionary whose /Length is an indirect reference is legal PDF (ISO 32000-1 7.3.8.2), and stream() resolves that reference through Reader::get_object, which re-enters object() and stream() again with no depth counter. We built a chain of N stream objects, each pointing at the next through /Length, and loaded it with the released crates: on 0.42.0, 1,300 links abort with a stack overflow, exit 134, while 1,200 load; on 0.47.0, the 1,200-link file aborts. Controls isolate the recursion: 1,300 flat stream objects load, and the parser’s own depth limit fires at 100 nested arrays. The defect is the parser’s other recursion path, not a failure of the added limit.
authors:
  - name: EZO Labs Research
section: environments
report_number: EZO-R-2026-26
lang: en
og:
  kicker: "EZO LABS // RESEARCH"
  title:
    - "lopdf 0.42.0 still overflows"
    - "its stack on a chain of"
    - "/Length references"
  foot:
    - "An independent write-up."
    - "Notes from the disclosure."
bibtex_key: lopdflengthchainstackoverflow
revisions:
  - date: "2026-11-12"
    note: "First publication."
license: cc-by-4.0
---

## Summary

A PDF can chain one stream’s length to another stream. lopdf follows that chain recursively, and the depth counter added in 0.42.0 does not reach it.

## The gap

A chain of stream objects whose `/Length` is an indirect reference recurses through `stream()` → `Reader::get_object` → `object()` → `stream()` with no depth limit, so a structurally valid PDF aborts the process with a stack overflow.

- `src/parser/mod.rs` (346 in 0.42.0; 438 in 0.47.0) — `stream()` resolves a referenced `/Length` through `Reader::get_object`; `get_object` → `read_object` → `parser::indirect_object` → `object()` → `stream()` again, with no depth counter.
- `src/reader.rs` (456 (MAX_NESTING_DEPTH), 1099 / 1269 (get_object)) — The 0.42.0 fix (commit `c755394`) adds `MAX_NESTING_DEPTH = 100` and threads a depth argument through `array`, `_dictionary` and `_direct_object` only. `stream()` and `Reader::get_object` receive no depth argument.

## Reproduction

Build a PDF with N stream objects, each `<< /Length <next> 0 R >> stream … endstream`, and load it with the released crate. Controls: the same 1,300 stream objects with plain-integer `/Length`; nested arrays at 99 and 100 levels.

0.42.0: a 1,300-link chain (101,888 bytes) aborts with `thread '<unknown>' has overflowed its stack` / `fatal runtime error: stack overflow, aborting`, exit 134; 1,200 links (93,888 bytes) loads. 0.47.0: the 93,888-byte 1,200-link chain aborts. Controls on both: the 1,300 plain-`/Length` stream objects load as 1,300 objects; a nested array loads at 99 levels and is dropped at 100.

## Impact

A caller that loads an attacker-supplied PDF aborts the process with a stack overflow. The abort cannot be caught with `catch_unwind`, so one document can take down a service that parses PDFs in-process.

## Affected versions

lopdf 0.42.0 through 0.47.0 (latest at 2026-11-04) carry the unbounded `stream()` path; the defect exists at least from 0.42.0, where the RUSTSEC record says it is patched. No release threads a depth counter through `stream()`’s `/Length` resolution.

## Fix status

The fix is to thread the existing depth argument through `stream()` and `Reader::get_object` (or pass an explicit depth to the `/Length` resolution) so the cycle is bounded. No release through 0.47.0 carries it.

## The vendor’s record

The maintainer published `GHSA-4597-5x94-x3xg` at 2026-11-12T01:35:47Z with `cve_id: null`, 31 minutes after the class-anchored publish ask. The published advisory records the same defect and the range `lopdf <= 0.47.0`; it carries no CVE and no patched version. RUSTSEC-2026-0187 is the published record that calls 0.42.0 patched; the execution contradicts that range for this input class.

## Timeline

- 2026-11-04 — lopdf 0.47.0 released
- 2026-11-09T08:44:05Z — Private report filed (`GHSA-4597-5x94-x3xg`)
- 2026-11-09T10:45:44Z — Draft accepted (inferred from `updated_at`)
- 2026-11-11T20:36:08Z — Publication ask posted in the advisory thread
- 2026-11-11T21:25Z — CNA request sent to VulnCheck (`disclosure@vulncheck.com`)
- 2026-11-11T23:22Z — Class-anchored follow-up sent to VulnCheck
- 2026-11-12T01:04:34Z — Class-anchored publish ask in the vendor advisory thread (369870)
- 2026-11-12T01:35:47Z — The maintainer publishes `GHSA-4597-5x94-x3xg` with `cve_id: null`
- 2026-12-03T01:35:47Z — CVE escalation (three weeks after publication) by email or the CNA; the advisory thread refuses comments once published

## References

- Stack overflow in lopdf via deeply nested PDF objects: https://github.com/rustsec/advisory-db/blob/main/crates/lopdf/RUSTSEC-2026-0187.md
- Private report: https://github.com/J-F-Liu/lopdf/security/advisories/GHSA-4597-5x94-x3xg

## Disclosure

This note is an independent write-up. It is published on 2026-11-12T18:52:53Z as the public reference for a CVE request. The private advisory and this write-up describe the same defect; the vendor advisory named above is the authority where one exists.
