Skip to content

Commit 50a921d

Browse files
author
mtlyzhang
committed
[Bug] Fix TOCTOU race condition in ZookeeperClientManager.getInstance()
ZookeeperClientManager.getInstance() uses a non-atomic check-then-act pattern (containsKey + put) on a ConcurrentHashMap. When multiple threads call getInstance() concurrently with the same ApplicationModel, each thread can create a separate ZookeeperClientManager instance and register duplicate destroy listeners, causing redundant Zookeeper client cleanup and potential resource waste. Fix: replace the containsKey/put pattern with computeIfAbsent to ensure exactly one manager instance and one destroy listener per ApplicationModel. Added testGetInstanceConcurrentRaceCondition to verify exactly 1 instance is created across concurrent threads. Made-with: Cursor
1 parent d95086c commit 50a921d

File tree

2 files changed

+98
-6
lines changed

2 files changed

+98
-6
lines changed

dubbo-remoting/dubbo-remoting-zookeeper-curator5/src/main/java/org/apache/dubbo/remoting/zookeeper/curator5/ZookeeperClientManager.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ public class ZookeeperClientManager {
4848
public ZookeeperClientManager() {}
4949

5050
public static ZookeeperClientManager getInstance(ApplicationModel applicationModel) {
51-
if (!managerMap.containsKey(applicationModel) || managerMap.get(applicationModel) == null) {
51+
return managerMap.computeIfAbsent(applicationModel, model -> {
5252
ZookeeperClientManager clientManager = new ZookeeperClientManager();
53-
applicationModel.addDestroyListener(m -> {
54-
// destroy zookeeper clients if any
53+
model.addDestroyListener(m -> {
5554
try {
5655
clientManager.destroy();
5756
} catch (Exception e) {
@@ -63,9 +62,8 @@ public static ZookeeperClientManager getInstance(ApplicationModel applicationMod
6362
e);
6463
}
6564
});
66-
managerMap.put(applicationModel, clientManager);
67-
}
68-
return managerMap.get(applicationModel);
65+
return clientManager;
66+
});
6967
}
7068

7169
public ZookeeperClient connect(URL url) {

dubbo-remoting/dubbo-remoting-zookeeper-curator5/src/test/java/org/apache/dubbo/remoting/zookeeper/curator5/support/ZookeeperClientManagerTest.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import org.apache.dubbo.remoting.zookeeper.curator5.Curator5ZookeeperClient;
2121
import org.apache.dubbo.remoting.zookeeper.curator5.ZookeeperClient;
2222
import org.apache.dubbo.remoting.zookeeper.curator5.ZookeeperClientManager;
23+
import org.apache.dubbo.rpc.model.ApplicationModel;
2324

2425
import java.util.List;
26+
import java.util.Map;
2527

2628
import org.junit.jupiter.api.AfterAll;
2729
import org.junit.jupiter.api.Assertions;
@@ -244,6 +246,98 @@ void testSameHostWithDifferentUser() {
244246
assertThat(client1, not(client2));
245247
}
246248

249+
@Test
250+
void testGetInstanceConcurrentRaceCondition() throws Exception {
251+
// Clear static managerMap via reflection to start fresh
252+
java.lang.reflect.Field managerMapField = ZookeeperClientManager.class.getDeclaredField("managerMap");
253+
managerMapField.setAccessible(true);
254+
@SuppressWarnings("unchecked")
255+
Map<?, ?> managerMap = (Map<?, ?>) managerMapField.get(null);
256+
managerMap.clear();
257+
258+
ApplicationModel appModel = ApplicationModel.defaultModel();
259+
int threadCount = 20;
260+
java.util.concurrent.CountDownLatch startGate = new java.util.concurrent.CountDownLatch(1);
261+
java.util.concurrent.CountDownLatch endGate = new java.util.concurrent.CountDownLatch(threadCount);
262+
java.util.Set<ZookeeperClientManager> uniqueInstances =
263+
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
264+
265+
for (int i = 0; i < threadCount; i++) {
266+
new Thread(() -> {
267+
try {
268+
startGate.await();
269+
ZookeeperClientManager instance = ZookeeperClientManager.getInstance(appModel);
270+
uniqueInstances.add(instance);
271+
} catch (Exception e) {
272+
e.printStackTrace();
273+
} finally {
274+
endGate.countDown();
275+
}
276+
}).start();
277+
}
278+
279+
startGate.countDown();
280+
endGate.await();
281+
282+
// With the buggy code, multiple different instances may be created.
283+
// With the fix (computeIfAbsent), exactly 1 instance should exist.
284+
Assertions.assertEquals(1, uniqueInstances.size(),
285+
"Expected exactly 1 ZookeeperClientManager instance, but got " + uniqueInstances.size()
286+
+ ". This indicates a TOCTOU race condition in getInstance().");
287+
288+
// Also verify only 1 entry in the static map
289+
Assertions.assertEquals(1, managerMap.size(),
290+
"Expected exactly 1 entry in managerMap");
291+
292+
managerMap.clear();
293+
}
294+
295+
@Test
296+
void testGetInstanceConcurrentRaceCondition() throws Exception {
297+
// Clear static managerMap via reflection to start fresh
298+
java.lang.reflect.Field managerMapField = ZookeeperClientManager.class.getDeclaredField("managerMap");
299+
managerMapField.setAccessible(true);
300+
@SuppressWarnings("unchecked")
301+
Map<?, ?> managerMap = (Map<?, ?>) managerMapField.get(null);
302+
managerMap.clear();
303+
304+
ApplicationModel appModel = ApplicationModel.defaultModel();
305+
int threadCount = 20;
306+
java.util.concurrent.CountDownLatch startGate = new java.util.concurrent.CountDownLatch(1);
307+
java.util.concurrent.CountDownLatch endGate = new java.util.concurrent.CountDownLatch(threadCount);
308+
java.util.Set<ZookeeperClientManager> uniqueInstances =
309+
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
310+
311+
for (int i = 0; i < threadCount; i++) {
312+
new Thread(() -> {
313+
try {
314+
startGate.await();
315+
ZookeeperClientManager instance = ZookeeperClientManager.getInstance(appModel);
316+
uniqueInstances.add(instance);
317+
} catch (Exception e) {
318+
e.printStackTrace();
319+
} finally {
320+
endGate.countDown();
321+
}
322+
}).start();
323+
}
324+
325+
startGate.countDown();
326+
endGate.await();
327+
328+
// With the buggy code, multiple different instances may be created.
329+
// With the fix (computeIfAbsent), exactly 1 instance should exist.
330+
Assertions.assertEquals(1, uniqueInstances.size(),
331+
"Expected exactly 1 ZookeeperClientManager instance, but got " + uniqueInstances.size()
332+
+ ". This indicates a TOCTOU race condition in getInstance().");
333+
334+
// Also verify only 1 entry in the static map
335+
Assertions.assertEquals(1, managerMap.size(),
336+
"Expected exactly 1 entry in managerMap");
337+
338+
managerMap.clear();
339+
}
340+
247341
@AfterAll
248342
public static void afterAll() {
249343
mockedCurator5ZookeeperClientConstruction.close();

0 commit comments

Comments
 (0)