Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Tracker504555886
Fix commitdf3e2a600e0d (pdfium) +29/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
core/fpdfdoc/cpdf_nametree.cpp
modified

Files Changed

  • core/fpdfdoc/cpdf_nametree.cpp
  • core/fpdfdoc/cpdf_nametree_unittest.cpp
From df3e2a600e0d961f15f9b01d479550ff74f7124f Mon Sep 17 00:00:00 2001
From: Tom Sepez <[email protected]>
Date: Thu, 23 Apr 2026 12:22:48 -0700
Subject: [PATCH] Do not truncate /Limits array for lookup-only operations

Close a theoretical object invalidation while traversing links.
In Chrome's PDF viewer this primitive is not exploitable.

-- Update test to account for different possible lengths.
-- Gemini-generated patch, minus some verbosity, then cleaned up
   significantly by the human.

Bug: 504555886
Change-Id: Iaaad6d8cd9aecba07ab056cca638ad930bdc36f3
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/146590
Commit-Queue: Lei Zhang <[email protected]>
Commit-Queue: Tom Sepez <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
---

diff --git a/core/fpdfdoc/cpdf_nametree.cpp b/core/fpdfdoc/cpdf_nametree.cpp
index b4f65c7..eb0106e 100644
--- a/core/fpdfdoc/cpdf_nametree.cpp
+++ b/core/fpdfdoc/cpdf_nametree.cpp
@@ -30,22 +30,24 @@
   int index = -1;
 };
 
