From e8ed7bcec4d001f6c2b5846a6818013afbfcd968 Mon Sep 17 00:00:00 2001 From: sirus20x6 Date: Tue, 28 Apr 2026 19:09:40 -0500 Subject: [PATCH] Fixed: Stack overflow in SizeSuffix and overflow in Fluent.Round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling NumberExtensions.SizeSuffix(long.MinValue) recurses forever: -bytes overflows back to long.MinValue (no representable positive twin in Int64), the `bytes < 0` branch always re-fires, and the .NET runtime kills the process with an uncatchable StackOverflowException. This actually fires in the wild — an indexer returned a release whose parsed size landed at MinValue, RssSyncService -> DownloadDecisionMaker -> AcceptableSizeSpecification called SizeSuffix on it, and Radarr was killed mid-RSS-sync. Also explains the "unable to load results for this movie search" UI symptom that collateral interactive searches hit when the process died mid-flight. Fluent.Round had a related but separate Convert.ToInt64 overflow on extreme values (Math.Floor of very negative `number` produced an out-of-range double); guard with Math.Ceiling for negatives. This is a port of Sonarr/Sonarr@be4a564456818fc18138bded9d981c93bb2cdd14 (by @mynameisbogdan), as suggested by them on the Radarr issue. Closes #11404 --- .../ExtensionTests/NumberExtensionFixture.cs | 2 ++ .../Extensions/NumberExtensions.cs | 15 ++++++++++----- src/NzbDrone.Core.Test/FluentTest.cs | 4 +++- src/NzbDrone.Core/Fluent.cs | 4 +++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs index c51ab7ad45..167899efcf 100644 --- a/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/ExtensionTests/NumberExtensionFixture.cs @@ -17,6 +17,8 @@ public class NumberExtensionFixture [TestCase(-1000000, "-976.6 KB")] [TestCase(-377487360, "-360.0 MB")] [TestCase(-1255864686, "-1.2 GB")] + [TestCase(long.MinValue, "-8.0 EB")] + [TestCase(long.MaxValue, "8.0 EB")] public void should_calculate_string_correctly(long bytes, string expected) { bytes.SizeSuffix().Should().Be(expected); diff --git a/src/NzbDrone.Common/Extensions/NumberExtensions.cs b/src/NzbDrone.Common/Extensions/NumberExtensions.cs index 15037b20bc..efa8b7fc1c 100644 --- a/src/NzbDrone.Common/Extensions/NumberExtensions.cs +++ b/src/NzbDrone.Common/Extensions/NumberExtensions.cs @@ -11,16 +11,21 @@ public static string SizeSuffix(this long bytes) { const int bytesInKb = 1024; - if (bytes < 0) - { - return "-" + SizeSuffix(-bytes); - } - if (bytes == 0) { return "0 B"; } + if (bytes == long.MinValue) + { + return "-" + SizeSuffix(long.MaxValue); + } + + if (bytes < 0) + { + return "-" + SizeSuffix(Math.Abs(bytes)); + } + var mag = (int)Math.Log(bytes, bytesInKb); var adjustedSize = bytes / (decimal)Math.Pow(bytesInKb, mag); diff --git a/src/NzbDrone.Core.Test/FluentTest.cs b/src/NzbDrone.Core.Test/FluentTest.cs index 30a3b1ab91..11b6282738 100644 --- a/src/NzbDrone.Core.Test/FluentTest.cs +++ b/src/NzbDrone.Core.Test/FluentTest.cs @@ -175,7 +175,9 @@ public void MinOrDefault_should_return_zero_when_collection_is_null() [TestCase(199, 100, 100)] [TestCase(1000, 100, 1000)] [TestCase(0, 100, 0)] - public void round_to_level(long number, int level, int result) + [TestCase(long.MinValue, 1000, -9223372036854775000L)] + [TestCase(long.MaxValue, 1000, 9223372036854775000L)] + public void round_to_level(long number, int level, long result) { number.Round(level).Should().Be(result); } diff --git a/src/NzbDrone.Core/Fluent.cs b/src/NzbDrone.Core/Fluent.cs index 483f52b6bc..91c1b9a916 100644 --- a/src/NzbDrone.Core/Fluent.cs +++ b/src/NzbDrone.Core/Fluent.cs @@ -22,7 +22,9 @@ public static string WithDefault(this string actual, object defaultValue) public static long Round(this long number, long level) { - return Convert.ToInt64(Math.Floor((decimal)number / level) * level); + return number < 0 + ? Convert.ToInt64(Math.Ceiling((decimal)number / level) * level) + : Convert.ToInt64(Math.Floor((decimal)number / level) * level); } public static string ToBestDateString(this DateTime dateTime)