Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fuzzing: add raft fuzzer from cncf-fuzzing #55

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/fuzzing.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Fuzzing
on: pull_request
permissions: read-all
jobs:
fuzzing:
runs-on: ubuntu-latest
strategy:
fail-fast: false
env:
TARGET_PATH: "."
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0
with:
go-version: "1.19.9"
- run: |
set -euo pipefail

GOARCH=amd64 CPU=4 make fuzz
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
if: failure()
with:
path: "${{env.TARGET_PATH}}/testdata/fuzz/**/*"
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ verify-genproto:
.PHONY: test
test:
PASSES="unit" ./scripts/test.sh $(GO_TEST_FLAGS)

.PHONY: fuzz
test:
./scripts/fuzzing.sh
161 changes: 161 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2022 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package raft

import (
"runtime"
"strings"
"testing"

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

pb "go.etcd.io/raft/v3/raftpb"
)

func getMsgType(i int) pb.MessageType {
allTypes := map[int]pb.MessageType{0: pb.MsgHup,
1: pb.MsgBeat,
2: pb.MsgProp,
3: pb.MsgApp,
4: pb.MsgAppResp,
5: pb.MsgVote,
6: pb.MsgVoteResp,
7: pb.MsgSnap,
8: pb.MsgHeartbeat,
9: pb.MsgHeartbeatResp,
10: pb.MsgUnreachable,
11: pb.MsgSnapStatus,
12: pb.MsgCheckQuorum,
13: pb.MsgTransferLeader,
14: pb.MsgTimeoutNow,
15: pb.MsgReadIndex,
16: pb.MsgReadIndexResp,
17: pb.MsgPreVote,
18: pb.MsgPreVoteResp}
return allTypes[i%len(allTypes)]
}

// All cases in shouldReport represent known errors in etcd
// as these are reported via manually added panics.
func shouldReport(err string) bool {
if strings.Contains(err, "stepped empty MsgProp") {
return false
}
if strings.Contains(err, "Was the raft log corrupted, truncated, or lost?") {
return false
}
if strings.Contains(err, "ConfStates not equivalent after sorting:") {
return false
}
if strings.Contains(err, "term should be set when sending ") {
return false
}
if (strings.Contains(err, "unable to restore config")) && (strings.Contains(err, "removed all voters")) {
return false
}
if strings.Contains(err, "ENCOUNTERED A PANIC OR FATAL") {
return false
}
if strings.Contains(err, "need non-empty snapshot") {
return false
}
if strings.Contains(err, "index, ") && strings.Contains(err, ", is out of range [") {
return false
}

return true
}

func catchPanics() {
if r := recover(); r != nil {
var errMsg string
switch r.(type) {
case string:
errMsg = r.(string)
case runtime.Error:
errMsg = r.(runtime.Error).Error()
}
if shouldReport(errMsg) {
// Getting to this point means that the fuzzer
// did not stop because of a manually added panic.
panic(errMsg)
}
}
}

func FuzzStep(f *testing.F) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried running this in IDE. Test failed and it took ~3mins. Is this expected? Runtime seems too long.

