Skip to content

Commit

Permalink
Add basic admit webhook fuzzing for VMs and VMIs
Browse files Browse the repository at this point in the history
Signed-off-by: Roman Mohr <[email protected]>
  • Loading branch information
rmohr committed Jun 13, 2023
1 parent 157677c commit f8f5a50
Show file tree
Hide file tree
Showing 5 changed files with 333 additions and 2 deletions.
5 changes: 3 additions & 2 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ build --define gotags=selinux
# let our unit tests produce our own junit reports
test --action_env=GO_TEST_WRAP=0

test --test_tag_filters=-cov
coverage --test_tag_filters=-nocov
test --test_tag_filters=-cov,-fuzz
test:fuzz --test_tag_filters=fuzz
coverage --test_tag_filters=-nocov,-fuzz

# Import user settings which may override the defaults
try-import user.bazelrc
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ go-test: go-build

test: bazel-test

fuzz:
hack/dockerized "./hack/fuzz.sh"

functest: build-functests
hack/functests.sh

Expand Down
10 changes: 10 additions & 0 deletions hack/fuzz.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
set -e

source hack/common.sh
source hack/bootstrap.sh
source hack/config.sh

bazel test \
--config=fuzz \
--features race \
--test_output=errors -- //pkg/...
19 changes: 19 additions & 0 deletions pkg/virt-api/webhooks/fuzz/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")

go_test(
name = "go_default_test",
srcs = ["fuzz_test.go"],
tags = ["fuzz"],
deps = [
"//pkg/testutils:go_default_library",
"//pkg/virt-api/webhooks:go_default_library",
"//pkg/virt-api/webhooks/validating-webhook/admitters:go_default_library",
"//pkg/virt-config:go_default_library",
"//staging/src/kubevirt.io/api/core/v1:go_default_library",
"//vendor/github.com/google/gofuzz:go_default_library",
"//vendor/k8s.io/api/admission/v1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
],
)
298 changes: 298 additions & 0 deletions pkg/virt-api/webhooks/fuzz/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,299 @@
package fuzz

import (
"encoding/json"
"fmt"
"reflect"
"testing"
"time"

gofuzz "github.com/google/gofuzz"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
v1 "kubevirt.io/api/core/v1"

"kubevirt.io/kubevirt/pkg/testutils"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks"
"kubevirt.io/kubevirt/pkg/virt-api/webhooks/validating-webhook/admitters"
virtconfig "kubevirt.io/kubevirt/pkg/virt-config"
)

type fuzzOption int

const withSyntaxErrors fuzzOption = 1

