]> git.feebdaed.xyz Git - 0xmirror/kubernetes.git/commitdiff
fixed the loophole that allows user to get around resource quota set by system admin
authoryliao <yliao@google.com>
Mon, 24 Nov 2025 19:38:14 +0000 (19:38 +0000)
committeryliao <yliao@google.com>
Thu, 18 Dec 2025 00:56:20 +0000 (00:56 +0000)
cmd/kube-controller-manager/app/core.go
pkg/controller/resourcequota/resource_quota_controller_test.go
pkg/controlplane/apiserver/admission/config.go
pkg/quota/v1/evaluator/core/registry.go
pkg/quota/v1/evaluator/core/resource_claims.go
pkg/quota/v1/evaluator/core/resource_claims_test.go
pkg/quota/v1/install/registry.go
plugin/pkg/admission/resourcequota/admission_test.go
test/integration/quota/quota_test.go

index 309286be8bbeef8f1ea1931876cc24f1b3182cbe..6cb6cdb6889f97e86e79a332513e78a4a89127c2 100644 (file)
@@ -597,7 +597,10 @@ func newResourceQuotaController(ctx context.Context, controllerContext Controlle
 
        discoveryFunc := resourceQuotaControllerDiscoveryClient.ServerPreferredNamespacedResources
        listerFuncForResource := generic.ListerFuncForResourceFunc(controllerContext.InformerFactory.ForResource)
-       quotaConfiguration := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, controllerContext.InformerFactory)
+       quotaConfiguration, err := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, controllerContext.InformerFactory)
+       if err != nil {
+               return nil, err
+       }
 
        resourceQuotaControllerOptions := &resourcequotacontroller.ControllerOptions{
                QuotaClient:               resourceQuotaControllerClient.CoreV1(),
index f58b7cdf27d027aade015ef410903ca8f6afc9c0..9341ac4674de4e16e429560b4abb1cb315fe47a6 100644 (file)
@@ -111,7 +111,10 @@ type quotaController struct {
 
 func setupQuotaController(t *testing.T, kubeClient kubernetes.Interface, lister quota.ListerForResourceFunc, discoveryFunc NamespacedResourcesFunc) quotaController {
        informerFactory := informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc())
-       quotaConfiguration := install.NewQuotaConfigurationForControllers(lister, informerFactory)
+       quotaConfiguration, err := install.NewQuotaConfigurationForControllers(lister, informerFactory)
+       if err != nil {
+               t.Fatal(err)
+       }
        alwaysStarted := make(chan struct{})
        close(alwaysStarted)
        resourceQuotaControllerOptions := &ControllerOptions{
index 55269b12a9cfe0f2a5bf9ec66cdb54e8f4baa58d..65558c366dc0c5ac430274f4e7ba3748a7b735bb 100644 (file)
@@ -42,8 +42,12 @@ func (c *Config) New(proxyTransport *http.Transport, egressSelector *egressselec
        webhookAuthResolverWrapper := webhook.NewDefaultAuthenticationInfoResolverWrapper(proxyTransport, egressSelector, c.LoopbackClientConfig, tp)
        webhookPluginInitializer := webhookinit.NewPluginInitializer(webhookAuthResolverWrapper, serviceResolver)
 
+       quotaConfiguration, err := quotainstall.NewQuotaConfigurationForAdmission(c.ExternalInformers)
+       if err != nil {
+               return nil, err
+       }
        kubePluginInitializer := NewPluginInitializer(
-               quotainstall.NewQuotaConfigurationForAdmission(c.ExternalInformers),
+               quotaConfiguration,
                exclusion.Excluded(),
        )
 
index c75f877421a0c9f3e30f56509d04a3d0e70c0f80..132d3abbd86489c5f7754f5ab7feb782599de7a9 100644 (file)
@@ -18,6 +18,7 @@ package core
 
 import (
        "context"
+       "fmt"
 
        corev1 "k8s.io/api/core/v1"
        "k8s.io/apimachinery/pkg/runtime/schema"
@@ -41,13 +42,14 @@ var legacyObjectCountAliases = map[schema.GroupVersionResource]corev1.ResourceNa
 }
 
 // NewEvaluators returns the list of static evaluators that manage more than counts
-func NewEvaluators(f quota.ListerForResourceFunc, i informers.SharedInformerFactory) []quota.Evaluator {
+func NewEvaluators(f quota.ListerForResourceFunc, i informers.SharedInformerFactory) ([]quota.Evaluator, error) {
        // these evaluators have special logic
        result := []quota.Evaluator{
                NewPodEvaluator(f, clock.RealClock{}),
                NewServiceEvaluator(f),
                NewPersistentVolumeClaimEvaluator(f),
        }
+       var claimGetter resourceClaimPodOwnerGetter
        if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) {
                var podLister corev1listers.PodLister
                var deviceClassMapping *extendedresourcecache.ExtendedResourceCache
@@ -56,10 +58,15 @@ func NewEvaluators(f quota.ListerForResourceFunc, i informers.SharedInformerFact
                        logger := klog.FromContext(context.Background())
                        deviceClassMapping = extendedresourcecache.NewExtendedResourceCache(logger)
                        if _, err := i.Resource().V1().DeviceClasses().Informer().AddEventHandler(deviceClassMapping); err != nil {
-                               logger.Error(err, "failed to add device class informer event handler")
+                               return nil, fmt.Errorf("failed to add device class informer event handler: %w", err)
+                       }
+                       var err error
+                       claimGetter, err = makeResourceClaimPodOwnerGetter(i.Resource().V1().ResourceClaims())
+                       if err != nil {
+                               return nil, err
                        }
                }
-               result = append(result, NewResourceClaimEvaluator(f, deviceClassMapping, podLister))
+               result = append(result, NewResourceClaimEvaluator(f, deviceClassMapping, podLister, claimGetter))
        }
 
        // these evaluators require an alias for backwards compatibility
@@ -67,5 +74,5 @@ func NewEvaluators(f quota.ListerForResourceFunc, i informers.SharedInformerFact
                result = append(result,
                        generic.NewObjectCountEvaluator(gvr.GroupResource(), generic.ListResourceUsingListerFunc(f, gvr), alias))
        }
-       return result
+       return result, nil
 }
index f9bf5c1498a6fd0f9fd9cfdd15e5bbca166e7504..b4fc8bfe24c6c7bb313954a6ef2e03550d83cbea 100644 (file)
@@ -26,11 +26,14 @@ import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        "k8s.io/apimachinery/pkg/runtime/schema"
+       "k8s.io/apimachinery/pkg/types"
        "k8s.io/apiserver/pkg/admission"
        quota "k8s.io/apiserver/pkg/quota/v1"
        "k8s.io/apiserver/pkg/quota/v1/generic"
        utilfeature "k8s.io/apiserver/pkg/util/feature"
+       v1 "k8s.io/client-go/informers/resource/v1"
        corev1listers "k8s.io/client-go/listers/core/v1"
+       "k8s.io/client-go/tools/cache"
        "k8s.io/dynamic-resource-allocation/deviceclass/extendedresourcecache"
        resourceinternal "k8s.io/kubernetes/pkg/apis/resource"
        resourceversioned "k8s.io/kubernetes/pkg/apis/resource/v1"
@@ -50,9 +53,9 @@ func V1ResourceByDeviceClass(className string) corev1.ResourceName {
 }
 
 // NewResourceClaimEvaluator returns an evaluator that can evaluate resource claims
-func NewResourceClaimEvaluator(f quota.ListerForResourceFunc, m *extendedresourcecache.ExtendedResourceCache, podsGetter corev1listers.PodLister) quota.Evaluator {
+func NewResourceClaimEvaluator(f quota.ListerForResourceFunc, m *extendedresourcecache.ExtendedResourceCache, podsGetter corev1listers.PodLister, claimGetter resourceClaimPodOwnerGetter) quota.Evaluator {
        listFuncByNamespace := generic.ListResourceUsingListerFunc(f, resourceapi.SchemeGroupVersion.WithResource("resourceclaims"))
-       claimEvaluator := &claimEvaluator{listFuncByNamespace: listFuncByNamespace, deviceClassMapping: m, podsGetter: podsGetter}
+       claimEvaluator := &claimEvaluator{listFuncByNamespace: listFuncByNamespace, deviceClassMapping: m, podsGetter: podsGetter, claimGetter: claimGetter}
        return claimEvaluator
 }
 
@@ -64,6 +67,8 @@ type claimEvaluator struct {
        deviceClassMapping *extendedresourcecache.ExtendedResourceCache
        // podsGetter is used to get pods
        podsGetter corev1listers.PodLister
+       // claimGetter is used to get claims for extended resources by namespace and pod owner uid
+       claimGetter resourceClaimPodOwnerGetter
 }
 
 // Constraints verifies that all required resources are present on the item.
@@ -167,7 +172,7 @@ func (p *claimEvaluator) getVerifiedPodUsage(claim *resourceapi.ResourceClaim) c
        if claim.Annotations[resourceapi.ExtendedResourceClaimAnnotation] != "true" {
                return nil
        }
-       controllerRef := metav1.GetControllerOf(claim)
+       controllerRef := metav1.GetControllerOfNoCopy(claim)
        if controllerRef == nil {
                return nil
        }
@@ -187,6 +192,30 @@ func (p *claimEvaluator) getVerifiedPodUsage(claim *resourceapi.ResourceClaim) c
        if pod.Status.ExtendedResourceClaimStatus != nil && pod.Status.ExtendedResourceClaimStatus.ResourceClaimName != claim.Name {
                return nil
        }
+       // if the pod doesn't identify its extended resource claim, make sure we're the first or only extended resource claim for this pod
+       if pod.Status.ExtendedResourceClaimStatus == nil {
+               ownedClaims, err := p.claimGetter(claim.Namespace, pod.UID)
+               if err != nil {
+                       return nil
+               }
+               for _, ownedClaim := range ownedClaims {
+                       switch ownedClaim.CreationTimestamp.Time.Compare(claim.CreationTimestamp.Time) {
+                       case -1:
+                               // There's another owned claim created earlier than this one.
+                               // Don't exempt this one based on pod usage.
+                               return nil
+                       case 0:
+                               if ownedClaim.Name < claim.Name {
+                                       // There's another owned claim created at the same time as this one with an earlier name.
+                                       // Don't exempt this one based on pod usage.
+                                       return nil
+                               }
+                       case 1:
+                               continue
+                       }
+               }
+       }
+
        quotaReqs, err := PodUsageFunc(pod, clock.RealClock{})
        if err != nil {
                return nil
@@ -288,3 +317,54 @@ func toExternalResourceClaimOrError(obj runtime.Object) (*resourceapi.ResourceCl
        }
        return claim, nil
 }
+
+const extendedResourceClaimPodOwnerIndexName = "extendedResourceClaimPodUIDOwner"
+
+func resourceClaimPodOwnerKey(namespace string, podUID types.UID) string {
+       return namespace + "/" + string(podUID)
+}
+
+type resourceClaimPodOwnerGetter func(namespace string, podUID types.UID) ([]*resourceapi.ResourceClaim, error)
+
+func makeResourceClaimPodOwnerGetter(resourceClaimInformer v1.ResourceClaimInformer) (resourceClaimPodOwnerGetter, error) {
+       indexers := cache.Indexers{extendedResourceClaimPodOwnerIndexName: extendedResourceClaimPodUIDOwnerIndex}
+       if err := resourceClaimInformer.Informer().AddIndexers(indexers); err != nil {
+               _, exists := resourceClaimInformer.Informer().GetIndexer().GetIndexers()[extendedResourceClaimPodOwnerIndexName]
+               if !exists {
+                       return nil, fmt.Errorf("failed to add resource claim pod owner indexer: %w", err)
+               }
+       }
+       indexer := resourceClaimInformer.Informer().GetIndexer()
+       return func(namespace string, podUID types.UID) ([]*resourceapi.ResourceClaim, error) {
+               objects, err := indexer.ByIndex(extendedResourceClaimPodOwnerIndexName, resourceClaimPodOwnerKey(namespace, podUID))
+               if err != nil {
+                       return nil, err
+               }
+               claims := make([]*resourceapi.ResourceClaim, 0, len(objects))
+               for _, obj := range objects {
+                       if claim, ok := obj.(*resourceapi.ResourceClaim); ok {
+                               claims = append(claims, claim)
+                       } else {
+                               return nil, fmt.Errorf("failed to get resource claim from indexer")
+                       }
+               }
+               return claims, nil
+       }, nil
+}
+func extendedResourceClaimPodUIDOwnerIndex(obj interface{}) ([]string, error) {
+       claim, ok := obj.(*resourceapi.ResourceClaim)
+       if !ok {
+               return nil, nil
+       }
+       if claim.Annotations[resourceapi.ExtendedResourceClaimAnnotation] != "true" {
+               return nil, nil
+       }
+       controllerRef := metav1.GetControllerOfNoCopy(claim)
+       if controllerRef == nil {
+               return nil, nil
+       }
+       if controllerRef.Kind != "Pod" || controllerRef.APIVersion != "v1" {
+               return nil, nil
+       }
+       return []string{resourceClaimPodOwnerKey(claim.Namespace, controllerRef.UID)}, nil
+}
index eeefb16fe3247636e6860443d59f37c21fb2e28e..5ebc6531dbff8c08634206edfa5f1fea2412c2e0 100644 (file)
@@ -28,6 +28,7 @@ import (
        "k8s.io/apimachinery/pkg/api/resource"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime/schema"
+       "k8s.io/apimachinery/pkg/types"
        "k8s.io/apiserver/pkg/admission"
        quota "k8s.io/apiserver/pkg/quota/v1"
        utilfeature "k8s.io/apiserver/pkg/util/feature"
@@ -167,6 +168,11 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
                        },
                },
        })
+       nilStatusExtendedResourceClaimExternal, err := toExternalResourceClaimOrError(nilStatusExtendedResourceClaim)
+       if err != nil {
+               t.Fatal(err)
+       }
+       nilStatusExtendedResourceClaim.Name = "foo-copy"
        initExtendedResourceClaim := testResourceClaim("foo", "ns", true, "pod-init", api.ResourceClaimSpec{
                Devices: api.DeviceClaim{
                        Requests: []api.DeviceRequest{
@@ -374,9 +380,13 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
        informerFactory := informers.NewSharedInformerFactory(client, 0)
        deviceclassmapping := extendedresourcecache.NewExtendedResourceCache(logger)
        if _, err := informerFactory.Resource().V1().DeviceClasses().Informer().AddEventHandler(deviceclassmapping); err != nil {
-               logger.Error(err, "failed to add device class informer event handler")
+               t.Fatal(err)
+       }
+       var otherOwnedClaims []*resourceapi.ResourceClaim
+       claimGetter := func(namespace string, podUID types.UID) ([]*resourceapi.ResourceClaim, error) {
+               return otherOwnedClaims, nil
        }
-       evaluatorWithDeviceMapping := NewResourceClaimEvaluator(nil, deviceclassmapping, informerFactory.Core().V1().Pods().Lister())
+       evaluatorWithDeviceMapping := NewResourceClaimEvaluator(nil, deviceclassmapping, informerFactory.Core().V1().Pods().Lister(), claimGetter)
 
        informerFactory.Start(tCtx.Done())
        t.Cleanup(func() {
@@ -390,12 +400,13 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
        // wait for informer sync
        time.Sleep(1 * time.Second)
 
-       evaluator := NewResourceClaimEvaluator(nil, nil, nil)
+       evaluator := NewResourceClaimEvaluator(nil, nil, nil, claimGetter)
        testCases := map[string]struct {
-               evaluator quota.Evaluator
-               claim     *api.ResourceClaim
-               usage     corev1.ResourceList
-               errMsg    string
+               evaluator   quota.Evaluator
+               claim       *api.ResourceClaim
+               otherClaims []*resourceapi.ResourceClaim
+               usage       corev1.ResourceList
+               errMsg      string
        }{
                "implicit-extended-resource-claim": {
                        evaluator: evaluatorWithDeviceMapping,
@@ -425,7 +436,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
                                "requests.example.com/gpu":                        resource.MustParse("1"),
                        },
                },
-               "ni-status-extended-resource-claim": {
+               "nil-status-extended-resource-claim": {
                        evaluator: evaluatorWithDeviceMapping,
                        claim:     nilStatusExtendedResourceClaim,
                        usage: corev1.ResourceList{
@@ -435,6 +446,21 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
                                "requests.example.com/gpu":                        resource.MustParse("1"),
                        },
                },
+               // both claims set the same pod as owner, the second claim cannot get the usage
+               // subtraction from pod requests.
+               "nil-status-two-extended-resource-claims": {
+                       evaluator: evaluatorWithDeviceMapping,
+                       claim:     nilStatusExtendedResourceClaim,
+                       otherClaims: []*resourceapi.ResourceClaim{
+                               nilStatusExtendedResourceClaimExternal,
+                       },
+                       usage: corev1.ResourceList{
+                               "count/resourceclaims.resource.k8s.io":            resource.MustParse("1"),
+                               "gpu.deviceclass.resource.k8s.io/devices":         resource.MustParse("2"),
+                               "requests.deviceclass.resource.kubernetes.io/gpu": resource.MustParse("2"),
+                               "requests.example.com/gpu":                        resource.MustParse("2"),
+                       },
+               },
                "init-extended-resource-claim": {
                        evaluator: evaluatorWithDeviceMapping,
                        claim:     initExtendedResourceClaim,
@@ -578,6 +604,7 @@ func TestResourceClaimEvaluatorUsage(t *testing.T) {
        }
        for testName, testCase := range testCases {
                t.Run(testName, func(t *testing.T) {
+                       otherOwnedClaims = testCase.otherClaims
                        featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DRAExtendedResource, true)
                        if testCase.evaluator == nil {
                                testCase.evaluator = evaluator
@@ -620,7 +647,7 @@ func TestResourceClaimEvaluatorMatchingResources(t *testing.T) {
        if _, err := informerFactory.Resource().V1().DeviceClasses().Informer().AddEventHandler(deviceclassmapping); err != nil {
                logger.Error(err, "failed to add device class informer event handler")
        }
-       evaluator := NewResourceClaimEvaluator(nil, deviceclassmapping, informerFactory.Core().V1().Pods().Lister())
+       evaluator := NewResourceClaimEvaluator(nil, deviceclassmapping, informerFactory.Core().V1().Pods().Lister(), nil)
 
        informerFactory.Start(tCtx.Done())
        t.Cleanup(func() {
@@ -677,7 +704,7 @@ func TestResourceClaimEvaluatorMatchingResources(t *testing.T) {
 }
 
 func TestResourceClaimEvaluatorHandles(t *testing.T) {
-       evaluator := NewResourceClaimEvaluator(nil, nil, nil)
+       evaluator := NewResourceClaimEvaluator(nil, nil, nil, nil)
        testCases := []struct {
                name  string
                attrs admission.Attributes
index dae800f6e3393174e0f8aee3f2dd8c903c62a5ec..a94e7aee7b4893b45ffdfae3d8a02095e16098d7 100644 (file)
@@ -28,15 +28,21 @@ import (
 )
 
 // NewQuotaConfigurationForAdmission returns a quota configuration for admission control.
-func NewQuotaConfigurationForAdmission(i informers.SharedInformerFactory) quota.Configuration {
-       evaluators := core.NewEvaluators(nil, i)
-       return generic.NewConfiguration(evaluators, DefaultIgnoredResources())
+func NewQuotaConfigurationForAdmission(i informers.SharedInformerFactory) (quota.Configuration, error) {
+       evaluators, err := core.NewEvaluators(nil, i)
+       if err != nil {
+               return nil, err
+       }
+       return generic.NewConfiguration(evaluators, DefaultIgnoredResources()), nil
 }
 
 // NewQuotaConfigurationForControllers returns a quota configuration for controllers.
-func NewQuotaConfigurationForControllers(f quota.ListerForResourceFunc, i informers.SharedInformerFactory) quota.Configuration {
-       evaluators := core.NewEvaluators(f, i)
-       return generic.NewConfiguration(evaluators, DefaultIgnoredResources())
+func NewQuotaConfigurationForControllers(f quota.ListerForResourceFunc, i informers.SharedInformerFactory) (quota.Configuration, error) {
+       evaluators, err := core.NewEvaluators(f, i)
+       if err != nil {
+               return nil, err
+       }
+       return generic.NewConfiguration(evaluators, DefaultIgnoredResources()), nil
 }
 
 // ignoredResources are ignored by quota by default
index 607f6036697e3a9705e58084398a2cf2d55348b4..8250580c69ae38993b1b78060d4452e9de9e83d2 100644 (file)
@@ -110,7 +110,10 @@ func createHandlerWithConfig(kubeClient kubernetes.Interface, informerFactory in
        if config == nil {
                config = &resourcequotaapi.Configuration{}
        }
-       quotaConfiguration := install.NewQuotaConfigurationForAdmission(nil)
+       quotaConfiguration, err := install.NewQuotaConfigurationForAdmission(nil)
+       if err != nil {
+               return nil, err
+       }
 
        handler, err := resourcequota.NewResourceQuota(config, 5)
        if err != nil {
index ec214d139c58fc2302de9187741b34198bb2c3ef..f3dcd4d8022459da0918870244a38cfd3b497867 100644 (file)
@@ -90,7 +90,10 @@ func TestQuota(t *testing.T) {
 
        discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
        listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
-       qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       qc, err := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       if err != nil {
+               t.Fatalf("unexpected err: %v", err)
+       }
        informersStarted := make(chan struct{})
        resourceQuotaControllerOptions := &resourcequotacontroller.ControllerOptions{
                QuotaClient:               clientset.CoreV1(),
@@ -319,7 +322,10 @@ plugins:
 
        discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
        listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
-       qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       qc, err := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       if err != nil {
+               t.Fatalf("unexpected err: %v", err)
+       }
        informersStarted := make(chan struct{})
        resourceQuotaControllerOptions := &resourcequotacontroller.ControllerOptions{
                QuotaClient:               clientset.CoreV1(),
@@ -445,7 +451,10 @@ plugins:
 
        discoveryFunc := clientset.Discovery().ServerPreferredNamespacedResources
        listerFuncForResource := generic.ListerFuncForResourceFunc(informers.ForResource)
-       qc := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       qc, err := quotainstall.NewQuotaConfigurationForControllers(listerFuncForResource, nil)
+       if err != nil {
+               t.Fatalf("unexpected err: %v", err)
+       }
        informersStarted := make(chan struct{})
        resourceQuotaControllerOptions := &resourcequotacontroller.ControllerOptions{
                QuotaClient:               clientset.CoreV1(),