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 }