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

go/storage/mkvs: Add MKVS fuzzing #2698

Merged
merged 4 commits into from
Feb 20, 2020
Merged
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
1 change: 1 addition & 0 deletions .changelog/2698.bugfix.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/storage/mkvs: Fix removal crash when key is too short
1 change: 1 addition & 0 deletions .changelog/2698.bugfix.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/storage/mkvs: Fix bug in key.Merge operation with extra bytes
1 change: 1 addition & 0 deletions .changelog/2698.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/storage/mkvs: Add MKVS fuzzing
8 changes: 7 additions & 1 deletion go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ integrationrunner:
@$(GO) test $(GOFLAGS) -c -covermode=atomic -coverpkg=./... -o oasis-node/$@/[email protected] ./oasis-node/$@

# Fuzzing binaries.
build-fuzz: consensus/tendermint/fuzz/fuzz-fuzz.zip storage/fuzz/fuzz-fuzz.zip
fuzz-targets := consensus/tendermint/fuzz/fuzz-fuzz.zip \
storage/fuzz/fuzz-fuzz.zip \
storage/mkvs/urkel/fuzz/fuzz-fuzz.zip

build-fuzz: $(fuzz-targets)
%/fuzz-fuzz.zip: .FORCE
@echo "Building $@"
@cd "$$(dirname "$@")"; go-fuzz-build
Expand All @@ -94,6 +98,8 @@ fuzz-storage: storage/fuzz/ oasis-node/oasis-node
@chmod 0700 /tmp/oasis-node-fuzz-storage/identity
@oasis-node/oasis-node identity init --datadir /tmp/oasis-node-fuzz-storage/identity
$(canned-fuzz-run)
fuzz-storage-mkvs: storage/mkvs/urkel/fuzz/
$(canned-fuzz-run)

# Clean.
clean:
Expand Down
5 changes: 2 additions & 3 deletions go/storage/client/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"time"

"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/oasislabs/oasis-core/go/common/crypto/hash"
"github.com/oasislabs/oasis-core/go/common/identity"
Expand Down Expand Up @@ -114,14 +112,15 @@ recvLoop:

// Try getting path.
// TimeOut is expected, as test nodes do not actually start storage worker.
ctx, cancel = context.WithTimeout(ctx, 1*time.Second)
defer cancel()
r, err = client.SyncGet(ctx, &api.GetRequest{
Tree: api.TreeID{
Root: root,
Position: root.Hash,
},
})
require.Error(err, "storage client should error")
require.Equal(codes.Unavailable, status.Code(err), "storage client should timeout")
require.Nil(r, "result should be nil")

rt.Cleanup(t, consensus.Registry(), consensus)
Expand Down
5 changes: 5 additions & 0 deletions go/storage/mkvs/urkel/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
corpus/
crashers/
suppressions/
*.zip
gencorpus/gencorpus
203 changes: 203 additions & 0 deletions go/storage/mkvs/urkel/fuzz/fuzz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// +build gofuzz

package fuzz

import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"sort"

commonFuzz "github.com/oasislabs/oasis-core/go/common/fuzz"
mkvs "github.com/oasislabs/oasis-core/go/storage/mkvs/urkel"
mkvsTests "github.com/oasislabs/oasis-core/go/storage/mkvs/urkel/tests"
)

var (
tree *TreeFuzz

fuzzer *commonFuzz.InterfaceFuzzer
)

// TreeFuzz is a wrapper around a mkvs.KeyValueTree for fuzzing purposes.
type TreeFuzz struct {
inner mkvs.KeyValueTree
reference map[string][]byte
history mkvsTests.TestVector
}

func (t *TreeFuzz) Insert(ctx context.Context, key []byte, value []byte) int {
if len(key) == 0 {
// Ignore zero-length keys as they are invalid.
return -1
}

if err := t.inner.Insert(ctx, key, value); err != nil {
t.fail("Insert failed: %s", err)
}

// Make sure the key has been set.
if getValue, err := t.inner.Get(ctx, key); err != nil || !bytes.Equal(value, getValue) {
t.fail("Insert check failed: %s", err)
}

t.reference[string(key)] = value
t.history = append(t.history, &mkvsTests.Op{Op: mkvsTests.OpInsert, Key: key, Value: value})
return 0
}

func (t *TreeFuzz) Get(ctx context.Context, key []byte) int {
if len(key) == 0 {
// Ignore zero-length keys as they are invalid.
return -1
}

value, err := t.inner.Get(ctx, key)
if err != nil {
t.fail("Get failed: %s", err)
}

t.assertCorrectValue(key, value)

return 0
}

