Skip to content

Commit

Permalink
move more fuzzers over to native fuzzers
Browse files Browse the repository at this point in the history
  • Loading branch information
howardjohn committed Jul 22, 2022
1 parent 7306d75 commit 062af0b
Show file tree
Hide file tree
Showing 18 changed files with 387 additions and 739 deletions.
39 changes: 39 additions & 0 deletions pilot/pkg/config/kube/gateway/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -1770,3 +1770,42 @@ func toNamespaceSet(name string, labels map[string]string) klabels.Set {
ret[NamespaceNameLabel] = name
return ret
}

func (r KubernetesResources) FuzzValidate() bool {
for _, gwc := range r.GatewayClass {
if gwc.Spec == nil {
return false
}
}
for _, rp := range r.ReferencePolicy {
if rp.Spec == nil {
return false
}
}
for _, rp := range r.ReferenceGrant {
if rp.Spec == nil {
return false
}
}
for _, hr := range r.HTTPRoute {
if hr.Spec == nil {
return false
}
}
for _, tr := range r.TLSRoute {
if tr.Spec == nil {
return false
}
}
for _, g := range r.Gateway {
if g.Spec == nil {
return false
}
}
for _, tr := range r.TCPRoute {
if tr.Spec == nil {
return false
}
}
return true
}
16 changes: 16 additions & 0 deletions pilot/pkg/config/kube/gateway/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gateway

import (
"testing"

"istio.io/istio/pkg/fuzz"
)

func FuzzConvertResources(f *testing.F) {
fuzz.BaseCases(f)
f.Fuzz(func(t *testing.T, data []byte) {
fg := fuzz.New(t, data)
r := fuzz.Struct[KubernetesResources](fg)
convertResources(r)
})
}
7 changes: 7 additions & 0 deletions pilot/pkg/model/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,13 @@ func (node *Proxy) IsProxylessGrpc() bool {
return node.Metadata != nil && node.Metadata.Generator == "grpc"
}

func (node *Proxy) FuzzValidate() bool {
if node.Metadata == nil {
return false
}
return len(node.IPAddresses) != 0
}