/Users/bk/.gvm/gos/go1.19.2/bin/go test -json go.etcd.io/raft/v3 -fuzz ^\QFuzzStep\E$ -run ^$
=== FUZZ  FuzzStep
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 3s, execs: 49356 (16446/sec), new interesting: 27 (total: 27)
fuzz: elapsed: 6s, execs: 153467 (34706/sec), new interesting: 32 (total: 32)
fuzz: elapsed: 9s, execs: 194647 (13726/sec), new interesting: 34 (total: 34)
fuzz: elapsed: 12s, execs: 214509 (6623/sec), new interesting: 35 (total: 35)
fuzz: elapsed: 15s, execs: 339183 (41546/sec), new interesting: 37 (total: 37)
fuzz: elapsed: 18s, execs: 404951 (21930/sec), new interesting: 37 (total: 37)
fuzz: elapsed: 21s, execs: 560220 (51739/sec), new interesting: 38 (total: 38)
fuzz: elapsed: 24s, execs: 653020 (30933/sec), new interesting: 42 (total: 42)
fuzz: elapsed: 27s, execs: 706390 (17791/sec), new interesting: 42 (total: 42)
fuzz: elapsed: 30s, execs: 772088 (21898/sec), new interesting: 42 (total: 42)
fuzz: elapsed: 33s, execs: 772088 (0/sec), new interesting: 42 (total: 42)
fuzz: elapsed: 36s, execs: 772088 (0/sec), new interesting: 42 (total: 42)
fuzz: elapsed: 39s, execs: 869852 (32588/sec), new interesting: 43 (total: 43)
fuzz: elapsed: 42s, execs: 869852 (0/sec), new interesting: 43 (total: 43)
fuzz: elapsed: 45s, execs: 869852 (0/sec), new interesting: 43 (total: 43)
fuzz: elapsed: 48s, execs: 1036345 (55479/sec), new interesting: 44 (total: 44)
fuzz: elapsed: 51s, execs: 1036345 (0/sec), new interesting: 44 (total: 44)
fuzz: elapsed: 54s, execs: 1036345 (0/sec), new interesting: 44 (total: 44)
fuzz: elapsed: 57s, execs: 1036345 (0/sec), new interesting: 44 (total: 44)
fuzz: elapsed: 1m0s, execs: 1036345 (0/sec), new interesting: 44 (total: 44)
fuzz: elapsed: 1m3s, execs: 1048211 (3955/sec), new interesting: 52 (total: 52)
fuzz: elapsed: 1m6s, execs: 1056240 (2677/sec), new interesting: 61 (total: 61)
fuzz: elapsed: 1m9s, execs: 1069088 (4283/sec), new interesting: 64 (total: 64)
fuzz: elapsed: 1m12s, execs: 1074428 (1780/sec), new interesting: 67 (total: 67)
fuzz: elapsed: 1m15s, execs: 1074428 (0/sec), new interesting: 67 (total: 67)
fuzz: elapsed: 1m18s, execs: 1103712 (9764/sec), new interesting: 72 (total: 72)
fuzz: elapsed: 1m21s, execs: 1111263 (2517/sec), new interesting: 72 (total: 72)
fuzz: elapsed: 1m24s, execs: 1122321 (3686/sec), new interesting: 74 (total: 74)
fuzz: elapsed: 1m27s, execs: 1223408 (33686/sec), new interesting: 77 (total: 77)
fuzz: elapsed: 1m30s, execs: 1240341 (5646/sec), new interesting: 78 (total: 78)
fuzz: elapsed: 1m33s, execs: 1250383 (3347/sec), new interesting: 78 (total: 78)
fuzz: elapsed: 1m36s, execs: 1257633 (2417/sec), new interesting: 78 (total: 78)
fuzz: elapsed: 1m39s, execs: 1331387 (24577/sec), new interesting: 80 (total: 80)
fuzz: elapsed: 1m42s, execs: 1469927 (46189/sec), new interesting: 80 (total: 80)
fuzz: elapsed: 1m45s, execs: 1503247 (11107/sec), new interesting: 81 (total: 81)
fuzz: elapsed: 1m48s, execs: 1545766 (14170/sec), new interesting: 81 (total: 81)
fuzz: elapsed: 1m51s, execs: 1550464 (1566/sec), new interesting: 81 (total: 81)
fuzz: elapsed: 1m54s, execs: 1558434 (2657/sec), new interesting: 83 (total: 83)
fuzz: elapsed: 1m57s, execs: 1558434 (0/sec), new interesting: 83 (total: 83)
fuzz: elapsed: 2m0s, execs: 1558434 (0/sec), new interesting: 83 (total: 83)
fuzz: elapsed: 2m3s, execs: 1558434 (0/sec), new interesting: 83 (total: 83)
fuzz: elapsed: 2m6s, execs: 1566754 (2773/sec), new interesting: 83 (total: 83)
fuzz: elapsed: 2m9s, execs: 1583562 (5603/sec), new interesting: 85 (total: 85)
fuzz: elapsed: 2m12s, execs: 1599171 (5204/sec), new interesting: 86 (total: 86)
fuzz: elapsed: 2m15s, execs: 1615828 (5553/sec), new interesting: 87 (total: 87)
fuzz: elapsed: 2m18s, execs: 1628572 (4248/sec), new interesting: 87 (total: 87)
fuzz: elapsed: 2m21s, execs: 1639432 (3619/sec), new interesting: 87 (total: 87)
fuzz: elapsed: 2m24s, execs: 1647428 (2666/sec), new interesting: 87 (total: 87)
fuzz: elapsed: 2m27s, execs: 1652544 (1706/sec), new interesting: 88 (total: 88)
fuzz: elapsed: 2m30s, execs: 1667946 (5134/sec), new interesting: 90 (total: 90)
fuzz: elapsed: 2m33s, execs: 1683152 (5069/sec), new interesting: 90 (total: 90)
fuzz: elapsed: 2m36s, execs: 1695981 (4276/sec), new interesting: 90 (total: 90)
fuzz: elapsed: 2m39s, execs: 1708469 (4162/sec), new interesting: 90 (total: 90)
fuzz: elapsed: 2m42s, execs: 1726815 (6117/sec), new interesting: 91 (total: 91)
fuzz: elapsed: 2m45s, execs: 1747196 (6792/sec), new interesting: 91 (total: 91)
fuzz: minimizing 71-byte failing input file
fuzz: elapsed: 2m47s, minimizing
--- FAIL: FuzzStep (167.36s)
    --- FAIL: FuzzStep (0.00s)
        testing.go:1356: panic: need non-empty snapshot
            goroutine 43783 [running]:
            runtime/debug.Stack()
            	/Users/bk/.gvm/gos/go1.19.2/src/runtime/debug/stack.go:24 +0xdb
            testing.tRunner.func1()
            	/Users/bk/.gvm/gos/go1.19.2/src/testing/testing.go:1356 +0x1f2
            panic({0x1930fe0, 0xc00a217580})
            	/Users/bk/.gvm/gos/go1.19.2/src/runtime/panic.go:884 +0x212
            go.etcd.io/raft/v3.catchPanics()
            	/Users/bk/github/raft/fuzz_test.go:101 +0x2bd
            panic({0x1930fe0, 0x1aaa838})
            	/Users/bk/.gvm/gos/go1.19.2/src/runtime/panic.go:884 +0x212
            go.etcd.io/raft/v3.(*raft).maybeSendAppend(0xc00a2ce300, 0x2, 0x1)
            	/Users/bk/github/raft/raft.go:608 +0xf51
            go.etcd.io/raft/v3.(*raft).sendAppend(...)
            	/Users/bk/github/raft/raft.go:560
            go.etcd.io/raft/v3.(*raft).bcastAppend.func1(0x2, 0xc00a2d4630?)
            	/Users/bk/github/raft/raft.go:662 +0x10b
            go.etcd.io/raft/v3/tracker.(*ProgressTracker).Visit(0xc00a2ce348, 0xc006998850)
            	/Users/bk/github/raft/tracker/tracker.go:211 +0x2ea
            go.etcd.io/raft/v3.(*raft).bcastAppend(0xc00a2ce300)
            	/Users/bk/github/raft/raft.go:658 +0x79
            go.etcd.io/raft/v3.stepLeader(0xc00a2ce300, {0x4, 0x1, 0x1, 0x1, 0x0, 0x1, {0x0, 0x0, 0x0}, ...})
            	/Users/bk/github/raft/raft.go:1432 +0xfaa
            go.etcd.io/raft/v3.(*raft).Step(0xc00a2ce300, {0x4, 0x1, 0x1, 0x1, 0x0, 0x1, {0x0, 0x0, 0x0}, ...})
            	/Users/bk/github/raft/raft.go:1156 +0x26d5
            go.etcd.io/raft/v3.(*raft).stepOrSend(0xc00a2ce300, {0xc00a2d2280, 0x1, 0x0?})
            	/Users/bk/github/raft/raft_test.go:85 +0x2e5
            go.etcd.io/raft/v3.(*raft).advanceMessagesAfterAppend(0xc00a2ce300)
            	/Users/bk/github/raft/raft_test.go:72 +0x6b
            go.etcd.io/raft/v3.(*raft).readMessages(...)
            	/Users/bk/github/raft/raft_test.go:60
            go.etcd.io/raft/v3.FuzzStep.func1(0x0?, {0xc00a0fa168, 0x14, 0x18})
            	/Users/bk/github/raft/fuzz_test.go:146 +0xab3
            reflect.Value.call({0x1937200?, 0x1a0c5e8?, 0x13?}, {0x19ce3e8, 0x4}, {0xc00a2d43f0, 0x2, 0x2?})
            	/Users/bk/.gvm/gos/go1.19.2/src/reflect/value.go:584 +0x8c5
            reflect.Value.Call({0x1937200?, 0x1a0c5e8?, 0x51b?}, {0xc00a2d43f0?, 0x1d04f78?, 0x1dc6700?})
            	/Users/bk/.gvm/gos/go1.19.2/src/reflect/value.go:368 +0xbc
            testing.(*F).Fuzz.func1.1(0x1176e7f?)
            	/Users/bk/.gvm/gos/go1.19.2/src/testing/fuzz.go:337 +0x231
            testing.tRunner(0xc00a2a7d40, 0xc00a2c5560)
            	/Users/bk/.gvm/gos/go1.19.2/src/testing/testing.go:1446 +0x10b
            created by testing.(*F).Fuzz.func1
            	/Users/bk/.gvm/gos/go1.19.2/src/testing/fuzz.go:324 +0x5b9
            
    
    Failing input written to testdata/fuzz/FuzzStep/7950126c9b19fb7cd77014af3e74a042cc85896da08e7de8423f8b45893a040a
    To re-run:
    go test -run=FuzzStep/7950126c9b19fb7cd77014af3e74a042cc85896da08e7de8423f8b45893a040a


