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

141 lines
5.1 KiB
Markdown

# 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
```java
// 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:
```java
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
```diff
--- 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.