Repository navigation
MathArrays: fix int overflow in the int[] L1 and Linf distance - #348
Conversation
18d117c to
601415d
Compare
| int sum = 0; | ||
| for (int i = 0; i < p1.length; i++) { | ||
| sum += JdkMath.abs(p1[i] - p2[i]); | ||
| final long delta = JdkMath.abs((long) p1[i] - p2[i]); |
There was a problem hiding this comment.
Compute sum as a long. Then do a final check against MAX_VALUE after the loop on the return.
The sum cannot overflow a long as two max length arrays with MIN in one and MAX in the other is (2^31-1) * (2^32-1) < 2^63.
| for (int i = 0; i < p1.length; i++) { | ||
| max = JdkMath.max(max, JdkMath.abs(p1[i] - p2[i])); | ||
| final long delta = JdkMath.abs((long) p1[i] - p2[i]); | ||
| max = (int) JdkMath.min(JdkMath.max(max, delta), Integer.MAX_VALUE); |
There was a problem hiding this comment.
Compute max as a long and then convert to int after the loop for the return
distance1(int[], int[]) and distanceInf(int[], int[]) computed the offset of each pair as JdkMath.abs(p1[i] - p2[i]) in int. The difference of two int values does not always fit in an int, and Math.abs does not make it fit: abs(Integer.MAX_VALUE - -1) overflows to Integer.MIN_VALUE, and abs(Integer.MIN_VALUE) is Integer.MIN_VALUE itself. A distance that should be large was therefore returned as a negative value, or as zero when the overflow was then "absolute-valued" a second time by max(). Widen the subtraction to long before taking the absolute value, and accumulate the whole result in a long: distance1 sums the offsets and distanceInf takes their maximum, and both clamp to Integer.MAX_VALUE only on return. The accumulator cannot overflow a long — two maximum-length arrays with Integer.MIN_VALUE in one and Integer.MAX_VALUE in the other give (2^31-1) * (2^32-1) < 2^63. Distances that fit in an int are unchanged, and the result is now monotonic in the offsets and independent of the order of the arguments, which it was not before. Adds regression tests covering both methods. The doubles overloads are not affected. Assisted-By: DeepSeek-V4.1 (deepseek-ai) via DeepSeek Harness Signed-off-by: simpleqt <2015900818@qq.com>
601415d to
37e1259
Compare
|
Done in 37e1259 — both methods now accumulate in a
Your bound is what makes the single check at the end sufficient: Verified with |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #348 +/- ##
============================================
+ Coverage 86.54% 87.23% +0.68%
+ Complexity 9787 91 -9696
============================================
Files 532 499 -33
Lines 35516 33531 -1985
Branches 6194 5853 -341
============================================
- Hits 30738 29251 -1487
+ Misses 3518 3162 -356
+ Partials 1260 1118 -142 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
One CI note so the red mark on Locally, on Windows with this patch applied, |
MathArrays.distance1(int[], int[])andMathArrays.distanceInf(int[], int[])computed the offset of each pair asJdkMath.abs(p1[i] - p2[i])inint.The difference of two
intvalues does not always fit in anint, andMath.absdoes not make it fit:Integer.MAX_VALUE - (-1)overflows toInteger.MIN_VALUE, andMath.abs(Integer.MIN_VALUE)isInteger.MIN_VALUEitself, sodistance1returns a negative distance;distanceInftakesmax(0, that)and therefore returns 0, i.e. it reports that two points far apart are identical.Reproducer
distance1returns a negative value whenever an offset exceedsInteger.MAX_VALUE. Other affected inputs include{Integer.MIN_VALUE}vs{0}(returns-2147483648) and{Integer.MAX_VALUE}vs{Integer.MIN_VALUE}(returns1). Oversized results can also accumulate from individually representable offsets, e.g.{Integer.MAX_VALUE, 0}vs{0, Integer.MAX_VALUE}sums to4294967294.Fix
Widen the subtraction to
longbefore taking the absolute value, so the offset is always exact, and saturate the accumulated result atInteger.MAX_VALUE, the largest representable distance.distance1now reportsInteger.MAX_VALUErather than a negative "distance", anddistanceInfreportsInteger.MAX_VALUErather than0. Distances that fit in anintare unchanged.This also makes the result monotonic in the offsets and symmetric in the arguments, which the previous result was not: before the fix
distance1({Integer.MAX_VALUE}, {-1})was negative whiledistance1({1, 1}, {0, 0})was2.The
double[]overloads are not affected:JdkMath.abson adoubledifference cannot overflow, andMathArraysTest.testL1DistanceDouble,testL2DistanceDoubleandtestLInfDistanceDoubleare unchanged.Tests
Adds
testL1DistanceIntOverflowandtestLInfDistanceIntOverflowtoMathArraysTest, covering the overflowing inputs, the saturated multi-element accumulation, the exactInteger.MAX_VALUEboundary, and the fitting values that must not change.Verified against the source tree at
e253f396ba47edf008f60c903c4623496c7eef49:testL1DistanceIntOverflow:197 distance1 of [-2147483648, 0] expected:<2147483647> but was:<-2147483648>testLInfDistanceIntOverflow:227 distanceInf of [-2147483648, 0] expected:<2147483647> but was:<0>commons-math-legacy-coremodule is green:Tests run: 295, Failures: 0, Errors: 0, Skipped: 0(47 inMathArraysTest).Notes
ManhattanDistanceandChebyshevDistanceonly via theirdouble[]overloads; inside this repository theint[]overloads are exercised by tests alone. I am fixing them because the overflow is silent and returns a plausible-looking but wrong number, not because there is a known in-tree caller.int, from an overflowed value to a saturatedInteger.MAX_VALUE. That is a behaviour change for those inputs, so I have documented it in the@returntags. I considered widening the return type but the two methods are part of the API and thedouble[]overloads already cover distances beyondintrange.CONTRIBUTING.mdasks for one, so please tell me the key to use and I will rename the branch and amend the commit.Assisted-Bytrailer. I ran the tests above myself and have not run the full multi-modulemvn clean verify. If you would prefer a different disclosure or none, say so and I will amend.