Repository: tokio-rs/prost (language: rust)

Issue:
afl fuzz tests
#267 shows that the current in-tree fuzzing based on `cargo-fuzz` has some blindspots.  Investigate adding afl-based fuzzing to the in-tree test suite.


Discussion / hints:
I could probably contribute something!

Do you already have a "sample language" somewhere? Ideally with a common root message, so that all code paths can be discovered from the same parse entry point.
Yes, see the existing `cargo-fuzz` tests in https://github.com/danburkert/prost/tree/master/fuzz, and which use https://github.com/danburkert/prost/blob/master/tests/src/lib.rs#L117-L170 as the success checker.
@dbrgn more of a point of curiosity than anything, how long did you have to run afl to produce the stack overflow?
It was only a few minutes on a single core :slightly_smiling_face: Not sure if I just got lucky.

I was fuzzing a separate project where most (but not all) messages are just a single prefix byte, followed by a protobuf message. I quickly got about half a dozen failures, which were all related to the stack overflow.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 541dc1d78..84ce19d22 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,6 +29,8 @@ exclude = [
   # The fuzz crate can't be compiled or tested without the 'cargo fuzz' command,
   # so exclude it from normal builds.
   "fuzz",
+  # Same counts for the afl fuzz targets
+  "afl",
 ]
 
 [lib]
diff --git a/FUZZING.md b/FUZZING.md
new file mode 100644
index 000000000..d47268d69
--- /dev/null
+++ b/FUZZING.md
@@ -0,0 +1,27 @@
+# Fuzzing
+
+Prost ships a few fuzz tests, using both libfuzzer and aflfuzz.
+
+
+## afl
+
+To run the afl fuzz tests, first install cargo-afl:
+
+    cargo install -f afl
+
+Then build a fuzz target and run afl on it:
+
+    cd afl/<target>/
+    cargo afl build --bin fuzz-target
+    cargo afl fuzz -i in -o out target/debug/fuzz-target
+
+To reproduce a crash:
+
+    cd afl/<target>/
+    cargo build --bin reproduce
+    cargo run --bin reproduce -- out/crashes/<crashfile>
+
+
+## libfuzzer
+
+TODO
diff --git a/afl/.gitignore b/afl/.gitignore
new file mode 100644
index 000000000..1776e1323
--- /dev/null
+++ b/afl/.gitignore
@@ -0,0 +1,2 @@
+out/
+core.*
diff --git a/afl/proto3/Cargo.toml b/afl/proto3/Cargo.toml
new file mode 100644
index 000000000..d1e00d5df
--- /dev/null
+++ b/afl/proto3/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "fuzz-target-proto3"
+version = "0.1.0"
+authors = ["Prost developers"]
+edition = "2018"
+
+[[bin]]
+name = "fuzz-target"
+path = "src/main.rs"
+
+[[bin]]
+name = "reproduce"
+path = "src/reproduce.rs"
+
+[dependencies]
+afl = "0.4"
+protobuf = { path = "../../protobuf/" }
+tests = { path = "../../tests/" }
diff --git a/afl/proto3/README.md b/afl/proto3/README.md
new file mode 100644
index 000000000..064bfdb1a
--- /dev/null
+++ b/afl/proto3/README.md
@@ -0,0 +1,30 @@
+# proto3 fuzz tests
+
+## Test corpus
+
+The test message `testmessage` was created like this:
+
+```rust
+use prost::Message;
+use protobuf::test_messages::proto3::TestAllTypesProto3;
+
+fn main() {
+    let msg = TestAllTypesProto3 {
+        optional_int32: 42,
+        optional_fixed64: 9983748923,
+        optional_bool: true,
+        recursive_message: Some(
+            Box::new(TestAllTypesProto3 {
+                repeated_int32: vec![1, 2, 99, 50, -5],
+                ..Default::default()
+            })
+        ),
+        repeated_sfixed32: vec![1, -1, 1, -1],
+        repeated_float: vec![-1.0, 10.10, 1.337, std::f32::NAN],
+        ..Default::default()
+    };
+    let mut buf = vec![];
+    msg.encode(&mut buf).unwrap();
+    std::fs::write("proto3-default.bin", buf).unwrap();
+}
+```
diff --git a/afl/proto3/in/empty b/afl/proto3/in/empty
new file mode 100644
index 000000000..e69de29bb
diff --git a/afl/proto3/src/main.rs b/afl/proto3/src/main.rs
new file mode 100644
index 000000000..e38c843f6
--- /dev/null
+++ b/afl/proto3/src/main.rs
@@ -0,0 +1,10 @@
+use afl::fuzz;
+
+use protobuf::test_messages::proto3::TestAllTypesProto3;
+use tests::roundtrip;
+
+fn main() {
+    fuzz!(|data: &[u8]| {
+        let _ = roundtrip::<TestAllTypesProto3>(data).unwrap_error();
+    });
+}
diff --git a/afl/proto3/src/reproduce.rs b/afl/proto3/src/reproduce.rs
new file mode 100644
index 000000000..ab0c0f9c5
--- /dev/null
+++ b/afl/proto3/src/reproduce.rs
@@ -0,0 +1,13 @@
+use protobuf::test_messages::proto3::TestAllTypesProto3;
+use tests::roundtrip;
+
+fn main() {
+    let args: Vec<String> = std::env::args().collect();
+    if args.len() != 2 {
+        println!("Usage: {} <path-to-crash>", args[0]);
+        std::process::exit(1);
+    }
+
+    let data = std::fs::read(&args[1]).expect(&format!("Could not open file {}", args[1]));
+    let _ = roundtrip::<TestAllTypesProto3>(&data).unwrap_error();
+}


Test patch:
diff --git a/afl/proto3/in/testmessage b/afl/proto3/in/testmessage
new file mode 100644
index 000000000..a95f31062
Binary files /dev/null and b/afl/proto3/in/testmessage differ