Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Tracker504545544
Fix commitc7dec2a900e0 (pdfium) +12/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • xfa/fwl/cfwl_caret.cpp
  • xfa/fwl/cfwl_caret.h
  • xfa/fwl/cfwl_scrollbar.cpp
  • xfa/fwl/cfwl_scrollbar.h
From c7dec2a900e0684ce1e500eb9835c21d503eb640 Mon Sep 17 00:00:00 2001
From: Tom Sepez <[email protected]>
Date: Mon, 20 Apr 2026 12:14:41 -0700
Subject: [PATCH] Reset CFWL_Caret timer at mark time, not sweep time.

Closes a theoretical window discovered by Fortify.

Gemini-generated fix, minus some verbosity.

-- Chrome-level reproduction was found to be infeasible.
-- Do the same for CFWL_ScrollBar.

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

diff --git a/xfa/fwl/cfwl_caret.cpp b/xfa/fwl/cfwl_caret.cpp
index 8aef853..276b767 100644
--- a/xfa/fwl/cfwl_caret.cpp
+++ b/xfa/fwl/cfwl_caret.cpp
@@ -32,6 +32,11 @@
 
 CFWL_Caret::~CFWL_Caret() = default;
 
+void CFWL_Caret::PreFinalize() {
+  timer_.reset();
+  CFWL_Widget::PreFinalize();
+}
+
 FWL_Type CFWL_Caret::GetClassID() const {
   return FWL_Type::Caret;
 }
diff --git a/xfa/fwl/cfwl_caret.h b/xfa/fwl/cfwl_caret.h
index e2626ed..9395154 100644
--- a/xfa/fwl/cfwl_caret.h
+++ b/xfa/fwl/cfwl_caret.h
@@ -21,6 +21,7 @@
   ~CFWL_Caret() override;
 
   // CFWL_Widget:
+  void PreFinalize() override;
   FWL_Type GetClassID() const override;
   void DrawWidget(CFGAS_GEGraphics* pGraphics,
                   const CFX_Matrix& matrix) override;
diff --git a/xfa/fwl/cfwl_scrollbar.cpp b/xfa/fwl/cfwl_scrollbar.cpp
index bb8145d..68d5aa5 100644
--- a/xfa/fwl/cfwl_scrollbar.cpp
+++ b/xfa/fwl/cfwl_scrollbar.cpp
@@ -34,6 +34,11 @@
 
 CFWL_ScrollBar::~CFWL_ScrollBar() = default;
 
+void CFWL_ScrollBar::PreFinalize() {
+  timer_.reset();
+  CFWL_Widget::PreFinalize();
+}
+
 FWL_Type CFWL_ScrollBar::GetClassID() const {
   return FWL_Type::ScrollBar;
 }
diff --git a/xfa/fwl/cfwl_scrollbar.h b/xfa/fwl/cfwl_scrollbar.h
index 41d7137..b01d789 100644
--- a/xfa/fwl/cfwl_scrollbar.h
+++ b/xfa/fwl/cfwl_scrollbar.h
@@ -29,6 +29,7 @@
   ~CFWL_ScrollBar() override;
 
   // CFWL_Widget:
+  void PreFinalize() override;
   FWL_Type GetClassID() const override;
   void Update() override;
   void DrawWidget(CFGAS_GEGraphics* pGraphics,
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in PDFium XFA via CFWL_ScrollBar/CFWL_Caret timer during cppgc sweeping

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 Use-After-Free (UAF) vulnerability exists in PDFium’s XFA implementation because CFWL_ScrollBar and CFWL_Caret widgets fail to stop their timers during the cppgc pre-finalization phase. If an incremental garbage collection sweeps a parent widget but yields before sweeping the child scrollbar, the active timer can fire and invoke a callback on the dead widget. This callback accesses the freed parent widget’s memory, potentially allowing an attacker to hijack a virtual method call.

Affected files:

  • third_party/pdfium/xfa/fwl/cfwl_scrollbar.cpp
  • third_party/pdfium/xfa/fwl/cfwl_caret.cpp
  • third_party/pdfium/xfa/fwl/cfwl_scrollbar.h
  • third_party/pdfium/xfa/fwl/cfwl_caret.h
  • third_party/pdfium/xfa/fwl/cfwl_widget.cpp

Estimated timestamp from git blame: 2025-04-03

Summary

A potential Use-After-Free (UAF) vulnerability exists in the FWL (Foxit Widget Layout) layer of PDFium’s XFA implementation. The issue is caused by CFWL_ScrollBar and CFWL_Caret widgets not properly deregistering their associated CFX_Timer during the cppgc pre-finalization phase. This allows a timer to fire after the widget’s parent has been swept and freed by cppgc’s incremental sweeper, leading to a virtual call on freed memory.

Technical Details

CFWL_ScrollBar and CFWL_Caret are cppgc::GarbageCollected objects. They own a std::unique_ptr<CFX_Timer>, which is a non-GC managed timer. The destruction of this timer is currently tied to the widget’s C++ destructor, which runs during the sweeping phase of garbage collection.

PDFium’s cppgc heap is configured for incremental and concurrent sweeping (cppgc::Heap::SweepingType::kIncrementalAndConcurrent). In this mode, the sweeper processes the heap page-by-page and yields to the main message loop to satisfy deadlines.

Because cppgc does not guarantee destruction order, a parent widget (like CFWL_Edit) may be swept and its memory freed before its child CFWL_ScrollBar is swept. If the sweeper yields to the main thread in between these two events, any pending tasks on the main thread—such as a base::RepeatingTimer callback associated with the CFX_Timer—will execute.

When the timer fires, CFWL_ScrollBar::OnTimerFired attempts to access its parent via the outer_ pointer. Because outer_ is a cppgc::Member<CFWL_Widget> (a strong pointer), it is not zeroed out when the target is swept. This results in a call to DispatchEvent(), which performs a virtual call through the dangling outer_ pointer:

void CFWL_Widget::DispatchEvent(CFWL_Event* pEvent) {
  if (outer_) {
    outer_->GetDelegate()->OnProcessEvent(pEvent); // UAF access on swept memory
    return;
  }
  ...
}

If the CFWL_Edit slot has been reclaimed, this results in a type-confused virtual call.

(Note: These are suggested steps based on static analysis; our tooling agent does not have the ability to run code to produce a working exploit).

Potential Exploitation Sequence

  1. An attacker loads a malicious PDF with an XFA form containing a CFWL_Edit widget, which implicitly creates a child CFWL_ScrollBar.
  2. The attacker’s JavaScript triggers the scrollbar’s timer (e.g., via simulated interaction) and then drops all references to the CFWL_Edit subtree, making it unreachable.
  3. A garbage collection cycle is triggered. cppgc marks both widgets as dead.
  4. CFWL_Widget::PreFinalize executes, but because CFWL_ScrollBar does not override it, the timer is not stopped.
  5. The incremental sweeper reclaims the CFWL_Edit object, placing its memory on the cppgc freelist, and then yields to the main thread.
  6. The attacker’s JavaScript grooms the cppgc heap, allocating a new object that reuses the CFWL_Edit slot, filling it with a fake delegate_ pointer.
  7. The CFX_Timer callback fires on the main thread, invoking CFWL_ScrollBar::OnTimerFired() -> DispatchEvent().
  8. DispatchEvent() accesses outer_->GetDelegate(), fetching the attacker’s fake pointer, and calls OnProcessEvent(pEvent) on it.
  9. The virtual call redirects execution, potentially leading to RCE within the sandboxed renderer process.

Reachability

This issue is mitigated by the fact that PDFium’s XFA support is disabled by default in Chrome. It is only reachable if the user has explicitly enabled the #pdf-xfa-forms flag or if the PdfXfaFormsEnabled enterprise policy is active.

Suggested Fix

Both CFWL_ScrollBar and CFWL_Caret should override the PreFinalize() method from CFWL_Widget. In their respective overrides, they should explicitly stop the timer (e.g., timer_.reset();) before calling CFWL_Widget::PreFinalize(). This ensures that timers are safely canceled while the object graph is still intact, preventing callbacks on unreachable 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