mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 12:43:23 +00:00
Merge pull request #127862 from dinhxuanvu/cbor-fuzz
KEP-4222: Add fuzz test for roundtrip unstructured objects to JSON/CBOR
This commit is contained in:
commit
e673417529
@ -17,12 +17,20 @@ limitations under the License.
|
||||
package unstructured_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
|
||||
@ -30,6 +38,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
cborserializer "k8s.io/apimachinery/pkg/runtime/serializer/cbor"
|
||||
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
)
|
||||
|
||||
func TestNilUnstructuredContent(t *testing.T) {
|
||||
@ -118,6 +128,18 @@ func TestUnstructuredMetadataOmitempty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoundTripJSONCBORUnstructured performs fuzz testing for roundtrip for
|
||||
// unstructured object between JSON and CBOR
|
||||
func TestRoundTripJSONCBORUnstructured(t *testing.T) {
|
||||
roundtripType[*unstructured.Unstructured](t)
|
||||
}
|
||||
|
||||
// TestRoundTripJSONCBORUnstructuredList performs fuzz testing for roundtrip for
|
||||
// unstructuredList object between JSON and CBOR
|
||||
func TestRoundTripJSONCBORUnstructuredList(t *testing.T) {
|
||||
roundtripType[*unstructured.UnstructuredList](t)
|
||||
}
|
||||
|
||||
func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
|
||||
if objectMeta == nil {
|
||||
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
|
||||
@ -148,3 +170,307 @@ func setObjectMetaUsingAccessors(u, uCopy *unstructured.Unstructured) {
|
||||
uCopy.SetFinalizers(u.GetFinalizers())
|
||||
uCopy.SetManagedFields(u.GetManagedFields())
|
||||
}
|
||||
|
||||
// roundtripType performs fuzz testing for roundtrip conversion for
|
||||
// unstructured or unstructuredList object between two formats (A and B) in forward
|
||||
// and backward directions
|
||||
// Original and final unstructured/list are compared along with all intermediate ones
|
||||
func roundtripType[U runtime.Unstructured](t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
fuzzer := fuzzer.FuzzerFor(fuzzer.MergeFuzzerFuncs(metafuzzer.Funcs, unstructuredFuzzerFuncs), rand.NewSource(getSeed(t)), serializer.NewCodecFactory(scheme))
|
||||
|
||||
jS := jsonserializer.NewSerializerWithOptions(jsonserializer.DefaultMetaFactory, scheme, scheme, jsonserializer.SerializerOptions{})
|
||||
cS := cborserializer.NewSerializer(scheme, scheme)
|
||||
|
||||
for i := 0; i < 50; i++ {
|
||||
original := reflect.New(reflect.TypeFor[U]().Elem()).Interface().(runtime.Unstructured)
|
||||
fuzzer.Fuzz(original)
|
||||
// unstructured -> JSON > unstructured > CBOR -> unstructured -> JSON -> unstructured
|
||||
roundtrip(t, original, jS, cS)
|
||||
// unstructured -> CBOR > unstructured > JSON -> unstructured -> CBOR -> unstructured
|
||||
roundtrip(t, original, cS, jS)
|
||||
}
|
||||
}
|
||||
|
||||
// roundtrip tests that an Unstructured object roundtrips faithfully along the
|
||||
// sequence Unstructured -> A -> Unstructured -> B -> Unstructured -> A -> Unstructured,
|
||||
// given serializers for two encodings A and B. The final object and both intermediate
|
||||
// objects must all be equal to the original.
|
||||
func roundtrip(t *testing.T, original runtime.Unstructured, a, b runtime.Serializer) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
buf.Reset()
|
||||
// (original) Unstructured -> A
|
||||
if err := a.Encode(original, &buf); err != nil {
|
||||
t.Fatalf("error encoding original unstructured to A: %v", err)
|
||||
}
|
||||
// A -> intermediate unstructured
|
||||
uA := reflect.New(reflect.TypeOf(original).Elem()).Interface().(runtime.Object)
|
||||
uA, _, err := a.Decode(buf.Bytes(), nil, uA)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding A to unstructured: %v", err)
|
||||
}
|
||||
|
||||
// Compare original unstructured vs intermediate unstructured
|
||||
tmp, ok := uA.(runtime.Unstructured)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected type %T for unstructured", tmp)
|
||||
}
|
||||
if !unstructuredEqual(t, original, uA.(runtime.Unstructured)) {
|
||||
t.Fatalf("original unstructured differed from unstructured via A: %v", cmp.Diff(original, uA))
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
// intermediate unstructured -> B
|
||||
if err := b.Encode(uA, &buf); err != nil {
|
||||
t.Fatalf("error encoding unstructured to B: %v", err)
|
||||
}
|
||||
// B -> intermediate unstructured
|
||||
uB := reflect.New(reflect.TypeOf(original).Elem()).Interface().(runtime.Object)
|
||||
uB, _, err = b.Decode(buf.Bytes(), nil, uB)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding B to unstructured: %v", err)
|
||||
}
|
||||
|
||||
// compare original vs intermediate unstructured
|
||||
tmp, ok = uB.(runtime.Unstructured)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected type %T for unstructured", tmp)
|
||||
}
|
||||
if !unstructuredEqual(t, original, uB.(runtime.Unstructured)) {
|
||||
t.Fatalf("unstructured via A differed from unstructured via B: %v", cmp.Diff(original, uB))
|
||||
}
|
||||
|
||||
// intermediate unstructured -> A
|
||||
buf.Reset()
|
||||
if err := a.Encode(uB, &buf); err != nil {
|
||||
t.Fatalf("error encoding unstructured to A: %v", err)
|
||||
}
|
||||
// A -> final unstructured
|
||||
final := reflect.New(reflect.TypeOf(original).Elem()).Interface().(runtime.Object)
|
||||
final, _, err = a.Decode(buf.Bytes(), nil, final)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding A to unstructured: %v", err)
|
||||
}
|
||||
|
||||
// Compare original unstructured vs final unstructured
|
||||
tmp, ok = final.(runtime.Unstructured)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected type %T for unstructured", tmp)
|
||||
}
|
||||
if !unstructuredEqual(t, original, final.(runtime.Unstructured)) {
|
||||
t.Errorf("object changed during unstructured->A->unstructured->B->unstructured roundtrip, diff: %s", cmp.Diff(original, final))
|
||||
}
|
||||
}
|
||||
|
||||
func getSeed(t *testing.T) int64 {
|
||||
seed := int64(time.Now().Nanosecond())
|
||||
if override := os.Getenv("TEST_RAND_SEED"); len(override) > 0 {
|
||||
overrideSeed, err := strconv.ParseInt(override, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seed = overrideSeed
|
||||
t.Logf("using overridden seed: %d", seed)
|
||||
} else {
|
||||
t.Logf("seed (override with TEST_RAND_SEED if desired): %d", seed)
|
||||
}
|
||||
return seed
|
||||
}
|
||||
|
||||
const (
|
||||
maxUnstructuredDepth = 64
|
||||
maxUnstructuredFanOut = 5
|
||||
)
|
||||
|
||||
func unstructuredFuzzerFuncs(codecs serializer.CodecFactory) []interface{} {
|
||||
return []interface{}{
|
||||
func(u *unstructured.Unstructured, c fuzz.Continue) {
|
||||
obj := make(map[string]interface{})
|
||||
obj["apiVersion"] = generateValidAPIVersionString(c)
|
||||
obj["kind"] = generateNonEmptyString(c)
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
}
|
||||
u.Object = obj
|
||||
},
|
||||
func(ul *unstructured.UnstructuredList, c fuzz.Continue) {
|
||||
obj := make(map[string]interface{})
|
||||
obj["apiVersion"] = generateValidAPIVersionString(c)
|
||||
obj["kind"] = generateNonEmptyString(c)
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
obj[c.RandString()] = generateRandomTypeValue(maxUnstructuredDepth, c)
|
||||
}
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
var item = unstructured.Unstructured{}
|
||||
c.Fuzz(&item)
|
||||
ul.Items = append(ul.Items, item)
|
||||
}
|
||||
ul.Object = obj
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func generateNonEmptyString(c fuzz.Continue) string {
|
||||
temp := c.RandString()
|
||||
for len(temp) == 0 {
|
||||
temp = c.RandString()
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
// generateNonEmptyNoSlashString generates a non-empty string without any slashes
|
||||
func generateNonEmptyNoSlashString(c fuzz.Continue) string {
|
||||
temp := strings.ReplaceAll(generateNonEmptyString(c), "/", "")
|
||||
for len(temp) == 0 {
|
||||
temp = strings.ReplaceAll(generateNonEmptyString(c), "/", "")
|
||||
}
|
||||
return temp
|
||||
}
|
||||
|
||||
// generateValidAPIVersionString generates valid apiVersion string with formats:
|
||||
// <string>/<string> or <string>
|
||||
func generateValidAPIVersionString(c fuzz.Continue) string {
|
||||
if c.RandBool() {
|
||||
return generateNonEmptyNoSlashString(c) + "/" + generateNonEmptyNoSlashString(c)
|
||||
} else {
|
||||
return generateNonEmptyNoSlashString(c)
|
||||
}
|
||||
}
|
||||
|
||||
// generateRandomTypeValue generates fuzzed valid JSON data types:
|
||||
// 1. numbers (float64, int64)
|
||||
// 2. string (utf-8 encodings)
|
||||
// 3. boolean
|
||||
// 4. array ([]interface{})
|
||||
// 5. object (map[string]interface{})
|
||||
// 6. null
|
||||
// Decoding into unstructured can only produce a nil interface{} value or the
|
||||
// concrete types map[string]interface{}, []interface{}, int64, float64, string, and bool
|
||||
// If a value of other types is put into an unstructured, it will roundtrip
|
||||
// to one of the above list of supported types. For example, if Time type is used,
|
||||
// it will be encoded into a RFC 3339 format string such as "2001-02-03T12:34:56Z"
|
||||
// and when decoding into Unstructured, there is no information to indicate
|
||||
// that this string was originally produced by encoding a metav1.Time.
|
||||
// All external-versioned builtin types are exercised through RoundtripToUnstructured
|
||||
// in apitesting package. Types like metav1.Time are implicitly being exercised
|
||||
// because they appear as fields in those types.
|
||||
func generateRandomTypeValue(depth int, c fuzz.Continue) interface{} {
|
||||
t := c.Rand.Intn(120)
|
||||
// If the max depth for unstructured is reached, only add non-recursive types
|
||||
// which is 20+ in range
|
||||
if depth == 0 {
|
||||
t = 20 + c.Rand.Intn(120-20)
|
||||
}
|
||||
|
||||
switch {
|
||||
case t < 10:
|
||||
item := make([]interface{}, c.Intn(maxUnstructuredFanOut))
|
||||
for k := range item {
|
||||
item[k] = generateRandomTypeValue(depth-1, c)
|
||||
}
|
||||
return item
|
||||
case t < 20:
|
||||
item := map[string]interface{}{}
|
||||
for j := c.Intn(maxUnstructuredFanOut); j >= 0; j-- {
|
||||
item[c.RandString()] = generateRandomTypeValue(depth-1, c)
|
||||
}
|
||||
return item
|
||||
case t < 40:
|
||||
// Only valid UTF-8 encodings
|
||||
var item string
|
||||
c.Fuzz(&item)
|
||||
return item
|
||||
case t < 60:
|
||||
var item int64
|
||||
c.Fuzz(&item)
|
||||
return item
|
||||
case t < 80:
|
||||
var item bool
|
||||
c.Fuzz(&item)
|
||||
return item
|
||||
case t < 100:
|
||||
return c.Rand.NormFloat64()
|
||||
case t < 120:
|
||||
return nil
|
||||
default:
|
||||
panic("invalid case")
|
||||
}
|
||||
}
|
||||
|
||||
func unstructuredEqual(t *testing.T, a, b runtime.Unstructured) bool {
|
||||
return anyEqual(t, a.UnstructuredContent(), b.UnstructuredContent())
|
||||
}
|
||||
|
||||
// numberEqual asserts equality of two numbers which one is int64 and one is float64
|
||||
// In JSON, a non-decimal float64 is converted to int64 automatically in case the
|
||||
// float64 fits into int64 range. Otherwise, the non-decimal float64 remains a float.
|
||||
// As a result, this func does an int64 to float64 conversion using math/big package
|
||||
// to ensure the conversion is lossless before comparison.
|
||||
func numberEqual(a int64, b float64) bool {
|
||||
// Ensure roundtrip int64 to float64 conversion is lossless
|
||||
f, accuracy := big.NewInt(a).Float64()
|
||||
if accuracy == big.Exact {
|
||||
// Distinction between int64 and float64 is not preserved during JSON roundtrip for all numbers.
|
||||
return f == b
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func anyEqual(t *testing.T, a, b interface{}) bool {
|
||||
switch b.(type) {
|
||||
case nil, bool, string, int64, float64, []interface{}, map[string]interface{}:
|
||||
default:
|
||||
t.Fatalf("unexpected value %v of type %T", b, b)
|
||||
}
|
||||
|
||||
switch ac := a.(type) {
|
||||
case nil, bool, string:
|
||||
return ac == b
|
||||
case int64:
|
||||
if bc, ok := b.(float64); ok {
|
||||
return numberEqual(ac, bc)
|
||||
}
|
||||
return ac == b
|
||||
case float64:
|
||||
if bc, ok := b.(int64); ok {
|
||||
return numberEqual(bc, ac)
|
||||
}
|
||||
return ac == b
|
||||
case []interface{}:
|
||||
bc, ok := b.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if len(ac) != len(bc) {
|
||||
return false
|
||||
}
|
||||
for i, aa := range ac {
|
||||
if !anyEqual(t, aa, bc[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case map[string]interface{}:
|
||||
bc, ok := b.(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if len(ac) != len(bc) {
|
||||
return false
|
||||
}
|
||||
for k, aa := range ac {
|
||||
bb, ok := bc[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !anyEqual(t, aa, bb) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
t.Fatalf("unexpected value %v of type %T", a, a)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user