Input validation / Validation lifecycle enforcement

MEDIUM
kubernetes/kubernetes
Commit: 7c66c6aa2351
Affected: v1.36.0-beta.0 (tracked version) and earlier in the v1.36.x line
2026-05-26 18:04 UTC

Description

This commit alters the declarative validation enforcement model from per-tag enforcement to lifecycle-based enforcement. It removes the DeclarativeEnforcement flag and relies on lifecycle prefixes (alpha/beta/standard) together with the DeclarativeValidationBeta gate to determine whether declarative validation is enforced. The changes aim to reduce the possibility of bypassing declarative validation by aligning enforcement with resource lifecycle, tightening input validation consistency across API server components. Overall, this appears to be a security-focused improvement to input validation and validation lifecycle handling, rather than a simple cleanup or dependency bump.

Commit Details

Author: Yongrui Lin

Date: 2026-05-19 16:35 UTC

Message:

Always enforce declarative validation Per-tag enforcement is now controlled by the lifecycle prefix (alpha/beta/standard) and the DeclarativeValidationBeta gate.

Triage Assessment

Vulnerability Type: Input validation / Validation lifecycle enforcement

Confidence: MEDIUM

Reasoning:

The commit adjusts how declarative validation is enforced across the API server components, removing a per-tag enforcement flag and introducing lifecycle-based enforcement. This directly affects input validation behavior and could mitigate bypasses where handwritten vs declarative validation could differ, thereby tightening security. The changes predominantly revolve around validation lifecycle and mismatch handling rather than non-security changes, supporting a security-focused improvement.

Verification Assessment

Vulnerability Type: Input validation / Validation lifecycle enforcement

Confidence: MEDIUM

Affected Versions: v1.36.0-beta.0 (tracked version) and earlier in the v1.36.x line

Code Diff

