Fixed: Stack overflow in SizeSuffix and overflow in Fluent.Round

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@be4a564456
(by @mynameisbogdan), as suggested by them on the Radarr issue.

Closes #11404
This commit is contained in:
sirus20x6 2026-04-28 19:09:40 -05:00
parent 9226876792
commit e8ed7bcec4
4 changed files with 18 additions and 7 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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);
}

View file

@ -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)