High patch-now ✔︎ verified CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PDFium
DescriptionUse after free in PDFium
ComponentPDFium
Sub-componentPDFium
Bug ClassUAF
Action tierpatch-now — corruption primitive, file-reachable in default config (confirmed) — factors set 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 AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Additionally cross-checked: a second, different model independently derived the same bug class, vulnerable function and fix from the commit.
Tracker513536416
Fix commitfa5db8d55093 (pdfium) +53/-15
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
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-05-16 (public git)
Disclosed2026-05-27
Patch gap11 days — fix public in source before the advisory

Background

`CJS_Field`
PDFium’s JavaScript binding object exposing a form field’s properties to document scripts (fxjs/cjs_field.cpp).
`form_fill_env_`
a nullable handle to the CPDFSDK_FormFillEnvironment, evaluated as a boolean in the guards if (!form_fill_env_) (cjs_field.cpp:1275); it becomes null when that environment is destroyed (inferred to be an observing pointer).
JS-reentrant conversion
pRuntime->To*Reentrant(vp) calls (e.g. ToInt32Reentrant, ToBooleanReentrant, ToByteStringReentrant) that can run attacker script, such as a valueOf handler, during value coercion (cjs_field.cpp:785, :1547-1550).

Root Cause Analysis

Verify before building a PoC

The fix in this commit is confirmed, but the trigger and sink live in code this commit does not touch — find and confirm them against the source before building a PoC.

Several CJS_Field setters read form_fill_env_.Get() and pass it to a native Set*/Update* helper, but they perform a JS-reentrant value conversion first and never re-check that the environment still exists. In CJS_Field::set_line_width, the pre-fix code calls SetLineWidth(form_fill_env_.Get(), field_name_, form_control_index_, pRuntime->ToInt32Reentrant(vp)) with both the raw environment pointer and the reentrant conversion as sibling arguments of one call (cjs_field.cpp:1549-1550). In CJS_Field::set_border_style the conversion is already hoisted to ByteString byte_str = pRuntime->ToByteStringReentrant(vp) (:785) but the code then still calls SetBorderStyle(form_fill_env_.Get(), ...) (:789) with no guard. The invariant these call sites assume — that form_fill_env_ is still valid when .Get() is dereferenced — is broken because the reentrant conversion can execute script that destroys the CPDFSDK_FormFillEnvironment, leaving form_fill_env_.Get() dangling.

The fix hoists each conversion into a local (const int value = pRuntime->ToInt32Reentrant(vp)) and inserts if (!form_fill_env_) return CJS_Result::Failure(JSMessage::kBadObjectError); after the conversion and before any use of form_fill_env_.Get(), across set_border_style, set_current_value_indices, set_hidden, set_line_width, set_print and other property setters. Notably CJS_Field::set_display already contained this guard in the pre-fix source (cjs_field.cpp:1274-1277), so for that function the commit only adds a clarifying comment — the reporter’s claim that set_display still passed a raw pointer as a sibling argument disagrees with the shown code, so this commit’s substance is fixing the sibling variants.

Key insight
The core mistake is fetching the form_fill_env_ pointer across a value conversion that can re-enter JavaScript and free that environment; the fix moves the conversion to a prior statement and re-validates form_fill_env_ before using it.

Attack Path

  1. Craft a field setter assignment A document script assigns one of the affected properties (e.g. line width) a value whose coercion runs script, such as an object with a valueOf method. (inferred mechanism per reporter)
  2. Re-enter during conversion The setter’s To*Reentrant(vp) call invokes that script before the native helper runs (cjs_field.cpp:1549-1550).
  3. Destroy the environment The script tears down the form fill environment (e.g. by removing the PDF), so form_fill_env_ is no longer valid. (inferred)
  4. Use freed pointer The native code resumes and calls the helper with form_fill_env_.Get(), dereferencing freed memory (:1549 / :789).

Impact Assessment

An attacker-supplied PDF with form-field script can cause a use-after-free of the CPDFSDK_FormFillEnvironment inside PDFium. The advisory rates this high severity with no in-the-wild exploitation. Reaching the freed-environment state requires the attacker’s script to destroy the environment during property-setter coercion.

Changed Functions

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

Files Changed

  • fxjs/cjs_field.cpp

Audit Directions

  • Raw handle fetched across reentrancy
    Flag any form_fill_env_.Get() (or similar owned pointer) passed to a callee in the same statement as a To*Reentrant conversion; hoist and re-check.
  • Missing post-reentrancy existence check
    Audit every CJS_Field setter that performs a JS-reentrant conversion for a following if (!form_fill_env_) guard.
  • Double reentrant evaluation
    Treat repeated To*Reentrant(vp) calls on the same value as both a correctness and lifetime hazard; evaluate once into a local.
From fa5db8d5509374c3014314771dd7f3621375402f Mon Sep 17 00:00:00 2001
From: Tom Sepez <[email protected]>
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 <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
---

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 [email protected]

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