java-topology/defects/kubernetes/patch/kubernetes-0007-pod-failure-policy-exit-code-scan.md

3.9 KiB
Raw Permalink Blame History

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.go lines 107-128
  • pkg/controller/job/job_controller.go lines 1271, 1384, 1744