CVE-2026-10002
Overview
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 guardsif (!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 avalueOfhandler, during value coercion (cjs_field.cpp:785,:1547-1550).
Root Cause Analysis
Verify 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.
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
- 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
valueOfmethod. (inferred mechanism per reporter) - Re-enter during conversion
The setter’s
To*Reentrant(vp)call invokes that script before the native helper runs (cjs_field.cpp:1549-1550). - 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) - Use freed pointer
The native code resumes and calls the helper with
form_fill_env_.Get(), dereferencing freed memory (:1549/:789).
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
iffxjs/cjs_field.cpp |
modified | |
forfxjs/cjs_field.cpp |
modified |
Files Changed
fxjs/cjs_field.cpp
Audit Directions
- Raw handle fetched across reentrancyFlag any
form_fill_env_.Get()(or similar owned pointer) passed to a callee in the same statement as aTo*Reentrantconversion; hoist and re-check. - Missing post-reentrancy existence checkAudit every
CJS_Fieldsetter that performs a JS-reentrant conversion for a followingif (!form_fill_env_)guard. - Double reentrant evaluationTreat repeated
To*Reentrant(vp)calls on the same value as both a correctness and lifetime hazard; evaluate once into a local.
Patch
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);
Original Bug Report
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.cppthird_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) callingSetDisplay(dereference at line 328).CJS_Field::set_line_width(line 1547) callingSetLineWidth(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
- Provide a PDF with a form field and a script.
- The script assigns a property (e.g.,
display) to an object with a customvalueOfmethod. - 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). - After the plugin is destroyed and the environment is freed, return from the
valueOfmethod. - 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.