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 8e2d1bed8 CompareToBuilder lacks cycle guard on Object[] recursion.
(#1669)
8e2d1bed8 is described below
commit 8e2d1bed8623e0f5b281504d151476689f21690a
Author: Gary Gregory <[email protected]>
AuthorDate: Sun May 24 09:01:39 2026 -0400
CompareToBuilder lacks cycle guard on Object[] recursion. (#1669)
---
.../commons/lang3/builder/AbstractReflection.java | 30 +++++++
.../commons/lang3/builder/CompareToBuilder.java | 93 +++++++++++++++++++---
.../commons/lang3/builder/EqualsBuilder.java | 25 +-----
.../lang3/builder/CompareToBuilderTest.java | 29 +++++++
4 files changed, 142 insertions(+), 35 deletions(-)
diff --git
a/src/main/java/org/apache/commons/lang3/builder/AbstractReflection.java
b/src/main/java/org/apache/commons/lang3/builder/AbstractReflection.java
index 169cd6ef7..3a33788c6 100644
--- a/src/main/java/org/apache/commons/lang3/builder/AbstractReflection.java
+++ b/src/main/java/org/apache/commons/lang3/builder/AbstractReflection.java
@@ -19,9 +19,11 @@
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
+import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.lang3.SystemProperties;
+import org.apache.commons.lang3.tuple.Pair;
/**
* Abstracts reflection access for reflection-based classes in this package.
@@ -111,6 +113,16 @@ static boolean getForceAccessible() {
return SystemProperties.getBoolean(AbstractReflection.class,
"forceAccessible", () -> true);
}
+ static boolean isRegistered(final Object lhs, final Object rhs, final
Set<Pair<IDKey, IDKey>> registry) {
+ final Pair<IDKey, IDKey> pair = toRegisterPair(lhs, rhs);
+ final Pair<IDKey, IDKey> swappedPair = Pair.of(pair.getRight(),
pair.getLeft());
+ return registry != null && (registry.contains(pair) ||
registry.contains(swappedPair));
+ }
+
+ static void register(final Object lhs, final Object rhs, final
Set<Pair<IDKey, IDKey>> registry) {
+ registry.add(toRegisterPair(lhs, rhs));
+ }
+
/**
* If {@code forceAccessible} flag is true, then the field is made
accessible by calling {@link AccessibleObject#setAccessible(boolean)
* AccessibleObject#setAccessible(true)} but <em>only</em> if a field is
not already accessible.
@@ -147,6 +159,24 @@ private static boolean setAccessibleTrue(final Field
field) {
return false;
}
+ /**
+ * Converters value pair into a register pair.
+ *
+ * @param lhs {@code this} object.
+ * @param rhs the other object.
+ * @return the pair.
+ */
+ static Pair<IDKey, IDKey> toRegisterPair(final Object lhs, final Object
rhs) {
+ return Pair.of(new IDKey(lhs), new IDKey(rhs));
+ }
+
+ static void unregister(final Object lhs, final Object rhs, final
Set<Pair<IDKey, IDKey>> registry, final ThreadLocal<Set<Pair<IDKey, IDKey>>>
registryTL) {
+ registry.remove(toRegisterPair(lhs, rhs));
+ if (registry.isEmpty()) {
+ registryTL.remove();
+ }
+ }
+
/**
* Whether to call {@link AccessibleObject#setAccessible(boolean)
AccessibleObject#setAccessible(true)} on inaccessible fields.
*/
diff --git
a/src/main/java/org/apache/commons/lang3/builder/CompareToBuilder.java
b/src/main/java/org/apache/commons/lang3/builder/CompareToBuilder.java
index a11307c58..e7cc0cfa9 100644
--- a/src/main/java/org/apache/commons/lang3/builder/CompareToBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/CompareToBuilder.java
@@ -20,11 +20,14 @@
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.Objects;
+import java.util.Set;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.builder.AbstractReflection.AbstractBuilder;
+import org.apache.commons.lang3.tuple.Pair;
/**
* Assists in implementing {@link Comparable#compareTo(Object)} methods.
@@ -118,6 +121,11 @@ public CompareToBuilder get() {
}
+ /**
+ * A registry of objects to detect cyclical object references, avoid
infinite loops, and stack overflows.
+ */
+ private static final ThreadLocal<Set<Pair<IDKey, IDKey>>> REGISTRY =
ThreadLocal.withInitial(HashSet::new);
+
/**
* Constructs a new Builder.
*
@@ -127,6 +135,32 @@ public static Builder builder() {
return new Builder();
}
+ /**
+ * Gets the registry of object pairs being traversed by the reflection
+ * methods in the current thread.
+ *
+ * @return Set the registry of objects being traversed
+ */
+ static Set<Pair<IDKey, IDKey>> getRegistry() {
+ return REGISTRY.get();
+ }
+
+ /**
+ * Tests whether the registry contains the given object pair.
+ * <p>
+ * Used by the reflection methods to avoid infinite loops.
+ * Objects might be swapped therefore a check is needed if the object pair
+ * is registered in the given or swapped order.
+ * </p>
+ *
+ * @param lhs {@code this} object to lookup in registry
+ * @param rhs the other object to lookup on registry
+ * @return boolean {@code true} if the registry contains the given object.
+ */
+ static boolean isRegistered(final Object lhs, final Object rhs) {
+ return isRegistered(lhs, rhs, getRegistry());
+ }
+
/**
* Appends to {@code builder} the comparison of {@code lhs}
* to {@code rhs} using the fields defined in {@code clazz}.
@@ -349,6 +383,31 @@ public static int reflectionCompare(final Object lhs,
final Object rhs, final St
return reflectionCompare(lhs, rhs, false, null, excludeFields);
}
+ /**
+ * Registers the given object pair.
+ * Used by the reflection methods to avoid infinite loops.
+ *
+ * @param lhs {@code this} object to register
+ * @param rhs the other object to register
+ */
+ private static void register(final Object lhs, final Object rhs) {
+ register(lhs, rhs, getRegistry());
+ }
+
+ /**
+ * Unregisters the given object pair.
+ *
+ * <p>
+ * Used by the reflection methods to avoid infinite loops.
+ * </p>
+ *
+ * @param lhs {@code this} object to unregister
+ * @param rhs the other object to unregister
+ */
+ private static void unregister(final Object lhs, final Object rhs) {
+ unregister(lhs, rhs, getRegistry(), REGISTRY);
+ }
+
/**
* Current state of the comparison as appended fields are checked.
*/
@@ -842,20 +901,28 @@ public CompareToBuilder append(final Object lhs, final
Object rhs, final Compara
comparison = 1;
return this;
}
- if (ObjectUtils.isArray(lhs)) {
- // factor out array case in order to keep method small enough to
be inlined
- appendArray(lhs, rhs, comparator);
- } else // the simple case, not an array, just test the element
- if (comparator == null) {
- @SuppressWarnings("unchecked") // assume this can be done; if not
throw CCE as per Javadoc
- final Comparable<Object> comparable = (Comparable<Object>) lhs;
- comparison = comparable.compareTo(rhs);
- } else {
- @SuppressWarnings("unchecked") // assume this can be done; if not
throw CCE as per Javadoc
- final Comparator<Object> comparator2 = (Comparator<Object>)
comparator;
- comparison = comparator2.compare(lhs, rhs);
+ if (isRegistered(lhs, rhs)) {
+ return this;
+ }
+ try {
+ register(lhs, rhs);
+ if (ObjectUtils.isArray(lhs)) {
+ // factor out array case in order to keep method small enough
to be inlined
+ appendArray(lhs, rhs, comparator);
+ } else // the simple case, not an array, just test the element
+ if (comparator == null) {
+ @SuppressWarnings("unchecked") // assume this can be done; if
not throw CCE as per Javadoc
+ final Comparable<Object> comparable = (Comparable<Object>) lhs;
+ comparison = comparable.compareTo(rhs);
+ } else {
+ @SuppressWarnings("unchecked") // assume this can be done; if
not throw CCE as per Javadoc
+ final Comparator<Object> comparator2 = (Comparator<Object>)
comparator;
+ comparison = comparator2.compare(lhs, rhs);
+ }
+ return this;
+ } finally {
+ unregister(lhs, rhs);
}
- return this;
}
/**
diff --git a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
index f665f4175..746edb944 100644
--- a/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/EqualsBuilder.java
@@ -141,17 +141,6 @@ public static Builder builder() {
* to disambiguate the duplicate ids.
*/
- /**
- * Converters value pair into a register pair.
- *
- * @param lhs {@code this} object
- * @param rhs the other object
- * @return the pair
- */
- static Pair<IDKey, IDKey> getRegisterPair(final Object lhs, final Object
rhs) {
- return Pair.of(new IDKey(lhs), new IDKey(rhs));
- }
-
/**
* Gets the registry of object pairs being traversed by the reflection
* methods in the current thread.
@@ -173,13 +162,9 @@ static Set<Pair<IDKey, IDKey>> getRegistry() {
* @param lhs {@code this} object to lookup in registry
* @param rhs the other object to lookup on registry
* @return boolean {@code true} if the registry contains the given object.
- * @since 3.0
*/
static boolean isRegistered(final Object lhs, final Object rhs) {
- final Set<Pair<IDKey, IDKey>> registry = getRegistry();
- final Pair<IDKey, IDKey> pair = getRegisterPair(lhs, rhs);
- final Pair<IDKey, IDKey> swappedPair = Pair.of(pair.getRight(),
pair.getLeft());
- return registry != null && (registry.contains(pair) ||
registry.contains(swappedPair));
+ return isRegistered(lhs, rhs, getRegistry());
}
/**
@@ -353,7 +338,7 @@ public static boolean reflectionEquals(final Object lhs,
final Object rhs, final
* @param rhs the other object to register
*/
private static void register(final Object lhs, final Object rhs) {
- getRegistry().add(getRegisterPair(lhs, rhs));
+ register(lhs, rhs, getRegistry());
}
/**
@@ -367,11 +352,7 @@ private static void register(final Object lhs, final
Object rhs) {
* @param rhs the other object to unregister
*/
private static void unregister(final Object lhs, final Object rhs) {
- final Set<Pair<IDKey, IDKey>> registry = getRegistry();
- registry.remove(getRegisterPair(lhs, rhs));
- if (registry.isEmpty()) {
- REGISTRY.remove();
- }
+ unregister(lhs, rhs, getRegistry(), REGISTRY);
}
/**
diff --git
a/src/test/java/org/apache/commons/lang3/builder/CompareToBuilderTest.java
b/src/test/java/org/apache/commons/lang3/builder/CompareToBuilderTest.java
index b3c3c4c04..c49d0e217 100644
--- a/src/test/java/org/apache/commons/lang3/builder/CompareToBuilderTest.java
+++ b/src/test/java/org/apache/commons/lang3/builder/CompareToBuilderTest.java
@@ -96,6 +96,35 @@ public int hashCode() {
}
}
+ /**
+ * Mutually-referential {@code Object[]}s: {@code a[0] = b}, {@code b[0] =
a}, with {@code a != b}. The recursion is
+ * {@code append(a,b) -> append(a[0]=b, b[0]=a) -> append(b,a) ->
+ * append(b[0]=a, a[0]=b) -> append(a,b)}, never terminating.
+ */
+ @Test
+ void testCycleMutuallyReferentialObjectArrays() {
+ final Object[] a = new Object[1];
+ final Object[] b = new Object[1];
+ a[0] = b;
+ b[0] = a;
+ assertEquals(0, new CompareToBuilder().append(a, b,
null).toComparison());
+ assertEquals(0, new CompareToBuilder().append((Object) a, (Object) b,
null).toComparison());
+ }
+
+ /**
+ * Two distinct self-referential {@code Object[]}s: {@code a[0] = a},
{@code b[0] = b}, with {@code a != b}. The recursion is
+ * {@code append(a,b) -> append(a[0]=a, b[0]=b) -> append(a,b)}, never
terminating.
+ */
+ @Test
+ void testCycleTwoDistinctSelfReferentialObjectArrays() {
+ final Object[] a = new Object[1];
+ final Object[] b = new Object[1];
+ a[0] = a;
+ b[0] = b;
+ assertEquals(0, new CompareToBuilder().append(a, b,
null).toComparison());
+ assertEquals(0, new CompareToBuilder().append((Object) a, (Object) b,
null).toComparison());
+ }
+
static class TestTransientSubObject extends TestObject {
@SuppressWarnings("unused")
private final transient int t;