Prevent import hang on circular page parents

Track visited page parent nodes while copying inherited page attributes
so invalid page trees with self-referential parents stop traversal
instead of looping forever. Add an embedder regression test covering
FPDF_ImportPagesByIndex() and FPDF_ImportPages().

Bug: 517126568
Change-Id: I103779059c32b4d9b9f6675ac621a3392851fe06
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/148630
Commit-Queue: Tom Sepez <tsepez@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Tom Sepez <tsepez@chromium.org>
diff --git a/core/fpdfapi/edit/cpdf_pageorganizer.cpp b/core/fpdfapi/edit/cpdf_pageorganizer.cpp
index a1810cd..cb63f2b 100644
--- a/core/fpdfapi/edit/cpdf_pageorganizer.cpp
+++ b/core/fpdfapi/edit/cpdf_pageorganizer.cpp
@@ -6,6 +6,7 @@
 
 #include "core/fpdfapi/edit/cpdf_pageorganizer.h"
 
+#include <set>
 #include <utility>
 #include <vector>
 
@@ -208,7 +209,12 @@
     return dict->GetObjectFor(src_tag);
   }
 
+  std::set<const CPDF_Dictionary*> visited_parent_dicts;
   while (pp) {
+    if (!visited_parent_dicts.insert(pp.Get()).second) {
+      return nullptr;
+    }
+
     if (pp->KeyExist(src_tag)) {
       return pp->GetObjectFor(src_tag);
     }
diff --git a/fpdfsdk/fpdf_ppo_embeddertest.cpp b/fpdfsdk/fpdf_ppo_embeddertest.cpp
index 0a8084d..6b385c0 100644
--- a/fpdfsdk/fpdf_ppo_embeddertest.cpp
+++ b/fpdfsdk/fpdf_ppo_embeddertest.cpp
@@ -618,6 +618,25 @@
   CompareBitmapWithExpectationSuffix(new_bitmap.get(), pdfium::kHelloWorldPng);
 }
 
+TEST_F(FPDFPPOEmbedderTest, ImportWithSelfReferentialPageParent) {
+  ASSERT_TRUE(OpenDocument("bug_517126568.pdf"));
+  ASSERT_EQ(1, FPDF_GetPageCount(document()));
+
+  ScopedPage page = LoadScopedPage(0);
+  ASSERT_TRUE(page);
+
+  ScopedFPDFDocument new_doc(FPDF_CreateNewDocument());
+  ASSERT_TRUE(new_doc);
+
+  static constexpr int kIndices[] = {0};
+  EXPECT_TRUE(FPDF_ImportPagesByIndex(new_doc.get(), document(), kIndices,
+                                      std::size(kIndices), 0));
+  EXPECT_EQ(1, FPDF_GetPageCount(new_doc.get()));
+
+  EXPECT_TRUE(FPDF_ImportPages(new_doc.get(), document(), "1", 1));
+  EXPECT_EQ(2, FPDF_GetPageCount(new_doc.get()));
+}
+
 TEST_F(FPDFPPOEmbedderTest, ImportIntoDestDocWithoutInfo) {
   ASSERT_TRUE(OpenDocument("hello_world.pdf"));
   EXPECT_EQ(1, FPDF_GetPageCount(document()));
diff --git a/testing/resources/bug_517126568.in b/testing/resources/bug_517126568.in
new file mode 100644
index 0000000..3522606
--- /dev/null
+++ b/testing/resources/bug_517126568.in
@@ -0,0 +1,36 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /Parent 2 0 R  % Deliberate self-reference for testing circular parent handling
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /MediaBox [0 0 200 200]
+  /Resources <<
+  >>
+  /Contents 4 0 R
+>>
+endobj
+{{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+q
+0 0 1 rg
+50 60 10 10 re f
+Q
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/bug_517126568.pdf b/testing/resources/bug_517126568.pdf
new file mode 100644
index 0000000..209e1c9
--- /dev/null
+++ b/testing/resources/bug_517126568.pdf
@@ -0,0 +1,47 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+2 0 obj <<
+  /Type /Pages
+  /Parent 2 0 R  % Deliberate self-reference for testing circular parent handling
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+  /Type /Page
+  /Parent 2 0 R
+  /MediaBox [0 0 200 200]
+  /Resources <<
+  >>
+  /Contents 4 0 R
+>>
+endobj
+4 0 obj <<
+  /Length 29
+>>
+stream
+q
+0 0 1 rg
+50 60 10 10 re f
+Q
+endstream
+endobj
+xref
+0 5
+0000000000 65535 f 
+0000000015 00000 n 
+0000000068 00000 n 
+0000000213 00000 n 
+0000000329 00000 n 
+trailer <<
+  /Root 1 0 R
+  /Size 5
+>>
+startxref
+410
+%%EOF