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 1023c0077 DoubleRange/Range accept NaN endpoints, silently producing a
half-unbounded range, contains()/fit() fail open for every value above the
minimum (f018).
1023c0077 is described below
commit 1023c00776aebb1b929fd873eb99e471e0dfd220
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 5 14:24:38 2026 -0400
DoubleRange/Range accept NaN endpoints, silently producing a
half-unbounded range, contains()/fit() fail open for every value above
the minimum (f018).
---
src/changes/changes.xml | 1 +
.../java/org/apache/commons/lang3/DoubleRange.java | 3 ++
.../java/org/apache/commons/lang3/NumberRange.java | 1 +
src/main/java/org/apache/commons/lang3/Range.java | 41 ++++++++++++++++++++++
.../org/apache/commons/lang3/DoubleRangeTest.java | 18 ++++++++++
.../apache/commons/lang3/RangeReadObjectTest.java | 15 ++++++++
.../java/org/apache/commons/lang3/RangeTest.java | 23 ++++++++++++
7 files changed, 102 insertions(+)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index b8e760504..f2bc1963f 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -267,6 +267,7 @@ java.lang.NullPointerException: Cannot invoke
<action type="fix" dev="ggregory" due-to="Gary
Gregory">JavaVersion.get() throws NumberFormatException where javadoc promises
null (f015).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Match Javadoc to code in SystemProperties.getProperty() and
SystemUtils.getEnvironmentVariable() and update inline comments (f016).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Fix LockingVisitors Javadoc to match code (f017).</action>
+ <action type="fix" dev="ggregory" due-to="Gary
Gregory">DoubleRange/Range accept NaN endpoints, silently producing a
half-unbounded range, contains()/fit() fail open for every value above the
minimum (f018).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/DoubleRange.java
b/src/main/java/org/apache/commons/lang3/DoubleRange.java
index 13aa5e372..da1185835 100644
--- a/src/main/java/org/apache/commons/lang3/DoubleRange.java
+++ b/src/main/java/org/apache/commons/lang3/DoubleRange.java
@@ -44,6 +44,7 @@ public final class DoubleRange extends NumberRange<Double> {
* @param fromInclusive The first value that defines the edge of the
range, inclusive.
* @param toInclusive The second value that defines the edge of the range,
inclusive.
* @return The range object, not null.
+ * @throws IllegalArgumentException if either value is NaN.
*/
public static DoubleRange of(final double fromInclusive, final double
toInclusive) {
return of(Double.valueOf(fromInclusive), Double.valueOf(toInclusive));
@@ -64,6 +65,7 @@ public static DoubleRange of(final double fromInclusive,
final double toInclusiv
* @param toInclusive The second value that defines the edge of the range,
inclusive.
* @return The range object, not null.
* @throws NullPointerException if either element is null.
+ * @throws IllegalArgumentException if either element is NaN.
*/
public static DoubleRange of(final Double fromInclusive, final Double
toInclusive) {
return new DoubleRange(fromInclusive, toInclusive);
@@ -76,6 +78,7 @@ public static DoubleRange of(final Double fromInclusive,
final Double toInclusiv
* @param number2 The second element, not null.
* @throws NullPointerException when element1 is null.
* @throws NullPointerException when element2 is null.
+ * @throws IllegalArgumentException when element1 or element2 is NaN.
*/
private DoubleRange(final Double number1, final Double number2) {
super(number1, number2, null);
diff --git a/src/main/java/org/apache/commons/lang3/NumberRange.java
b/src/main/java/org/apache/commons/lang3/NumberRange.java
index f1200cfe7..1e46992f2 100644
--- a/src/main/java/org/apache/commons/lang3/NumberRange.java
+++ b/src/main/java/org/apache/commons/lang3/NumberRange.java
@@ -40,6 +40,7 @@ public class NumberRange<N extends Number> extends Range<N> {
* @param comp The comparator to be used, null for natural ordering.
* @throws NullPointerException when element1 is null.
* @throws NullPointerException when element2 is null.
+ * @throws IllegalArgumentException when element1 or element2 is a
floating-point NaN.
*/
public NumberRange(final N number1, final N number2, final Comparator<N>
comp) {
super(number1, number2, comp);
diff --git a/src/main/java/org/apache/commons/lang3/Range.java
b/src/main/java/org/apache/commons/lang3/Range.java
index 0256bcc07..bf2c456b8 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -78,6 +78,7 @@ public int compare(final Object obj1, final Object obj2) {
* @throws NullPointerException when fromInclusive is null.
* @throws NullPointerException when toInclusive is null.
* @throws ClassCastException if the elements are not {@link Comparable}.
+ * @throws IllegalArgumentException if either element is a floating-point
NaN.
* @deprecated Use {@link #of(Comparable, Comparable)}.
*/
@Deprecated
@@ -102,6 +103,7 @@ public static <T extends Comparable<? super T>> Range<T>
between(final T fromInc
* @throws NullPointerException when fromInclusive is null.
* @throws NullPointerException when toInclusive is null.
* @throws ClassCastException if using natural ordering and the elements
are not {@link Comparable}.
+ * @throws IllegalArgumentException if either element is a floating-point
NaN.
* @deprecated Use {@link #of(Object, Object, Comparator)}.
*/
@Deprecated
@@ -125,6 +127,7 @@ private static int hash(final Object value1, final Object
value2) {
* @return The range object, not null.
* @throws NullPointerException if the element is null.
* @throws ClassCastException if the element is not {@link Comparable}.
+ * @throws IllegalArgumentException if the element is a floating-point NaN.
*/
public static <T extends Comparable<? super T>> Range<T> is(final T
element) {
return of(element, element, null);
@@ -143,11 +146,25 @@ public static <T extends Comparable<? super T>> Range<T>
is(final T element) {
* @return The range object, not null.
* @throws NullPointerException if the element is null.
* @throws ClassCastException if using natural ordering and the elements
are not {@link Comparable}.
+ * @throws IllegalArgumentException if the element is a floating-point NaN.
*/
public static <T> Range<T> is(final T element, final Comparator<T>
comparator) {
return of(element, element, comparator);
}
+ /**
+ * Tests whether the element is a floating-point NaN. A NaN endpoint sorts
above every value under the natural
+ * total order ({@link Double#compareTo(Double)} / {@link
Float#compareTo(Float)}), silently producing a
+ * half-unbounded range whose {@code contains}/{@code fit} accept every
value above the minimum.
+ *
+ * @param element The element to test, may be null.
+ * @return Whether the element is a floating-point NaN.
+ */
+ private static boolean isNaN(final Object element) {
+ return element instanceof Double && ((Double) element).isNaN()
+ || element instanceof Float && ((Float) element).isNaN();
+ }
+
/**
* Creates a range with the specified minimum and maximum values (both
inclusive).
*
@@ -163,6 +180,7 @@ public static <T> Range<T> is(final T element, final
Comparator<T> comparator) {
* @return The range object, not null.
* @throws NullPointerException if either element is null.
* @throws ClassCastException if the elements are not {@link Comparable}.
+ * @throws IllegalArgumentException if either element is a floating-point
NaN.
* @since 3.13.0
*/
public static <T extends Comparable<? super T>> Range<T> of(final T
fromInclusive, final T toInclusive) {
@@ -186,12 +204,27 @@ public static <T extends Comparable<? super T>> Range<T>
of(final T fromInclusiv
* @throws NullPointerException when fromInclusive is null.
* @throws NullPointerException when toInclusive is null.
* @throws ClassCastException if using natural ordering and the elements
are not {@link Comparable}.
+ * @throws IllegalArgumentException if either element is a floating-point
NaN.
* @since 3.13.0
*/
public static <T> Range<T> of(final T fromInclusive, final T toInclusive,
final Comparator<T> comparator) {
return new Range<>(fromInclusive, toInclusive, comparator);
}
+ /**
+ * Validates that a floating-point endpoint is not NaN, mirroring the
fail-closed posture of
+ * {@link Validate#notNaN(double, String, Object...)}.
+ *
+ * @param element The endpoint to validate.
+ * @param name The parameter name for the exception message.
+ * @throws IllegalArgumentException if the endpoint is a floating-point
NaN.
+ */
+ private static void requireNotNaN(final Object element, final String name)
{
+ if (isNaN(element)) {
+ throw new IllegalArgumentException(name + " must not be NaN");
+ }
+ }
+
/**
* The ordering scheme used in this range.
*/
@@ -225,11 +258,14 @@ public static <T> Range<T> of(final T fromInclusive,
final T toInclusive, final
* @param comp The comparator to be used, null for natural ordering.
* @throws NullPointerException when element1 is null.
* @throws NullPointerException when element2 is null.
+ * @throws IllegalArgumentException when element1 or element2 is a
floating-point NaN.
*/
@SuppressWarnings("unchecked")
Range(final T element1, final T element2, final Comparator<T> comp) {
Objects.requireNonNull(element1, "element1");
Objects.requireNonNull(element2, "element2");
+ requireNotNaN(element1, "element1");
+ requireNotNaN(element2, "element2");
if (comp == null) {
this.comparator = ComparableComparator.INSTANCE;
} else {
@@ -553,6 +589,11 @@ private void readObject(final ObjectInputStream in) throws
IOException, ClassNot
SerializationUtils.requireNonNull(maximum, "maximum null");
SerializationUtils.requireNonNull(minimum, "minimum null");
SerializationUtils.requireNonNull(comparator, "comparator null");
+ // Mirror the constructor's NaN endpoint rejection: a crafted stream
cannot smuggle in the degenerate
+ // half-unbounded range that construction refuses.
+ if (isNaN(minimum) || isNaN(maximum)) {
+ throw new InvalidObjectException("Range minimum/maximum must not
be NaN.");
+ }
if (comparator.compare(minimum, maximum) > 0) {
throw new InvalidObjectException("Range minimum is greater than
maximum under the comparator.");
}
diff --git a/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
b/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
index 64c0c2611..ab74c8b59 100644
--- a/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
+++ b/src/test/java/org/apache/commons/lang3/DoubleRangeTest.java
@@ -385,6 +385,24 @@ void testOfWithContains() {
assertNullPointerException(() -> of(null, null));
}
+ /**
+ * A NaN endpoint sorts above every double under Double.compareTo's total
order, so it used to construct a
+ * half-unbounded range whose contains()/fit() accepted every value above
the minimum. Construction must fail
+ * closed instead (mirroring Validate.notNaN).
+ */
+ @Test
+ void testNaNEndpointsRejected() {
+ assertIllegalArgumentException(() -> of(5.0, Double.NaN));
+ assertIllegalArgumentException(() -> of(Double.NaN, 5.0));
+ assertIllegalArgumentException(() -> of(Double.NaN, Double.NaN));
+ assertIllegalArgumentException(() -> of(Double.valueOf(5.0),
Double.valueOf(Double.NaN)));
+ // computed-NaN routes, not just the literal
+ assertIllegalArgumentException(() -> of(0.0, 0.0 / 0.0));
+ assertIllegalArgumentException(() -> of(1.0, 0.0 *
Double.POSITIVE_INFINITY));
+ // infinities remain legal endpoints
+ assertTrue(of(0.0,
Double.POSITIVE_INFINITY).contains(Double.MAX_VALUE));
+ }
+
@Test
void testRangeOfChars() {
final DoubleRange chars = of('a', 'z');
diff --git a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
index 88188f629..e2a0cdc07 100644
--- a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
@@ -165,6 +165,21 @@ void testMinimumNullViaForgedStream() throws Exception {
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
+ /**
+ * Forged stream with a NaN maximum and a canonically matching hashCode:
the hashCode gate passes and the
+ * comparator ordering gate passes (NaN sorts above everything under
Double.compareTo), so only the NaN
+ * endpoint gate stands between the stream and a half-unbounded fail-open
range.
+ */
+ @Test
+ void testNaNEndpointViaForgedStream() throws Exception {
+ final Double min = Double.valueOf(5.0);
+ final Double max = Double.valueOf(Double.NaN);
+ final int canonicalHash = Objects.hash(min, max);
+ final Object comparator = Range.of(Integer.valueOf(1),
Integer.valueOf(2)).getComparator();
+ final byte[] forged = forgeRangeStream(comparator, min, max,
canonicalHash);
+ assertThrows(InvalidObjectException.class, () -> deserialize(forged));
+ }
+
@Test
void testRoundTripPreservesCorrectHashCode() throws Exception {
final Range<String> range = Range.of("apple", "mango");
diff --git a/src/test/java/org/apache/commons/lang3/RangeTest.java
b/src/test/java/org/apache/commons/lang3/RangeTest.java
index 8d69e1e07..611a89a52 100644
--- a/src/test/java/org/apache/commons/lang3/RangeTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeTest.java
@@ -346,6 +346,29 @@ void testIsWithCompare() {
assertTrue(ri.contains(11), "should contain 11");
}
+ @Test
+ void testNaNEndpointsRejected() {
+ // A NaN endpoint sorts above every value under
Double/Float.compareTo's total order, silently
+ // constructing a half-unbounded range: contains()/fit() then accept
every value above the minimum.
+ // Construction fails closed instead (mirroring Validate.notNaN);
non-NaN behavior is unchanged.
+ assertIllegalArgumentException(() -> Range.of(5.0, Double.NaN));
+ assertIllegalArgumentException(() -> Range.of(Double.NaN, 5.0));
+ assertIllegalArgumentException(() -> Range.of(5.0f, Float.NaN));
+ assertIllegalArgumentException(() -> Range.of(Float.NaN, 5.0f));
+ assertIllegalArgumentException(() -> Range.between(5.0, Double.NaN));
+ assertIllegalArgumentException(() -> Range.between(5.0f, Float.NaN));
+ assertIllegalArgumentException(() -> Range.is(Double.NaN));
+ assertIllegalArgumentException(() -> Range.is(Float.NaN));
+ // rejected under a custom comparator as well: no comparator gives a
NaN endpoint a sound meaning
+ assertIllegalArgumentException(() -> Range.of(5.0, Double.NaN,
Comparator.naturalOrder()));
+ // NumberRange over Float/Double shares the same root constructor
+ assertIllegalArgumentException(() -> new NumberRange<>(5.0f,
Float.NaN, null));
+ assertIllegalArgumentException(() -> new NumberRange<>(Double.NaN,
5.0, null));
+ assertNotNull(new NumberRange<>(5.0f, 6.0f, null));
+ // infinities remain legal endpoints
+ assertTrue(Range.of(0.0f,
Float.POSITIVE_INFINITY).contains(Float.MAX_VALUE));
+ }
+
@Test
void testOfWithCompare() {
// all integers are equal