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 7b7be629d Keep HashCodeBuilder.append(Object) from poisoning
reflection hashCode (#1747)
7b7be629d is described below
commit 7b7be629d6383ae8ddd7e0ed8c6d700ec23aebd0
Author: alhuda <[email protected]>
AuthorDate: Sun Jul 12 00:46:52 2026 +0530
Keep HashCodeBuilder.append(Object) from poisoning reflection hashCode
(#1747)
---
.../commons/lang3/builder/HashCodeBuilder.java | 49 +++++++++++++++--
.../commons/lang3/builder/HashCodeBuilderTest.java | 61 ++++++++++++++++++++++
2 files changed, 105 insertions(+), 5 deletions(-)
diff --git
a/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
b/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
index c8edf4878..218fdfb21 100644
--- a/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
@@ -169,6 +169,13 @@ public Builder setMultiplierOddNumber(final int
multiplierOddNumber) {
*/
private static final ThreadLocal<Set<IDKey>> REGISTRY =
ThreadLocal.withInitial(HashSet::new);
+ /**
+ * A registry of objects being appended by {@link #append(Object)}, kept
separate from {@link #REGISTRY} so that
+ * guarding {@code append} against its own re-entrant cycles does not trip
the reflection cycle guard checked by
+ * {@link #reflectionAppend(Object, Class, HashCodeBuilder, boolean,
String[], boolean)}.
+ */
+ private static final ThreadLocal<Set<IDKey>> APPEND_REGISTRY =
ThreadLocal.withInitial(HashSet::new);
+
/**
* Constructs a new Builder.
*
@@ -541,6 +548,38 @@ private static void unregister(final Object value) {
}
}
+ /**
+ * Tests whether the append registry contains the given object. Used by
{@link #append(Object)} to break its own re-entrant cycles.
+ *
+ * @param value The object to look up in the append registry.
+ * @return {@code true} if the append registry contains the given object.
+ */
+ private static boolean isAppendRegistered(final Object value) {
+ return APPEND_REGISTRY.get().contains(new IDKey(value));
+ }
+
+ /**
+ * Registers the given object in the append registry.
+ *
+ * @param value The object to register.
+ */
+ private static void appendRegister(final Object value) {
+ APPEND_REGISTRY.get().add(new IDKey(value));
+ }
+
+ /**
+ * Unregisters the given object from the append registry.
+ *
+ * @param value The object to unregister.
+ */
+ private static void appendUnregister(final Object value) {
+ final Set<IDKey> registry = APPEND_REGISTRY.get();
+ registry.remove(new IDKey(value));
+ if (registry.isEmpty()) {
+ APPEND_REGISTRY.remove();
+ }
+ }
+
/**
* Constant to use in building the hashCode.
*/
@@ -820,22 +859,22 @@ public HashCodeBuilder append(final long[] array) {
public HashCodeBuilder append(final Object object) {
if (object == null) {
total = total * constant;
- } else if (isRegistered(object)) {
+ } else if (isRegistered(object) || isAppendRegistered(object)) {
// Cycle detected: skip to avoid infinite recursion (mirrors
reflectionAppend).
total = total * constant;
} else if (ObjectUtils.isArray(object)) {
try {
- register(object);
+ appendRegister(object);
appendArray(object);
} finally {
- unregister(object);
+ appendUnregister(object);
}
} else {
try {
- register(object);
+ appendRegister(object);
total = total * constant + object.hashCode();
} finally {
- unregister(object);
+ appendUnregister(object);
}
}
return this;
diff --git
a/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderTest.java
b/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderTest.java
index 01f0e775b..ae5038248 100644
--- a/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderTest.java
+++ b/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderTest.java
@@ -66,6 +66,48 @@ public int hashCode() {
}
}
+ /**
+ * A reflection test fixture whose {@code hashCode()} is reflection based.
+ */
+ static final class ReflectionHashCodeMember {
+ final int value;
+
+ ReflectionHashCodeMember(final int value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ return EqualsBuilder.reflectionEquals(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+ }
+
+ /**
+ * Holds a {@link ReflectionHashCodeMember} and is itself reflection
hashed.
+ */
+ static final class ReflectionHashCodeHolder {
+ final ReflectionHashCodeMember member;
+
+ ReflectionHashCodeHolder(final ReflectionHashCodeMember member) {
+ this.member = member;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ return EqualsBuilder.reflectionEquals(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+ }
+
static class TestObject {
private int a;
@@ -636,4 +678,23 @@ void testToHashCodeExclude() {
assertEquals(INITIAL, HashCodeBuilder.reflectionHashCode(two));
}
+ /**
+ * {@code append(Object)} must not collapse an object whose own {@code
hashCode()} is reflection based to the bare
+ * initial value. Regression for the cycle guard leaking into the
reflection registry.
+ */
+ @Test
+ void testAppendObjectReflectionHashCodeNotPoisoned() {
+ final ReflectionHashCodeMember member = new
ReflectionHashCodeMember(42);
+ assertEquals(INITIAL * CONSTANT + member.hashCode(),
+ new HashCodeBuilder(INITIAL, CONSTANT).append((Object)
member).toHashCode());
+
+ // The same defect reached through a normal, acyclic graph: a nested
object's content must still influence
+ // the enclosing reflection hash.
+ final int h1 = new ReflectionHashCodeHolder(new
ReflectionHashCodeMember(1)).hashCode();
+ final int h2 = new ReflectionHashCodeHolder(new
ReflectionHashCodeMember(2)).hashCode();
+ assertNotEquals(h1, h2);
+
+ assertTrue(HashCodeBuilder.getRegistry().isEmpty());
+ }
+
}