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 <[email protected]> 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 <[email protected]> Commit-Queue: Tom Sepez <[email protected]> --- 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; }