Skip to content

Commit 4b3f23e

Browse files
prestonvasquezJulien-Beezeelinx
authored andcommitted
GODRIVER-2550 Add fuzzer to bson packages (mongodb#1077)
1 parent 025b2d9 commit 4b3f23e

9 files changed

+241
-17
lines changed

.evergreen/config.yml

+31-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ command_type: setup
1515
# Fail builds when pre tasks fail.
1616
pre_error_fails_task: true
1717

18-
# Protect ourself against rogue test case, or curl gone wild, that runs forever
19-
# 12 minutes is the longest we'll ever run
20-
exec_timeout_secs: 3600 # 12 minutes is the longest we'll ever run
18+
# Protect the CI from long or indefinite runtimes.
19+
exec_timeout_secs: 3600
2120

2221
# What to do when evergreen hits the timeout (`post:` tasks are run automatically)
2322
timeout:
@@ -203,6 +202,16 @@ functions:
203202
permissions: public-read
204203
content_type: ${content_type|application/x-gzip}
205204
display_name: "mongodb-logs.tar.gz"
205+
- command: s3.put
206+
params:
207+
aws_key: ${aws_key}
208+
aws_secret: ${aws_secret}
209+
local_file: ${PROJECT_DIRECTORY}/fuzz.tgz
210+
remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/${task_id}-${execution}-fuzz.tgz
211+
bucket: mciuploads
212+
permissions: public-read
213+
content_type: application/x-gzip
214+
display_name: "fuzz.tgz"
206215

207216
bootstrap-mongohoused:
208217
- command: shell.exec
@@ -1004,6 +1013,14 @@ functions:
10041013
PKG_CONFIG_PATH=$PKG_CONFIG_PATH \
10051014
LD_LIBRARY_PATH=$LD_LIBRARY_PATH
10061015
1016+
run-fuzz-tests:
1017+
- command: shell.exec
1018+
type: test
1019+
params:
1020+
working_dir: "src"
1021+
script: |
1022+
${PREPARE_SHELL}
1023+
${PROJECT_DIRECTORY}/.evergreen/run-fuzz.sh
10071024
pre:
10081025
- func: fetch-source
10091026
- func: prepare-resources
@@ -1983,6 +2000,11 @@ tasks:
19832000
EXPECT_ERROR='unable to retrieve GCP credentials' \
19842001
./testgcpkms
19852002
2003+
- name: "test-fuzz"
2004+
commands:
2005+
- func: bootstrap-mongo-orchestration
2006+
- func: run-fuzz-tests
2007+
19862008
axes:
19872009
- id: version
19882010
display_name: MongoDB Version
@@ -2378,6 +2400,12 @@ buildvariants:
23782400
tasks:
23792401
- name: ".kms-kmip"
23802402

2403+
- matrix_name: "fuzz-test"
2404+
matrix_spec: { version: ["5.0"], os-ssl-40: ["ubuntu1804-64-go-1-18"] }
2405+
display_name: "Fuzz ${version} ${os-ssl-40}"
2406+
tasks:
2407+
- name: "test-fuzz"
2408+
23812409
- name: testgcpkms-variant
23822410
display_name: "GCP KMS"
23832411
run_on:

.evergreen/run-fuzz.sh

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/bin/bash
2+
3+
set -o errexit # Exit the script with error if any of the commands fail
4+
5+
FUZZTIME=10m
6+
7+
# Change the working directory to the root of the mongo repository directory
8+
cd $PROJECT_DIRECTORY
9+
10+
# Get all go test files that contain a fuzz test.
11+
FILES=$(grep -r --include='**_test.go' --files-with-matches 'func Fuzz' .)
12+
13+
# For each file, run all of the fuzz tests in sequence, each for -fuzztime=FUZZTIME.
14+
for FILE in ${FILES}
15+
do
16+
PARENTDIR="$(dirname -- "$FILE")"
17+
18+
# Get a list of all fuzz tests in the file.
19+
FUNCS=$(grep -o 'func Fuzz[A-Za-z0-9]*' $FILE | cut -d' ' -f2)
20+
21+
# For each fuzz test in the file, run it for FUZZTIME.
22+
for FUNC in ${FUNCS}
23+
do
24+
echo "Fuzzing \"${FUNC}\" in \"${FILE}\""
25+
26+
# Create a set of directories that are already in the subdirectories testdata/fuzz/$fuzzer corpus. This
27+
# set will be used to differentiate between new and old corpus files.
28+
declare -a cset
29+
30+
if [ -d $PARENTDIR/testdata/fuzz/$FUNC ]; then
31+
# Iterate over the files in the corpus directory and add them to the set.
32+
for SEED in $PARENTDIR/testdata/fuzz/$FUNC/*
33+
do
34+
cset+=("$SEED")
35+
done
36+
fi
37+
38+
go test ${PARENTDIR} -run=${FUNC} -fuzz=${FUNC} -fuzztime=${FUZZTIME} || true
39+
40+
# Check if any new corpus files were generated for the fuzzer. If there are new corpus files, move them
41+
# to $PROJECT_DIRECTORY/fuzz/$FUNC/* so they can be tarred up and uploaded to S3.
42+
if [ -d $PARENTDIR/testdata/fuzz/$FUNC ]; then
43+
# Iterate over the files in the corpus directory and check if they are in the set.
44+
for CORPUS_FILE in $PARENTDIR/testdata/fuzz/$FUNC/*
45+
do
46+
# Check to see if the value for CORPUS_FILE is in cset.
47+
if [[ ! " ${cset[@]} " =~ " ${CORPUS_FILE} " ]]; then
48+
# Create the directory if it doesn't exist.
49+
if [ ! -d $PROJECT_DIRECTORY/fuzz/$FUNC ]; then
50+
mkdir -p $PROJECT_DIRECTORY/fuzz/$FUNC
51+
fi
52+
53+
# Move the file to the directory.
54+
mv $CORPUS_FILE $PROJECT_DIRECTORY/fuzz/$FUNC
55+
56+
echo "Moved $CORPUS_FILE to $PROJECT_DIRECTORY/fuzz/$FUNC"
57+
fi
58+
done
59+
fi
60+
done
61+
done
62+
63+
# If the fuzz directory exists, then tar it up in preparation to upload to S3.
64+
if [ -d $PROJECT_DIRECTORY/fuzz ]; then
65+
echo "Tarring up fuzz directory"
66+
tar -czf $PROJECT_DIRECTORY/fuzz.tgz $PROJECT_DIRECTORY/fuzz
67+
fi
68+

bson/bson_corpus_spec_test.go

+98-14
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"encoding/hex"
1111
"encoding/json"
1212
"fmt"
13-
"io/ioutil"
1413
"math"
14+
"os"
1515
"path"
1616
"strconv"
1717
"strings"
@@ -60,11 +60,13 @@ type parseErrorTestCase struct {
6060

6161
const dataDir = "../testdata/bson-corpus/"
6262

63-
func findJSONFilesInDir(t *testing.T, dir string) []string {
63+
func findJSONFilesInDir(dir string) ([]string, error) {
6464
files := make([]string, 0)
6565

66-
entries, err := ioutil.ReadDir(dir)
67-
require.NoError(t, err)
66+
entries, err := os.ReadDir(dir)
67+
if err != nil {
68+
return nil, err
69+
}
6870

6971
for _, entry := range entries {
7072
if entry.IsDir() || path.Ext(entry.Name()) != ".json" {
@@ -74,7 +76,65 @@ func findJSONFilesInDir(t *testing.T, dir string) []string {
7476
files = append(files, entry.Name())
7577
}
7678

77-
return files
79+
return files, nil
80+
}
81+
82+
// seedExtJSON will add the byte representation of the "extJSON" string to the fuzzer's coprus.
83+
func seedExtJSON(f *testing.F, extJSON string, extJSONType string, desc string) {
84+
jbytes, err := jsonToBytes(extJSON, extJSONType, desc)
85+
if err != nil {
86+
f.Fatalf("failed to convert JSON to bytes: %v", err)
87+
}
88+
89+
f.Add(jbytes)
90+
}
91+
92+
// seedTestCase will add the byte representation for each "extJSON" string of each valid test case to the fuzzer's
93+
// corpus.
94+
func seedTestCase(f *testing.F, tcase *testCase) {
95+
for _, vtc := range tcase.Valid {
96+
seedExtJSON(f, vtc.CanonicalExtJSON, "canonical", vtc.Description)
97+
98+
// Seed the relaxed extended JSON.
99+
if vtc.RelaxedExtJSON != nil {
100+
seedExtJSON(f, *vtc.RelaxedExtJSON, "relaxed", vtc.Description)
101+
}
102+
103+
// Seed the degenerate extended JSON.
104+
if vtc.DegenerateExtJSON != nil {
105+
seedExtJSON(f, *vtc.DegenerateExtJSON, "degenerate", vtc.Description)
106+
}
107+
108+
// Seed the converted extended JSON.
109+
if vtc.ConvertedExtJSON != nil {
110+
seedExtJSON(f, *vtc.ConvertedExtJSON, "converted", vtc.Description)
111+
}
112+
}
113+
}
114+
115+
// seedBSONCorpus will unmarshal the data from "testdata/bson-corpus" into a slice of "testCase" structs and then
116+
// marshal the "*_extjson" field of each "validityTestCase" into a slice of bytes to seed the fuzz corpus.
117+
func seedBSONCorpus(f *testing.F) {
118+
fileNames, err := findJSONFilesInDir(dataDir)
119+
if err != nil {
120+
f.Fatalf("failed to find JSON files in directory %q: %v", dataDir, err)
121+
}
122+
123+
for _, fileName := range fileNames {
124+
filePath := path.Join(dataDir, fileName)
125+
126+
file, err := os.Open(filePath)
127+
if err != nil {
128+
f.Fatalf("failed to open file %q: %v", filePath, err)
129+
}
130+
131+
var tcase testCase
132+
if err := json.NewDecoder(file).Decode(&tcase); err != nil {
133+
f.Fatal(err)
134+
}
135+
136+
seedTestCase(f, &tcase)
137+
}
78138
}
79139

80140
func needsEscapedUnicode(bsonType string) bool {
@@ -196,11 +256,27 @@ func nativeToBSON(t *testing.T, cB []byte, doc D, testDesc, bType, docSrcDesc st
196256
}
197257

198258
// jsonToNative decodes the extended JSON string (ej) into a native Document
199-
func jsonToNative(t *testing.T, ej, ejType, testDesc string) D {
259+
func jsonToNative(ej, ejType, testDesc string) (D, error) {
200260
var doc D
201-
err := UnmarshalExtJSON([]byte(ej), ejType != "relaxed", &doc)
202-
expectNoError(t, err, fmt.Sprintf("%s: decoding %s extended JSON", testDesc, ejType))
203-
return doc
261+
if err := UnmarshalExtJSON([]byte(ej), ejType != "relaxed", &doc); err != nil {
262+
return nil, fmt.Errorf("%s: decoding %s extended JSON: %w", testDesc, ejType, err)
263+
}
264+
return doc, nil
265+
}
266+
267+
// jsonToBytes decodes the extended JSON string (ej) into canonical BSON and then encodes it into a byte slice.
268+
func jsonToBytes(ej, ejType, testDesc string) ([]byte, error) {
269+
native, err := jsonToNative(ej, ejType, testDesc)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
b, err := Marshal(native)
275+
if err != nil {
276+
return nil, fmt.Errorf("%s: encoding %s BSON: %w", testDesc, ejType, err)
277+
}
278+
279+
return b, nil
204280
}
205281

206282
// nativeToJSON encodes the native Document (doc) into an extended JSON string
@@ -217,7 +293,7 @@ func nativeToJSON(t *testing.T, ej string, doc D, testDesc, ejType, ejShortName,
217293

218294
func runTest(t *testing.T, file string) {
219295
filepath := path.Join(dataDir, file)
220-
content, err := ioutil.ReadFile(filepath)
296+
content, err := os.ReadFile(filepath)
221297
require.NoError(t, err)
222298

223299
// Remove ".json" from filename.
@@ -260,14 +336,16 @@ func runTest(t *testing.T, file string) {
260336
nativeToJSON(t, rEJ, doc, v.Description, "relaxed", "rEJ", "bson_to_native(cB)")
261337

262338
/*** relaxed extended JSON round-trip tests (if exists) ***/
263-
doc = jsonToNative(t, rEJ, "relaxed", v.Description)
339+
doc, err = jsonToNative(rEJ, "relaxed", v.Description)
340+
require.NoError(t, err)
264341

265342
// native_to_relaxed_extended_json(json_to_native(rEJ)) = rEJ
266343
nativeToJSON(t, rEJ, doc, v.Description, "relaxed", "eJR", "json_to_native(rEJ)")
267344
}
268345

