# UNDF: UNDF-2026-000000484
# micronaut-0004: AbstractAnnotationMetadataBuilder.processAnnotation — O(2^D) diamond recursion in meta-annotation traversal

## CWE-407 — Algorithmic Complexity: Exponential Recursion on Diamond Annotation Graphs

| Field        | Value |
|--------------|-------|
| ID           | micronaut-0004 |
| Severity     | MEDIUM |
| Ecosystem    | micronaut |
| Package      | core-processor |
| File         | `core-processor/src/main/java/io/micronaut/inject/annotation/AbstractAnnotationMetadataBuilder.java` |
| Lines        | 1128–1158 (processAnnotation), 1235–1272 (extractStereotypes) |
| Complexity   | O(2^D) where D = depth of diamond-shaped meta-annotation hierarchy |
| Hot path     | Called for every annotated element during Micronaut annotation processing (compile time) |

## Description

`AbstractAnnotationMetadataBuilder.processAnnotation()` traverses the meta-annotation
stereotype hierarchy recursively. The cycle guard `context.isProcessed(annotationValue)` only
checks whether the annotation appears as an **ancestor in the current recursion path** — it does
NOT prevent re-visiting an already-processed node via a diamond-shaped meta-annotation graph.

```java
private Stream<ProcessedAnnotation> processAnnotation(ProcessingContext context,
                                                      ProcessedAnnotation processedAnnotation) {
    AnnotationValue<?> annotationValue = processedAnnotation.getAnnotationValue();
    // isProcessed checks only parentAnnotations (current path ancestors), NOT a global visited set
    if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationValue.getAnnotationName())
        || context.isProcessed(annotationValue)) {
        return Stream.empty();
    }
    // ...
    processedAnnotation = addStereotypes(context, processedAnnotation, stereotypesProvided);
    // addStereotypes → extractStereotypes → processAnnotation (recursive call)
```

```java
private List<ProcessedAnnotation> extractStereotypes(ProcessingContext context,
                                                     ProcessedAnnotation processedAnnotation) {
    ProcessingContext newContext = context.withParent(processedAnnotation.annotationValue);
    // ...
    return ... .flatMap(stereotype -> processAnnotation(newContext, stereotype)).toList();
}
```

`ProcessingContext.isProcessed` is defined as:
```java
boolean isProcessed(AnnotationValue<?> annotationValue) {
    return parentAnnotations.contains(annotationValue.getAnnotationName());
}
```

`parentAnnotations` tracks only the **current traversal path** (ancestors), not globally
visited nodes. With a diamond meta-annotation graph:

```
@MyAnnotation  →  @Transactional  ─┐
                                   ├─→  @InterceptorBinding  (visited twice)
               →  @Retryable      ─┘
```

When processing `@MyAnnotation`:
1. Process `@Transactional` (context = {MyAnnotation}) → process `@InterceptorBinding` [visit 1]
2. Process `@Retryable` (context = {MyAnnotation}) → process `@InterceptorBinding` [visit 2]

`@InterceptorBinding` is NOT in `{MyAnnotation}` during step 2, so the guard misses it.

At diamond depth D=5: `@InterceptorBinding` is visited 2^4 = 16 times.
At depth D=10: 512 times.

Each visit calls `getAnnotationsForType()` (a compiler API call to read the annotation type's
own annotations) and potentially invokes annotation mappers/transformers — all redundant work.

### Real-world trigger

Micronaut applications commonly use composite annotations that meta-annotate with multiple
interceptor bindings (e.g., `@Cacheable`, `@Transactional`, `@Validated`, `@Retryable`).
When two custom composite annotations share a common base meta-annotation, the diamond
is formed. Large enterprise Micronaut apps with 5-10 custom composite annotations
can reach diamond depth 4-6, causing 16-64x redundant annotation processing per element.

## Fix

Replace the path-only `isProcessed` guard with a **globally shared visited set** passed
through the recursion:

```java
// In ProcessingContext or as a separate memoization map:
// Key: annotationName; Value: already-computed result
private final Set<String> globalVisited = new HashSet<>();

private Stream<ProcessedAnnotation> processAnnotation(ProcessingContext context,
                                                      ProcessedAnnotation processedAnnotation) {
    String annotationName = processedAnnotation.getAnnotationValue().getAnnotationName();
    if (AnnotationUtil.INTERNAL_ANNOTATION_NAMES.contains(annotationName)
        || context.isProcessed(processedAnnotation.getAnnotationValue())
        || !globalVisited.add(annotationName)) {  // ADD: global visited guard
        return Stream.empty();
    }
    // ... rest of method unchanged
```

Alternatively, the top-level caller can pass a `Set<String> globalVisited` down through
`ProcessingContext` and check it early in `processAnnotation`.

## Speedup

| Diamond depth (D) | Before (calls to getAnnotationsForType) | After | Speedup |
|-------------------|-----------------------------------------|-------|---------|
| 4                 | 16                                      | 4     | 4×      |
| 6                 | 64                                      | 6     | 11×     |
| 8                 | 256                                     | 8     | 32×     |
| 10                | 1,024                                   | 10    | 102×    |

## References

- CWE-407: Inefficient Algorithmic Complexity
- Micronaut `core-processor` annotation processing infrastructure
- `AbstractAnnotationMetadataBuilder.processAnnotation` lines 1128–1158
- `AbstractAnnotationMetadataBuilder.extractStereotypes` lines 1235–1272
- `ProcessingContext.isProcessed` lines 1817–1819
