diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp index 344b3cff8..517824819 100644 --- a/fpdfsdk/fpdf_formfill_embeddertest.cpp +++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp @@ -3674,3 +3674,62 @@ TEST_F(FPDFFormFillTextFormEmbedderTest, CutAllTextUndoRestoresAllCharacters) { EXPECT_TRUE(CanUndo()); EXPECT_TRUE(CanRedo()); } + +// Regression test for use-after-free in CFFL_CheckBox::OnChar. +// +// CFFL_CheckBox::OnChar() calls OnButtonUp() which dispatches the /AA /U +// JavaScript action. If that action causes the embedder to re-entrantly close +// the page (FORM_OnBeforeClosePage -> RemovePageView -> ~CPDFSDK_PageView -> +// ~CPDFSDK_Widget -> CFFL_InteractiveFormFiller::OnDelete -> map_.erase()), +// the live CFFL_CheckBox is destroyed while CFFL_CheckBox::OnChar is still on +// the stack. OnChar then writes |widget_ = nullptr| to freed |this|. +class CFFLCheckBoxOnCharUAFTest : public EmbedderTest { + public: + class ClosePageDelegate : public EmbedderTest::Delegate { + public: + int Alert(FPDF_WIDESTRING message, + FPDF_WIDESTRING title, + int type, + int icon) override { + // Re-entrantly remove the page view while CFFL_CheckBox::OnChar / + // OnButtonUp is on the stack. This destroys the CPDFSDK_Widget, which + // destroys the live CFFL_CheckBox. + if (page_ && form_) { + FORM_OnBeforeClosePage(page_, form_); + page_ = nullptr; + } + return 0; + } + + FPDF_PAGE page_ = nullptr; + FPDF_FORMHANDLE form_ = nullptr; + }; +}; + +TEST_F(CFFLCheckBoxOnCharUAFTest, ReentrantPageCloseDuringButtonUp) { + ClosePageDelegate delegate; + SetDelegate(&delegate); + + ASSERT_TRUE(OpenDocument("checkbox_buttonup_uaf.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + + delegate.page_ = page; + delegate.form_ = form_handle(); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 0)); + ASSERT_TRUE(annot); + ASSERT_TRUE(FORM_SetFocusedAnnot(form_handle(), annot.get())); + } + + // Space on a focused checkbox: CFFL_CheckBox::OnChar -> OnButtonUp -> + // /AA /U JavaScript -> app.alert() -> ClosePageDelegate::Alert -> + // FORM_OnBeforeClosePage -> RemovePageView -> ~CFFL_CheckBox. + // Then OnChar writes widget_ = nullptr to freed |this|. + FORM_OnChar(form_handle(), page, pdfium::ascii::kSpace, 0); + + // The page view was already removed by FORM_OnBeforeClosePage above; the + // FPDF_PAGE handle is still owned by the test fixture's page_map_. + UnloadPageNoEvents(page); +} diff --git a/testing/resources/checkbox_buttonup_uaf.in b/testing/resources/checkbox_buttonup_uaf.in new file mode 100644 index 000000000..9c3347d43 --- /dev/null +++ b/testing/resources/checkbox_buttonup_uaf.in @@ -0,0 +1,58 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R + /AcroForm << /Fields [4 0 R] >> +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 200 200] + /Annots [4 0 R] +>> +endobj +{{object 4 0}} << + /Type /Annot + /Subtype /Widget + /FT /Btn + /F 4 + /P 3 0 R + /Rect [50 100 70 120] + /T (cb) + /AS /Off + /V /Off + /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> + /AA << /U << /S /JavaScript /JS (app.alert\("x"\);) >> >> +>> +endobj +{{object 5 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q 0 0 20 20 re f Q +endstream +endobj +{{object 6 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q 0 0 20 20 re S Q +endstream +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/checkbox_buttonup_uaf.pdf b/testing/resources/checkbox_buttonup_uaf.pdf new file mode 100644 index 000000000..10999d51e --- /dev/null +++ b/testing/resources/checkbox_buttonup_uaf.pdf @@ -0,0 +1,71 @@ +%PDF-1.7 +% ò¤ô +1 0 obj << + /Type /Catalog + /Pages 2 0 R + /AcroForm << /Fields [4 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 200 200] + /Annots [4 0 R] +>> +endobj +4 0 obj << + /Type /Annot + /Subtype /Widget + /FT /Btn + /F 4 + /P 3 0 R + /Rect [50 100 70 120] + /T (cb) + /AS /Off + /V /Off + /AP << /N << /Yes 5 0 R /Off 6 0 R >> >> + /AA << /U << /S /JavaScript /JS (app.alert\("x"\);) >> >> +>> +endobj +5 0 obj << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + /Length 18 +>> +stream +q 0 0 20 20 re f Q +endstream +endobj +6 0 obj << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + /Length 18 +>> +stream +q 0 0 20 20 re S Q +endstream +endobj +xref +0 7 +0000000000 65535 f +0000000015 00000 n +0000000102 00000 n +0000000165 00000 n +0000000260 00000 n +0000000502 00000 n +0000000626 00000 n +trailer << + /Root 1 0 R + /Size 7 +>> +startxref +750 +%%EOF