CVE-2026-9958
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcore/fpdfdoc/cpdf_nametree.cpp |
modified |
Files Changed
core/fpdfdoc/cpdf_nametree.cppcore/fpdfdoc/cpdf_nametree_unittest.cpp
Patch
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());
Regression Test / PoC
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());
Original Bug Report
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.cpppdf/pdfium/pdfium_page.ccthird_party/pdfium/core/fpdfapi/parser/cpdf_array.cppthird_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:
PDFiumEngine::LoadBody()skips callingLoadForm()if the document is not yet complete and has more than one page.- When a page is requested early,
PDFiumPage::GetPage()is invoked. It checksif (engine_->form()), but sinceLoadForm()was skipped,engine_->form()returns null. - 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:
PDFiumPage::PopulateAnnotationLinks()iterates over the page’s annotations usingFPDFLink_Enumerate().FPDFLink_Enumerateretrieves the inline Link Annotation from the aliased/Annotsarray and returns it as a rawFPDF_LINKhandle.PopulateAnnotationLinks()callsGetLinkTarget(), which attempts to resolve the link’s destination viaFPDFLink_GetDest().- Resolving the destination triggers a Name Tree lookup (
CPDF_NameTree::LookupNamedDest). - The Name Tree traversal encounters the aliased array acting as a
/Limitsnode and callsGetNodeLimitsAndSanitize(). - The sanitization loop truncates the array, freeing the inline Link Annotation dictionary.
- Execution returns to
PopulateAnnotationLinks(), which subsequently callsFPDFLink_GetAnnotRect(link_annot). - The
link_annothandle is a raw C pointer that now points to freed memory. It is cast to aCPDF_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:
- Prevent Array Truncation: Modify
GetNodeLimitsAndSanitizeto return a sanitized copy or pair of strings without mutating the underlyingCPDF_Arrayin place. Name Tree traversals should generally be read-only operations. - Enforce AnnotList Initialization: Ensure that
FORM_OnAfterLoadPageis 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.