func (t *TreeFuzz) RemoveExisting(ctx context.Context, key []byte) int {
if len(key) == 0 {
// Ignore zero-length keys as they are invalid.
return -1
}

value, err := t.inner.RemoveExisting(ctx, key)
if err != nil {
t.fail("RemoveExisting failed: %s", err)
}

// Make sure the key has been removed.
if value, err := t.inner.Get(ctx, key); err != nil || value != nil {
t.fail("RemoveExisting check failed: %s", err)
}

t.assertCorrectValue(key, value)

delete(t.reference, string(key))
t.history = append(t.history, &mkvsTests.Op{Op: mkvsTests.OpRemove, Key: key})

return 0
}

func (t *TreeFuzz) Remove(ctx context.Context, key []byte) int {
if len(key) == 0 {
// Ignore zero-length keys as they are invalid.
return -1
}

if err := t.inner.Remove(ctx, key); err != nil {
t.fail("Remove failed: %s", err)
}

// Make sure the key has been removed.
if value, err := t.inner.Get(ctx, key); err != nil || value != nil {
t.fail("Remove check failed: %s", err)
}

delete(t.reference, string(key))
t.history = append(t.history, &mkvsTests.Op{Op: mkvsTests.OpRemove, Key: key})

return 0
}

func (t *TreeFuzz) IteratorSeek(ctx context.Context, key []byte) int {
it := t.inner.NewIterator(ctx)
defer it.Close()

it.Seek(key)
if it.Err() != nil {
t.fail("IteratorSeek failed: %s", it.Err())
}

// Check that the iterator is in the correct position.
var ordered []string
for k := range t.reference {
ordered = append(ordered, k)
}
sort.Strings(ordered)

var expectedKey, expectedValue []byte
for _, k := range ordered {
if k >= string(key) {
expectedKey = []byte(k)
expectedValue = t.reference[k]
break
}
}
if !bytes.Equal(expectedKey, it.Key()) || !bytes.Equal(expectedValue, it.Value()) {
// Add the final IteratorSeek operation.
t.history = append(t.history, &mkvsTests.Op{
Op: mkvsTests.OpIteratorSeek,
Key: key,
Value: expectedValue,
ExpectedKey: expectedKey,
})

t.fail("iterator Seek returned incorrect key/value (expected: %s/%s got: %s/%s)",
hex.EncodeToString(expectedKey),
hex.EncodeToString(expectedValue),
hex.EncodeToString(it.Key()),
hex.EncodeToString(it.Value()),
)
}

return 0
}

func (t *TreeFuzz) assertCorrectValue(key, value []byte) {
if refValue := t.reference[string(key)]; !bytes.Equal(value, refValue) {
// Add the final Get operation.
t.history = append(t.history, &mkvsTests.Op{Op: mkvsTests.OpGet, Key: key, Value: refValue})

t.fail("Get returned incorrect value for key %s (expected: %s got: %s)",
hex.EncodeToString(key),
hex.EncodeToString(refValue),
hex.EncodeToString(value),
)
}
}

func (t *TreeFuzz) fail(format string, a ...interface{}) {
// In case there is a failure, dump the operation history so it can be used to create a test
// vector for unit tests.
fmt.Printf("--- FAILURE: Dumping operation history ---\n")
history, _ := json.MarshalIndent(t.history, "", " ")
fmt.Printf("%s\n", history)
fmt.Printf("------------------------------------------\n")

panic(fmt.Sprintf(format, a...))
}

func NewTreeFuzz() *TreeFuzz {
return &TreeFuzz{
inner: mkvs.New(nil, nil, mkvs.WithoutWriteLog()),
reference: make(map[string][]byte),
}
}

func init() {
// Create the in-memory tree.
tree = NewTreeFuzz()

// Create and prepare the fuzzer.
fuzzer = commonFuzz.NewInterfaceFuzzer(tree)
}

func Fuzz(data []byte) int {
values, result := fuzzer.DispatchBlob(data)
if !result {
return -1
}

// Return value is another result.
return values[0].Interface().(int)
}
30 changes: 30 additions & 0 deletions go/storage/mkvs/urkel/fuzz/gencorpus/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// +build gofuzz

// Gencorpus implements a simple utility to generate corpus files for the fuzzer.
// It has no command-line options and creates the files in the current working directory.
package main

