Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Tracker504551617
Fix commit43d7e7335473 (pdfium) +2/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
while
fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
modified
if
fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
modified

Files Changed

  • fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
From 43d7e7335473060ea993d1029ee81e6b2b5d6f07 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Mon, 20 Apr 2026 16:47:07 -0700
Subject: [PATCH] Observe CPDFSDK_Annot across RenderWidget() call.

Theoretical issue noted by Fortify.

A Chrome-level POC is not feasible here but may be reachable
nonetheless.

-- Gemini-generated patch vetted by human.

Bug: 504551617
Change-Id: I54b4ccb96abbe0c0a2c91341c97107402bf85113
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/146610
Commit-Queue: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
---

diff --git a/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp b/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
index acd9a1a..f8db18b 100644
--- a/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
+++ b/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
@@ -327,6 +327,7 @@
       xfaView, Mask<XFA_WidgetStatus>{XFA_WidgetStatus::kVisible,
                                       XFA_WidgetStatus::kViewable});
 
+  ObservedPtr<CPDFSDK_Annot> pObservedAnnot(pAnnot);
   while (true) {
     CXFA_FFWidget* pWidget = pWidgetIterator.MoveToNext();
     if (!pWidget) {
@@ -341,7 +342,7 @@
     }
   }
 
-  CPDFXFA_Widget* pXFAWidget = ToXFAWidget(pAnnot);
+  CPDFXFA_Widget* pXFAWidget = ToXFAWidget(pObservedAnnot.Get());
   if (!pXFAWidget) {
     return;
   }
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in CPDFXFA_Page::DrawFocusAnnot due to raw pointer across JS execution

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 (UAF) vulnerability exists in PDFium’s XFA implementation. A raw pointer to a CPDFSDK_Annot is held across a loop that can trigger synchronous JavaScript execution, allowing an attacker to destroy the annotation and subsequently trigger a virtual call on the dangling pointer.

Affected files:

  • third_party/pdfium/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp
  • third_party/pdfium/fpdfsdk/cpdfsdk_pageview.cpp
  • third_party/pdfium/fpdfsdk/cpdfsdk_annot.h

Estimated timestamp from git blame: 2025-04-09

Summary

A potential Use-After-Free (UAF) vulnerability exists in CPDFXFA_Page::DrawFocusAnnot within PDFium’s XFA handling logic. The function maintains a raw CPDFSDK_Annot* pointer (pAnnot) while iterating through widgets in a loop. This iteration can trigger the loading of uninitialized XFA widgets, which can synchronously execute attacker-controlled JavaScript. This JavaScript can destroy the focused annotation, leaving pAnnot dangling. When the loop finishes, a virtual call (ToXFAWidget) on this dangling pointer results in a UAF.

Root Cause Analysis

In third_party/pdfium/fpdfsdk/fpdfxfa/cpdfxfa_page.cpp, the DrawFocusAnnot function receives the focused annotation as a raw pointer pAnnot (passed by value):

void CPDFXFA_Page::DrawFocusAnnot(CFX_RenderDevice* pDevice,
                                  CPDFSDK_Annot* pAnnot,
                                  const CFX_Matrix& mtUser2Device,
                                  const FX_RECT& rtClip) {
  // ...
  CXFA_FFPageWidgetIterator pWidgetIterator(...);
  while (true) {
    CXFA_FFWidget* pWidget = pWidgetIterator.MoveToNext(); // (1) Re-entrant JS
    if (!pWidget) break;
    // ...
  }

  CPDFXFA_Widget* pXFAWidget = ToXFAWidget(pAnnot); // (2) UAF on pAnnot
  // ...
}

The call to pWidgetIterator.MoveToNext() (1) can trigger FilteredLoadedWidgetFromLayoutItem -> EnsureWidgetLoadedIfVisible(), which calls LoadWidget(). For certain widgets like a CXFA_FFTextEdit, initializing default text synchronously triggers an event (OnTextWillChange), which calls node_->ProcessEvent(). This ultimately evaluates arbitrary JavaScript defined in the XFA document.

An attacker’s JS can call removeInstance() on the subform containing the focused annotation. When the JS execution finishes and the event’s UpdateScope is destroyed, an immediate layout update is triggered (UpdateDocView -> RunLayout). The layout processor identifies the removed subform and triggers OnLayoutItemRemoving, which propagates to CPDFSDK_PageView::DeleteAnnotForFFWidget. This removes the unique_ptr holding the annotation, destroying the object and freeing its memory.

After the iteration loop completes, ToXFAWidget(pAnnot) (2) is called. This inline function calls the virtual method pAnnot->AsXFAWidget(), dereferencing the dangling pointer and leading to the UAF.

Impact

This is a heap Use-After-Free in the renderer process. By grooming the heap during the JavaScript execution, an attacker can control the contents of the freed memory, leading to a vtable-hijack primitive and potential Remote Code Execution (RCE).

Note: XFA support in Chrome is disabled by default via the kPdfXfaSupport feature flag (chrome://flags#pdf-xfa-forms). It can be enabled via enterprise policy on Chrome OS (PdfXfaFormsEnabled).

Potential Steps to Reproduce

  1. Enable chrome://flags#pdf-xfa-forms in Chrome.
  2. Create an XFA PDF with:
    • A focusable field inside a repeatable subform.
    • A text edit field configured to lazily load and trigger a change event.
    • JavaScript on the change event that calls removeInstance() to delete the subform containing the focused field, followed by heap grooming (allocating objects of sizeof(CPDFSDK_Annot) with a fake vtable).
  3. Focus the first field in the subform.
  4. Trigger a page repaint (e.g., scroll or resize window).
  5. DrawFocusAnnot is invoked. The iterator reaches the text field, triggering the JS, freeing the annotation, grooming the heap, and finally dereferencing the fake vtable upon reaching ToXFAWidget(pAnnot).

Suggested Fix

The raw pointer pAnnot should be protected using an ObservedPtr to track its lifecycle across the layout/JS evaluation boundary, similar to the pattern used in GCedWidgetIteratorForAnnot.

  ObservedPtr<CPDFSDK_Annot> pObservedAnnot(pAnnot);
  // ... run the loop ...
  if (!pObservedAnnot) {
    return;
  }
  CPDFXFA_Widget* pXFAWidget = ToXFAWidget(pObservedAnnot.Get());

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