From f992a6aa00ac65e68f41d7aacc616ddb10e6e3cf Mon Sep 17 00:00:00 2001 From: daguimu Date: Wed, 25 Mar 2026 19:32:34 +0800 Subject: [PATCH] fix: use ConcurrentHashMap for Version.VERSION2INT to prevent concurrent corruption VERSION2INT uses a plain HashMap that is read and written concurrently from the RPC hot path (isSupportResponseAttachment is checked on every call). Concurrent put() during HashMap resize can corrupt the internal hash table, causing threads to hang in infinite loops. --- .../java/org/apache/dubbo/common/Version.java | 4 +- .../dubbo/common/version/VersionTest.java | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/Version.java b/dubbo-common/src/main/java/org/apache/dubbo/common/Version.java index 1b1f474ce4a..266920c2f4b 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/Version.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/Version.java @@ -28,10 +28,10 @@ import java.nio.charset.StandardCharsets; import java.security.CodeSource; import java.util.Enumeration; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,7 +58,7 @@ public final class Version { public static final int LOWEST_VERSION_FOR_RESPONSE_ATTACHMENT = 2000200; // 2.0.2 public static final int HIGHEST_PROTOCOL_VERSION = 2009900; // 2.0.99 - private static final Map VERSION2INT = new HashMap<>(); + private static final Map VERSION2INT = new ConcurrentHashMap<>(); static { // get dubbo version and last commit id diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/version/VersionTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/version/VersionTest.java index 0c37dc79c77..a6e45766adc 100644 --- a/dubbo-common/src/test/java/org/apache/dubbo/common/version/VersionTest.java +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/version/VersionTest.java @@ -24,6 +24,8 @@ import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.Enumeration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -98,6 +100,42 @@ void testIsFramework263OrHigher() { Assertions.assertTrue(Version.isRelease263OrHigher("2.6.3.0")); } + @Test + void testGetIntVersionConcurrency() throws InterruptedException { + int threadCount = 10; + int iterations = 1000; + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch doneLatch = new CountDownLatch(threadCount); + AtomicInteger errors = new AtomicInteger(0); + + String[] versions = {"2.6.1", "2.7.0", "3.0.0", "2.0.2", "2.0.99", "3.1.0", "2.6.3.1", "2.7.0.RC1"}; + int[] expected = {2060100, 2070000, 3000000, 2000200, 2009900, 3010000, 2060301, 2070000}; + + for (int t = 0; t < threadCount; t++) { + new Thread(() -> { + try { + startLatch.await(); + for (int i = 0; i < iterations; i++) { + int idx = i % versions.length; + int result = Version.getIntVersion(versions[idx]); + if (result != expected[idx]) { + errors.incrementAndGet(); + } + } + } catch (Exception e) { + errors.incrementAndGet(); + } finally { + doneLatch.countDown(); + } + }) + .start(); + } + + startLatch.countDown(); + doneLatch.await(); + Assertions.assertEquals(0, errors.get(), "Concurrent getIntVersion should return consistent results"); + } + @Test void testGetVersion() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {