CVE-2026-11306
Overview
Files Changed
fxjs/gc/heap.cpp
Patch
From 9e5d491ff73630b6a423689698290650050e7b3f Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Thu, 23 Apr 2026 14:53:32 -0700
Subject: [PATCH] Add second form of GetForegroundTaskRunner() override.
Technically, not providing the override is not good because destructors
are deferred. However, on GC finalization via the regular GC paths that
also run without stack, CPPGC always finishes sweeping and thus invokes
the destructors.
Nonetheless, this makes the XFA code more closely match what other
embedders of CPPGC are doing.
-- Gemini-suggested patch.
Fixed: 504548949
Change-Id: I97719180c016b1fcf138190388e4f66b900dfa07
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/146810
Commit-Queue: Tom Sepez <tsepez@chromium.org>
Auto-Submit: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
---
diff --git a/fxjs/gc/heap.cpp b/fxjs/gc/heap.cpp
index 7161cf4..4cbc860 100644
--- a/fxjs/gc/heap.cpp
+++ b/fxjs/gc/heap.cpp
@@ -41,6 +41,14 @@
return g_platform->GetForegroundTaskRunner(g_isolate);
}
+ std::shared_ptr<cppgc::TaskRunner> GetForegroundTaskRunner(
+ cppgc::TaskPriority priority) override {
+ // V8's default platform creates a new task runner when passed the
+ // v8::Isolate pointer the first time. For non-default platforms this will
+ // require getting the appropriate task runner.
+ return g_platform->GetForegroundTaskRunner(g_isolate, priority);
+ }
+
std::unique_ptr<cppgc::JobHandle> PostJob(
cppgc::TaskPriority priority,
std::unique_ptr<cppgc::JobTask> job_task) override {
Original Bug Report
Potential UAF in PDFium XFA due to broken cppgc finalization and CFWL_ComboEdit
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: PDFium’s CFXGC_Platform fails to properly override GetForegroundTaskRunner, causing cppgc object destructors to be indefinitely deferred. Combined with a focus-handling bug in CFWL_ComboEdit that leaves a timer active, an attacker can reallocate a parent widget while a child caret’s timer remains firing. When the timer eventually fires, it performs a virtual method call on the attacker-controlled parent memory, potentially leading to RCE.
Affected files:
third_party/pdfium/fxjs/gc/heap.cppthird_party/pdfium/xfa/fwl/cfwl_comboedit.cppthird_party/pdfium/xfa/fwl/cfwl_edit.cppthird_party/pdfium/xfa/fwl/cfwl_caret.cppthird_party/pdfium/xfa/fwl/cfwl_widgetmgr.cppthird_party/pdfium/core/fxcrt/cfx_timer.cpp
Estimated timestamp from git blame: 2025-04-03
Vulnerability Details
This potential vulnerability arises from the intersection of two distinct bugs in PDFium’s XFA implementation:
1. Broken cppgc Foreground Finalization
In third_party/pdfium/fxjs/gc/heap.cpp, the CFXGC_Platform class acts as an adapter between the V8 platform and the cppgc heap. However, it fails to override the priority-based GetForegroundTaskRunner(TaskPriority) virtual method from cppgc::Platform. When the cppgc Sweeper attempts to schedule foreground finalization tasks (which execute destructors for garbage-collected objects) via platform_->GetForegroundTaskRunner(kForegroundRegularPriority), the call resolves to the base class implementation and returns nullptr. Consequently, the Sweeper silently aborts scheduling these tasks, causing objects to remain in swept_unfinalized_pages queues with their destructors deferred indefinitely.
2. Flawed Caret Cleanup in CFWL_ComboEdit
When a ComboBox widget receives a kKillFocus message, CFWL_ComboEdit::OnProcessMessage intercepts it, clears the FWL_STATE_WGT_Focused flag, and explicitly sets backDefault = false to prevent the base class (CFWL_Edit::OnProcessMessage) from handling the event (third_party/pdfium/xfa/fwl/cfwl_comboedit.cpp:40). Because the base class is bypassed, CFWL_Edit::OnFocusLost() is never called, and HideCaret() is skipped. The caret’s CFX_Timer remains active, holding an UnownedPtr (which is an unprotected raw pointer within the cppgc cage) back to the caret.
Potential Attack Scenario
While our tooling agent does not run code to provide a working PoC, the code strongly indicates an attacker could trigger a Use-After-Free (UAF) using the following suggested steps:
- Focus the Widget: The attacker’s script grants focus to an XFA
ComboBox. This initializes aCFWL_Caretobject and starts a 600msCFX_Timerfor blinking. - Bypass Cleanup: The script sends a
kKillFocusevent to the widget. Due to Bug #2, the focus flag is cleared, but the timer is never stopped. - Orphan Objects & GC: The script removes the widget from the DOM and forces a garbage collection cycle.
- Bypass PreFinalize: During GC,
CFWL_Edit::PreFinalizeexecutes. Because the focus flag was already maliciously cleared in step 2, theHideCaret()cleanup check fails. The objects are queued for finalization but are indefinitely deferred due to Bug #1. - Heap Grooming:
CFWL_Caret(112 bytes) lives incppgc’skNormal3space, whileCFWL_ComboEdit(~244 bytes) lives inkNormal4. The attacker forces numerous allocations specifically sized for thekNormal4bucket. - Targeted Reallocation: This forces the allocator to call
SweepForAllocationIfRunningon thekNormal4space, which finally reclaims the unfinalizedCFWL_ComboEdit. The attacker controls the newly allocated memory, forging a fake vtable. TheCFWL_Caret(inkNormal3) remains unfinalized, its memory intact, and its timer still active. - Exploitation: When the 600ms timer expires,
CFX_Timer::TimerProcdereferences theUnownedPtrto the unfinalized caret. The caret attempts to repaint, walking up its parent pointer (outer_) to the now-attacker-controlledCFWL_ComboEditmemory. The loop invokes a virtual method (pNative->GetWidgetRect()), hijacking the execution flow and potentially allowing arbitrary code execution within the sandboxed renderer process.
Proposed Fixes
- Fix
CFXGC_Platformoverride: Inthird_party/pdfium/fxjs/gc/heap.cpp, implement the missing override forGetForegroundTaskRunner(cppgc::TaskPriority)so that foreground finalization tasks are properly scheduled.std::shared_ptr<cppgc::TaskRunner> GetForegroundTaskRunner( cppgc::TaskPriority priority) override { return g_platform->GetForegroundTaskRunner(g_isolate, priority); } - Fix
CFWL_ComboEditfocus handling: Inthird_party/pdfium/xfa/fwl/cfwl_comboedit.cpp, ensure that whenkKillFocusis processed, proper caret cleanup is performed. This can be done by invokingHideCaret()or by allowingCFWL_Edit::OnProcessMessageto handle focus loss normally.
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.