diff --git a/pkg/registry/resource/resourcepoolstatusrequest/strategy.go b/pkg/registry/resource/resourcepoolstatusrequest/strategy.go index 8eb1d9072705f..2bb9f59446a8a 100644 --- a/pkg/registry/resource/resourcepoolstatusrequest/strategy.go +++ b/pkg/registry/resource/resourcepoolstatusrequest/strategy.go @@ -72,7 +72,7 @@ func (*resourcePoolStatusRequestStrategy) Validate(ctx context.Context, obj runt // DeclarativeValidationConfig implements rest.DeclarativeValidationConfigurer to supply declarative // validation options to the generic BeforeCreate/BeforeUpdate code path. func (*resourcePoolStatusRequestStrategy) DeclarativeValidationConfig(ctx context.Context, obj, oldObj runtime.Object) rest.DeclarativeValidationConfig { - return rest.DeclarativeValidationConfig{DeclarativeEnforcement: true} + return rest.DeclarativeValidationConfig{} } func (*resourcePoolStatusRequestStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { diff --git a/pkg/registry/scheduling/podgroup/strategy.go b/pkg/registry/scheduling/podgroup/strategy.go index 89b95bd635f11..2aef4b67cd063 100644 --- a/pkg/registry/scheduling/podgroup/strategy.go +++ b/pkg/registry/scheduling/podgroup/strategy.go @@ -86,7 +86,7 @@ func (*podGroupStrategy) DeclarativeValidationConfig(ctx context.Context, obj, o if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) { opts = append(opts, string(features.WorkloadAwarePreemption)) } - return rest.DeclarativeValidationConfig{Options: opts, DeclarativeEnforcement: true} + return rest.DeclarativeValidationConfig{Options: opts} } func (*podGroupStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { diff --git a/pkg/registry/scheduling/workload/strategy.go b/pkg/registry/scheduling/workload/strategy.go index 22f2726452040..2bcdbd54af61c 100644 --- a/pkg/registry/scheduling/workload/strategy.go +++ b/pkg/registry/scheduling/workload/strategy.go @@ -65,7 +65,7 @@ func (workloadStrategy) DeclarativeValidationConfig(ctx context.Context, obj, ol if utilfeature.DefaultFeatureGate.Enabled(features.WorkloadAwarePreemption) { opts = append(opts, string(features.WorkloadAwarePreemption)) } - return rest.DeclarativeValidationConfig{DeclarativeEnforcement: true, Options: opts} + return rest.DeclarativeValidationConfig{Options: opts} } func (workloadStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go index 779645e6017d8..0ce5554cea45f 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go @@ -85,11 +85,6 @@ type DeclarativeValidationConfig struct { // expect. These often correspond to feature gates. Options []string - // DeclarativeEnforcement indicates that declarative validations should - // follow the fine-grained Validation Lifecycle. When set, declarative - // validation is always executed regardless of feature gates. - DeclarativeEnforcement bool - // NormalizationRules are applied to field paths when comparing // handwritten and declarative validation errors. NormalizationRules []field.NormalizationRule @@ -323,34 +318,23 @@ func gatherDeclarativeValidationMismatches(imperativeErrs, declarativeErrs field } // createDeclarativeValidationPanicHandler returns a function with panic recovery logic -// that will increment the panic metric and either log or append errors based on the shouldFail parameter. -func createDeclarativeValidationPanicHandler(ctx context.Context, errs *field.ErrorList, shouldFail bool, validationIdentifier string) func() { - logger := klog.FromContext(ctx) +// that increments the panic metric and appends an InternalError to errs. +func createDeclarativeValidationPanicHandler(errs *field.ErrorList, validationIdentifier string) func() { return func() { if r := recover(); r != nil { - // Increment the panic metric counter validationmetrics.Metrics.IncDeclarativeValidationPanicMetric(validationIdentifier) - - const errorFmt = "panic during declarative validation: %v" - if shouldFail { - // If shouldFail is enabled, output as a validation error as authoritative validator panicked and validation should error - *errs = append(*errs, field.InternalError(nil, fmt.Errorf(errorFmt, r))) - } else { - // if shouldFail not enabled, log the panic as an error message - logger.Error(nil, fmt.Sprintf(errorFmt, r)) - } + *errs = append(*errs, field.InternalError(nil, fmt.Errorf("panic during declarative validation: %v", r))) } } } -// panicSafeValidateFunc wraps an validation function with panic recovery logic. -// The returned function will execute the wrapped function and handle any panics by -// incrementing the panic metric, and logging an error message +// panicSafeValidateFunc wraps a validation function with panic recovery logic. +// On panic, the panic metric is incremented and an InternalError is returned in errs. func panicSafeValidateFunc( validateFunc func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *ValidationConfigOption) field.ErrorList, ) func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *ValidationConfigOption) field.ErrorList { return func(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *ValidationConfigOption) (errs field.ErrorList) { - defer createDeclarativeValidationPanicHandler(ctx, &errs, o.DeclarativeEnforcement, o.ValidationIdentifier)() + defer createDeclarativeValidationPanicHandler(&errs, o.ValidationIdentifier)() return validateFunc(ctx, scheme, obj, oldObj, o) } } @@ -393,19 +377,17 @@ func metricIdentifier(ctx context.Context, scheme *runtime.Scheme, obj runtime.O } // ValidateDeclarativelyWithMigrationChecks executes declarative validation and implements the Validation Lifecycle strategy. -// It manages the transition from handwritten (HV) to declarative (DV) validation by controlling enforcement: -// - Standard: Enforced if declarativeEnforcement is set. HV counterparts are expected to be deleted from source. -// - Beta: Enforced if declarativeEnforcement is set AND DeclarativeValidationBeta feature gate is enabled. -// When enforced, corresponding HV errors are filtered out. Otherwise, DV is shadowed. -// - Alpha: Always shadowed; HV remains authoritative. +// Declarative validation is always authoritative; the lifecycle prefix on each tag controls the visible behavior: +// - Standard (no prefix): Enforced. HV counterparts are expected to be deleted from source. +// - Beta (+k8s:beta): Enforced when DeclarativeValidationBeta is enabled. Otherwise shadowed (HV remains authoritative). +// - Alpha (+k8s:alpha): Always shadowed; HV remains authoritative. // -// Mismatches between HV and DV are logged if the DeclarativeValidation gate is enabled. -// Mismatch checking is limited to Alpha and Beta stages when explicit enforcement is active. +// Mismatches between HV and DV are logged when the DeclarativeValidation gate is enabled. Only Alpha and +// Beta errors are mismatch-checked, since Standard DV errors may have no HV counterpart in new APIs. // -// For testing purposes, WithAllDeclarativeEnforcedForTest can be used to enforce all declarative validations -// regardless of feature gates and filter all covered handwritten validations. +// For testing purposes, WithAllDeclarativeEnforcedForTest enforces all declarative validations regardless +// of lifecycle and filters all covered handwritten validations. func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, errs field.ErrorList, opType operation.Type, config DeclarativeValidationConfig) field.ErrorList { - declarativeValidationEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) betaEnabled := utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidationBeta) // allDeclarativeEnforced indicates that we should check all declarative errors for testing purposes. allDeclarativeEnforced := ctx.Value(allDeclarativeEnforcedKey) == true @@ -417,44 +399,24 @@ func ValidateDeclarativelyWithMigrationChecks(ctx context.Context, scheme *runti klog.FromContext(ctx).Error(err, "failed to generate complete validation identifier for declarative validation") } - // Directly create the config and call the core validation logic. cfg := &ValidationConfigOption{ OpType: opType, ValidationIdentifier: validationIdentifier, DeclarativeValidationConfig: config, } - // Short-circuit if neither DeclarativeValidation is enabled nor the object is explicitly configured for declarative enforcement. - if !declarativeValidationEnabled && !cfg.DeclarativeEnforcement && !allDeclarativeEnforced { - return errs - } - - // Call the panic-safe wrapper with the real validation function. - // We should fail if validation is enforced. declarativeErrs := panicSafeValidateFunc(validateDeclaratively)(ctx, scheme, obj, oldObj, cfg) - if declarativeValidationEnabled { - // Log mismatches. - // When explicit strategy is used (declarativeEnforcement), Standard errors are authoritative - // and may not have handwritten counterparts (e.g., in new APIs). - // We only mismatch check Alpha and Beta errors in this mode. - mismatchCandidateErrs := declarativeErrs - if cfg.DeclarativeEnforcement { - mismatchCandidateErrs = nil - for _, err := range declarativeErrs { - if err.IsAlpha() || err.IsBeta() { - mismatchCandidateErrs = append(mismatchCandidateErrs, err) - } + if utilfeature.DefaultFeatureGate.Enabled(features.DeclarativeValidation) { + // Standard errors are authoritative and may not have handwritten counterparts (e.g., in new APIs). + // Only Alpha and Beta errors are eligible for mismatch checking. + var mismatchCandidateErrs field.ErrorList + for _, err := range declarativeErrs { + if err.IsAlpha() || err.IsBeta() { + mismatchCandidateErrs = append(mismatchCandidateErrs, err) } } - - // We pass betaEnabled (and enforcement) as the takeover flag to avoid changing logic elsewhere for now. - compareDeclarativeErrorsAndEmitMismatches(ctx, errs, mismatchCandidateErrs, validationIdentifier, cfg.DeclarativeEnforcement && betaEnabled, *cfg) - } - - if !cfg.DeclarativeEnforcement && !allDeclarativeEnforced { - // If enforcement is not enabled, we shadow declarative errors with hand-written ones, so we return early here. - return errs + compareDeclarativeErrorsAndEmitMismatches(ctx, errs, mismatchCandidateErrs, validationIdentifier, betaEnabled, *cfg) } // Filter HV errors diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go index d96fbeba820da..8101d9621bf6a 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate_test.go @@ -465,11 +465,9 @@ func TestWithRecover(t *testing.T) { obj := &runtime.Unknown{} testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList - enforcementEnabled bool - wantErrs field.ErrorList - expectLogRegex string + name string + validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList + wantErrs field.ErrorList }{ { name: "no panic", @@ -478,74 +476,36 @@ func TestWithRecover(t *testing.T) { field.Invalid(field.NewPath("field"), "value", "reason"), } }, - enforcementEnabled: false, wantErrs: field.ErrorList{ field.Invalid(field.NewPath("field"), "value", "reason"), }, - expectLogRegex: "", }, { - name: "panic with enforcement disabled", + name: "panic returns InternalError", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList { panic("test panic") }, - enforcementEnabled: false, - wantErrs: nil, - // logs have a prefix of the form - E0309 21:05:33.865030 1926106 validate.go:199] - expectLogRegex: "E.*panic during declarative validation: test panic", - }, - { - name: "panic with enforcement enabled", - validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList { - panic("test panic") - }, - enforcementEnabled: true, wantErrs: field.ErrorList{ field.InternalError(nil, fmt.Errorf("panic during declarative validation: test panic")), }, - expectLogRegex: "", }, { name: "nil return, no panic", validateFn: func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList { return nil }, - enforcementEnabled: false, - wantErrs: nil, - expectLogRegex: "", + wantErrs: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var buf bytes.Buffer - klog.SetOutput(&buf) - klog.LogToStderr(false) - defer klog.LogToStderr(true) - wrapped := panicSafeValidateFunc(tc.validateFn) - gotErrs := wrapped(ctx, scheme, obj, nil, &ValidationConfigOption{ValidationIdentifier: "test_validationIdentifier", OpType: operation.Create, DeclarativeValidationConfig: DeclarativeValidationConfig{Options: options, DeclarativeEnforcement: tc.enforcementEnabled}}) - - klog.Flush() - logOutput := buf.String() + gotErrs := wrapped(ctx, scheme, obj, nil, &ValidationConfigOption{ValidationIdentifier: "test_validationIdentifier", OpType: operation.Create, DeclarativeValidationConfig: DeclarativeValidationConfig{Options: options}}) - // Compare gotErrs vs. tc.wantErrs if !equalErrorLists(gotErrs, tc.wantErrs) { t.Errorf("panicSafeValidateFunc() gotErrs = %#v, want %#v", gotErrs, tc.wantErrs) } - - // Check logs if needed - if tc.expectLogRegex != "" { - matched, err := regexp.MatchString(tc.expectLogRegex, logOutput) - if err != nil { - t.Fatalf("Bad regex: %v", err) - } - if !matched { - t.Errorf("Expected log output %q, but got:\n%s", tc.expectLogRegex, logOutput) - } - } else if strings.Contains(logOutput, "panic during declarative validation") { - t.Errorf("Unexpected panic log found: %s", logOutput) - } }) } } @@ -558,11 +518,9 @@ func TestWithRecoverUpdate(t *testing.T) { oldObj := &runtime.Unknown{} testCases := []struct { - name string - validateFn func(context.Context, *runtime.Scheme, runtime.Object, runtime.Object, *ValidationConfigOption) field.ErrorList - enforcementEnabled bool - wantErrs field.ErrorL ... [truncated]
← Back to Alerts View on GitHub →