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

GODRIVER-3451 Add fuzz test for BSON MarshalValue/UnmarshalValue. #1993

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
69 changes: 69 additions & 0 deletions bson/decode_value_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (C) MongoDB, Inc. 2025-present.
//
// 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

package bson

import (
"math"
"strings"
"testing"
)

func FuzzDecodeValue(f *testing.F) {
// Seed the fuzz corpus with all BSON values from the MarshalValue test
// cases.
for _, tc := range marshalValueTestCases {
f.Add(byte(tc.bsontype), tc.bytes)
}

// Also seed the fuzz corpus with special values that we want to test.
values := []any{
// int32, including max and min values.
int32(0), math.MaxInt32, math.MinInt32,
// int64, including max and min values.
int64(0), math.MaxInt64, math.MinInt64,
// string, including empty and large string.
"", strings.Repeat("z", 10_000),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we extend this to include a nested structure and a slice?

map[string]any{"nested": []any{1, "two", map[string]any{"three", 3}}}
[]any{1, 2,3, "four"}

// map
map[string]any{"nested": []any{1, "two", map[string]any{"three": 3}}},
// array
[]any{1, 2, 3, "four"},
}

for _, v := range values {
typ, b, err := MarshalValue(v)
if err != nil {
f.Fatal(err)
}
f.Add(byte(typ), b)
}

f.Fuzz(func(t *testing.T, bsonType byte, data []byte) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

AFAIK we can run these tests in parallel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure what you mean. I believe f.Fuzz already runs the provided funcs in parallel (limited by GOMAXPROCS). Each go test invocation can only target a single fuzz test, or you get an error like this:

testing: will not fuzz, -fuzz matches more than one fuzz test: [FuzzDecodeValue FuzzDecode]

Is there another way to run fuzz tests or test cases in parallel?

var v any
if err := UnmarshalValue(Type(bsonType), data, &v); err != nil {
return
}

// There is no value encoder for Go "nil" (nil values handled
// differently by each type encoder), so skip anything that unmarshals
// to "nil". It's not clear if MarshalValue should support "nil", but
// for now we skip it.
if v == nil {
t.Logf("data unmarshaled to nil: %v", data)
return
Copy link
Collaborator

Choose a reason for hiding this comment

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

It might be worth logging such cases.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Test logs only seems to be printed when the fuzz test encounters another error, but I can add the log statement in case it's helpful.

}

typ, encoded, err := MarshalValue(v)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}

var v2 any
if err := UnmarshalValue(typ, encoded, &v2); err != nil {
t.Fatalf("failed to unmarshal: %v", err)
}
})
}
9 changes: 7 additions & 2 deletions bson/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,11 @@
// 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

// fuzz_test.go is used by the "oss-fuzz" integration. Use caution when
// modifying this file because it may break that integration.
//
// See https://github.com/google/oss-fuzz/tree/master/projects/mongo-go-driver

package bson

import (
@@ -29,11 +34,11 @@ func FuzzDecode(f *testing.F) {

encoded, err := Marshal(i)
if err != nil {
t.Fatal("failed to marshal", err)
t.Fatalf("failed to marshal: %v", err)
}

if err := Unmarshal(encoded, i); err != nil {
t.Fatal("failed to unmarshal", err)
t.Fatalf("failed to unmarshal: %v", err)
}
}
})
12 changes: 9 additions & 3 deletions etc/run-fuzz.sh
Original file line number Diff line number Diff line change
@@ -2,7 +2,13 @@

set -o errexit # Exit the script with error if any of the commands fail

FUZZTIME=10m
# Default fuzztime to 10m.
FUZZTIME=${FUZZTIME:-10m}

if [ -z "$PROJECT_DIRECTORY" ]; then
echo "Please set PROJECT_DIRECTORY env variable."
exit 1
fi

# Change the working directory to the root of the mongo repository directory
cd $PROJECT_DIRECTORY
@@ -21,7 +27,7 @@ do
# For each fuzz test in the file, run it for FUZZTIME.
for FUNC in ${FUNCS}
do
echo "Fuzzing \"${FUNC}\" in \"${FILE}\""
echo "Fuzzing \"${FUNC}\" in \"${FILE}\" for ${FUZZTIME}"

# Create a set of directories that are already in the subdirectories testdata/fuzz/$fuzzer corpus. This
# set will be used to differentiate between new and old corpus files.
@@ -35,7 +41,7 @@ do
done
fi

GOMAXPROCS=2 go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
GOMAXPROCS=2 go test -race ${PARENTDIR} -run=${FUNC} -fuzz=^${FUNC}\$ -fuzztime=${FUZZTIME} || true

# Check if any new corpus files were generated for the fuzzer. If there are new corpus files, move them
# to $PROJECT_DIRECTORY/fuzz/$FUNC/* so they can be tarred up and uploaded to S3.
Loading
Oops, something went wrong.