type testCase struct {
name string
fuzzFuncs []interface{}
gvk metav1.GroupVersionResource
objType interface{}
admit func(config *virtconfig.ClusterConfig, request *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse
debug bool
focus bool
}

// FuzzAdmitter tests the Validation webhook execution logic with random input: It does schema validation (syntactic check), followed by executing the domain specific validation logic (semantic checks).
func FuzzAdmitter(f *testing.F) {
testCases := []testCase{
{
name: "SyntacticVirtualMachineInstanceFuzzing",
gvk: webhooks.VirtualMachineInstanceGroupVersionResource,
objType: &v1.VirtualMachineInstance{},
admit: func(config *virtconfig.ClusterConfig, request *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
adm := &admitters.VMICreateAdmitter{ClusterConfig: config}
return adm.Admit(request)
},
fuzzFuncs: fuzzFuncs(withSyntaxErrors),
},
{
name: "SemanticVirtualMachineInstanceFuzzing",
gvk: webhooks.VirtualMachineInstanceGroupVersionResource,
objType: &v1.VirtualMachineInstance{},
admit: func(config *virtconfig.ClusterConfig, request *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
adm := &admitters.VMICreateAdmitter{ClusterConfig: config}
return adm.Admit(request)
},
fuzzFuncs: fuzzFuncs(),
},
{
name: "SyntacticVirtualMachineFuzzing",
gvk: webhooks.VirtualMachineGroupVersionResource,
objType: &v1.VirtualMachine{},
admit: func(config *virtconfig.ClusterConfig, request *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
adm := &admitters.VMsAdmitter{
ClusterConfig: config,
InstancetypeMethods: testutils.NewMockInstancetypeMethods(),
}
return adm.Admit(request)
},
fuzzFuncs: fuzzFuncs(withSyntaxErrors),
},
{
name: "SemanticVirtualMachineFuzzing",
gvk: webhooks.VirtualMachineGroupVersionResource,
objType: &v1.VirtualMachine{},
admit: func(config *virtconfig.ClusterConfig, request *admissionv1.AdmissionReview) *admissionv1.AdmissionResponse {
adm := &admitters.VMsAdmitter{
ClusterConfig: config,
InstancetypeMethods: testutils.NewMockInstancetypeMethods(),
}
return adm.Admit(request)
},
fuzzFuncs: fuzzFuncs(),
},
}

for i := 0; i < 500; i++ {
f.Add(int64(i))
}
f.Fuzz(func(t *testing.T, seed int64) {
var focused []testCase
for idx, tc := range testCases {
if tc.focus {
focused = append(focused, testCases[idx])
}
}

if len(focused) == 0 {
focused = testCases
}

timeoutDuration := 500 * time.Millisecond

for _, tc := range focused {
t.Run(tc.name, func(t *testing.T) {
newObj := reflect.New(reflect.TypeOf(tc.objType))
obj := newObj.Interface()

gofuzz.NewWithSeed(seed).NilChance(0.1).NumElements(0, 15).Funcs(
tc.fuzzFuncs...,
).Fuzz(obj)
request := toAdmissionReview(obj, tc.gvk)
config := fuzzKubeVirtConfig(seed)
startTime := time.Now()
response := tc.admit(config, request)
endTime := time.Now()
if startTime.Add(timeoutDuration).Before(endTime) {
fmt.Printf("Execution time %v is more than %v\n", endTime.Sub(startTime), timeoutDuration)
fmt.Println(response.Result.Message)
j, err := json.MarshalIndent(obj, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(j))
t.Fail()
}

if tc.debug && !response.Allowed {
fmt.Println(response.Result.Message)
j, err := json.MarshalIndent(obj, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(j))
}
})
}
})
}

func toAdmissionReview(obj interface{}, gvr metav1.GroupVersionResource) *admissionv1.AdmissionReview {
raw, err := json.Marshal(obj)
if err != nil {
panic(err)
}

return &admissionv1.AdmissionReview{
Request: &admissionv1.AdmissionRequest{
Resource: gvr,
Object: runtime.RawExtension{
Raw: raw,
},
},
}
}

func fuzzKubeVirtConfig(seed int64) *virtconfig.ClusterConfig {
kv := &v1.KubeVirt{}
gofuzz.NewWithSeed(seed).Funcs(
func(dc *v1.DeveloperConfiguration, c gofuzz.Continue) {
c.FuzzNoCustom(dc)
featureGates := []string{
virtconfig.ExpandDisksGate,
virtconfig.CPUManager,
virtconfig.NUMAFeatureGate,
virtconfig.IgnitionGate,
virtconfig.LiveMigrationGate,
virtconfig.SRIOVLiveMigrationGate,
virtconfig.CPUNodeDiscoveryGate,
virtconfig.HypervStrictCheckGate,
virtconfig.SidecarGate,
virtconfig.GPUGate,
virtconfig.HostDevicesGate,
virtconfig.SnapshotGate,
virtconfig.VMExportGate,
virtconfig.HotplugVolumesGate,
virtconfig.HostDiskGate,
virtconfig.VirtIOFSGate,
virtconfig.MacvtapGate,
virtconfig.PasstGate,
virtconfig.DownwardMetricsFeatureGate,
virtconfig.NonRootDeprecated,
virtconfig.NonRoot,
virtconfig.Root,
virtconfig.ClusterProfiler,
virtconfig.WorkloadEncryptionSEV,
virtconfig.DockerSELinuxMCSWorkaround,
virtconfig.PSA,
virtconfig.VSOCKGate,
}

idxs := c.Perm(c.Int() % len(featureGates))
for idx := range idxs {
dc.FeatureGates = append(dc.FeatureGates, featureGates[idx])
}
},
).Fuzz(kv)
config, _, _ := testutils.NewFakeClusterConfigUsingKV(kv)
return config
}

func fuzzFuncs(options ...fuzzOption) []interface{} {
addSyntaxErrors := false
for _, opt := range options {
if opt == withSyntaxErrors {
addSyntaxErrors = true
}
}

enumFuzzers := []interface{}{
func(e *metav1.FieldsV1, c gofuzz.Continue) {},
func(objectmeta *metav1.ObjectMeta, c gofuzz.Continue) {
c.FuzzNoCustom(objectmeta)
objectmeta.DeletionGracePeriodSeconds = nil
objectmeta.Generation = 0
objectmeta.ManagedFields = nil
},
func(obj *corev1.URIScheme, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.URIScheme{corev1.URISchemeHTTP, corev1.URISchemeHTTPS}, c)
},
func(obj *corev1.TaintEffect, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.TaintEffect{corev1.TaintEffectNoExecute, corev1.TaintEffectNoSchedule, corev1.TaintEffectPreferNoSchedule}, c)
},
func(obj *corev1.NodeInclusionPolicy, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.NodeInclusionPolicy{corev1.NodeInclusionPolicyHonor, corev1.NodeInclusionPolicyIgnore}, c)
},
func(obj *corev1.UnsatisfiableConstraintAction, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.UnsatisfiableConstraintAction{corev1.DoNotSchedule, corev1.ScheduleAnyway}, c)
},
func(obj *corev1.PullPolicy, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.PullPolicy{corev1.PullAlways, corev1.PullNever, corev1.PullIfNotPresent}, c)
},
func(obj *corev1.NodeSelectorOperator, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.NodeSelectorOperator{corev1.NodeSelectorOpDoesNotExist, corev1.NodeSelectorOpExists, corev1.NodeSelectorOpGt, corev1.NodeSelectorOpIn, corev1.NodeSelectorOpLt, corev1.NodeSelectorOpNotIn}, c)
},
func(obj *corev1.TolerationOperator, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.TolerationOperator{corev1.TolerationOpExists, corev1.TolerationOpEqual}, c)
},
func(obj *corev1.PodQOSClass, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.PodQOSClass{corev1.PodQOSBestEffort, corev1.PodQOSGuaranteed, corev1.PodQOSBurstable}, c)
},
func(obj *corev1.PersistentVolumeMode, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.PersistentVolumeMode{corev1.PersistentVolumeBlock, corev1.PersistentVolumeFilesystem}, c)
},
func(obj *corev1.DNSPolicy, c gofuzz.Continue) {
pickType(addSyntaxErrors, obj, []corev1.DNSPolicy{corev1.DNSClusterFirst, corev1.DNSClusterFirstWithHostNet, corev1.DNSDefault, corev1.DNSNone}, c)
},
func(obj *corev1.TypedObjectReference, c gofuzz.Continue) {
c.FuzzNoCustom(obj)
str := c.RandString()
obj.APIGroup = &str
},
func(obj *corev1.TypedLocalObjectReference, c gofuzz.Continue) {
c.FuzzNoCustom(obj)
str := c.RandString()
obj.APIGroup = &str
},
}

