[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