Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Bug ClassUAF
Tracker513536416
Fix commitfa5db8d55093 (pdfium) +53/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
fxjs/cjs_field.cpp
modified
for
fxjs/cjs_field.cpp
modified

Files Changed

  • fxjs/cjs_field.cpp
From fa5db8d5509374c3014314771dd7f3621375402f Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Fri, 15 May 2026 18:48:52 -0700
Subject: [PATCH] Fix variants of prior CJS_Field::set_display() issue.

Test for existence of form_field_env_ following JS re-entrancy.

Bug: 513536416
Change-Id: Icbcc26ccefaea9d332a9e8da8a130d9823830a35
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/147790
Commit-Queue: Lei Zhang <thestig@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
---

diff --git a/fxjs/cjs_field.cpp b/fxjs/cjs_field.cpp
index 1d2acf3..c020254 100644
--- a/fxjs/cjs_field.cpp
+++ b/fxjs/cjs_field.cpp
@@ -777,12 +777,16 @@
 
 CJS_Result CJS_Field::set_border_style(CJS_Runtime* pRuntime,
                                        v8::Local<v8::Value> vp) {
-  DCHECK(form_fill_env_);
   if (!can_set_) {
     return CJS_Result::Failure(JSMessage::kReadOnlyError);
   }
-
   ByteString byte_str = pRuntime->ToByteStringReentrant(vp);
+
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
+
   if (delay_) {
     AddDelay_String(FP_BORDERSTYLE, byte_str);
   } else {
@@ -1130,6 +1134,11 @@
     }
   }
 
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
+
   if (delay_) {
     AddDelay_WordArray(FP_CURRENTVALUEINDICES, array);
   } else {
@@ -1272,9 +1281,12 @@
     return CJS_Result::Failure(JSMessage::kReadOnlyError);
   }
   int value = pRuntime->ToInt32Reentrant(vp);
+
+  // Check if still exists following JS re-entrancy.
   if (!form_fill_env_) {
     return CJS_Result::Failure(JSMessage::kBadObjectError);
   }
+
   if (delay_) {
     AddDelay_Int(FP_DISPLAY, value);
   } else {
@@ -1461,12 +1473,17 @@
   if (!can_set_) {
     return CJS_Result::Failure(JSMessage::kReadOnlyError);
   }
+  const bool value = pRuntime->ToBooleanReentrant(vp);
+
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
 
   if (delay_) {
-    AddDelay_Bool(FP_HIDDEN, pRuntime->ToBooleanReentrant(vp));
+    AddDelay_Bool(FP_HIDDEN, value);
   } else {
-    SetHidden(form_fill_env_.Get(), field_name_, form_control_index_,
-              pRuntime->ToBooleanReentrant(vp));
+    SetHidden(form_fill_env_.Get(), field_name_, form_control_index_, value);
   }
   return CJS_Result::Success();
 }
@@ -1542,12 +1559,17 @@
   if (!can_set_) {
     return CJS_Result::Failure(JSMessage::kReadOnlyError);
   }
+  const int value = pRuntime->ToInt32Reentrant(vp);
+
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
 
   if (delay_) {
-    AddDelay_Int(FP_LINEWIDTH, pRuntime->ToInt32Reentrant(vp));
+    AddDelay_Int(FP_LINEWIDTH, value);
   } else {
-    SetLineWidth(form_fill_env_.Get(), field_name_, form_control_index_,
-                 pRuntime->ToInt32Reentrant(vp));
+    SetLineWidth(form_fill_env_.Get(), field_name_, form_control_index_, value);
   }
   return CJS_Result::Success();
 }
@@ -1712,10 +1734,15 @@
   if (FieldArray.empty()) {
     return CJS_Result::Failure(JSMessage::kBadObjectError);
   }
-
   if (!can_set_) {
     return CJS_Result::Failure(JSMessage::kReadOnlyError);
   }
