Skip to content

Deep Recursion Use-After-Free in PDF Dictionary Parsing #276

@err2zero

Description

@err2zero

Summary

During fuzzing of the PoDoFo PDF library's podofoencrypt tool, a critical heap-use-after-free vulnerability was discovered that occurs specifically during deep recursive parsing of nested PDF dictionary structures. The vulnerability manifests when extremely nested dictionary structures (96+ levels of recursion) cause stack exhaustion leading to heap memory corruption. This vulnerability is distinct from standard dictionary parsing issues as it involves recursive depth limits and stack/heap interaction causing memory management failures in the PdfTokenizer::ReadDictionary function.

Technical Details

  • Vulnerability Type: Heap Use-After-Free (Deep Recursion Induced)
  • Affected Component: PoDoFo PDF Library - PdfTokenizer
  • Affected Function: PdfTokenizer::ReadDictionary (recursive calls)
  • Source File: PdfTokenizer.cpp
  • Line Number: 464-505 (recursive execution)
  • Signal: SIGABRT (6)
  • Memory Access: READ of size 4
  • Recursion Depth: 96+ levels
  • Stack Frame Pattern: Repeated ReadDictionaryReadNextVariantReadDictionary

Vulnerability Mechanism and Root Cause

This heap-use-after-free vulnerability is triggered by excessively deep recursion in PDF dictionary parsing, creating a unique failure mode distinct from normal dictionary parsing issues. The root cause involves the interaction between stack exhaustion and heap memory management.

The vulnerability sequence involves:

  1. Deep Recursion Initiation: Malformed PDF contains deeply nested dictionary structures (<<</>>)
  2. Stack Frame Accumulation: Each dictionary level creates new stack frames via ReadDictionarytryReadDataTypeReadNextVariantReadDictionary
  3. Resource Exhaustion: At approximately 96+ recursion levels, stack space approaches limits
  4. Heap Corruption: Stack pressure causes heap allocator corruption or premature cleanup
  5. Use-After-Free: PdfName::NameData objects are accessed after being freed due to corrupted memory management state

The extended call chain demonstrates the recursion pattern:

ReadDictionary() #1 → ReadDictionary() #2 → ... → ReadDictionary() #96+

This recursive depth creates a fundamentally different memory corruption scenario compared to normal dictionary parsing, requiring separate mitigation strategies focused on recursion limits rather than just PdfName lifetime management.

AddressSanitizer Report

WARNING: Invalid number while parsing content
=================================================================
==3576531==ERROR: AddressSanitizer: heap-use-after-free on address 0x603000000498 at pc 0x7f26bed23147 bp 0x7fff2716f780 sp 0x7fff2716f778
READ of size 4 at 0x603000000498 thread T0
    #0 0x7f26bed23146 in __gnu_cxx::__exchange_and_add_single(int*, int) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/atomicity.h:84:29
    #1 0x7f26bed23146 in __gnu_cxx::__exchange_and_add_dispatch(int*, int) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/atomicity.h:99:14
    #2 0x7f26bed23146 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/shared_ptr_base.h:165:6
    #3 0x7f26bed23146 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/shared_ptr_base.h:705:11
    #4 0x7f26bed23146 in std::__shared_ptr<PoDoFo::PdfName::NameData, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/shared_ptr_base.h:1154:31
    #5 0x7f26bed23146 in PoDoFo::PdfName::~PdfName() /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfName.cpp:33:16
    #6 0x7f26be9cdeac in std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>::~pair() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_iterator.h:2488:12
    #7 0x7f26be9cdeac in void __gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::destroy<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>(std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/new_allocator.h:168:10
    #8 0x7f26be9cdeac in void std::allocator_traits<std::allocator<std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>>::destroy<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>(std::allocator<std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>&, std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/alloc_traits.h:535:8
    #9 0x7f26be9cdeac in std::_Rb_tree<PoDoFo::PdfName, std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>, std::_Select1st<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>, PoDoFo::PdfNameInequality, std::allocator<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::_M_destroy_node(std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_tree.h:623:2
    #10 0x7f26be9cdeac in std::_Rb_tree<PoDoFo::PdfName, std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>, std::_Select1st<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>, PoDoFo::PdfNameInequality, std::allocator<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::_M_drop_node(std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_tree.h:631:2
    #11 0x7f26be9cdeac in std::_Rb_tree<PoDoFo::PdfName, std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>, std::_Select1st<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>, PoDoFo::PdfNameInequality, std::allocator<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::_M_erase(std::_Rb_tree_node<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_tree.h:1891:4
    #12 0x7f26beec3dc0 in std::_Rb_tree<PoDoFo::PdfName, std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>, std::_Select1st<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>, PoDoFo::PdfNameInequality, std::allocator<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::~_Rb_tree() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_tree.h:984:9
    #13 0x7f26beec3dc0 in std::map<PoDoFo::PdfName, PoDoFo::PdfObject, PoDoFo::PdfNameInequality, std::allocator<std::pair<PoDoFo::PdfName const, PoDoFo::PdfObject>>>::~map() /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_map.h:302:22
    #14 0x7f26beec3dc0 in PoDoFo::PdfDictionary::~PdfDictionary() /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfDictionary.h:81:18
    #15 0x7f26beec3dc0 in PoDoFo::PdfVariant::~PdfVariant() /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfVariant.cpp:94:13
    #16 0x7f26beebc1f9 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:505:1
    #17 0x7f26beeba5a6 in PoDoFo::PdfTokenizer::tryReadDataType(PoDoFo::InputStreamDevice&, PoDoFo::PdfTokenizer::PdfLiteralDataType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:416:19
    #18 0x7f26beeb8952 in PoDoFo::PdfTokenizer::TryReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:251:12
    #19 0x7f26beeb8952 in PoDoFo::PdfTokenizer::ReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:243:10
    #20 0x7f26beebaf55 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:464:15
    #21 0x7f26beeba5a6 in PoDoFo::PdfTokenizer::tryReadDataType(PoDoFo::InputStreamDevice&, PoDoFo::PdfTokenizer::PdfLiteralDataType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:416:19
    #22 0x7f26beeb8952 in PoDoFo::PdfTokenizer::TryReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:251:12
    #23 0x7f26beeb8952 in PoDoFo::PdfTokenizer::ReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:243:10
    #24 0x7f26beebaf55 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:464:15
    #25 0x7f26beeba5a6 in PoDoFo::PdfTokenizer::tryReadDataType(PoDoFo::InputStreamDevice&, PoDoFo::PdfTokenizer::PdfLiteralDataType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:416:19
    #26 0x7f26beeb8952 in PoDoFo::PdfTokenizer::TryReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:251:12
    #27 0x7f26beeb8952 in PoDoFo::PdfTokenizer::ReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:243:10
    #28 0x7f26beebaf55 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:464:15
    #29 0x7f26beeba5a6 in PoDoFo::PdfTokenizer::tryReadDataType(PoDoFo::InputStreamDevice&, PoDoFo::PdfTokenizer::PdfLiteralDataType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:416:19
    #30 0x7f26beeb8952 in PoDoFo::PdfTokenizer::TryReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:251:12
    #31 0x7f26beeb8952 in PoDoFo::PdfTokenizer::ReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:243:10
    [... pattern repeats for 96+ stack frames ...]
    #92 0x7f26beebaf55 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:464:15
    #93 0x7f26beeba5a6 in PoDoFo::PdfTokenizer::tryReadDataType(PoDoFo::InputStreamDevice&, PoDoFo::PdfTokenizer::PdfLiteralDataType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:416:19
    #94 0x7f26beeb8952 in PoDoFo::PdfTokenizer::TryReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:251:12
    #95 0x7f26beeb8952 in PoDoFo::PdfTokenizer::ReadNextVariant(PoDoFo::InputStreamDevice&, std::basic_string_view<char, std::char_traits<char>> const&, PoDoFo::PdfTokenType, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:243:10
    #96 0x7f26beebaf55 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:464:15

0x603000000498 is located 8 bytes inside of 24-byte region [0x603000000490,0x6030000004a8)
freed by thread T0 here:
    #0 0x55ec338944fd in operator delete(void*) (/workspace/fuzzdir/fz-podofo/fz-podofoencrypt/podofoencrypt+0xf64fd) (BuildId: 199c644fed58464afa945df05fe57a4a79121f2b)
    #1 0x7f26beebc1f9 in PoDoFo::PdfTokenizer::ReadDictionary(PoDoFo::InputStreamDevice&, PoDoFo::PdfVariant&, PoDoFo::PdfStatefulEncrypt const*) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfTokenizer.cpp:505:1

previously allocated by thread T0 here:
    #0 0x55ec33893c9d in operator new(unsigned long) (/workspace/fuzzdir/fz-podofo/fz-podofoencrypt/podofoencrypt+0xf5c9d) (BuildId: 199c644fed58464afa945df05fe57a4a79121f2b)
    #1 0x7f26bed2a7b5 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<PoDoFo::PdfName::NameData*>(PoDoFo::PdfName::NameData*) /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/shared_ptr_base.h:596:16
    #2 0x7f26bed260d5 in PoDoFo::PdfName::FromEscaped(std::basic_string_view<char, std::char_traits<char>> const&) /workspace/program/podofo-053cf47-Jul30/src/podofo/main/PdfName.cpp:166:16

SUMMARY: AddressSanitizer: heap-use-after-free /usr/lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/ext/atomicity.h:84:29 in __gnu_cxx::__exchange_and_add_single(int*, int)

Proof of Concept

The vulnerability can be triggered by processing the malformed PDF file provided as POC_podofoencrypt_deep_recursion_use_after_free. This file contains extremely deep nested dictionary structures (96+ levels) that exhaust stack resources and trigger the recursion-induced use-after-free condition.

POC Download: POC_podofoencrypt_deep_recursion_use_after_free

Reproduction Steps

  1. Compile PoDoFo with AddressSanitizer enabled
  2. Execute: podofoencrypt -o test POC_podofoencrypt_deep_recursion_use_after_free output.pdf
  3. The program will crash with a heap-use-after-free error after reaching deep recursion levels

Affected Versions

PoDoFo version 1.1.0-dev (commit 053cf47) compiled on Jul 30 2025 and the newest master version.

Credit

  • Xudong Cao (UCAS)
  • Yuqing Zhang (UCAS, Zhongguancun Laboratory)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions