CVE Patch Review

CVE-2026-6860: Vert.x caps SNI cache growth with bounded LRU eviction

CVE-2026-6860 · Updated 2026-05-09 Root-cause

Summary

The patch addresses a remotely triggerable memory exhaustion issue in Eclipse Vert.x by replacing unbounded per-SNI TLS context caches with bounded LRU caches and adding regression tests that verify cache size under repeated unique SNI inputs. The fix materially addresses the root cause: attacker-controlled cache cardinality.

Analysis

Vulnerability

CVE-2026-6860 describes an out-of-memory denial of service in Eclipse Vert.x where remote clients can send many TLS ClientHello messages with distinct SNI hostnames that still match a wildcard certificate, causing repeated allocation and retention of SslContext instances. The vulnerable implementation in SslContextProvider used unbounded ConcurrentHashMap instances for SNI-derived context caching, so cache growth was directly influenced by attacker-controlled hostname diversity rather than a fixed resource budget. The upstream patch is tracked in PR #6102, and the issue context is also reflected in the CVE record.

// Vulnerable behavior: attacker-controlled key space in unbounded maps
new ConcurrentHashMap<>(), new ConcurrentHashMap<>()
...
return sslContextMaps[idx].computeIfAbsent(serverName,
  s -> createContext(server, kmf, trustManagers, s, useAlpn));

Because each unique serverName could create a new cached SslContext, repeated handshakes with novel names could force unbounded heap retention. This is a classic memory amplification pattern: low-cost network input drives expensive object creation and indefinite caching.

Patch

The patch replaces the unbounded SNI cache with a bounded LRU cache and introduces explicit default limits. In SslContextProvider, the cache now uses LruCache with DEFAULT_SNI_CACHE_SIZE = 16 instead of ConcurrentHashMap. Access is synchronized around the map because the new cache implementation is intentionally not thread-safe. In SslContextManager, the provider cache default is also reduced and centralized as DEFAULT_SSL_CONTEXT_PROVIDER_CACHE_SIZE = 64. A reusable io.vertx.core.impl.utils.LruCache class was added to avoid duplicated ad hoc cache logic.

public static final int DEFAULT_SNI_CACHE_SIZE = 16;

new LruCache<>(DEFAULT_SNI_CACHE_SIZE), new LruCache<>(DEFAULT_SNI_CACHE_SIZE)
...
Map<String, SslContext> ctxMap = sslContextMaps[idx];
synchronized (ctxMap) {
  return ctxMap.computeIfAbsent(serverName,
    s -> createContext(server, kmf, trustManagers, s, useAlpn));
}

The new LruCache is a LinkedHashMap configured for access-order eviction:

public class LruCache<K, V> extends LinkedHashMap<K, V> {
  private final int maxSize;

  public LruCache(int maxSize) {
    super(8, 0.75f, true);
    if (maxSize < 1) {
      throw new IllegalArgumentException("Max size must be > 0");
    }
    this.maxSize = maxSize;
  }

  @Override
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > maxSize;
  }
}

Regression coverage was added in NetTest to verify that 100 distinct SNI values do not grow the cache beyond DEFAULT_SNI_CACHE_SIZE when SNI is enabled, and remain at zero when SNI is disabled. LruCacheTest also validates eviction semantics. These tests directly exercise the exploit shape described by the vulnerability.

Review

Pros

  • The patch addresses the actual resource-exhaustion primitive: unbounded attacker-influenced cache cardinality. Bounding the cache converts an unbounded memory sink into a fixed-size structure.
  • The use of LRU eviction is technically appropriate for SNI context reuse because it preserves locality for active hostnames while preventing indefinite retention of cold entries.
  • The synchronization added around cache access is necessary because the new LruCache is not thread-safe. This preserves correctness around computeIfAbsent and size accounting.
  • The tests are source-grounded and meaningful: they simulate many unique SNI values and assert the cache remains capped at SslContextProvider.DEFAULT_SNI_CACHE_SIZE, which is the key security property.
  • Refactoring the cache into io.vertx.core.impl.utils.LruCache removes duplicated cache code and standardizes bounded-cache behavior across the TLS path.

Cons

  • The patch changes concurrency characteristics by replacing lock-free ConcurrentHashMap access with synchronized blocks on each cache map. This is likely acceptable for small bounded caches, but it may introduce contention on handshake-heavy workloads.
  • The chosen default DEFAULT_SNI_CACHE_SIZE = 16 is conservative and security-friendly, but it may increase SslContext churn for deployments serving many legitimate SNI names. That is a performance tradeoff rather than a security flaw.
  • The snippets do not show whether the cache size is externally configurable. If it is fixed, operators with large multi-tenant SNI fleets may need follow-up tuning support.
  • Eviction bounds retained memory, but it does not reduce the per-request cost of creating new contexts for never-before-seen names. An attacker can still induce repeated context creation work, though no longer unbounded retention. That means the patch is strongest against OOM and weaker against pure CPU churn.

Verdict

Root-cause.

This patch fixes the core design issue by eliminating unbounded SNI cache growth and replacing it with explicit bounded eviction. The vulnerable behavior stemmed from storing attacker-controlled SNI-derived entries in unbounded maps; the patch directly removes that condition. The added tests validate the intended invariant under exploit-like input, which increases confidence that the OOM vector described in NVD and the upstream change set in the Vert.x patch are aligned. Residual performance considerations remain, but the memory-exhaustion root cause is materially addressed.

Sources