istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tests/fuzz/crd_roundtrip_fuzzer.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fuzz
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  	"sync"
    24  
    25  	fuzz "github.com/AdaLogics/go-fuzz-headers"
    26  	"github.com/davecgh/go-spew/spew"
    27  	legacyproto "github.com/golang/protobuf/proto" // nolint: staticcheck
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	"google.golang.org/protobuf/testing/protocmp"
    31  	apimeta "k8s.io/apimachinery/pkg/api/meta"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	"k8s.io/apimachinery/pkg/runtime/serializer/json"
    35  
    36  	clientextensions "istio.io/client-go/pkg/apis/extensions/v1alpha1"
    37  	clientnetworkingalpha "istio.io/client-go/pkg/apis/networking/v1alpha3"
    38  	clientnetworkingbeta "istio.io/client-go/pkg/apis/networking/v1beta1"
    39  	clientsecurity "istio.io/client-go/pkg/apis/security/v1beta1"
    40  	clienttelemetry "istio.io/client-go/pkg/apis/telemetry/v1alpha1"
    41  	"istio.io/istio/pkg/config/schema/collections"
    42  )
    43  
    44  var (
    45  	scheme  = runtime.NewScheme()
    46  	initter sync.Once
    47  )
    48  
    49  func initRoundTrip() {
    50  	clientnetworkingalpha.AddToScheme(scheme)
    51  	clientnetworkingbeta.AddToScheme(scheme)
    52  	clientsecurity.AddToScheme(scheme)
    53  	clientextensions.AddToScheme(scheme)
    54  	clienttelemetry.AddToScheme(scheme)
    55  }
    56  
    57  // FuzzRoundtrip tests whether the pilot CRDs
    58  // can be encoded and decoded.
    59  func FuzzCRDRoundtrip(data []byte) int {
    60  	initter.Do(initRoundTrip)
    61  	if len(data) < 100 {
    62  		return 0
    63  	}
    64  
    65  	// select a target:
    66  	r := collections.Pilot.All()[int(data[0])%len(collections.Pilot.All())]
    67  	gvk := r.GroupVersionKind()
    68  	kgvk := schema.GroupVersionKind{
    69  		Group:   gvk.Group,
    70  		Version: gvk.Version,
    71  		Kind:    gvk.Kind,
    72  	}
    73  	object, err := scheme.New(kgvk)
    74  	if err != nil {
    75  		return 0
    76  	}
    77  
    78  	typeAcc, err := apimeta.TypeAccessor(object)
    79  	if err != nil {
    80  		panic(fmt.Sprintf("%q is not a TypeMeta and cannot be tested - add it to nonRoundTrippableInternalTypes: %v\n", kgvk, err))
    81  	}
    82  	f := fuzz.NewConsumer(data[1:])
    83  	err = f.GenerateStruct(object)
    84  	if err != nil {
    85  		return 0
    86  	}
    87  	err = checkForNilValues(object)
    88  	if err != nil {
    89  		return 0
    90  	}
    91  	typeAcc.SetKind(kgvk.Kind)
    92  	typeAcc.SetAPIVersion(kgvk.GroupVersion().String())
    93  
    94  	roundTrip(json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false), object)
    95  	return 1
    96  }
    97  
    98  // roundTrip performs the roundtrip of the object.
    99  func roundTrip(codec runtime.Codec, object runtime.Object) {
   100  	printer := spew.ConfigState{DisableMethods: true}
   101  
   102  	// deep copy the original object
   103  	object = object.DeepCopyObject()
   104  	name := reflect.TypeOf(object).Elem().Name()
   105  
   106  	// encode (serialize) the deep copy using the provided codec
   107  	data, err := runtime.Encode(codec, object)
   108  	if err != nil {
   109  		return
   110  	}
   111  
   112  	// encode (serialize) a second time to verify that it was not varying
   113  	secondData, err := runtime.Encode(codec, object)
   114  	if err != nil {
   115  		panic("This should not fail since we are encoding for the second time")
   116  	}
   117  
   118  	// serialization to the wire must be stable to ensure that we don't write twice to the DB
   119  	// when the object hasn't changed.
   120  	if !bytes.Equal(data, secondData) {
   121  		panic(fmt.Sprintf("%v: serialization is not stable: %s\n", name, printer.Sprintf("%#v", object)))
   122  	}
   123  
   124  	// decode (deserialize) the encoded data back into an object
   125  	obj2, err := runtime.Decode(codec, data)
   126  	if err != nil {
   127  		panic(fmt.Sprintf("%v: %v\nCodec: %#v\nData: %s\nSource: %#v\n", name, err, codec, dataAsString(data), printer.Sprintf("%#v", object)))
   128  	}
   129  
   130  	// decode the encoded data into a new object (instead of letting the codec
   131  	// create a new object)
   132  	obj3 := reflect.New(reflect.TypeOf(object).Elem()).Interface().(runtime.Object)
   133  	if err := runtime.DecodeInto(codec, data, obj3); err != nil {
   134  		panic(fmt.Sprintf("%v: %v\n", name, err))
   135  	}
   136  
   137  	// ensure that the object produced from decoding the encoded data is equal
   138  	// to the original object
   139  	if diff := cmp.Diff(obj2, obj3, protocmp.Transform(), cmpopts.EquateNaNs()); diff != "" {
   140  		panic("These should not be different: " + diff)
   141  	}
   142  }
   143  
   144  // dataAsString is a simple helper.
   145  func dataAsString(data []byte) string {
   146  	dataString := string(data)
   147  	if !strings.HasPrefix(dataString, "{") {
   148  		dataString = "\n" + hex.Dump(data)
   149  		legacyproto.NewBuffer(make([]byte, 0, 1024)).DebugPrint("decoded object", data)
   150  	}
   151  	return dataString
   152  }
   153  
   154  // checkForNilValues is a helper to check for nil
   155  // values in the runtime objects.
   156  // This part only converts the interface to a reflect.Value.
   157  func checkForNilValues(targetStruct any) error {
   158  	v := reflect.ValueOf(targetStruct)
   159  	e := v.Elem()
   160  	err := checkForNil(e)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	return nil
   165  }
   166  
   167  // Checks for nil values in a reflect.Value.
   168  func checkForNil(e reflect.Value) error {
   169  	switch e.Kind() {
   170  	case reflect.Struct:
   171  		for i := 0; i < e.NumField(); i++ {
   172  			err := checkForNil(e.Field(i))
   173  			if err != nil {
   174  				return err
   175  			}
   176  		}
   177  	case reflect.Array, reflect.Slice:
   178  		for i := 0; i < e.Len(); i++ {
   179  			err := checkForNil(e.Index(i))
   180  			if err != nil {
   181  				return err
   182  			}
   183  		}
   184  	case reflect.Map:
   185  		if e.IsNil() {
   186  			return fmt.Errorf("field is nil")
   187  		}
   188  		for _, k := range e.MapKeys() {
   189  			if e.IsNil() {
   190  				return fmt.Errorf("field is nil")
   191  			}
   192  			err := checkForNil(e.MapIndex(k))
   193  			if err != nil {
   194  				return err
   195  			}
   196  		}
   197  	case reflect.Ptr:
   198  		if e.IsNil() {
   199  			return fmt.Errorf("field is nil")
   200  		}
   201  
   202  		err := checkForNil(e.Elem())
   203  		if err != nil {
   204  			return err
   205  		}
   206  	default:
   207  		return nil
   208  	}
   209  	return nil
   210  }