Overview

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

Files Changed

  • fxjs/gc/heap.cpp
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 {
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cpp
  • third_party/pdfium/xfa/fwl/cfwl_comboedit.cpp
  • third_party/pdfium/xfa/fwl/cfwl_edit.cpp
  • third_party/pdfium/xfa/fwl/cfwl_caret.cpp
  • third_party/pdfium/xfa/fwl/cfwl_widgetmgr.cpp
  • third_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:

  1. Focus the Widget: The attacker’s script grants focus to an XFA ComboBox. This initializes a CFWL_Caret object and starts a 600ms CFX_Timer for blinking.
  2. Bypass Cleanup: The script sends a kKillFocus event to the widget. Due to Bug #2, the focus flag is cleared, but the timer is never stopped.
  3. Orphan Objects & GC: The script removes the widget from the DOM and forces a garbage collection cycle.
  4. Bypass PreFinalize: During GC, CFWL_Edit::PreFinalize executes. Because the focus flag was already maliciously cleared in step 2, the HideCaret() cleanup check fails. The objects are queued for finalization but are indefinitely deferred due to Bug #1.
  5. Heap Grooming: CFWL_Caret (112 bytes) lives in cppgc’s kNormal3 space, while CFWL_ComboEdit (~244 bytes) lives in kNormal4. The attacker forces numerous allocations specifically sized for the kNormal4 bucket.
  6. Targeted Reallocation: This forces the allocator to call SweepForAllocationIfRunning on the kNormal4 space, which finally reclaims the unfinalized CFWL_ComboEdit. The attacker controls the newly allocated memory, forging a fake vtable. The CFWL_Caret (in kNormal3) remains unfinalized, its memory intact, and its timer still active.
  7. Exploitation: When the 600ms timer expires, CFX_Timer::TimerProc dereferences the UnownedPtr to the unfinalized caret. The caret attempts to repaint, walking up its parent pointer (outer_) to the now-attacker-controlled CFWL_ComboEdit memory. 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

  1. Fix CFXGC_Platform override: In third_party/pdfium/fxjs/gc/heap.cpp, implement the missing override for GetForegroundTaskRunner(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);
    }
    
  2. Fix CFWL_ComboEdit focus handling: In third_party/pdfium/xfa/fwl/cfwl_comboedit.cpp, ensure that when kKillFocus is processed, proper caret cleanup is performed. This can be done by invoking HideCaret() or by allowing CFWL_Edit::OnProcessMessage to 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.

View on issue tracker