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 fc67697eb Range.readObject() does not re-assert the
comparator/ordering invariant. (#1686)
fc67697eb is described below
commit fc67697ebe7d8c4a6ad5b9344e3216522ec83113
Author: Gary Gregory <[email protected]>
AuthorDate: Wed Jun 3 09:05:25 2026 -0400
Range.readObject() does not re-assert the comparator/ordering invariant.
(#1686)
---
pom.xml | 8 +-
.../spotbugs-exclude-filter-java8-sb-4.8.6.xml | 242 +++++++++++++++++++++
src/main/java/org/apache/commons/lang3/Range.java | 3 +
.../apache/commons/lang3/RangeReadObjectTest.java | 21 ++
4 files changed, 272 insertions(+), 2 deletions(-)
diff --git a/pom.xml b/pom.xml
index 1ec4020d8..6242d65c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -174,6 +174,7 @@
<commons.jacoco.complexityRatio>0.91</commons.jacoco.complexityRatio>
<commons.text.version>1.15.0</commons.text.version>
<commons.io.version>2.22.0</commons.io.version>
+
<commons.spotbugs.filter>${commons.conf.dir}/spotbugs-exclude-filter.xml</commons.spotbugs.filter>
</properties>
<build>
<defaultGoal>clean verify apache-rat:check checkstyle:check japicmp:cmp
spotbugs:check pmd:check javadoc:javadoc</defaultGoal>
@@ -316,7 +317,7 @@
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
-
<excludeFilterFile>${basedir}/src/conf/spotbugs-exclude-filter.xml</excludeFilterFile>
+ <excludeFilterFile>${commons.spotbugs.filter}</excludeFilterFile>
</configuration>
</plugin>
</plugins>
@@ -376,7 +377,7 @@
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<configuration>
-
<excludeFilterFile>${basedir}/src/conf/spotbugs-exclude-filter.xml</excludeFilterFile>
+ <excludeFilterFile>${commons.spotbugs.filter}</excludeFilterFile>
</configuration>
</plugin>
<plugin>
@@ -476,6 +477,9 @@
<activation>
<jdk>1.8</jdk>
</activation>
+ <properties>
+
<commons.spotbugs.filter>${commons.conf.dir}/spotbugs-exclude-filter-java8-sb-4.8.6.xml</commons.spotbugs.filter>
+ </properties>
<build>
<!-- Don't run javadoc:javadoc on Java 8 due to Java's incompatible
expectation of package-list vs element-list on remote links. -->
<defaultGoal>clean verify apache-rat:check checkstyle:check
japicmp:cmp spotbugs:check pmd:check</defaultGoal>
diff --git a/src/conf/spotbugs-exclude-filter-java8-sb-4.8.6.xml
b/src/conf/spotbugs-exclude-filter-java8-sb-4.8.6.xml
new file mode 100644
index 000000000..8c4cb6c3c
--- /dev/null
+++ b/src/conf/spotbugs-exclude-filter-java8-sb-4.8.6.xml
@@ -0,0 +1,242 @@
+<?xml version="1.0"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!--
+ This file contains some false positive bugs detected by findbugs. Their
+ false positive nature has been analyzed individually and they have been
+ put here to instruct findbugs it must ignore them.
+-->
+<FindBugsFilter
+ xmlns="https://github.com/spotbugs/filter/3.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0
https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
+
+ <!-- SpotBugs 4.8.6 suffers from
https://github.com/spotbugs/spotbugs/issues/2957 -->
+ <!-- MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT false positive in 4.8.4 -->
+ <Match>
+ <Class name="org.apache.commons.lang3.Range" />
+ <Bug pattern="MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT" />
+ </Match>
+
+ <!-- TODO Can any of these be done without breaking binary compatibility? -->
+ <Match>
+ <Class name="~.*" />
+ <Or>
+ <Bug pattern="EI_EXPOSE_REP" />
+ <Bug pattern="EI_EXPOSE_REP2" />
+ <Bug pattern="MS_EXPOSE_REP" />
+ </Or>
+ </Match>
+
+ <Match>
+ <!-- TODO ? -->
+ <Bug pattern="CT_CONSTRUCTOR_THROW" />
+ </Match>
+
+ <!-- TODO Can any of these be done without breaking binary compatibility? -->
+ <Match>
+ <Class name="org.apache.commons.lang3.reflect.FieldUtils" />
+ <Bug pattern="REFLF_REFLECTION_MAY_INCREASE_ACCESSIBILITY_OF_FIELD" />
+ </Match>
+
+ <!-- TODO Cannot be done without breaking binary compat. -->
+ <Match>
+ <Class name="org.apache.commons.lang3.tuple.MutablePair" />
+ <Bug pattern="PA_PUBLIC_PRIMITIVE_ATTRIBUTE" />
+ </Match>
+
+ <!-- TODO Cannot be done without breaking binary compat. -->
+ <Match>
+ <Class name="org.apache.commons.lang3.tuple.MutableTriple" />
+ <Bug pattern="PA_PUBLIC_PRIMITIVE_ATTRIBUTE" />
+ </Match>
+
+ <!-- https://github.com/spotbugs/spotbugs/issues/2710 -->
+ <Match>
+ <Class name="org.apache.commons.lang3.ThreadUtils$ThreadIdPredicate" />
+ <Bug pattern="CT_CONSTRUCTOR_THROW" />
+ </Match>
+
+ <Match>
+ <Class name="org.apache.commons.lang3.ArrayUtils" />
+ <Method name="addFirst" />
+ <Bug pattern="NP_LOAD_OF_KNOWN_NULL_VALUE" />
+ </Match>
+
+ <!-- Reason: Optimization to use == -->
+ <Match>
+ <Class name="org.apache.commons.lang3.BooleanUtils" />
+ <Or>
+ <Method name="toBoolean" />
+ <Method name="toBooleanObject" />
+ </Or>
+ <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+ </Match>
+ <Match>
+ <Class name="org.apache.commons.lang3.BooleanUtils" />
+ <Method name="toBoolean" />
+ <Bug pattern="RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN" />
+ </Match>
+
+ <!-- Reason: Behavior documented in javadoc -->
+ <Match>
+ <Class name="org.apache.commons.lang3.BooleanUtils" />
+ <Or>
+ <Method name="negate" />
+ <Method name="toBooleanObject" />
+ </Or>
+ <Bug pattern="NP_BOOLEAN_RETURN_NULL" />
+ </Match>
+
+ <!-- Reason: base class cannot be changed and field is properly checked
against null so behavior is OK -->
+ <Match>
+ <Class name="org.apache.commons.lang3.text.ExtendedMessageFormat" />
+ <Method name="applyPattern" />
+ <Bug pattern="UR_UNINIT_READ_CALLED_FROM_SUPER_CONSTRUCTOR" />
+ </Match>
+
+ <!-- Reason: Optimization to use == -->
+ <Match>
+ <Class name="org.apache.commons.lang3.StringUtils" />
+ <Or>
+ <Method name="indexOfDifference"/>
+ <Method name="compare"
params="java.lang.String,java.lang.String,boolean"/>
+ <Method name="compareIgnoreCase"
params="java.lang.String,java.lang.String,boolean"/>
+ </Or>
+ <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+ </Match>
+ <Match>
+ <Class name="org.apache.commons.lang3.Strings$CiStrings" />
+ <Method name="compare" params="java.lang.String,java.lang.String"/>
+ <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+ </Match>
+ <Match>
+ <Class name="org.apache.commons.lang3.Strings$CsStrings" />
+ <Method name="compare" params="java.lang.String,java.lang.String"/>
+ <Bug pattern="ES_COMPARING_PARAMETER_STRING_WITH_EQ" />
+ </Match>
+
+ <!-- Reason: Intended -->
+ <Match>
+ <Class name="org.apache.commons.lang3.math.NumberUtils" />
+ <Method name="createNumber"/>
+ <Bug pattern="SF_SWITCH_FALLTHROUGH" />
+ </Match>
+ <!-- Reason: Intended -->
+ <Match>
+ <Class name="org.apache.commons.lang3.time.DateUtils" />
+ <Method name="getFragment"/>
+ <Bug pattern="SF_SWITCH_FALLTHROUGH" />
+ </Match>
+ <!-- Reason: False positive -->
+ <Match>
+ <Class name="org.apache.commons.lang3.text.ExtendedMessageFormat" />
+ <Method name="applyPattern"/>
+ <Bug pattern="SF_SWITCH_FALLTHROUGH" />
+ </Match>
+
+ <!-- Reason: toProperString is lazily loaded -->
+ <Match>
+ <Class name="org.apache.commons.lang3.math.Fraction" />
+ <Field name="toProperString" />
+ <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED" />
+ </Match>
+
+ <!-- Reason: It does call super.clone(), but via a subsequent method -->
+ <Match>
+ <Class name="org.apache.commons.lang3.text.StrTokenizer" />
+ <Method name="clone"/>
+ <Bug pattern="CN_IDIOM_NO_SUPER_CALL" />
+ </Match>
+
+ <!-- Reason: FindBugs 2.0.2 used in maven-findbugs-plugin 2.5.2 seems to
have problems with detection of default cases
+ in switch statements. All the excluded methods have switch statements that
contain a default case. -->
+ <Match>
+ <Class name="org.apache.commons.lang3.math.NumberUtils"/>
+ <Method name="createNumber" />
+ <Bug pattern="SF_SWITCH_NO_DEFAULT" />
+ </Match>
+ <!-- Reason: FindBugs does not correctly recognize default branches in
switch statements without break statements.
+ See, e.g., the report at https://sourceforge.net/p/findbugs/bugs/1298 -->
+ <Match>
+ <Class name="org.apache.commons.lang3.time.FastDateParser"/>
+ <Or>
+ <Method name="getStrategy" />
+ <Method name="simpleQuote" params="java.lang.StringBuilder,
java.lang.String"/>
+ </Or>
+ <Bug pattern="SF_SWITCH_NO_DEFAULT" />
+ </Match>
+
+ <!-- Reason: FindBugs cannot correctly recognize default branches in switch
statements without break statements.
+ See, e.g., the report at https://sourceforge.net/p/findbugs/bugs/1298 -->
+ <Match>
+ <Class name="org.apache.commons.lang3.time.FastDatePrinter"/>
+ <Method name="appendFullDigits" params="java.lang.Appendable, int, int"/>
+ <Bug pattern="SF_SWITCH_NO_DEFAULT" />
+ </Match>
+
+ <!-- Reason: The fallthrough on the switch statement is intentional -->
+ <Match>
+ <Class name="org.apache.commons.lang3.time.FastDatePrinter"/>
+ <Method name="appendFullDigits" params="java.lang.Appendable, int, int"/>
+ <Bug pattern="SF_SWITCH_FALLTHROUGH" />
+ </Match>
+
+ <!-- Reason: Internal class that is used only as a key for an internal
FormatCache. For this reason we can
+ be sure, that equals will never be called with null or types other than
MultipartKey.
+ -->
+ <Match>
+ <Class name="org.apache.commons.lang3.time.FormatCache$MultipartKey" />
+ <Method name="equals" />
+ <Bug pattern="BC_EQUALS_METHOD_SHOULD_WORK_FOR_ALL_OBJECTS" />
+ </Match>
+ <Match>
+ <Class name="org.apache.commons.lang3.time.FormatCache$MultipartKey" />
+ <Method name="equals" />
+ <Bug pattern="NP_EQUALS_SHOULD_HANDLE_NULL_ARGUMENT" />
+ </Match>
+
+ <!-- Reason: toString() can return null! -->
+ <Match>
+ <Class name="org.apache.commons.lang3.compare.ObjectToStringComparator" />
+ <Method name="compare" />
+ <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE,
ES_COMPARING_STRINGS_WITH_EQ" />
+ </Match>
+
+ <!-- Reason: requireNonNull is supposed to take a nullable parameter,
+ whatever Spotbugs thinks of it. -->
+ <Match>
+ <Class name="org.apache.commons.lang3.function.Objects" />
+ <Method name="requireNonNull" />
+ <Bug pattern="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE" />
+ </Match>
+ <!-- False positive https://github.com/spotbugs/spotbugs/issues/2957 to be
fixed in 4.9.0 -->
+ <Match>
+ <Class name="org.apache.commons.lang3.event.EventListenerSupport" />
+ <Method name="readObject" />
+ <Bug pattern="MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT" />
+ </Match>
+ <!-- Fundamental disagreement with SB: It should be OK to have predefined
instances and allow new instances. -->
+ <Match>
+ <Bug pattern="SING_SINGLETON_HAS_NONPRIVATE_CONSTRUCTOR" />
+ </Match>
+ <!-- Fundamental disagreement with SB: It should be OK to have predefined
instances and allow new instances. -->
+ <Match>
+ <Bug pattern="SING_SINGLETON_IMPLEMENTS_SERIALIZABLE" />
+ </Match>
+</FindBugsFilter>
diff --git a/src/main/java/org/apache/commons/lang3/Range.java
b/src/main/java/org/apache/commons/lang3/Range.java
index e0fca54d1..0d2165de8 100644
--- a/src/main/java/org/apache/commons/lang3/Range.java
+++ b/src/main/java/org/apache/commons/lang3/Range.java
@@ -559,6 +559,9 @@ private void readObject(final ObjectInputStream in) throws
IOException, ClassNot
if (comparator == null) {
throw new InvalidObjectException("comparator null");
}
+ 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/RangeReadObjectTest.java
b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
index be2866ff2..88188f629 100644
--- a/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
+++ b/src/test/java/org/apache/commons/lang3/RangeReadObjectTest.java
@@ -20,6 +20,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -28,6 +29,8 @@
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.Objects;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -117,6 +120,23 @@ void testComparatorNullViaForgedStream() throws Exception {
assertThrows(InvalidObjectException.class, () -> deserialize(forged));
}
+ /**
+ * Forged stream: minimum=1, maximum=10, hashCode=hash(1,10) (all
legitimate), but comparator replaced with a reversed ordering. The hashCode
gate passes
+ * (comparator excluded from the hash) and the null gates pass (comparator
is non-null); the ordering invariant is the only one violated. The deserialized
+ * Range still reports endpoints [1,10] but {@code contains(5)} returns
false because it trusts the reversed comparator.
+ */
+ @Test
+ void testForgedReversedComparatorBreaksContains() throws Exception {
+ final Range<Integer> reference = Range.of(Integer.valueOf(1),
Integer.valueOf(10));
+ final Range<Integer> forged = Range.of(Integer.valueOf(1),
Integer.valueOf(10));
+ final Comparator<Integer> reversed = Collections.reverseOrder();
+ FieldUtils.writeDeclaredField(forged, "comparator", reversed, true);
+ assertThrows(InvalidObjectException.class, () ->
deserialize(SerializationUtils.serialize(forged)));
+ assertThrows(SerializationException.class, () ->
SerializationUtils.deserialize(SerializationUtils.serialize(forged)));
+ assertThrows(SerializationException.class, () ->
SerializationUtils.roundtrip(forged));
+ assertTrue(reference.contains(Integer.valueOf(5)));
+ }
+
/**
* Forged stream with {@code maximum == null}; symmetric to F-061b.
*/
@@ -152,4 +172,5 @@ void testRoundTripPreservesCorrectHashCode() throws
Exception {
assertEquals(range.hashCode(), roundtrip.hashCode(), "Round-trip
serialization must preserve the correct hashCode");
assertEquals(range, roundtrip);
}
+
}