Input validation / Resource exhaustion
Description
The commit adds per-item maxBytes validation for device attribute strings in ResourceSlice validation. Specifically, each string value in a DeviceAttribute.StringValues list is now validated against a per-item maximum length (DeviceAttributeMaxValueLength, 64 bytes). The validation results are annotated with origin 'maxBytes' and marked as covered by declarative validation. This hardens input validation to prevent potential resource exhaustion or abuse from excessively long string values in device attributes. Tests have been updated to enforce the boundary behavior and to reflect the maxBytes constraint.
Proof of Concept
Proof of concept (POC) to demonstrate exploitation prior to the fix (high-level, actionable):
Prerequisites:
- Access to a Kubernetes cluster with the ResourceSlice API (or the relevant Resource API) enabled for testing.
- cluster that validates DeviceAttribute strings via the Resource API prior to the fix.
1) Construct a device attribute with a string value longer than the allowed per-item maxBytes (64 bytes). Since UTF-8 may use multiple bytes per character, using multi-byte characters can easily exceed the 64-byte boundary.
2) Create a ResourceSlice manifest that includes a device attribute whose StringValues contains a single item equal to 33 occurrences of the multi-byte character 'é' (each é is 2 bytes in UTF-8; 33 * 2 = 66 bytes > 64).
3) Apply the manifest to the cluster (e.g., kubectl apply -f payload.yaml).
4) Observe API validation failure indicating the string value exceeds the maximum allowed bytes (origin: maxBytes) and that the item is rejected with a TooLong/validation error.
Example payload snippet (illustrative, adjust to actual CRD structure):
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
name: dv-long-strings
spec:
devices:
- name: dv1
attributes:
test.io/list_of_strings:
strings:
- "éééééééééééééééééééééééééé" # 33× 'é' -> 66 bytes in UTF-8
Expected outcome after the fix:
- The API server rejects this payload with a validation error surfaced as an origin 'maxBytes' validation message, e.g., TooLongMaxLength for the first item, instead of proceeding with the request and potentially consuming excessive resources.
Commit Details
Author: Kubernetes Prow Robot
Date: 2026-04-30 23:33 UTC
Message:
Merge pull request #138629 from aaron-prindle/dv-migrate-eachval-case
feat(validation-gen): add eachVal + maxBytes validation for resource string values
Triage Assessment
Vulnerability Type: Input validation / Resource exhaustion (MaxBytes for strings)
Confidence: HIGH
Reasoning:
The commit adds per-item maxBytes validation for device attribute strings and updates tests to enforce and reflect this limit. This prevents excessively long string values, mitigating potential resource exhaustion or related abuse (input validation/security hardening).
Verification Assessment
Vulnerability Type: Input validation / Resource exhaustion
Confidence: HIGH
Affected Versions: Unreleased; planned for v1.37.0+ (alpha since 1.37)
Code Diff
diff --git a/pkg/apis/resource/validation/validation.go b/pkg/apis/resource/validation/validation.go
index 050e7a422c1a3..dc3a3f173e58f 100644
--- a/pkg/apis/resource/validation/validation.go
+++ b/pkg/apis/resource/validation/validation.go
@@ -1017,7 +1017,7 @@ func validateDeviceAttribute(attribute resource.DeviceAttribute, fldPath *field.
allErrs = append(allErrs, field.Invalid(fldPath.Child("strings"), attribute.StringValues, "must not be empty if specified"))
}
for i, item := range attribute.StringValues {
- allErrs = append(allErrs, validateDeviceAttributeStringValue(&item, fldPath.Child("strings").Index(i))...)
+ allErrs = append(allErrs, validateDeviceAttributeStringValue(&item, fldPath.Child("strings").Index(i)).WithOrigin("maxBytes").MarkCoveredByDeclarative()...)
}
}
if attribute.VersionValues != nil {
diff --git a/pkg/apis/resource/validation/validation_resourceslice_test.go b/pkg/apis/resource/validation/validation_resourceslice_test.go
index e79e663fe67bf..78400a93d18fd 100644
--- a/pkg/apis/resource/validation/validation_resourceslice_test.go
+++ b/pkg/apis/resource/validation/validation_resourceslice_test.go
@@ -503,7 +503,7 @@ func TestValidateResourceSlice(t *testing.T) {
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("attributes").Key(goodName), badMultipleListValue, "exactly one value must be specified").WithOrigin("union").MarkCoveredByDeclarative(),
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(goodName).Child("strings"), []string{}, "must not be empty if specified"),
field.Invalid(field.NewPath("spec", "devices").Index(1).Child("attributes").Key(goodName), badMultipleListValueWithEmptyList, "exactly one value must be specified").WithOrigin("union").MarkCoveredByDeclarative(),
- field.TooLongMaxLength(field.NewPath("spec", "devices").Index(2).Child("attributes").Key(goodName).Child("strings").Index(0), badListStringValueTooLong.StringValues[0], resourceapi.DeviceAttributeMaxValueLength),
+ field.TooLongMaxLength(field.NewPath("spec", "devices").Index(2).Child("attributes").Key(goodName).Child("strings").Index(0), badListStringValueTooLong.StringValues[0], resourceapi.DeviceAttributeMaxValueLength).WithOrigin("maxBytes").MarkCoveredByDeclarative(),
field.Invalid(field.NewPath("spec", "devices").Index(3).Child("attributes").Key(goodName).Child("versions").Index(0), badListVersionValueTooLong.VersionValues[0], "must be a string compatible with semver.org spec 2.0.0"),
field.TooLongMaxLength(field.NewPath("spec", "devices").Index(3).Child("attributes").Key(goodName).Child("versions").Index(0), badListVersionValueTooLong.VersionValues[0], resourceapi.DeviceAttributeMaxValueLength),
field.Invalid(field.NewPath("spec", "devices").Index(4).Child("attributes").Key(goodName).Child("bools"), []bool{}, "must not be empty if specified"),
diff --git a/pkg/registry/resource/resourceslice/declarative_validation_test.go b/pkg/registry/resource/resourceslice/declarative_validation_test.go
index c5ee783ad5852..623758b8150e6 100644
--- a/pkg/registry/resource/resourceslice/declarative_validation_test.go
+++ b/pkg/registry/resource/resourceslice/declarative_validation_test.go
@@ -18,6 +18,7 @@ package resourceslice
import (
"fmt"
+ "strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -119,6 +120,23 @@ func TestDeclarativeValidate(t *testing.T) {
"valid: device attribute list of strings": {
input: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_strings", resource.DeviceAttribute{StringValues: []string{"a", "b", "c"}})),
},
+ "valid: device attribute list of strings at max bytes": {
+ input: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_strings", resource.DeviceAttribute{
+ // The tag literal is 64, which must stay aligned with DeviceAttributeMaxValueLength.
+ // "é" is two bytes in UTF-8, so repeating it max/2 times verifies the exact maxBytes boundary.
+ StringValues: []string{strings.Repeat("é", resource.DeviceAttributeMaxValueLength/2)},
+ })),
+ },
+ "invalid: device attribute list of strings with too-long item": {
+ input: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_strings", resource.DeviceAttribute{
+ // The tag literal is 64, which must stay aligned with DeviceAttributeMaxValueLength.
+ // "é" is two bytes in UTF-8, so max/2+1 repeats exceeds the maxBytes boundary.
+ StringValues: []string{strings.Repeat("é", resource.DeviceAttributeMaxValueLength/2+1)},
+ })),
+ expectedErrs: field.ErrorList{
+ field.TooLong(field.NewPath("spec", "devices").Index(0).Child("attributes").Key("test.io/list_of_strings").Child("strings").Index(0), "", resource.DeviceAttributeMaxValueLength).WithOrigin("maxBytes").MarkAlpha(),
+ },
+ },
"valid: device attribute list of versions": {
input: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_versions", resource.DeviceAttribute{VersionValues: []string{"1.2.3", "2.3.4"}})),
},
@@ -335,6 +353,25 @@ func TestDeclarativeValidateUpdate(t *testing.T) {
field.Invalid(field.NewPath("spec", "devices").Index(0).Child("attributes").Key("test.io/empty"), "", "").WithOrigin("union").MarkAlpha(),
},
},
+ "valid update: device attribute list of strings at max bytes": {
+ old: mkResourceSliceWithDevices(),
+ update: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_strings", resource.DeviceAttribute{
+ // The tag literal is 64, which must stay aligned with DeviceAttributeMaxValueLength.
+ // "é" is two bytes in UTF-8, so repeating it max/2 times verifies the exact maxBytes boundary.
+ StringValues: []string{strings.Repeat("é", resource.DeviceAttributeMaxValueLength/2)},
+ })),
+ },
+ "invalid update: device attribute list of strings with too-long item": {
+ old: mkResourceSliceWithDevices(),
+ update: mkResourceSliceWithDevices(tweakDeviceAttribute("test.io/list_of_strings", resource.DeviceAttribute{
+ // The tag literal is 64, which must stay aligned with DeviceAttributeMaxValueLength.
+ // "é" is two bytes in UTF-8, so max/2+1 repeats exceeds the maxBytes boundary.
+ StringValues: []string{strings.Repeat("é", resource.DeviceAttributeMaxValueLength/2+1)},
+ })),
+ expectedErrs: field.ErrorList{
+ field.TooLong(field.NewPath("spec", "devices").Index(0).Child("attributes").Key("test.io/list_of_strings").Child("strings").Index(0), "", resource.DeviceAttributeMaxValueLength).WithOrigin("maxBytes").MarkAlpha(),
+ },
+ },
// spec.sharedCounters
"valid update: at limit shared counters": {
old: mkResourceSliceWithSharedCounters(),
diff --git a/staging/src/k8s.io/api/resource/v1/generated.proto b/staging/src/k8s.io/api/resource/v1/generated.proto
index ea801046cfd8f..8b4c4f11461a6 100644
--- a/staging/src/k8s.io/api/resource/v1/generated.proto
+++ b/staging/src/k8s.io/api/resource/v1/generated.proto
@@ -621,6 +621,7 @@ message DeviceAttribute {
// +k8s:listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
repeated string strings = 8;
diff --git a/staging/src/k8s.io/api/resource/v1/types.go b/staging/src/k8s.io/api/resource/v1/types.go
index c9abc5aed03e7..e8665ca305c16 100644
--- a/staging/src/k8s.io/api/resource/v1/types.go
+++ b/staging/src/k8s.io/api/resource/v1/types.go
@@ -755,6 +755,7 @@ type DeviceAttribute struct {
// +k8s:listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
StringValues []string `json:"strings,omitempty" protobuf:"bytes,8,opt,name=strings"`
diff --git a/staging/src/k8s.io/api/resource/v1beta1/generated.proto b/staging/src/k8s.io/api/resource/v1beta1/generated.proto
index 606b2515ec539..eec28bb57c152 100644
--- a/staging/src/k8s.io/api/resource/v1beta1/generated.proto
+++ b/staging/src/k8s.io/api/resource/v1beta1/generated.proto
@@ -627,6 +627,7 @@ message DeviceAttribute {
// +listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
repeated string strings = 8;
diff --git a/staging/src/k8s.io/api/resource/v1beta1/types.go b/staging/src/k8s.io/api/resource/v1beta1/types.go
index cdb4b4a28aa95..3ac1c109f3221 100644
--- a/staging/src/k8s.io/api/resource/v1beta1/types.go
+++ b/staging/src/k8s.io/api/resource/v1beta1/types.go
@@ -746,6 +746,7 @@ type DeviceAttribute struct {
// +listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
StringValues []string `json:"strings,omitempty" protobuf:"bytes,8,opt,name=strings"`
diff --git a/staging/src/k8s.io/api/resource/v1beta2/generated.proto b/staging/src/k8s.io/api/resource/v1beta2/generated.proto
index 96a85cff320a8..ff9d8597e53d0 100644
--- a/staging/src/k8s.io/api/resource/v1beta2/generated.proto
+++ b/staging/src/k8s.io/api/resource/v1beta2/generated.proto
@@ -621,6 +621,7 @@ message DeviceAttribute {
// +k8s:listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
repeated string strings = 8;
diff --git a/staging/src/k8s.io/api/resource/v1beta2/types.go b/staging/src/k8s.io/api/resource/v1beta2/types.go
index 9c5befbbbe996..ef3ddee944429 100644
--- a/staging/src/k8s.io/api/resource/v1beta2/types.go
+++ b/staging/src/k8s.io/api/resource/v1beta2/types.go
@@ -740,6 +740,7 @@ type DeviceAttribute struct {
// +k8s:listType=atomic
// +k8s:alpha(since: "1.36")=+k8s:optional
// +k8s:alpha(since: "1.36")=+k8s:unionMember
+ // +k8s:alpha(since: "1.37")=+k8s:eachVal=+k8s:maxBytes=64
// +featureGate=DRAListTypeAttributes
StringValues []string `json:"strings,omitempty" protobuf:"bytes,8,opt,name=strings"`