3.9 KiB
UNDF: UNDF-2026-000000446
kubernetes-0007: CWE-407 — Quadratic exit-code scan in pod failure policy matching
Severity: MEDIUM
Repository
github.com/kubernetes/kubernetes Commit: (depth-1 clone, branch main)
File
pkg/controller/job/pod_failure_policy.go
Defective Lines
107: func getMatchingContainerFromList(containerStatuses []v1.ContainerStatus,
requirement *batch.PodFailurePolicyOnExitCodesRequirement) *v1.ContainerStatus {
for _, containerStatus := range containerStatuses { // O(C) containers
...
114: if isOnExitCodesOperatorMatching(containerStatus.State.Terminated.ExitCode, requirement) {
return &containerStatus
}
}
}
123: func isOnExitCodesOperatorMatching(exitCode int32,
requirement *batch.PodFailurePolicyOnExitCodesRequirement) bool {
switch requirement.Operator {
case batch.PodFailurePolicyOnExitCodesOpIn:
126: return slices.Contains(requirement.Values, exitCode) // O(V) linear scan
case batch.PodFailurePolicyOnExitCodesOpNotIn:
128: return !slices.Contains(requirement.Values, exitCode) // O(V) linear scan
}
}
Call Chain
syncJob() → nonIgnoredFailedPodsCount(failedPods) →
for _, p := range failedPods { // O(P) pods
matchPodFailurePolicy(policy, p) →
for _, rule := range policy.Rules { // O(R) rules
matchOnExitCodes(podStatus, rule.OnExitCodes) →
getMatchingContainerFromList(containerStatuses, req) →
for _, cs := range containerStatuses { // O(C) containers
isOnExitCodesOperatorMatching(exitCode, req) →
slices.Contains(req.Values, exitCode) // O(V)
Also called from syncJob() directly for per-pod action selection (line 1384).
Complexity
O(P × R × C × V) where:
- P = number of failed pods (can reach thousands in large batch jobs)
- R = number of PodFailurePolicyRules (up to 20 by API validation)
- C = containers per pod (typically 1-5)
- V = exit code values per rule (user-configured, typically 1-10)
The dominant factor is P × R: for a large job with 1000 failed pods and 20 rules,
this performs 20,000 rule evaluations each doing a V-length linear scan.
requirement.Values is a []int32 slice — never pre-indexed.
Impact
Large batch jobs with podFailurePolicy configured experience quadratic reconciliation
cost in the job controller sync loop. The nonIgnoredFailedPodsCount call is in the hot
path of every syncJob invocation when PodFailurePolicy is set.
Fix
Pre-build a map[int32]struct{} from requirement.Values once per rule evaluation
instead of calling slices.Contains per container per pod.
// Before (defective):
func isOnExitCodesOperatorMatching(exitCode int32,
requirement *batch.PodFailurePolicyOnExitCodesRequirement) bool {
switch requirement.Operator {
case batch.PodFailurePolicyOnExitCodesOpIn:
return slices.Contains(requirement.Values, exitCode)
case batch.PodFailurePolicyOnExitCodesOpNotIn:
return !slices.Contains(requirement.Values, exitCode)
}
return false
}
// After (fixed): build set once at rule-match time
func exitCodeSet(values []int32) map[int32]struct{} {
s := make(map[int32]struct{}, len(values))
for _, v := range values {
s[v] = struct{}{}
}
return s
}
func isOnExitCodesOperatorMatchingSet(exitCode int32,
valueSet map[int32]struct{}) bool {
_, found := valueSet[exitCode]
return found
}
// Build set once per rule before iterating containers/pods.
References
- CWE-407: Inefficient Algorithmic Complexity
pkg/controller/job/pod_failure_policy.golines 107-128pkg/controller/job/job_controller.golines 1271, 1384, 1744