[PDFium] Prevent infinite loop in AGG with degenerate dash patterns

When rendering paths with extremely small dash patterns (e.g.,
[0.000015, 0.000015]), the AGG backend enters a near-infinite loop
attempting to generate millions of dash segments. This causes the
renderer process to hang indefinitely.

This CL mitigates the issue by detecting degenerate dash patterns in
`RasterizeStroke()`. If the total length of a dash cycle is less than a
safety threshold (0.1f), the dash pattern is ignored, and the path is
rendered as a solid line.

Note: AGG and Skia render this case differently because Skia's stroke
path implementation likely has built-in robustness checks or different
precision handling for extremely small dash segments that prevent
infinite loops, whereas AGG requires this explicit safety threshold to
avoid hanging.

Added regression test: bug_467170761.in

Bug: 467170761
Change-Id: I7dbc20945f46f687e7e8b26c0538ffb371a95f00
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/139210
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fxge/agg/cfx_agg_devicedriver.cpp b/core/fxge/agg/cfx_agg_devicedriver.cpp
index 2a6c52c..09680a6 100644
--- a/core/fxge/agg/cfx_agg_devicedriver.cpp
+++ b/core/fxge/agg/cfx_agg_devicedriver.cpp
@@ -310,7 +310,38 @@
   }
   width = std::max(width, unit);
   const std::vector<float>& dash_array = pGraphState->dash_array();
-  if (!dash_array.empty()) {
+
+  // If the dash pattern cycle is too small (< 0.1 device pixels), render as
+  // a solid line instead. This prevents performance issues in AGG while
+  // maintaining visual fidelity (such small dashes are invisible anyway).
+  bool should_apply_dash_pattern = !dash_array.empty();
+  if (should_apply_dash_pattern) {
+    float dash_cycle_len = 0.0f;
+    for (float val : dash_array) {
+      // Reject non-finite values (NaN, Infinity) per PDF spec
+      if (!std::isfinite(val)) {
+        should_apply_dash_pattern = false;
+        break;
+      }
+      // Clamp negatives per PDF spec (non-negative expected)
+      dash_cycle_len += std::max(0.0f, val);
+    }
+
+    // Minimum dash cycle length in device pixels.
+    // Based on empirical testing:
+    // - At 96 DPI: 0.1px ≈ 0.001 inches (imperceptible to human eye)
+    // - At 300 DPI: 0.1px ≈ 0.0003 inches (still imperceptible)
+    constexpr float kMinDashCycleThreshold = 0.1f;
+
+    // If the dash cycle length is less than this value, the gaps would
+    // be nearly invisible, but rendering them would cause significant
+    // performance overhead.
+    if (dash_cycle_len * scale < kMinDashCycleThreshold) {
+      should_apply_dash_pattern = false;
+    }
+  }
+
+  if (should_apply_dash_pattern) {
     using DashConverter = agg::conv_dash<agg::path_storage>;
     DashConverter dash(*path_data);
     for (float dash_len : dash_array) {
diff --git a/testing/resources/pixel/bug_467170761.in b/testing/resources/pixel/bug_467170761.in
new file mode 100644
index 0000000..135bfa2
--- /dev/null
+++ b/testing/resources/pixel/bug_467170761.in
@@ -0,0 +1,53 @@
+{{header}}
+{{object 1 0}} <<
+  /Type /Catalog
+  /Pages 2 0 R
+>>
+endobj
+{{object 2 0}} <<
+  /Type /Pages
+  /Count 1
+  /Kids [3 0 R]
+>>
+endobj
+{{object 3 0}} <<
+  /Type /Page
+  /Parent 2 0 R
+  /Contents 4 0 R
+  /MediaBox [0 0 200 200]
+>>
+endobj
+{{object 4 0}} <<
+  {{streamlen}}
+>>
+stream
+q
+  % Set line width to 10
+  10 w
+
+  % Dash entry equal to Skia clamp threshold (1e-6)
+  [0.000001, 0.000001] 0 d
+  20 80 m
+  180 80 l
+  S
+
+  % Key point: Set an extremely small dash pattern
+  % Before the fix, this would cause AGG to attempt
+  % drawing 160 / 0.00003 approx 5.33 million loops
+  [0.000015 0.000015] 0 d
+  20 100 m
+  180 100 l
+  S
+
+  % Exactly at threshold
+  [0.05 0.05] 0 d
+  20 120 m
+  180 120 l
+  S
+Q
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
\ No newline at end of file
diff --git a/testing/resources/pixel/bug_467170761_expected_agg.pdf.0.png b/testing/resources/pixel/bug_467170761_expected_agg.pdf.0.png
new file mode 100644
index 0000000..6517531
--- /dev/null
+++ b/testing/resources/pixel/bug_467170761_expected_agg.pdf.0.png
Binary files differ
diff --git a/testing/resources/pixel/bug_467170761_expected_gdi.pdf.0.png b/testing/resources/pixel/bug_467170761_expected_gdi.pdf.0.png
new file mode 100644
index 0000000..08497d2
--- /dev/null
+++ b/testing/resources/pixel/bug_467170761_expected_gdi.pdf.0.png
Binary files differ
diff --git a/testing/resources/pixel/bug_467170761_expected_skia.pdf.0.png b/testing/resources/pixel/bug_467170761_expected_skia.pdf.0.png
new file mode 100644
index 0000000..fa3f49f
--- /dev/null
+++ b/testing/resources/pixel/bug_467170761_expected_skia.pdf.0.png
Binary files differ