java-topology/defects/elasticsearch/patch/elasticsearch-004-index-graveyard-dangling-list-contains.md

2.6 KiB
Raw Blame History

UNDF: UNDF-2026-000000385

elasticsearch-004: IndexGraveyard.containsIndex O(n²) List scan in DanglingIndicesState loop

Classification

  • CWE: CWE-407 (Inefficient Algorithmic Complexity)
  • Severity: MEDIUM
  • Path: Node startup and periodic dangling-index detection

Location

server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java:120 server/src/main/java/org/elasticsearch/gateway/DanglingIndicesState.java:67

Defect

// IndexGraveyard.java
private final List<Tombstone> tombstones;   // up to 500 tombstones (cluster.indices.tombstones.size)

public boolean containsIndex(final Index index) {
    for (Tombstone tombstone : tombstones) {   // O(T) — linear scan
        if (tombstone.getIndex().equals(index)) {
            return true;
        }
    }
    return false;
}

// DanglingIndicesState.java — called at node startup and on cluster state change
for (IndexMetadata indexMetadata : indexMetadataList) {   // O(I) — for each index file on disk
    Index index = indexMetadata.getIndex();
    if (graveyard.containsIndex(index) == false) {        // O(T) — linear scan per index
        danglingIndices.put(index, stripAliases(indexMetadata));
    }
}

Total complexity: O(I × T) where I = index files on disk, T = tombstones (default max 500).

In a cluster that has experienced heavy index churn (many create/delete cycles), both I and T approach their limits, producing 500 × 500 = 250,000 equality checks on every dangling-index scan. The scan runs at node startup (blocking) and on every cluster state update.

Fix

Pre-build a HashSet<Index> from the tombstones once, then do O(1) membership tests:

// IndexGraveyard.java — add a helper or cache
public Set<Index> tombstoneIndexSet() {
    Set<Index> set = new HashSet<>(tombstones.size() * 2);
    for (Tombstone t : tombstones) {
        set.add(t.getIndex());
    }
    return set;
}

// DanglingIndicesState.java
Set<Index> graveyardSet = graveyard.tombstoneIndexSet();   // O(T) once
for (IndexMetadata indexMetadata : indexMetadataList) {    // O(I)
    Index index = indexMetadata.getIndex();
    if (graveyardSet.contains(index) == false) {           // O(1)
        danglingIndices.put(index, stripAliases(indexMetadata));
    }
}

Result: O(I + T) — linear rather than quadratic.

Overhead Measurement

I (indices) T (tombstones) Slow ops Fast ops Ratio
100 100 10,000 200 50x
500 500 250,000 1,000 250x
1000 500 500,000 1,500 333x