269346
/*** canonical extended JSON round-trip tests ***/
270-
doc = jsonToNative(t, cEJ, "canonical", v.Description)
347+
doc, err = jsonToNative(cEJ, "canonical", v.Description)
348+
require.NoError(t, err)
271349

272350
// native_to_canonical_extended_json(json_to_native(cEJ)) = cEJ
273351
nativeToJSON(t, cEJ, doc, v.Description, "canonical", "cEJ", "json_to_native(cEJ)")
@@ -295,7 +373,8 @@ func runTest(t *testing.T, file string) {
295373
dEJ = normalizeCanonicalDouble(t, *test.TestKey, dEJ)
296374
}
297375

298-
doc = jsonToNative(t, dEJ, "degenerate canonical", v.Description)
376+
doc, err = jsonToNative(dEJ, "degenerate canonical", v.Description)
377+
require.NoError(t, err)
299378

300379
// native_to_canonical_extended_json(json_to_native(dEJ)) = cEJ
301380
nativeToJSON(t, cEJ, doc, v.Description, "degenerate canonical", "cEJ", "json_to_native(dEJ)")
@@ -366,7 +445,12 @@ func runTest(t *testing.T, file string) {
366445
}
367446

368447
func Test_BsonCorpus(t *testing.T) {
369-
for _, file := range findJSONFilesInDir(t, dataDir) {
448+
jsonFiles, err := findJSONFilesInDir(dataDir)
449+
if err != nil {
450+
t.Fatalf("error finding JSON files in %s: %v", dataDir, err)
451+
}
452+
453+
for _, file := range jsonFiles {
370454
runTest(t, file)
371455
}
372456
}

bson/fuzz_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package bson
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func FuzzDecode(f *testing.F) {
8+
seedBSONCorpus(f)
9+
10+
f.Fuzz(func(t *testing.T, data []byte) {
11+
for _, typ := range []func() interface{}{
12+
func() interface{} { return new(D) },
13+
func() interface{} { return new([]E) },
14+
func() interface{} { return new(M) },
15+
func() interface{} { return new(interface{}) },
16+
func() interface{} { return make(map[string]interface{}) },
17+
func() interface{} { return new([]interface{}) },
18+
} {
19+
i := typ()
20+
if err := Unmarshal(data, i); err != nil {
21+
return
22+
}
23+
24+
encoded, err := Marshal(i)
25+
if err != nil {
26+
t.Fatal("failed to marshal", err)
27+
}
28+
29+
if err := Unmarshal(encoded, i); err != nil {
30+
t.Fatal("failed to unmarshal", err)
31+
}
32+
}
33+
})
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("\x10\x00\x00\x00\v\x00\x00\x00\b\x00\x00\v\x00\x00\x00\x00")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("0\\x00\\x00\\x00\\x0f\\x00000\\x8a00000000000000000000000000000000000000\n")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("\\x80\\x00\\x00\\x00\\x03000000\\x00s\\x00\\x00\\x00\\x0300000\\x00g\\x00\\x00\\x00\\x100z\\x000000\\x11\\x00000\\x150000\\x020\\x00\\x02\\x00\\x00\\x000\\x12\\x00\\x050\\x00\\x01\\x00\\x00\\x0000\\x050\\x00\\x01\\x00\\x00\\x0000\\x040\\x00200000\\x00\\x000\\x02\\x00\\x10\\x0000000\\x110\\x0000000000\\x020\\x00\\x02\\x00\\x00\\x000\\x00\\x050\\x00\\x01\\x00\\x00\\x0000\\x050\\x00\\x01\\x00\\x00\\x0000\\x00\\x00\\x00\\x00\n")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("\\x59\\x01\\x00\\x00\\x01\\x64\\x6f\\x75\\x62\\x6c\\x65\\x00\\x9a\\x99\\x99\\x99\\x99\\x99\\xf1\\x3f\\x02\\x73\\x74\\x72\\x69\\x6e\\x67\\x00\\x06\\x00\\x00\\x00\\x68\\x65\\x6c\\x6c\\x6f\\x00\\x03\\x65\\x6d\\x62\\x65\\x64\\x64\\x65\\x64\\x00\\x4b\\x00\\x00\\x00\\x04\\x61\\x72\\x72\\x61\\x79\\x00\\x3f\\x00\\x00\\x00\\x10\\x30\\x00\\x01\\x00\\x00\\x00\\x01\\x31\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x40\\x02\\x32\\x00\\x02\\x00\\x00\\x00\\x33\\x00\\x04\\x33\\x00\\x0c\\x00\\x00\\x00\\x10\\x30\\x00\\x04\\x00\\x00\\x00\\x00\\x03\\x34\\x00\\x0d\\x00\\x00\\x00\\x03\\x35\\x00\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x05\\x62\\x69\\x6e\\x61\\x72\\x79\\x00\\x03\\x00\\x00\\x00\\x00\\x01\\x02\\x03\\x07\\x6f\\x62\\x6a\\x65\\x63\\x74\\x69\\x64\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x08\\x62\\x6f\\x6f\\x6c\\x65\\x61\\x6e\\x00\\x01\\x09\\x64\\x61\\x74\\x65\\x74\\x69\\x6d\\x65\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x0a\\x6e\\x75\\x6c\\x6c\\x00\\x0b\\x72\\x65\\x67\\x65\\x78\\x00\\x68\\x65\\x6c\\x6c\\x6f\\x00\\x69\\x00\\x0d\\x6a\\x73\\x00\\x0e\\x00\\x00\\x00\\x66\\x75\\x6e\\x63\\x74\\x69\\x6f\\x6e\\x28\\x29\\x20\\x7b\\x7d\\x00\\x0f\\x73\\x63\\x6f\\x70\\x65\\x00\\x2c\\x00\\x00\\x00\\x0e\\x00\\x00\\x00\\x66\\x75\\x6e\\x63\\x74\\x69\\x6f\\x6e\\x28\\x29\\x20\\x7b\\x7d\\x00\\x16\\x00\\x00\\x00\\x02\\x68\\x65\\x6c\\x6c\\x6f\\x00\\x06\\x00\\x00\\x00\\x77\\x6f\\x72\\x6c\\x64\\x00\\x00\\x10\\x69\\x6e\\x74\\x33\\x32\\x00\\x20\\x00\\x00\\x00\\x11\\x74\\x69\\x6d\\x65\\x73\\x74\\x61\\x6d\\x70\\x00\\x02\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x12\\x69\\x6e\\x74\\x36\\x34\\x00\\x40\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\x6d\\x69\\x6e\\x6b\\x65\\x79\\x00\\x7f\\x6d\\x61\\x78\\x6b\\x65\\x79\\x00\\x00\"\n")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("\\x05\\xf0\\xff\\x00\\x7f\n")

0 commit comments

Comments
 (0)