CVE-2026-95338
Overview
Background
- Passive FFI callback
- an embedder notification such as
FFI_OutputSelectedRectorFFI_Invalidatethat 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 thein_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
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.
in_passive_ffi_callback_ and rejects re-entrant mutating entry points during it.Attack Path
- Craft a form-bearing PDF
Build a document whose rendering triggers a passive callback such as
FFI_InvalidateorFFI_OutputSelectedRectduring painting or focus handling. - Enter the passive callback
Trigger paint/selection so
CPDFSDK_FormFillEnvironmentcalls into the embedder’sinfo_table while holding live page-view and form-object references. - Re-enter PDFium
From inside the callback, invoke a mutating entry point like
FORM_OnBeforeClosePageor aFORM_Do*Action, which closes/loads a page or runs a form action. - 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.
- Use after free Control returns to the outer operation, which touches the now-freed memory, yielding a controllable UAF.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
iffpdfsdk/cpdfsdk_formfillenvironment.cpp |
modified | |
iffpdfsdk/fpdf_formfill.cpp |
modified | |
FORM_DoDocumentJSActionfpdfsdk/fpdf_formfill.cpp |
modified | |
FORM_DoDocumentOpenActionfpdfsdk/fpdf_formfill.cpp |
modified |
Files Changed
fpdfsdk/fpdf_formfill.cppfpdfsdk/cpdfsdk_formfillenvironment.hfpdfsdk/cpdfsdk_formfillenvironment.cpp
Audit Directions
- Passive callback dispatch sitesaudit every
info_->FFI_*dispatch inCPDFSDK_FormFillEnvironmentfor ones that still lack thein_passive_ffi_callback_guard yet run during painting/notification. - Re-entrant mutating entry pointsreview other
FORM_*andFPDF_*exported functions that load, close, or mutate pages/forms to confirm they honorInPassiveFFICallback()before mutating. - Callback held-reference lifetimelook 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.
Patch
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;
}