diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 2593c4b475..96580d9d49 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -696,6 +696,14 @@ namespace Avalonia.Controls private static double Clamp(double value, double min, double max) { + // A NaN offset must never survive. Offset is two-way coerced between the ScrollViewer and its + // ScrollContentPresenter; because NaN != NaN, a NaN offset never compares equal, so the + // coerce/raise cycle never converges and recurses until it overflows the stack (see #21444). + if (double.IsNaN(value)) + { + return min; + } + return (value < min) ? min : (value > max) ? max : value; } diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index ec1cd5fb66..35e25f6538 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -44,6 +44,25 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(10, 10), target.Offset); } + [Fact] + public void Setting_Offset_To_NaN_Does_Not_Cause_Infinite_Coerce_Recursion() + { + var target = new ScrollViewer + { + Template = new FuncControlTemplate(CreateTemplate), + Content = "Foo", + Extent = new Size(100, 100), + Viewport = new Size(10, 10), + }; + + InitializeScrollViewer(target); + + target.Offset = new Vector(0, double.NaN); + + Assert.False(double.IsNaN(target.Offset.X)); + Assert.False(double.IsNaN(target.Offset.Y)); + } + [Fact] public void Test_ScrollToHome() {