High patch-now ~ plausible CVSS 8.8 chrome UAF 🔧 Commit mapped 🧬 A nested callout frees the object, then it is used

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Action tierpatch-now — corruption primitive, file-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Diff only adds re-entrancy guards; it doesn't show which object is freed or that Chrome's embedder callbacks actually re-enter from passive FFI callbacks. The analysis states the freeing of in-use objects as fact. The preconditions it lists (embedder re-entry) contradict its 'default' claim.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker556535630
Fix commit4a36628222bb (pdfium) +37/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSeungMyung Lee (@sm1ee), Siung kim (@ksw9722)
Reported2026-09-03 (per vendor advisory)
Fix landed2026-09-08 (public git)
Disclosed2026-09-22
Patch gap14 days — fix public in source before the advisory

Background

Passive FFI callback
an embedder notification such as FFI_OutputSelectedRect or FFI_Invalidate that PDFium fires during internal work like painting, where re-entrant form or page mutation is not expected.
`CPDFSDK_FormFillEnvironment`
the PDFium object that owns page views and form state and dispatches embedder callbacks through its info_ function table.
`AutoRestorer<bool>`
an RAII helper that saves a bool’s value and restores it when the guard leaves scope, used here to scope the in_passive_ffi_callback_ flag to a single callback.
Re-entrancy UAF
a use-after-free that occurs when an embedder, called back mid-operation, re-enters PDFium to load or close pages or run form actions, freeing objects the outer operation still holds.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff only adds re-entrancy guards; it doesn't show which object is freed or that Chrome's embedder callbacks actually re-enter from passive FFI callbacks. The analysis states the freeing of in-use objects as fact. The preconditions it lists (embedder re-entry) contradict its 'default' claim.

While CPDFSDK_FormFillEnvironment was painting or notifying, it invoked passive embedder callbacks such as info_->FFI_OutputSelectedRect and info_->FFI_Invalidate while still holding live references to page views and form objects. An embedder could re-enter PDFium from inside such a callback and call entry points like FORM_OnAfterLoadPage, FORM_OnBeforeClosePage, or FORM_Do*Action, which load, close, or mutate pages and thereby free the objects the outer frame was still using — violating the invariant that these mutating operations never run nested inside a passive notification.

The fix adds an in_passive_ffi_callback_ flag set via AutoRestorer<bool> around every passive callback dispatch, exposes it through InPassiveFFICallback(), and makes the re-entrant entry points early-return when the flag is set, so the dangerous nested mutations are simply ignored and the outer frame’s pointers stay valid.

Key insight
The core mistake was dispatching passive embedder callbacks without guarding against re-entrant page/form mutation, so an embedder could free objects still in use up the stack; the fix marks the passive-callback window with in_passive_ffi_callback_ and rejects re-entrant mutating entry points during it.

Attack Path

  1. Craft a form-bearing PDF Build a document whose rendering triggers a passive callback such as FFI_Invalidate or FFI_OutputSelectedRect during painting or focus handling.
  2. Enter the passive callback Trigger paint/selection so CPDFSDK_FormFillEnvironment calls into the embedder’s info_ table while holding live page-view and form-object references.
  3. Re-enter PDFium From inside the callback, invoke a mutating entry point like FORM_OnBeforeClosePage or a FORM_Do*Action, which closes/loads a page or runs a form action.
  4. Free the in-use object The nested mutation destroys the page view or form object that the still-unwound outer frame will continue to dereference.
  5. Use after free Control returns to the outer operation, which touches the now-freed memory, yielding a controllable UAF.

Impact Assessment

An attacker who gets a victim to render a malicious PDF gains a use-after-free in the PDFium component of the renderer process, potentially leading to memory corruption and code execution within the renderer sandbox. The preconditions are an embedder configured to process form-fill callbacks and a document engineered to re-enter a mutating form/page entry point from within a passive callback.

Changed Functions

FunctionChangeNotes
if
fpdfsdk/cpdfsdk_formfillenvironment.cpp
modified
if
fpdfsdk/fpdf_formfill.cpp
modified
FORM_DoDocumentJSAction
fpdfsdk/fpdf_formfill.cpp
modified
FORM_DoDocumentOpenAction
fpdfsdk/fpdf_formfill.cpp
modified

Files Changed

  • fpdfsdk/fpdf_formfill.cpp
  • fpdfsdk/cpdfsdk_formfillenvironment.h
  • fpdfsdk/cpdfsdk_formfillenvironment.cpp

Audit Directions

  • Passive callback dispatch sites
    audit every info_->FFI_* dispatch in CPDFSDK_FormFillEnvironment for ones that still lack the in_passive_ffi_callback_ guard yet run during painting/notification.
  • Re-entrant mutating entry points
    review other FORM_* and FPDF_* exported functions that load, close, or mutate pages/forms to confirm they honor InPassiveFFICallback() before mutating.
  • Callback held-reference lifetime
    look for other embedder-callback paths where a live page-view or form-object pointer is dereferenced after control returns from an embedder that could have freed it.
From 4a36628222bb647c23b4ef3b8f3fc64e93e1dd3b Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 08 Sep 2026 11:52:29 -0700
Subject: [PATCH] Ignore re-entrant form calls during passive FFI callbacks

Passive FFI callbacks (e.g. FFI_OutputSelectedRect, FFI_Invalidate)
are notifications invoked during internal operations such as painting.
Embedders must not re-entrantly execute form actions or load/close
pages during these callbacks.

Track passive callbacks using CPDFSDK_FormFillEnvironment's
in_passive_ffi_callback_ flag, and early-return from entry points
such as FORM_OnAfterLoadPage, FORM_OnBeforeClosePage, and
FORM_Do*Action when invoked re-entrantly.

Active retrieval callbacks such as FFI_GetPage remain unflagged, as
page loading is expected in those paths.

TAG=agy
CONV=9e87637a-cb01-4106-9c04-ad4da437fc61

Bug: 556535630
Change-Id: Id892141ce082cc8895b4ed5dc66af3edaf5ef740
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/156913
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
---
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
index 6c9b796..1568fc9 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.cpp
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.cpp
@@ -16,6 +16,7 @@
 #include "core/fpdfapi/parser/cpdf_array.h"
 #include "core/fpdfapi/parser/cpdf_dictionary.h"
 #include "core/fpdfdoc/cpdf_nametree.h"
+#include "core/fxcrt/autorestorer.h"
 #include "core/fxcrt/check.h"
 #include "core/fxcrt/containers/contains.h"
 #include "core/fxcrt/data_vector.h"
@@ -120,6 +121,9 @@
 
   CFX_PointF ptA = pFormField->PWLtoFFL(CFX_PointF(rect.left, rect.bottom));
   CFX_PointF ptB = pFormField->PWLtoFFL(CFX_PointF(rect.right, rect.top));
+
+  AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+  in_passive_ffi_callback_ = true;
   info_->FFI_OutputSelectedRect(info_, pPage, ptA.x, ptB.y, ptB.x, ptA.y);
 }
 
@@ -366,6 +370,8 @@
 void CPDFSDK_FormFillEnvironment::Invalidate(IPDF_Page* page,
                                              const FX_RECT& rect) {
   if (info_ && info_->FFI_Invalidate) {
+    AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+    in_passive_ffi_callback_ = true;
     info_->FFI_Invalidate(info_, FPDFPageFromIPDFPage(page), rect.left,
                           rect.top, rect.right, rect.bottom);
   }
@@ -374,6 +380,8 @@
 void CPDFSDK_FormFillEnvironment::SetCursor(
     IPWL_FillerNotify::CursorStyle nCursorType) {
   if (info_ && info_->FFI_SetCursor) {
+    AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+    in_passive_ffi_callback_ = true;
     info_->FFI_SetCursor(info_, static_cast<int>(nCursorType));
   }
 }
@@ -394,6 +402,8 @@
 
 void CPDFSDK_FormFillEnvironment::OnChange() {
   if (info_ && info_->FFI_OnChange) {
+    AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+    in_passive_ffi_callback_ = true;
     info_->FFI_OnChange(info_);
   }
 }
@@ -416,6 +426,8 @@
     size_t nCharacters = text.GetLength();
     ByteString bsUTFText = text.ToUTF16LE();
     auto* pBuffer = reinterpret_cast<const unsigned short*>(bsUTFText.c_str());
+    AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+    in_passive_ffi_callback_ = true;
     info_->FFI_SetTextFieldFocus(
         info_, pBuffer, pdfium::checked_cast<FPDF_DWORD>(nCharacters), bFocus);
   }
@@ -484,6 +496,8 @@
                                                double right,
                                                double bottom) {
   if (info_ && info_->version >= 2 && info_->FFI_DisplayCaret) {
+    AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+    in_passive_ffi_callback_ = true;
     info_->FFI_DisplayCaret(info_, FPDFPageFromIPDFPage(page), bVisible, left,
                             top, right, bottom);
   }
@@ -901,6 +915,8 @@
   FPDF_ANNOTATION fpdf_annot =
       FPDFAnnotationFromCPDFAnnotContext(focused_annot.get());
 
+  AutoRestorer<bool> ffi_restorer(&in_passive_ffi_callback_);
+  in_passive_ffi_callback_ = true;
   info_->FFI_OnFocusChange(info_, fpdf_annot, pPageView->GetPageIndex());
 }
 
diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.h b/fpdfsdk/cpdfsdk_formfillenvironment.h
index 56613dc..74efbd8 100644
--- a/fpdfsdk/cpdfsdk_formfillenvironment.h
+++ b/fpdfsdk/cpdfsdk_formfillenvironment.h
@@ -85,6 +85,8 @@
   bool HasPermissions(uint32_t flags) const override;
   void OnChange() override;
 
+  bool InPassiveFFICallback() const { return in_passive_ffi_callback_; }
+
   CPDFSDK_PageView* GetPageViewAtIndex(int nIndex);
   void RemovePageView(IPDF_Page* pUnderlyingPage);
   void UpdateAllViews(CPDFSDK_Annot* pAnnot);
@@ -287,6 +289,7 @@
   std::unique_ptr<CFFL_InteractiveFormFiller> interactive_form_filler_;
   bool change_mask_ = false;
   bool being_destroyed_ = false;
+  bool in_passive_ffi_callback_ = false;
 
   // Holds the list of focusable annot types.
   // Annotations of type WIDGET are by default focusable.
diff --git a/fpdfsdk/fpdf_formfill.cpp b/fpdfsdk/fpdf_formfill.cpp
index 451cdd5..d05882b 100644
--- a/fpdfsdk/fpdf_formfill.cpp
+++ b/fpdfsdk/fpdf_formfill.cpp
@@ -850,6 +850,12 @@
 
 FPDF_EXPORT void FPDF_CALLCONV FORM_OnAfterLoadPage(FPDF_PAGE page,
                                                     FPDF_FORMHANDLE hHandle) {
+  CPDFSDK_FormFillEnvironment* pFormFillEnv =
+      CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
+    return;
+  }
+
   if (CPDFSDK_PageView* pPageView = FormHandleToPageView(hHandle, page)) {
     pPageView->SetValid(true);
   }
@@ -859,7 +865,7 @@
                                                       FPDF_FORMHANDLE hHandle) {
   CPDFSDK_FormFillEnvironment* pFormFillEnv =
       CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
-  if (!pFormFillEnv) {
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
     return;
   }
 
@@ -880,7 +886,11 @@
 FORM_DoDocumentJSAction(FPDF_FORMHANDLE hHandle) {
   CPDFSDK_FormFillEnvironment* pFormFillEnv =
       CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
-  if (pFormFillEnv && pFormFillEnv->IsJSPlatformPresent()) {
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
+    return;
+  }
+
+  if (pFormFillEnv->IsJSPlatformPresent()) {
     pFormFillEnv->ProcJavascriptAction();
   }
 }
@@ -889,16 +899,18 @@
 FORM_DoDocumentOpenAction(FPDF_FORMHANDLE hHandle) {
   CPDFSDK_FormFillEnvironment* pFormFillEnv =
       CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
-  if (pFormFillEnv) {
-    pFormFillEnv->ProcOpenAction();
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
+    return;
   }
+
+  pFormFillEnv->ProcOpenAction();
 }
 
 FPDF_EXPORT void FPDF_CALLCONV FORM_DoDocumentAAction(FPDF_FORMHANDLE hHandle,
                                                       int aaType) {
   CPDFSDK_FormFillEnvironment* pFormFillEnv =
       CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
-  if (!pFormFillEnv) {
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
     return;
   }
 
@@ -920,7 +932,7 @@
                                                   int aaType) {
   CPDFSDK_FormFillEnvironment* pFormFillEnv =
       CPDFSDKFormFillEnvironmentFromFPDFFormHandle(hHandle);
-  if (!pFormFillEnv) {
+  if (!pFormFillEnv || pFormFillEnv->InPassiveFFICallback()) {
     return;
   }
 
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.