-std::pair<WideString, WideString> GetNodeLimitsAndSanitize(
-    CPDF_Array* pLimits) {
-  DCHECK(pLimits);
-  WideString csLeft = pLimits->GetUnicodeTextAt(0);
-  WideString csRight = pLimits->GetUnicodeTextAt(1);
+std::pair<WideString, WideString> GetNodeLimits(CPDF_Array* limits) {
+  WideString left = limits->GetUnicodeTextAt(0);
+  WideString right = limits->GetUnicodeTextAt(1);
+
   // If the lower limit is greater than the upper limit, swap them.
-  if (csLeft.Compare(csRight) > 0) {
-    pLimits->SetNewAt<CPDF_String>(0, csRight.AsStringView());
-    pLimits->SetNewAt<CPDF_String>(1, csLeft.AsStringView());
-    csLeft = pLimits->GetUnicodeTextAt(0);
-    csRight = pLimits->GetUnicodeTextAt(1);
+  if (left.Compare(right) > 0) {
+    std::swap(left, right);
+    limits->SetNewAt<CPDF_String>(0, left.AsStringView());
+    limits->SetNewAt<CPDF_String>(1, right.AsStringView());
   }
-  while (pLimits->size() > 2) {
-    pLimits->RemoveAt(pLimits->size() - 1);
+
+  return {std::move(left), std::move(right)};
+}
+
+void TrimNodeLimits(CPDF_Array* limits) {
+  while (limits->size() > 2) {
+    limits->RemoveAt(limits->size() - 1);
   }
-  return {csLeft, csRight};
 }
 
 // Get the limit arrays that leaf array |pFind| is under in the tree with root
@@ -108,7 +110,8 @@
   WideString csLeft;
   WideString csRight;
   if (pLimits) {
-    std::tie(csLeft, csRight) = GetNodeLimitsAndSanitize(pLimits.Get());
+    TrimNodeLimits(pLimits.Get());
+    std::tie(csLeft, csRight) = GetNodeLimits(pLimits.Get());
   }
 
   RetainPtr<const CPDF_Array> pNames = pNode->GetArrayFor("Names");
@@ -242,13 +245,18 @@
   }
 
   if (pLimits) {
-    auto [csLeft, csRight] = GetNodeLimitsAndSanitize(pLimits.Get());
+    // When lookup-only: do not truncate the /Limits array.
+    if (!node_to_insert) {
+      TrimNodeLimits(pLimits.Get());
+    }
+    auto [left, right] = GetNodeLimits(pLimits.Get());
+
     // Skip this node if the name to look for is smaller than its lower limit.
-    if (csName.Compare(csLeft) < 0) {
+    if (csName.Compare(left) < 0) {
       return nullptr;
     }
 
-    if (csName.Compare(csRight) > 0) {
+    if (csName.Compare(right) > 0) {
       // If only trying to find the name node, and not where the name should be
       // added, skip this node.
       if (!node_to_insert) {
diff --git a/core/fpdfdoc/cpdf_nametree_unittest.cpp b/core/fpdfdoc/cpdf_nametree_unittest.cpp
index 939dd9a..d7435e7 100644
--- a/core/fpdfdoc/cpdf_nametree_unittest.cpp
+++ b/core/fpdfdoc/cpdf_nametree_unittest.cpp
@@ -40,10 +40,12 @@
   ASSERT_TRUE(node);
   RetainPtr<const CPDF_Array> limits = node->GetArrayFor("Limits");
   ASSERT_TRUE(limits);
-  EXPECT_EQ(2u, limits->size());
+
+  // Sorted, but not always truncated.
+  EXPECT_GE(limits->size(), 2u);
   RetainPtr<const CPDF_String> left = limits->GetStringAt(0);
-  ASSERT_TRUE(left);
   RetainPtr<const CPDF_String> right = limits->GetStringAt(1);
+  ASSERT_TRUE(left);
   ASSERT_TRUE(right);
   EXPECT_EQ(least, left->GetString());
   EXPECT_EQ(greatest, right->GetString());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/core/fpdfdoc/cpdf_nametree_unittest.cpp b/core/fpdfdoc/cpdf_nametree_unittest.cpp
index 939dd9a..d7435e7 100644
--- a/core/fpdfdoc/cpdf_nametree_unittest.cpp
+++ b/core/fpdfdoc/cpdf_nametree_unittest.cpp
@@ -40,10 +40,12 @@
   ASSERT_TRUE(node);
   RetainPtr<const CPDF_Array> limits = node->GetArrayFor("Limits");
   ASSERT_TRUE(limits);
-  EXPECT_EQ(2u, limits->size());
+
+  // Sorted, but not always truncated.
+  EXPECT_GE(limits->size(), 2u);
   RetainPtr<const CPDF_String> left = limits->GetStringAt(0);
-  ASSERT_TRUE(left);
   RetainPtr<const CPDF_String> right = limits->GetStringAt(1);
+  ASSERT_TRUE(left);
   ASSERT_TRUE(right);
   EXPECT_EQ(least, left->GetString());
   EXPECT_EQ(greatest, right->GetString());
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in PDFium via aliased NameTree Limits array and linearized PDFs

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free exists in PDFium when handling linearized PDFs. By aliasing an array as both a NameTree /Limits array and a Page’s /Annots array, an attacker can trigger the premature deletion of inline annotation dictionaries. This leads to a dangling raw pointer dereference during link enumeration.

Affected files:

  • third_party/pdfium/core/fpdfdoc/cpdf_nametree.cpp
  • pdf/pdfium/pdfium_page.cc
  • third_party/pdfium/core/fpdfapi/parser/cpdf_array.cpp
  • third_party/pdfium/core/fpdfdoc/cpdf_annotlist.cpp

Estimated timestamp from git blame: 2025-08-29

Root Cause

In PDFium, the GetNodeLimitsAndSanitize() function (third_party/pdfium/core/fpdfdoc/cpdf_nametree.cpp) is responsible for sanitizing /Limits arrays during Name Tree traversal. If an array contains more than two elements, the function truncates it by calling RemoveAt in a loop:

while (pLimits->size() > 2) {
  pLimits->RemoveAt(pLimits->size() - 1);
}

If the truncated elements are inline dictionaries (i.e., not indirect objects), the array holds their only reference. Truncating the array drops the reference count to zero, immediately freeing the dictionary.

Mitigation Bypass

PDFium typically mitigates this by promoting inline annotation dictionaries to indirect objects via CPDF_AnnotList, which is initialized during FORM_OnAfterLoadPage(). However, this mitigation can be bypassed in multi-page linearized PDFs during progressive loading:

  1. PDFiumEngine::LoadBody() skips calling LoadForm() if the document is not yet complete and has more than one page.
  2. When a page is requested early, PDFiumPage::GetPage() is invoked. It checks if (engine_->form()), but since LoadForm() was skipped, engine_->form() returns null.
  3. Consequently, FORM_OnAfterLoadPage() is skipped. Inline link annotation dictionaries are never promoted to indirect objects.

Vulnerability Mechanism

An attacker can craft a PDF where a single array is aliased as both a NameTree node’s /Limits and a page’s /Annots. The array is populated with string limits and an inline Link Annotation dictionary.

During progressive loading, if the user interacts with the page or an accessibility query fires, PDFiumPage::CalculateLinks() is triggered. The execution flows as follows:

  1. PDFiumPage::PopulateAnnotationLinks() iterates over the page’s annotations using FPDFLink_Enumerate().
  2. FPDFLink_Enumerate retrieves the inline Link Annotation from the aliased /Annots array and returns it as a raw FPDF_LINK handle.
  3. PopulateAnnotationLinks() calls GetLinkTarget(), which attempts to resolve the link’s destination via FPDFLink_GetDest().
  4. Resolving the destination triggers a Name Tree lookup (CPDF_NameTree::LookupNamedDest).
  5. The Name Tree traversal encounters the aliased array acting as a /Limits node and calls GetNodeLimitsAndSanitize().
  6. The sanitization loop truncates the array, freeing the inline Link Annotation dictionary.
  7. Execution returns to PopulateAnnotationLinks(), which subsequently calls FPDFLink_GetAnnotRect(link_annot).
  8. The link_annot handle is a raw C pointer that now points to freed memory. It is cast to a CPDF_Dictionary* and dereferenced, resulting in a Use-After-Free.

Note: These steps trace the potential vulnerability pathway based on static analysis.

Impact

This is a potential heap-based Use-After-Free in the sandboxed PDF renderer process. Because the FPDF_LINK handle is a raw pointer defined in PDFium’s public C API (fpdfview.h), it is not protected by MiraclePtr (BackupRefPtr). An attacker who can predict or control heap allocations may be able to replace the freed CPDF_Dictionary to hijack virtual function calls, leading to arbitrary code execution in the renderer process.

Suggested Fix

There are two primary approaches to fixing this:

  1. Prevent Array Truncation: Modify GetNodeLimitsAndSanitize to return a sanitized copy or pair of strings without mutating the underlying CPDF_Array in place. Name Tree traversals should generally be read-only operations.
  2. Enforce AnnotList Initialization: Ensure that FORM_OnAfterLoadPage is called consistently for linearized PDFs, even if the entire document hasn’t finished loading, guaranteeing that inline annotations are properly promoted to indirect objects.

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker