This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new a9ae10e31 Fraction.readObject does not re-assert denominator != 0
(#1688)
a9ae10e31 is described below
commit a9ae10e31066a183050b356ecf667b23ace21a61
Author: Gary Gregory <[email protected]>
AuthorDate: Wed Jun 3 13:44:18 2026 -0400
Fraction.readObject does not re-assert denominator != 0 (#1688)
---
.../org/apache/commons/lang3/math/Fraction.java | 19 +++++++-------
.../commons/lang3/math/FractionReadObjectTest.java | 29 +++++++++++++++++++++-
2 files changed, 38 insertions(+), 10 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/math/Fraction.java
b/src/main/java/org/apache/commons/lang3/math/Fraction.java
index b0a9476f4..a1dd4a1ae 100644
--- a/src/main/java/org/apache/commons/lang3/math/Fraction.java
+++ b/src/main/java/org/apache/commons/lang3/math/Fraction.java
@@ -121,6 +121,12 @@ private static int addAndCheck(final int x, final int y) {
return (int) s;
}
+ private static void checkDenominator(final int denominator) {
+ if (denominator == 0) {
+ throw new ArithmeticException("The denominator must not be zero");
+ }
+ }
+
/**
* Creates a {@link Fraction} instance from a {@code double} value.
* <p>
@@ -194,9 +200,7 @@ public static Fraction getFraction(double value) {
* @throws ArithmeticException if the denominator is {@code zero} or the
denominator is {@code negative} and the numerator is {@code Integer#MIN_VALUE}
*/
public static Fraction getFraction(int numerator, int denominator) {
- if (denominator == 0) {
- throw new ArithmeticException("The denominator must not be zero");
- }
+ checkDenominator(denominator);
if (denominator < 0) {
if (numerator == Integer.MIN_VALUE || denominator ==
Integer.MIN_VALUE) {
throw new ArithmeticException("overflow: can't negate");
@@ -223,9 +227,7 @@ public static Fraction getFraction(int numerator, int
denominator) {
* @throws ArithmeticException if the resulting numerator exceeds {@code
Integer.MAX_VALUE}
*/
public static Fraction getFraction(final int whole, final int numerator,
final int denominator) {
- if (denominator == 0) {
- throw new ArithmeticException("The denominator must not be zero");
- }
+ checkDenominator(denominator);
if (denominator < 0) {
throw new ArithmeticException("The denominator must not be
negative");
}
@@ -313,9 +315,7 @@ public static Fraction getFraction(String str) {
* @throws ArithmeticException if the denominator is {@code zero}
*/
public static Fraction getReducedFraction(int numerator, int denominator) {
- if (denominator == 0) {
- throw new ArithmeticException("The denominator must not be zero");
- }
+ checkDenominator(denominator);
if (numerator == 0) {
return ZERO; // normalize zero.
}
@@ -855,6 +855,7 @@ public Fraction pow(final int power) {
*/
private void readObject(final ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
+ checkDenominator(denominator);
if (hashCode != hash(denominator, numerator)) {
throw new InvalidObjectException("Fraction hashCode does not match
numerator/denominator.");
}
diff --git
a/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
index ecbc944c6..62537f818 100644
--- a/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/math/FractionReadObjectTest.java
@@ -21,9 +21,12 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.io.ByteArrayInputStream;
import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import org.apache.commons.lang3.SerializationException;
import org.apache.commons.lang3.SerializationUtils;
@@ -36,6 +39,16 @@
*/
public class FractionReadObjectTest {
+ private static Object deserialize(final byte[] bytes) throws Exception {
+ try (ObjectInputStream ois = new ObjectInputStream(new
ByteArrayInputStream(bytes))) {
+ return ois.readObject();
+ }
+ }
+
+ private static void setInt(final Object target, final String name, final
int value) throws Exception {
+ FieldUtils.writeDeclaredField(target, name, value, true);
+ }
+
@Test
public void testBadHashCodeStreamIsRejected() throws Exception {
final Fraction fraction = Fraction.getFraction(3, 7);
@@ -46,7 +59,22 @@ public void testBadHashCodeStreamIsRejected() throws
Exception {
"Bad hashCode in stream must be rejected with
InvalidObjectException");
assertInstanceOf(InvalidObjectException.class, ex.getCause());
assertEquals("java.io.InvalidObjectException: Fraction hashCode does
not match numerator/denominator.", ex.getMessage());
+ }
+ /**
+ * Forged stream: numerator=0, denominator=0, hashCode=hash(0,0). The
public factory refuses denominator 0 (negative control); readObject does not,
so the
+ * forged Fraction(0,0) deserializes and divides by zero on {@code
intValue()}.
+ */
+ @Test
+ void testForgedZeroDenominatorDividesByZero() throws Exception {
+ // Negative control: no public factory can build a zero-denominator
Fraction.
+ assertThrows(ArithmeticException.class, () -> Fraction.getFraction(0,
0));
+ final Fraction seed = Fraction.getFraction(1, 2);
+ setInt(seed, "numerator", 0);
+ setInt(seed, "denominator", 0);
+ setInt(seed, "hashCode", Objects.hash(Integer.valueOf(0),
Integer.valueOf(0)));
+ assertThrows(ArithmeticException.class, () ->
deserialize(SerializationUtils.serialize(seed)));
+ assertThrows(ArithmeticException.class, () ->
SerializationUtils.roundtrip(seed));
}
@Test
@@ -65,6 +93,5 @@ public void testRoundTripPreservesHashCode() throws Exception
{
final Fraction roundtrip = SerializationUtils.roundtrip(fraction);
assertEquals(fraction.hashCode(), roundtrip.hashCode(), "Round-trip
serialization must preserve the correct hashCode");
assertEquals(fraction, roundtrip);
-
}
}