FAIL
exit status 1
FAIL	go.etcd.io/raft/v3	167.784s

Process finished with the exit code 1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That stacktrace looks normal. The fuzzer runs into this panic:

raft/raft.go

Line 608 in 0c22de0

panic("need non-empty snapshot")
. I can follow-up with some fine tunings for this fuzzer, but at the moment I don't know what the scope of that is and would prefer to do it in a separate PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure that this code works at least in CI. Please provide the tuning required for fuzzer to run on workflows.

f.Fuzz(func(t *testing.T, data []byte) {
defer SetLogger(getLogger())
SetLogger(discardLogger)

defer catchPanics()
f := fuzz.NewConsumer(data)
msg := pb.Message{}
err := f.GenerateStruct(&msg)
if err != nil {
return
}

msgTypeIndex, err := f.GetInt()
if err != nil {
return
}
msg.Type = getMsgType(msgTypeIndex)

cfg := newTestConfig(1, 5, 1, newTestMemoryStorage(withPeers(1, 2)))
cfg.Logger = &ZapRaftLogger{}
r := newRaft(cfg)
r.becomeCandidate()
r.becomeLeader()
r.prs.Progress[2].BecomeReplicate()
_ = r.Step(msg)
_ = r.readMessages()
})
}

type ZapRaftLogger struct {
}

