From fcbd5d037a6be605b1d4145c5068f1b4cfb4433b Mon Sep 17 00:00:00 2001 From: Nicholas Lachapelle <102546121+NicholasLachapelle@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:44:11 -0400 Subject: [PATCH] Fix StackOverflow when a NaN offset is set on ScrollViewer (#21558) ScrollViewer.Offset and ScrollContentPresenter.Offset form a two-way coerced binding whose convergence is decided with the default equality comparer. Because NaN != NaN, a NaN offset is reported "changed" on every pass, so the coerce/raise cycle never converges and recurses until the stack overflows -- an unrecoverable crash (on Mono/AOT targets such as Android it surfaces as a native SIGSEGV). CoerceOffset's Clamp passed NaN straight through (NaN < min and NaN > max are both false), so a NaN component survived coercion. Sanitize it to the lower bound so the coerce always converges, regardless of where the NaN came from. Fixes #21444 Co-authored-by: Julien Lebosquain --- src/Avalonia.Controls/ScrollViewer.cs | 8 ++++++++ .../ScrollViewerTests.cs | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) 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() {