import (
"fmt"
"io/ioutil"

commonFuzz "github.com/oasislabs/oasis-core/go/common/fuzz"
mkvsFuzz "github.com/oasislabs/oasis-core/go/storage/mkvs/urkel/fuzz"
)

const (
samplesPerMethod int = 20
)

func main() {
tree := mkvsFuzz.NewTreeFuzz()
fuzzer := commonFuzz.NewInterfaceFuzzer(tree)

for i := 0; i < samplesPerMethod; i++ {
blobs := fuzzer.MakeSampleBlobs()
for meth := 0; meth < len(blobs); meth++ {
fileName := fmt.Sprintf("%s_%02d.bin", fuzzer.Method(meth).Name, i)
_ = ioutil.WriteFile(fileName, blobs[meth], 0644)
}
}
}
4 changes: 4 additions & 0 deletions go/storage/mkvs/urkel/insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import (

// Implements Tree.
func (t *tree) Insert(ctx context.Context, key []byte, value []byte) error {
if value == nil {
value = []byte{}
}

t.cache.Lock()
defer t.cache.Unlock()

Expand Down
15 changes: 10 additions & 5 deletions go/storage/mkvs/urkel/node/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,24 @@ func (k Key) Split(splitPoint Depth, keyLen Depth) (prefix Key, suffix Key) {
// another key in bits.
// This function is immutable and returns a new instance of Key.
func (k Key) Merge(keyLen Depth, k2 Key, k2Len Depth) Key {
keyLenBytes := int(keyLen) / 8
if keyLen%8 != 0 {
keyLenBytes++
}

newKey := make(Key, (keyLen + k2Len).ToBytes())
copy(newKey[:], k[:])
copy(newKey[:], k[:keyLenBytes])

for i := 0; i < len(k2); i++ {
// First set the right chunk of the previous byte
if keyLen%8 != 0 && len(k) > 0 {
newKey[len(k)+i-1] |= k2[i] >> (keyLen % 8)
if keyLen%8 != 0 && keyLenBytes > 0 {
newKey[keyLenBytes+i-1] |= k2[i] >> (keyLen % 8)
}
// ...and the next left chunk, if we haven't reached the end of newKey
// yet.
if len(k)+i < len(newKey) {
if keyLenBytes+i < len(newKey) {
// another mod 8 to prevent bit shifting for 8 bits
newKey[len(k)+i] |= k2[i] << ((8 - keyLen%8) % 8)
newKey[keyLenBytes+i] |= k2[i] << ((8 - keyLen%8) % 8)
}
}

Expand Down
10 changes: 10 additions & 0 deletions go/storage/mkvs/urkel/node/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ func TestKeyAppendSplitMerge(t *testing.T) {
newKey = p.Merge(21, s, 8)
// Merge doesn't obtain original key, because the split cleaned unused bits!
require.Equal(t, Key{0xff, 0xff, 0xff, 0xf8}, newKey)

// Special case with zero-length key.
key = Key{0x80}
newKey = key.Merge(0, Key{0xf0}, 4)
require.Equal(t, Key{0xf0}, newKey)

// Special case with extra bytes.
key = Key{0x41, 0x6b, 0x00}
newKey = key.Merge(16, Key{0x37}, 8)
require.Equal(t, Key{0x41, 0x6b, 0x37}, newKey)
}

func TestKeyCommonPrefixLen(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion go/storage/mkvs/urkel/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ func (t *tree) doRemove(

var changed bool
var existing []byte
if key.BitLength() == bitLength {
if key.BitLength() < bitLength {
// Lookup key is too short for the current n.Label, so it doesn't exist.
return ptr, false, nil, nil
} else if key.BitLength() == bitLength {
n.LeafNode, changed, existing, err = t.doRemove(ctx, n.LeafNode, bitLength, key, depth)
} else if key.GetBit(bitLength) {
n.Right, changed, existing, err = t.doRemove(ctx, n.Right, bitLength, key, depth+1)
Expand Down
21 changes: 21 additions & 0 deletions go/storage/mkvs/urkel/testdata/case-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"op": "Insert",
"key": "y/XyJC9ZURWPCA==",
"value": "gg=="
},
{
"op": "Insert",
"key": "y/XyJA==",
"value": "Wc0="
},
{
"op": "Remove",
"key": "y/Xy"
},
{
"op": "Get",
"key": "y/XyJA==",
"value": "Wc0="
}
]
Loading