Add CBOR fuzz test for unreasonable allocations during decode.

This commit is contained in:
Ben Luddy
2024-05-03 09:01:00 -04:00
parent 44446e1c9c
commit a68f9712f7
2 changed files with 122 additions and 4 deletions

View File

@@ -17,8 +17,90 @@ limitations under the License.
package cbor
import (
// Adds this package and its dependencies to the dependency chain of k8s.io/kubernetes
// without affecting the size of kube binaries. Once fuzz targets are added here, this will
// become a real import.
_ "k8s.io/apimachinery/pkg/runtime/serializer/cbor"
"fmt"
goruntime "runtime"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/cbor"
)
var (
scheme = runtime.NewScheme()
serializers = []cbor.Serializer{
cbor.NewSerializer(scheme, scheme),
cbor.NewSerializer(scheme, scheme, cbor.Strict(true)),
}
)
// FuzzDecodeAllocations is a go-fuzz target that panics on inputs that cause an unreasonably large
// number of bytes to be allocated at decode time.
func FuzzDecodeAllocations(data []byte) (result int) {
const (
MaxInputBytes = 128
MaxAllocatedBytes = 16 * 1024
)
if len(data) > MaxInputBytes {
// Longer inputs can require more allocations by unmarshaling to larger
// objects. Focus on identifying short inputs that allocate an unreasonable number
// of bytes to identify pathological cases.
return -1
}
decode := func(serializer cbor.Serializer, data []byte) int {
var u unstructured.Unstructured
o, gvk, err := serializer.Decode(data, &schema.GroupVersionKind{}, &u)
if err != nil {
if o != nil {
panic("returned non-nil error and non-nil runtime.Object")
}
return 0
}
if o == nil || gvk == nil {
panic("returned nil error and nil runtime.Object or nil schema.GroupVersionKind")
}
return 1
}
for _, serializer := range serializers {
// The first pass pre-warms anything that is lazily initialized. Doing things like
// logging for the first time in a process can account for allocations on the order
// of tens of kB.
decode(serializer, data)
var nBytesAllocated uint64
for trial := 1; trial <= 10; trial++ {
func() {
defer goruntime.GOMAXPROCS(goruntime.GOMAXPROCS(1))
var mem goruntime.MemStats
goruntime.ReadMemStats(&mem)
result |= decode(serializer, data)
nBytesAllocated = mem.TotalAlloc
goruntime.ReadMemStats(&mem)
nBytesAllocated = mem.TotalAlloc - nBytesAllocated
}()
// The exact number of bytes allocated may vary due to allocations in
// concurrently-executing goroutines or implementation details of the
// runtime. Only panic on inputs that consistently exceed the allocation
// threshold to reduce the false positive rate.
if nBytesAllocated <= MaxAllocatedBytes {
break
}
}
if nBytesAllocated > MaxAllocatedBytes {
panic(fmt.Sprintf("%d bytes allocated to decode input of length %d exceeds maximum of %d", nBytesAllocated, len(data), MaxAllocatedBytes))
}
}
return result
}