java-topology/defects/dubbo/patch/dubbo-0002-method-walker-diamond-recursion.md
russell@unturf.com 25c2bafdee undf: assign 694-720; stamp patches; ruby-0003/elixir-0002/r-source-0002/victoria-metrics-0002
New UNDF assignments (693→720):
  elixir-0002 → UNDF-2026-000000698 (typespec used_type_pairs O(T²))
  r-source-0002 → UNDF-2026-000000711 (.walkClassGraph match dedup O(S²))
  ruby-0003 → UNDF-2026-000000712 (RubyGems dependent_gems O(N²×D))
  victoria-metrics-0002 → UNDF-2026-000000717 (MetricName tag-filter O(T×I))

Total: 720 UNDF assigned
2026-03-29 22:28:31 -04:00

5.1 KiB

UNDF: UNDF-2026-000000697

dubbo-0002: MethodWalker.walkHierarchy diamond recursion O(2^D)

Classification

  • CWE: CWE-407 (Inefficient Algorithmic Complexity)
  • Severity: MEDIUM
  • Component: Apache Dubbo — dubbo-rpc-triple
  • File: dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/util/MethodWalker.java
  • Method: walkHierarchy(Class<?>)
  • Complexity: O(2^D) where D = diamond depth in interface inheritance hierarchy

Description

MethodWalker.walkHierarchy recursively traverses the class and interface hierarchy to collect method mappings for Triple REST service registration. It recurses into clazz.getSuperclass() and all elements of clazz.getInterfaces(), but has no visited set guard. The classes field is a LinkedHashSet, but it is not used to guard recursion — it is updated only conditionally (if classes.isEmpty() || clazz has annotations), not as a visited guard.

When the hierarchy contains a diamond pattern — two interfaces A and B both extend a common interface C, and a service class implements both A and B — walkHierarchy(C) is called twice. At depth D, the call count is O(2^D).

This is called from DefaultRequestMappingRegistry.register() during service startup/registration for Triple REST endpoints. For a service class that implements multiple interfaces with shared superinterfaces (common with Spring proxy classes), this causes redundant traversal and duplicate method entries in methodsMap.

Defect Code

// dubbo-rpc/dubbo-rpc-triple/.../rest/util/MethodWalker.java
private void walkHierarchy(Class<?> clazz) {
    if (classes.isEmpty() || clazz.getDeclaredAnnotations().length > 0) {
        classes.add(clazz);   // NOT a visited guard - conditional add only
    }
    for (Method method : clazz.getDeclaredMethods()) {
        // adds methods to methodsMap...
    }
    Class<?> superClass = clazz.getSuperclass();
    if (superClass != null && superClass != Object.class) {
        walkHierarchy(superClass);           // no visited check
    }
    for (Class<?> itf : clazz.getInterfaces()) {
        walkHierarchy(itf);                  // no visited check - diamond paths revisited!
    }
}

Example Diamond

interface Base { void baseMethod(); }
interface A extends Base {}
interface B extends Base {}
class Service implements A, B { ... }

walkHierarchy(Service) calls:

  • walkHierarchy(A)walkHierarchy(Base) (1st visit, baseMethod added once)
  • walkHierarchy(B)walkHierarchy(Base) (2nd visit, baseMethod added again)

Result: methodsMap contains [baseMethod: [Method, Method]] — duplicate entries. For the consuming resolver, duplicate method processing causes redundant work.

Fix

Add a visited set and check before recursing:

private final Set<Class<?>> visited = new HashSet<>();

private void walkHierarchy(Class<?> clazz) {
    if (!visited.add(clazz)) {
        return;   // already processed this class/interface — skip
    }
    if (classes.isEmpty() || clazz.getDeclaredAnnotations().length > 0) {
        classes.add(clazz);
    }
    for (Method method : clazz.getDeclaredMethods()) {
        int modifiers = method.getModifiers();
        if ((modifiers & (Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC) {
            methodsMap
                    .computeIfAbsent(Key.of(method), k -> new ArrayList<>())
                    .add(method);
        }
    }
    Class<?> superClass = clazz.getSuperclass();
    if (superClass != null && superClass != Object.class) {
        walkHierarchy(superClass);
    }
    for (Class<?> itf : clazz.getInterfaces()) {
        walkHierarchy(itf);
    }
}

Patch

--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/util/MethodWalker.java
+++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/util/MethodWalker.java
@@ -29,8 +29,11 @@ import java.util.function.BiConsumer;
 import java.util.function.Consumer;

 public final class MethodWalker {

     private final Set<Class<?>> classes = new LinkedHashSet<>();
+    private final Set<Class<?>> visited = new HashSet<>();
     private final Map<Key, List<Method>> methodsMap = new HashMap<>();

@@ -48,6 +51,9 @@ public final class MethodWalker {
     }

     private void walkHierarchy(Class<?> clazz) {
+        if (!visited.add(clazz)) {
+            return;
+        }
         if (classes.isEmpty() || clazz.getDeclaredAnnotations().length > 0) {
             classes.add(clazz);
         }

Complexity Comparison

Depth D (diamond depth) Old (no guard) visits New (HashSet guard) visits
1 2 1
3 8 1
5 32 1
10 1,024 1

At D=3 (common in Spring proxy hierarchies): 8x speedup.

Hot Path

Called during Triple REST service registration in DefaultRequestMappingRegistry.register(). Executed once per service at startup, but in large applications with hundreds of service beans and deep interface hierarchies (especially Spring CGLIB proxies), the cumulative effect is significant.