-
Notifications
You must be signed in to change notification settings - Fork 112
Description
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
ReadDictionary→ReadNextVariant→ReadDictionary
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:
- Deep Recursion Initiation: Malformed PDF contains deeply nested dictionary structures (<<</>>)
- Stack Frame Accumulation: Each dictionary level creates new stack frames via
ReadDictionary→tryReadDataType→ReadNextVariant→ReadDictionary - Resource Exhaustion: At approximately 96+ recursion levels, stack space approaches limits
- Heap Corruption: Stack pressure causes heap allocator corruption or premature cleanup
- Use-After-Free:
PdfName::NameDataobjects 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
- Compile PoDoFo with AddressSanitizer enabled
- Execute:
podofoencrypt -o test POC_podofoencrypt_deep_recursion_use_after_free output.pdf - 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)