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 0953617b7 HashCodeBuilder.append(Object) StackOverflowError on (#1650)
0953617b7 is described below

commit 0953617b75115d2bedb50f8c11bac433b68da182
Author: Gary Gregory <[email protected]>
AuthorDate: Sun May 17 13:22:46 2026 -0400

    HashCodeBuilder.append(Object) StackOverflowError on (#1650)
    
    mutually-referential objects
---
 .../commons/lang3/builder/HashCodeBuilder.java     | 19 +++--
 .../lang3/builder/HashCodeBuilderCycleTest.java    | 84 ++++++++++++++++++++++
 2 files changed, 99 insertions(+), 4 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 fe5d611a4..b45990f53 100644
--- a/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/HashCodeBuilder.java
@@ -830,12 +830,23 @@ public HashCodeBuilder append(final long[] array) {
     public HashCodeBuilder append(final Object object) {
         if (object == null) {
             total = total * constant;
+        } else if (isRegistered(object)) {
+            // Cycle detected: skip to avoid infinite recursion (mirrors 
reflectionAppend).
+            total = total * constant;
         } else if (ObjectUtils.isArray(object)) {
-            // factor out array case in order to keep method small enough
-            // to be inlined
-            appendArray(object);
+            try {
+                register(object);
+                appendArray(object);
+            } finally {
+                unregister(object);
+            }
         } else {
-            total = total * constant + object.hashCode();
+            try {
+                register(object);
+                total = total * constant + object.hashCode();
+            } finally {
+                unregister(object);
+            }
         }
         return this;
     }
diff --git 
a/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderCycleTest.java 
b/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderCycleTest.java
new file mode 100644
index 000000000..c4440f220
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/lang3/builder/HashCodeBuilderCycleTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+package org.apache.commons.lang3.builder;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests cycles in {@link HashCodeBuilder}.
+ * <p>
+ * {@link HashCodeBuilder#append(Object)} calls object.hashCode() directly 
without the ThreadLocal cycle-guard registry that reflectionHashCode() uses.
+ * </p>
+ *
+ * <p>
+ * When two objects reference each other and each implements hashCode() via 
{@code new HashCodeBuilder().append(peer)}, calling hashCode() recurses 
infinitely:
+ * a.hashCode() → append(b) → b.hashCode() → append(a) → ...
+ * </p>
+ *
+ * <p>
+ * Pre-patch: StackOverflowError is thrown. Post-patch: cycle detected; 
completes without error.
+ * </p>
+ */
+class HashCodeBuilderCycleTest {
+
+    static class CyclicNode {
+
+        final String label;
+        CyclicNode peer;
+
+        CyclicNode(final String label) {
+            this.label = label;
+        }
+
+        @Override
+        public int hashCode() {
+            return new HashCodeBuilder(17, 
37).append(label).append(peer).toHashCode();
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            return o instanceof CyclicNode && label.equals(((CyclicNode) 
o).label);
+        }
+    }
+
+    @Test
+    void cyclicPeerDoesNotOverflowStack() {
+        final CyclicNode a = new CyclicNode("a");
+        final CyclicNode b = new CyclicNode("b");
+        a.peer = b;
+        b.peer = a;
+        assertNotEquals(0, a.hashCode());
+    }
+
+    @Test
+    void selfReferentialDoesNotOverflowStack() {
+        final CyclicNode self = new CyclicNode("self");
+        self.peer = self;
+        assertNotEquals(0, self.hashCode());
+    }
+
+    @Test
+    void acyclicChainProducesValue() {
+        final CyclicNode a = new CyclicNode("a");
+        final CyclicNode b = new CyclicNode("b");
+        a.peer = b; // b.peer is null, no cycle
+        assertNotEquals(0, a.hashCode());
+    }
+}

Reply via email to