CVE-2026-11305
Overview
Files Changed
xfa/fwl/cfwl_caret.cppxfa/fwl/cfwl_caret.hxfa/fwl/cfwl_scrollbar.cppxfa/fwl/cfwl_scrollbar.h
Patch
From c7dec2a900e0684ce1e500eb9835c21d503eb640 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
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 <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
---
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,
Original Bug Report
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.cppthird_party/pdfium/xfa/fwl/cfwl_caret.cppthird_party/pdfium/xfa/fwl/cfwl_scrollbar.hthird_party/pdfium/xfa/fwl/cfwl_caret.hthird_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
- An attacker loads a malicious PDF with an XFA form containing a
CFWL_Editwidget, which implicitly creates a childCFWL_ScrollBar. - The attacker’s JavaScript triggers the scrollbar’s timer (e.g., via simulated interaction) and then drops all references to the
CFWL_Editsubtree, making it unreachable. - A garbage collection cycle is triggered.
cppgcmarks both widgets as dead. CFWL_Widget::PreFinalizeexecutes, but becauseCFWL_ScrollBardoes not override it, the timer is not stopped.- The incremental sweeper reclaims the
CFWL_Editobject, placing its memory on thecppgcfreelist, and then yields to the main thread. - The attacker’s JavaScript grooms the
cppgcheap, allocating a new object that reuses theCFWL_Editslot, filling it with a fakedelegate_pointer. - The
CFX_Timercallback fires on the main thread, invokingCFWL_ScrollBar::OnTimerFired()->DispatchEvent(). DispatchEvent()accessesouter_->GetDelegate(), fetching the attacker’s fake pointer, and callsOnProcessEvent(pEvent)on it.- 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.