func (zl *ZapRaftLogger) Debug(_ ...interface{}) {}

func (zl *ZapRaftLogger) Debugf(_ string, _ ...interface{}) {}

func (zl *ZapRaftLogger) Error(_ ...interface{}) {}

func (zl *ZapRaftLogger) Errorf(_ string, _ ...interface{}) {}

func (zl *ZapRaftLogger) Info(_ ...interface{}) {}

func (zl *ZapRaftLogger) Infof(_ string, _ ...interface{}) {}

func (zl *ZapRaftLogger) Warning(_ ...interface{}) {}

func (zl *ZapRaftLogger) Warningf(_ string, _ ...interface{}) {}

func (zl *ZapRaftLogger) Fatal(_ ...interface{}) {
panic("ENCOUNTERED A PANIC OR FATAL")
}

func (zl *ZapRaftLogger) Fatalf(_ string, _ ...interface{}) {
panic("ENCOUNTERED A PANIC OR FATAL")
}

func (zl *ZapRaftLogger) Panic(_ ...interface{}) {
panic("ENCOUNTERED A PANIC OR FATAL")
}

func (zl *ZapRaftLogger) Panicf(_ string, _ ...interface{}) {
panic("ENCOUNTERED A PANIC OR FATAL")
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module go.etcd.io/raft/v3
go 1.19

require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/cockroachdb/datadriven v1.0.2
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
19 changes: 19 additions & 0 deletions scripts/fuzzing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

set -euo pipefail

source ./scripts/test_lib.sh

GO_CMD="go"
fuzz_time=${FUZZ_TIME:-"300s"}
target_path=${TARGET_PATH:-"."}
TARGETS="FuzzStep"


for target in ${TARGETS}; do
log_callout -e "\\nExecuting fuzzing with target ${target} in $target_path with a timeout of $fuzz_time\\n"
run pushd "${target_path}"
$GO_CMD test -fuzz "${target}" -fuzztime "${fuzz_time}"
run popd
log_success -e "\\COMPLETED: fuzzing with target $target in $target_path \\n"
done
Loading