Open bartonjs opened 9 months ago
Tagging subscribers to this area: @dotnet/area-system-numerics See info in area-owners.md if you want to be subscribed.
Author: | bartonjs |
---|---|
Assignees: | - |
Labels: | `area-System.Numerics`, `untriaged` |
Milestone: | - |
Tentatively marked this as 9.0.0. If it's not fully blocking though, we'll move it to Future.
This is blocking test authoring for https://github.com/dotnet/runtime/pull/97738; so, yes, it's "fully blocking"
It appears that there is a flaw in the SubMod
implementation in the "FastReducer" algorithm:
in that it incorrectly assumes that left
is greater than or equal to right
after truncating to the k
least significant digits of the representation. The FastReducer was introduced as a performance optimization in https://github.com/dotnet/corefx/pull/2182.
BigInteger modulus = (BigInteger)3 << 1024;
BigInteger value = modulus;
int exponent = 2;
BigInteger.ModPow(value, exponent, value); // Assertion failure in debug builds
It should be noted that superficially, the result appears to be correct using the following validation logic:
BigInteger result = BigInteger.ModPow(value, exponent, modulus);
BigInteger expectedResult = 1;
for (int i = 0; i < exponent; i++)
{
expectedResult = (expectedResult * value) % modulus;
}
Assert.Equal(expectedResult, result); // Passes for given inputs
However, I don't think it would be safe to simply comment out the asserts. The SubtractSelf
algorithm --where the assertion is failing-- has not been designed for cases where left < right
:
The final assert checking that carry == 0
also ends up failing. It isn't clear to me whether the final result is accidentally correct for the particular examined inputs or because it happens to work with the invariants required by this particular implementation of Barrett's algorithm.
I fuzzed the .NET 8 implementation with FsCheck, using parameters that trigger the assert failing:
using FsCheck;
using FsCheck.Fluent;
using System.Numerics;
Config configuration = Config.Quick.WithMaxTest(10_000_000);
Parallel.For(0, Environment.ProcessorCount, i =>
{
Prop.ForAll<(int, int, byte)>(t => ModPowPropertyTest(t.Item1, t.Item2, t.Item3))
.Check(configuration);
});
static bool ModPowPropertyTest(int valueSeed, int modulusSpread, ushort exponent)
{
if (Math.Abs(valueSeed) < 2 || exponent < 2)
{
return true; // Skip trivial values
}
// Arrange
BigInteger value = (BigInteger)valueSeed << 1024; // To trigger the bug, uint[] representation needs to start with at least 32 zeros.
BigInteger modulus = value + modulusSpread; // The modulus must be approximately the same size as the value.
// Act
BigInteger result = BigInteger.ModPow(value, exponent, modulus);
// Assert
BigInteger expectedResult = 1;
for (int i = 0; i < exponent; i++)
{
expectedResult = (expectedResult * value) % modulus;
}
return expectedResult == result;
}
After 160 million runs, no invalid outputs were detected. @jeffhandley I think we could try removing the asserts to unblock @bartonjs's work.
BigInteger value = (BigInteger)valueSeed << 1024; // To trigger the bug, uint[] representation needs to start with at least 32 zeros.
If keeping a bit set in the least significant segment works around the problem, I'll give that a go.
It seems to be more nuanced than just keeping a low bit set, since the number described by
byte[] buf = new byte[2048 >> 3];
buf[1] = 2;
buf.AsSpan(2, 8).Fill(1);
buf[^1] = 1;
still asserted; but 00 02 01 01 01 01 ... 01 01 01 01
didn't). So it's probably a "can't have a chunk of all zeros" where chunk is, presumably, defined.
byte[] buf = new byte[2048 >> 3];
buf[1] = 2;
buf.AsSpan(2, 8).Fill(1);
yields
Process terminated. Assertion failed.
at System.Numerics.BigIntegerCalculator.SubtractSelf(Span`1 left, ReadOnly
Span`1 right) in C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numer
ics\src\System\Numerics\BigIntegerCalculator.AddSub.cs:line 135
at System.Numerics.BigIntegerCalculator.FastReducer.SubMod(Span`1 left, Re
adOnlySpan`1 right, ReadOnlySpan`1 modulus, Int32 k) in C:\git\bartonjs\runti
me.3\src\libraries\System.Runtime.Numerics\src\System\Numerics\BigIntegerCalc
ulator.FastReducer.cs:line 114
at System.Numerics.BigIntegerCalculator.FastReducer.Reduce(Span`1 value) i
n C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numerics\src\System\
Numerics\BigIntegerCalculator.FastReducer.cs:line 61
at System.Numerics.BigIntegerCalculator.PowCore(Span`1 value, Int32 valueL
ength, UInt32 power, FastReducer& reducer, Span`1 result, Int32 resultLength,
Span`1 temp) in C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numer
ics\src\System\Numerics\BigIntegerCalculator.PowMod.cs:line 539
at System.Numerics.BigIntegerCalculator.PowCore(Span`1 value, Int32 valueL
ength, UInt32 power, ReadOnlySpan`1 modulus, Span`1 temp, Span`1 bits) in C:\
git\bartonjs\runtime.3\src\libraries\System.Runtime.Numerics\src\System\Numer
ics\BigIntegerCalculator.PowMod.cs:line 416
at System.Numerics.BigIntegerCalculator.Pow(ReadOnlySpan`1 value, UInt32 p
ower, ReadOnlySpan`1 modulus, Span`1 bits) in C:\git\bartonjs\runtime.3\src\l
ibraries\System.Runtime.Numerics\src\System\Numerics\BigIntegerCalculator.Pow
Mod.cs:line 243
at System.Numerics.BigInteger.ModPow(BigInteger value, BigInteger exponent
, BigInteger modulus) in C:\git\bartonjs\runtime.3\src\libraries\System.Runti
me.Numerics\src\System\Numerics\BigInteger.cs:line 1005
adding in buf[^1] = 1
yields
Process terminated. Assertion failed.
at System.Numerics.BigIntegerCalculator.SubtractSelf(Span`1 left, ReadOnly
Span`1 right) in C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numer
ics\src\System\Numerics\BigIntegerCalculator.AddSub.cs:line 135
at System.Numerics.BigIntegerCalculator.FastReducer.SubMod(Span`1 left, Re
adOnlySpan`1 right, ReadOnlySpan`1 modulus, Int32 k) in C:\git\bartonjs\runti
me.3\src\libraries\System.Runtime.Numerics\src\System\Numerics\BigIntegerCalc
ulator.FastReducer.cs:line 114
at System.Numerics.BigIntegerCalculator.FastReducer.Reduce(Span`1 value) i
n C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numerics\src\System\
Numerics\BigIntegerCalculator.FastReducer.cs:line 61
at System.Numerics.BigIntegerCalculator.PowCore(Span`1 value, Int32 valueL
ength, UInt32 power, FastReducer& reducer, Span`1 result, Int32 resultLength,
Span`1 temp) in C:\git\bartonjs\runtime.3\src\libraries\System.Runtime.Numer
ics\src\System\Numerics\BigIntegerCalculator.PowMod.cs:line 539
at System.Numerics.BigIntegerCalculator.PowCore(Span`1 value, Int32 valueL
ength, UInt32 power, ReadOnlySpan`1 modulus, Span`1 temp, Span`1 bits) in C:\
git\bartonjs\runtime.3\src\libraries\System.Runtime.Numerics\src\System\Numer
ics\BigIntegerCalculator.PowMod.cs:line 416
at System.Numerics.BigIntegerCalculator.Pow(ReadOnlySpan`1 value, UInt32 p
ower, ReadOnlySpan`1 modulus, Span`1 bits) in C:\git\bartonjs\runtime.3\src\l
ibraries\System.Runtime.Numerics\src\System\Numerics\BigIntegerCalculator.Pow
Mod.cs:line 243
at System.Numerics.BigInteger.ModPow(BigInteger value, BigInteger exponent
, BigInteger modulus) in C:\git\bartonjs\runtime.3\src\libraries\System.Runti
me.Numerics\src\System\Numerics\BigInteger.cs:line 1005
Which looks the same to me.
The two inputs trigger the same condition. TL;DR the implementation calculates the value $n^2$ and then attempts to subtract from it a value $q_2$ that is known to be smaller than that square (modulo $m + 1$). The problem is that it does so by applying a performance optimization that truncates the input values down to the $\log (m + 1)$ least significant digits, however this truncation operation can invalidate the original assumption that $n^2 \ge q_2$.
If the least significant digits of the input value $n$ are zero, or almost zero, then squaring that value will essentially double the number of least significant digits that are zero, which triggers the above condition.
Description
Trying to use BigInteger for test authoring is currently impeded by it asserting.
Reproduction Steps
Expected behavior
No assertion.
Actual behavior
Regression?
No response
Known Workarounds
No response
Configuration
No response
Other information
Definitely happens on bc0b28f564f0f3fdfa0c0856bd1b3b38d5371c4d on both osx and win. The osx build merged up through 3bf9beecd5f68a59fc407488ce8120ee3d4240c7 (main as of about an hour ago) and still sees the assert.