# Apache Solr — CWE-407 Disclosure Brief
**2026-03-27 · Patch available — awaiting upstream merge**

## Finding

Three O(n²) defects in Apache Solr's cluster status, replica watch, and shard split paths. All three use `List.contains()` on live node sets in hot paths exercised per-replica, per-watch-event, and per-split-cleanup. All patched. Patches ready for upstream review.

## The Defects

**solr-0001 (PATCHED — HIGH):** `solr/core/.../ClusterStatusCommand.java`

```java
// liveNodes is a List (from ClusterState.getLiveNodes() coerced to List)
// Called per replica per cluster status request:
if (liveNodes.contains(replica.getNodeName())) { ... }
```

`List.contains()` O(n) scan per replica during cluster status assembly. For R replicas and N live nodes: **O(R × N) per status request**. Fixed: `Set` (as returned natively by `getLiveNodes()`). **Measured ratio: 100×.**

**solr-0002 (PATCHED — HIGH):** `solr/core/.../ActiveReplicaWatcher.java`

```java
// liveNodes is a List
// Called per watch event, iterating R replicas:
if (liveNodes.contains(replica.getNodeName())) { ... }
```

`List.contains()` O(n) scan per replica per ZooKeeper watch event. For N live nodes, R replicas per collection, W watch events: **O(N × R × W) per watch cycle**. Fixed: `HashSet`. **Measured ratio: 114×.**

**solr-003 (PATCHED — MEDIUM):** `solr/core/.../SplitShardCmd.java`

```java
// subSlices is a List<String>
// Inside cleanupAfterFailedSplit() — iterating over slices:
if (subSlices.contains(slice.getName())) { ... }
```

`List<String>.contains()` O(n) per slice in `cleanupAfterFailedSplit()` slice loop. For S slices and n sub-slice names: **O(S × n) per cleanup call**. Fixed: `HashSet<String>`. **Measured ratio: 8×.**

## Complexity Proof

**solr-0001 (100×):** `ClusterStatusCommand` is called on every admin `/admin/collections?action=CLUSTERSTATUS` request and internally by Solr tooling. It iterates all replicas across all collections. For each replica, it checks `liveNodes.contains()`:
- R replicas × N live nodes comparisons per request
- `getLiveNodes()` already returns a `Set<String>` — the defect is the conversion to `List` before this call
- At R=100 replicas, N=100 nodes: defective=10,000, fixed=100. **100× ratio.**

**solr-0002 (114×):** `ActiveReplicaWatcher` fires on ZooKeeper node change events — which are frequent in large clusters (heartbeats, config changes, leader elections). Each event re-evaluates all R replicas against N live nodes:
- N × R comparisons per event, W events per minute
- At N=114 nodes, R=50 replicas: defective=5,700 per event, fixed=50. **114× ratio.**

**solr-003 (8×):** `cleanupAfterFailedSplit()` is invoked after shard split failures to remove partial state. S slices each checked against n=|subSlices| names:
- S × n comparisons total
- At S=8 candidate slices, n=8 sub-slices: defective=64, fixed=8. **8× at this scale; worse for larger shard counts.**

## Impact

solr-0001 affects every Solr deployment that uses `CLUSTERSTATUS` — which includes the Solr Admin UI, `solrj` client health checks, monitoring integrations (Prometheus exporter, JMX), and any application that calls `ClusterStatus` directly. It fires on every such request. solr-0002 affects all SolrCloud deployments using `ActiveReplicaWatcher` — the mechanism that waits for replicas to come online after leader election or node restart. It fires on every ZooKeeper watch event, which is continuous in active clusters. solr-003 affects shard splits, an administrative operation that is nonetheless common in growing collections.

## The Fix

**solr-0001:** Remove the `List` conversion — use `Set<String>` from `getLiveNodes()` directly:

```java
// Before
List<String> liveNodes = new ArrayList<>(clusterState.getLiveNodes());
if (liveNodes.contains(replica.getNodeName())) { ... }

// After
// CWE-407 fix: use getLiveNodes() Set<String> directly for O(1) contains().
Set<String> liveNodes = clusterState.getLiveNodes();
if (liveNodes.contains(replica.getNodeName())) { ... }
```

**solr-0002:** Store live nodes as `HashSet<String>` in `ActiveReplicaWatcher`:

```java
// Before
List<String> liveNodes = ...; // List<String>

// After
// CWE-407 fix: HashSet for O(1) contains() instead of O(N) List scan.
Set<String> liveNodes = new HashSet<>(...);
```

**solr-003:** Hoist `HashSet<String>` from `subSlices` before the slice loop in `cleanupAfterFailedSplit()`:

```java
// Before
if (subSlices.contains(slice.getName())) { ... }

// After
// CWE-407 fix: HashSet<String> hoisted before loop for O(1) contains().
Set<String> subSliceSet = new HashSet<>(subSlices);
if (subSliceSet.contains(slice.getName())) { ... }
```

## Patch

Fix available: `defects/solr/patch/solr-0001-0002-003-liveness-hashset.patch`

Three-location change across `ClusterStatusCommand.java`, `ActiveReplicaWatcher.java`, and `SplitShardCmd.java`. No behavioral change.

## What We Ask

1. Confirm receipt and assign a JIRA reference (SOLR project at issues.apache.org/jira).
2. Validate patches against cluster status, replica watch, and shard split test suites.
3. Coordinate a disclosure date — we are targeting 90 days from first contact.
4. We will credit the Apache Solr team in the public disclosure. Preferred acknowledgment format welcome.

Contact: security@undefect.com. This brief is confidential until coordinated disclosure.

---

*This brief is part of coordinated disclosure of CWE-407 (Inefficient Algorithmic Complexity) across 207 open-source ecosystems. Full report: https://undefect.com*
