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());
+ }
+}