typeFuzzers := []interface{}{}
if !addSyntaxErrors {
typeFuzzers = []interface{}{
func(obj *int, c gofuzz.Continue) {
*obj = c.Intn(100000)
},
func(obj *uint, c gofuzz.Continue) {
*obj = uint(c.Intn(100000))
},
func(obj *int32, c gofuzz.Continue) {
*obj = int32(c.Intn(100000))
},
func(obj *int64, c gofuzz.Continue) {
*obj = int64(c.Intn(100000))
},
func(obj *uint64, c gofuzz.Continue) {
*obj = uint64(c.Intn(100000))
},
func(obj *uint32, c gofuzz.Continue) {
*obj = uint32(c.Intn(100000))
},
}
}

return append(enumFuzzers, typeFuzzers...)
}

func pickType(withSyntaxError bool, target interface{}, arr interface{}, c gofuzz.Continue) {
arrPtr := reflect.ValueOf(arr)
targetPtr := reflect.ValueOf(target)

if withSyntaxError {
arrPtr = reflect.Append(arrPtr, reflect.ValueOf("fake").Convert(targetPtr.Elem().Type()))
}

idx := c.Int() % arrPtr.Len()

targetPtr.Elem().Set(arrPtr.Index(idx))
}

0 comments on commit f8f5a50

Please sign in to comment.