+  const bool value = pRuntime->ToBooleanReentrant(vp);
+
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
 
   for (CPDF_FormField* pFormField : FieldArray) {
     if (form_control_index_ < 0) {
@@ -1724,23 +1751,20 @@
         if (CPDFSDK_Widget* pWidget =
                 pForm->GetWidget(pFormField->GetControl(i))) {
           uint32_t dwFlags = pWidget->GetFlags();
-          if (pRuntime->ToBooleanReentrant(vp)) {
+          if (value) {
             dwFlags |= pdfium::annotation_flags::kPrint;
           } else {
             dwFlags &= ~pdfium::annotation_flags::kPrint;
           }
-
           if (dwFlags != pWidget->GetFlags()) {
             pWidget->SetFlags(dwFlags);
             bSet = true;
           }
         }
       }
-
       if (bSet) {
         UpdateFormField(form_fill_env_.Get(), pFormField, false);
       }
-
       continue;
     }
 
@@ -1752,12 +1776,11 @@
             pFormField->GetControl(form_control_index_)) {
       if (CPDFSDK_Widget* pWidget = pForm->GetWidget(pFormControl)) {
         uint32_t dwFlags = pWidget->GetFlags();
-        if (pRuntime->ToBooleanReentrant(vp)) {
+        if (value) {
           dwFlags |= pdfium::annotation_flags::kPrint;
         } else {
           dwFlags &= ~pdfium::annotation_flags::kPrint;
         }
-
         if (dwFlags != pWidget->GetFlags()) {
           pWidget->SetFlags(dwFlags);
           UpdateFormControl(form_fill_env_.Get(),
@@ -1877,6 +1900,11 @@
   float f3 = static_cast<float>(pRuntime->ToInt32Reentrant(
       pRuntime->GetArrayElementReentrant(rcArray, 3)));
 
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
+
   CFX_FloatRect crRect(f0, f1, f2, f3);
   if (delay_) {
     AddDelay_Rect(FP_RECT, crRect);
@@ -2321,6 +2349,11 @@
     strArray.push_back(pRuntime->ToWideStringReentrant(vp));
   }
 
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
+
   if (delay_) {
     AddDelay_WideStringArray(FP_VALUE, strArray);
   } else {
@@ -2499,6 +2532,11 @@
     bCheckit = pRuntime->ToBooleanReentrant(params[1]);
   }
 
+  // Check if still exists following JS re-entrancy.
+  if (!form_fill_env_) {
+    return CJS_Result::Failure(JSMessage::kBadObjectError);
+  }
+
   CPDF_FormField* pFormField = GetFirstFormField();
   if (!pFormField) {
     return CJS_Result::Failure(JSMessage::kBadObjectError);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in PDFium CJS_Field due to indeterminate argument evaluation order

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in PDFium’s JavaScript engine when setting certain field properties. Indeterminate C++ evaluation order allows a raw pointer to the form environment to be captured as a function argument before a reentrant JavaScript call destroys the underlying object.

Affected files:

  • third_party/pdfium/fxjs/cjs_field.cpp
  • third_party/pdfium/fxjs/cjs_field.h

Estimated timestamp from git blame: 2017-10-19

Root Cause Analysis

In third_party/pdfium/fxjs/cjs_field.cpp, the functions CJS_Field::set_display and CJS_Field::set_line_width contain a sequencing hazard that can lead to a Use-After-Free (UAF).

These functions pass a raw pointer obtained from an ObservedPtr (form_fill_env_.Get()) and a value conversion that triggers JavaScript reentrancy (pRuntime->ToInt32(vp)) as sibling arguments to a function call. For example, in CJS_Field::set_display:

// third_party/pdfium/fxjs/cjs_field.cpp:1278-1279
SetDisplay(form_fill_env_.Get(), field_name_, form_control_index_,
           pRuntime->ToInt32(vp));

Per the C++17 specification ([expr.call]/8), the evaluation of function arguments is indeterminately sequenced. If the compiler evaluates form_fill_env_.Get() before pRuntime->ToInt32(vp), it captures a raw pointer to the CPDFSDK_FormFillEnvironment object.

The pRuntime->ToInt32(vp) call can execute arbitrary JavaScript (e.g., via a valueOf method). If this JavaScript causes the destruction of the CPDFSDK_FormFillEnvironment (e.g., by navigating the page or removing the PDF element from the DOM), the captured raw pointer becomes dangling. The subsequent execution of the callee (SetDisplay or SetLineWidth) immediately dereferences this dangling pointer.

Affected Code

  • CJS_Field::set_display (line 1278) calling SetDisplay (dereference at line 328).
  • CJS_Field::set_line_width (line 1547) calling SetLineWidth (dereference at line 377).

Other properties, such as border_style, correctly hoist the conversion call to a separate statement, avoiding this hazard.

Potential Attack Steps

  1. Provide a PDF with a form field and a script.
  2. The script assigns a property (e.g., display) to an object with a custom valueOf method.
  3. Inside valueOf, trigger a nested event loop (e.g., app.alert) and simultaneously cause the PDF plugin to be destroyed (e.g., by the embedding page removing the iframe).
  4. After the plugin is destroyed and the environment is freed, return from the valueOf method.
  5. The native code resumes and calls the callee with the dangling environment pointer.

Suggested Fix

Hoist the JavaScript-reentrant conversion calls into a separate statement before retrieving the raw pointer from the ObservedPtr. This ensures the pointer is retrieved (and potentially checked) only after all JavaScript execution for that setter has finished:

int nValue = pRuntime->ToInt32(vp);
if (!form_fill_env_)
  return CJS_Result::Failure(JSMessage::kBadObjectError);
SetDisplay(form_fill_env_.Get(), field_name_, form_control_index_, nValue);

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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