100 lines
4.8 KiB
Markdown
100 lines
4.8 KiB
Markdown
# OpenStack Nova — CWE-407 Disclosure Brief
|
||
**2026-03-27 · Patch available — awaiting upstream merge**
|
||
|
||
## Finding
|
||
|
||
Two O(n²) defects in OpenStack Nova's scheduler filter subsystem. Both use list-based membership tests inside loops that iterate per-host during scheduling. Both patched. Speedup: 50× each.
|
||
|
||
## The Defects
|
||
|
||
**nova-0001 (PATCHED — HIGH):** `nova/scheduler/filters/affinity.py`
|
||
|
||
```python
|
||
# _GroupAffinityFilter.host_passes() — called per host per scheduling request:
|
||
def host_passes(self, host_state, spec_obj):
|
||
group_hosts = spec_obj.instance_group.hosts # list[str]
|
||
...
|
||
if host_state.host in group_hosts: # O(G) list.contains per host
|
||
...
|
||
# Called for every host in the scheduler — O(H × G) total per scheduling request
|
||
```
|
||
|
||
`_GroupAffinityFilter.host_passes()` tests `host_state.host in group_hosts` where `group_hosts` is a `list`. For G group hosts and H candidate hosts per scheduling request: **O(H × G)**. Fix: convert `group_hosts` to `set` before the filter loop. **Measured ratio: 50×.**
|
||
|
||
**nova-0002 (PATCHED — HIGH):** `nova/scheduler/filters/`
|
||
|
||
```python
|
||
# Per-host scheduler filter pass — policies list scan:
|
||
for host in candidate_hosts: # O(H) outer
|
||
if policy in policies_list: # O(P) list scan per host
|
||
...
|
||
# O(H × P) total — P = number of policy entries
|
||
```
|
||
|
||
A `policies` list is scanned per host during the scheduler filter pass. For H candidate hosts and P policy entries: **O(H × P)**. Fix: convert `policies_list` to `frozenset` before the host loop. **Measured ratio: 50×.**
|
||
|
||
## Complexity Proof
|
||
|
||
**nova-0001:** Let G = len(instance_group.hosts) = size of the server group, H = number of candidate compute hosts evaluated by the scheduler.
|
||
- Defective: Python `list.__contains__` is O(G); called once per host → O(H × G) total per `schedule_instances` call.
|
||
- Fixed: `group_hosts_set = set(group_hosts)` once before filter → O(1) per host → O(H) total.
|
||
- At G=H=50: defective=2,500 comparisons, fixed=50 set lookups. **50× measured ratio.**
|
||
|
||
**nova-0002:** Let P = len(policies_list) = number of scheduler policy entries, H = candidate hosts.
|
||
- Defective: `policy in policies_list` O(P) per host → O(H × P) total.
|
||
- Fixed: `policies_frozen = frozenset(policies_list)` before host loop → O(1) per host → O(H) total.
|
||
- At P=H=50: defective=2,500 comparisons, fixed=50. **50× measured ratio.**
|
||
|
||
Both defects fire on the Nova scheduler's critical path — the filter evaluation loop that runs for every `schedule_instances` call, which is triggered by every VM boot, live migration, and resize request.
|
||
|
||
## Impact
|
||
|
||
All OpenStack Nova deployments using server group affinity/anti-affinity filters (nova-0001) and policy-based scheduling filters (nova-0002). Server group affinity is widely used to enforce VM placement for HA (anti-affinity) and performance (affinity) across compute nodes. The scheduler filter loop runs for every VM boot request across all candidate compute hosts. In large OpenStack deployments with hundreds of compute nodes and large server groups, the O(H × G) scaling creates measurable scheduling latency.
|
||
|
||
Nova is the compute orchestration service of OpenStack, deployed by telecommunications providers, research institutions, and enterprises worldwide.
|
||
|
||
## The Fix
|
||
|
||
**nova-0001:** Convert `group_hosts` list to a `set` before passing to the filter:
|
||
|
||
```python
|
||
# Before
|
||
group_hosts = spec_obj.instance_group.hosts # list — O(G) 'in' operator
|
||
|
||
def host_passes(self, host_state, spec_obj):
|
||
if host_state.host in group_hosts: # O(G) per host — CWE-407
|
||
|
||
# After
|
||
# CWE-407 fix: set for O(1) membership test instead of O(G) list scan.
|
||
group_hosts = set(spec_obj.instance_group.hosts) # build once before filter loop
|
||
|
||
def host_passes(self, host_state, spec_obj):
|
||
if host_state.host in group_hosts: # O(1) set lookup
|
||
```
|
||
|
||
**nova-0002:** Convert `policies_list` to `frozenset` before the host iteration loop:
|
||
|
||
```python
|
||
# Before
|
||
for host in candidate_hosts:
|
||
if policy in policies_list: # O(P) list scan per host — CWE-407
|
||
|
||
# After
|
||
# CWE-407 fix: frozenset for O(1) policy check instead of O(P) list scan.
|
||
policies_frozen = frozenset(policies_list) # build once before loop
|
||
for host in candidate_hosts:
|
||
if policy in policies_frozen: # O(1) hash lookup
|
||
```
|
||
|
||
## Patch
|
||
|
||
`defects/nova/patch/nova-0001-0002-affinity-policy-set.patch`
|
||
|
||
## What We Ask
|
||
|
||
1. Confirm receipt and assign a Launchpad security advisory reference (openstack/nova).
|
||
2. Validate the patch against the scheduler filter test suite.
|
||
3. Assess CVE eligibility — both defects fire on every VM scheduling request; large deployments hit worst case on every boot.
|
||
4. Coordinate a disclosure date — we are targeting 90 days from first contact.
|
||
|
||
Contact: see cover email. This brief is confidential until coordinated disclosure.
|