type GatewayController interface {
ConfigStoreController
// Recompute updates the internal state of the gateway controller for a given input. This should be
Expand Down
31 changes: 31 additions & 0 deletions pilot/pkg/networking/core/v1alpha3/envoyfilter/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package envoyfilter

import (
"testing"

cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"

meshconfig "istio.io/api/mesh/v1alpha1"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pilot/pkg/serviceregistry/memory"
"istio.io/istio/pkg/config/host"
"istio.io/istio/pkg/fuzz"
)

func FuzzApplyClusterMerge(f *testing.F) {
f.Fuzz(func(t *testing.T, patchCount int, hostname string, data []byte) {
fg := fuzz.New(t, data)
patches := fuzz.Slice[*networking.EnvoyFilter_EnvoyConfigObjectPatch](fg, patchCount%30)
proxy := fuzz.Struct[*model.Proxy](fg)
mesh := fuzz.Struct[*meshconfig.MeshConfig](fg)
c := fuzz.Struct[*cluster.Cluster](fg)

serviceDiscovery := memory.NewServiceDiscovery()
env := newTestEnvironment(serviceDiscovery, mesh, buildEnvoyFilterConfigStore(patches))
push := model.NewPushContext()
push.InitContext(env, nil, nil)
efw := push.EnvoyFilters(proxy)
ApplyClusterMerge(networking.EnvoyFilter_GATEWAY, efw, c, []host.Name{host.Name(hostname)})
})
}
14 changes: 14 additions & 0 deletions pilot/pkg/networking/core/v1alpha3/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ type TestOptions struct {
ClusterID cluster2.ID
}

func (to TestOptions) FuzzValidate() bool {
for _, csc := range to.ConfigStoreCaches {
if csc == nil {
return false
}
}
for _, sr := range to.ServiceRegistries {
if sr == nil {
return false
}
}
return true
}

type ConfigGenTest struct {
t test.Failer
pushContextLock *sync.RWMutex
Expand Down
48 changes: 48 additions & 0 deletions pilot/pkg/networking/core/v1alpha3/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package v1alpha3

import (
"testing"

route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"

"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/fuzz"
)

func FuzzBuildGatewayListeners(f *testing.F) {
f.Fuzz(func(t *testing.T, patchCount int, hostname string, data []byte) {
fg := fuzz.New(t, data)
proxy := fuzz.Struct[*model.Proxy](fg)
to := fuzz.Struct[TestOptions](fg)
lb := fuzz.Struct[*ListenerBuilder](fg)
cg := NewConfigGenTest(t, to)
lb.node = cg.SetupProxy(proxy)
lb.push = cg.PushContext()
cg.ConfigGen.buildGatewayListeners(lb)
})
}

func FuzzBuildSidecarOutboundHTTPRouteConfig(f *testing.F) {
f.Fuzz(func(t *testing.T, patchCount int, hostname string, data []byte) {
fg := fuzz.New(t, data)
proxy := fuzz.Struct[*model.Proxy](fg)
to := fuzz.Struct[TestOptions](fg)
cg := NewConfigGenTest(t, to)
req := fuzz.Struct[*model.PushRequest](fg)
req.Push = cg.PushContext()
vHostCache := make(map[int][]*route.VirtualHost)
cg.ConfigGen.buildSidecarOutboundHTTPRouteConfig(cg.SetupProxy(proxy), req, "80", vHostCache, nil, nil)
})
}

func FuzzBuildSidecarOutboundListeners(f *testing.F) {
f.Fuzz(func(t *testing.T, patchCount int, hostname string, data []byte) {
fg := fuzz.New(t, data)
proxy := fuzz.Struct[*model.Proxy](fg)
to := fuzz.Struct[TestOptions](fg)
cg := NewConfigGenTest(t, to)
req := fuzz.Struct[*model.PushRequest](fg)
req.Push = cg.PushContext()
NewListenerBuilder(proxy, cg.env.PushContext).buildSidecarOutboundListeners(cg.SetupProxy(proxy), cg.env.PushContext)
})
}
45 changes: 45 additions & 0 deletions pilot/pkg/security/authz/builder/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package builder

import (
"testing"

"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pilot/pkg/security/trustdomain"
"istio.io/istio/pkg/fuzz"
)

func FuzzBuildHTTP(f *testing.F) {
fuzz.BaseCases(f)
f.Fuzz(func(t *testing.T, data []byte) {
fg := fuzz.New(t, data)
bundle := fuzz.Struct[trustdomain.Bundle](fg)
push := fuzz.Struct[*model.PushContext](fg, validatePush)
node := fuzz.Struct[*model.Proxy](fg)
policies := push.AuthzPolicies.ListAuthorizationPolicies(node.ConfigNamespace, node.Metadata.Labels)
option := fuzz.Struct[Option](fg)
New(bundle, push, policies, option).BuildHTTP()
})
}

func FuzzBuildTCP(f *testing.F) {
fuzz.BaseCases(f)
f.Fuzz(func(t *testing.T, data []byte) {
fg := fuzz.New(t, data)
bundle := fuzz.Struct[trustdomain.Bundle](fg)
push := fuzz.Struct[*model.PushContext](fg, validatePush)
node := fuzz.Struct[*model.Proxy](fg)
policies := push.AuthzPolicies.ListAuthorizationPolicies(node.ConfigNamespace, node.Metadata.Labels)
option := fuzz.Struct[Option](fg)
New(bundle, push, policies, option).BuildTCP()
})
}

func validatePush(in *model.PushContext) bool {
if in == nil {
return false
}
if in.AuthzPolicies == nil {
return false
}
return true
}
30 changes: 30 additions & 0 deletions pilot/pkg/serviceregistry/kube/controller/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package controller

import (
"testing"

corev1 "k8s.io/api/core/v1"

"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pkg/fuzz"
"istio.io/istio/pkg/network"
)

func FuzzKubeController(f *testing.F) {
fuzz.BaseCases(f)
f.Fuzz(func(t *testing.T, data []byte) {
fg := fuzz.New(t, data)
networkID := network.ID("fakeNetwork")
fco := fuzz.Struct[FakeControllerOptions](fg)
fco.SkipRun = true
controller, _ := NewFakeControllerWithOptions(t, fco)
controller.network = networkID

p := fuzz.Struct[*corev1.Pod](fg)
controller.pods.onEvent(p, model.EventAdd)
s := fuzz.Struct[*corev1.Service](fg)
controller.onServiceEvent(s, model.EventAdd)
e := fuzz.Struct[*corev1.Endpoints](fg)
controller.endpoints.onEvent(e, model.EventAdd)
})
}
48 changes: 48 additions & 0 deletions pkg/fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Fuzzing Istio Code

The Istio (Go) code base is fuzzed using native Go fuzzing.
For general docs on how to fuzz in Go, see [Getting started with fuzzing](https://go.dev/doc/tutorial/fuzz).

## Writing a test

Generally, writing a fuzz test for Istio is the same as any other Go program.
However, because most of our fuzzing is based on complex structs rather than the primitives Go supports natively,
the `pkg/fuzz` package contains a number of helpers to fuzz.

Here is an example:

```go
// Define a new fuzzer. Must have Fuzz prefix
func FuzzBuildHTTP(f *testing.F) {
fuzz.BaseCases(f) // Insert basic cases so a few trivial cases run in presubmit
f.Fuzz(func(t *testing.T, data []byte) {
fg := fuzz.New(t, data)
// Setup a few structs for testing
bundle := fuzz.Struct[trustdomain.Bundle](fg)
// This one has a custom validator
push := fuzz.Struct[*model.PushContext](fg, validatePush)
// *model.Proxy, and other types, implement the fuzz.Validator interface and already validate some basics.
node := fuzz.Struct[*model.Proxy](fg)
option := fuzz.Struct[Option](fg)

// Run our actual test code. In this case, we are just checking nothing crashes.
// In other tests, explicit assertions may be helpful.
policies := push.AuthzPolicies.ListAuthorizationPolicies(node.ConfigNamespace, node.Metadata.Labels)
New(bundle, push, policies, option).BuildHTTP()
})
}
```

## Running tests

Fuzz tests can be run using standard Go tooling:

```shell
go test ./path/to/pkg -v -run=^$ -fuzz=Fuzz
```

## CI testing

Go fuzzers are run as part of standard unit tests against known test cases (from `f.Add` (which `fuzz.BaseCases` calls), or `testdata`).
For continuous fuzzing, [`OSS-Fuzz`](https://github.com/google/oss-fuzz) continually builds and runs the fuzzers and reports any failures.
These results are private to the Istio Product Security WG until disclosed.
80 changes: 80 additions & 0 deletions pkg/fuzz/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package fuzz

import (
"bytes"
"fmt"
"testing"

fuzzheaders "github.com/AdaLogics/go-fuzz-headers"
)

// Helper is a helper struct for fuzzing
type Helper struct {
cf *fuzzheaders.ConsumeFuzzer
t *testing.T
}

type Validator interface {
// FuzzValidate returns true if the current struct is valid for fuzzing.
FuzzValidate() bool
}

// New creates a new fuzz.Helper, capable of generating more complex types
func New(t *testing.T, data []byte) Helper {
return Helper{cf: fuzzheaders.NewConsumer(data), t: t}
}

// Struct generates a Struct. Validation patterns can be passed in - if any return false, the fuzz case is skipped.
// Additionally, if the T implements Validator, it will implicitly be used.
func Struct[T any](h Helper, validators ...func(T) bool) T {
d := new(T)
if err := h.cf.GenerateStruct(d); err != nil {
h.t.Skip(err.Error())
}
r := *d
validate(h, validators, r)
return r
}

// Slice generates a slice of Structs
func Slice[T any](h Helper, count int, validators ...func(T) bool) []T {
if count < 0 {
// Make it easier to just pass fuzzer generated counts, typically with %max applied
count *= -1
}
res := make([]T, 0, count)
for i := 0; i < count; i++ {
d := new(T)
if err := h.cf.GenerateStruct(d); err != nil {
h.t.Skip(err.Error())
}
r := *d
validate(h, validators, r)
res = append(res, r)
}
return res
}

func validate[T any](h Helper, validators []func(T) bool, r T) {
if fz, ok := any(r).(Validator); ok {
if !fz.FuzzValidate() {
h.t.Skip(fmt.Sprintf("struct didn't pass validator"))
}
}
for i, v := range validators {
if !v(r) {
h.t.Skipf(fmt.Sprintf("struct didn't pass validator %d", i))
}
}
}

// BaseCases inserts a few trivial test cases to do a very brief sanity check of a test that relies on []byte inputs
func BaseCases(f *testing.F) {
for _, c := range [][]byte{
{},
[]byte("."),
bytes.Repeat([]byte("."), 1000),
} {
f.Add(c)
}
}
Loading

0 comments on commit 062af0b

Please sign in to comment.