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

Add fuzz support #1033

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target
corpus
artifacts
33 changes: 33 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[package]
name = "fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
apollo-smith = "0.1.0"

[dependencies.juniper]
path = "../juniper"
features = ["arbitrary1"]

# Prevent this from interfering with workspaces
[workspace]
members = ["."]

[[bin]]
name = "document_fuzzer"
path = "fuzz_targets/document_fuzzer.rs"
test = false
doc = false

[[bin]]
name = "introspection_fuzzer"
path = "fuzz_targets/introspection_fuzzer.rs"
test = false
doc = false
32 changes: 32 additions & 0 deletions fuzz/fuzz_targets/document_fuzzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#![no_main]
use apollo_smith::DocumentBuilder;
use libfuzzer_sys::{
arbitrary::{self, Unstructured},
fuzz_target,
};

use juniper::{DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLObject, RootNode};

#[derive(Default, GraphQLObject, arbitrary::Arbitrary)]
struct Query {
x: i32,
y: String,
z: Vec<bool>,
}

fuzz_target!(|input: &[u8]| {
let mut u = Unstructured::new(input);
let mut u2 = Unstructured::new(input);
if let Ok(gql_doc) = DocumentBuilder::new(&mut u) {
let document = gql_doc.finish();
let doc = String::from(document);

let arbitrary_schema: arbitrary::Result<
RootNode<Query, EmptyMutation<()>, EmptySubscription<()>, DefaultScalarValue>,
> = u2.arbitrary();

if let Ok(schema) = arbitrary_schema {
let _ = juniper::parser::parse_document_source(doc.as_str(), &schema.schema);
}
}
});
27 changes: 27 additions & 0 deletions fuzz/fuzz_targets/introspection_fuzzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![no_main]
use libfuzzer_sys::{
arbitrary::{self, Unstructured},
fuzz_target,
};

use juniper::{
DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLObject, IntrospectionFormat,
};

#[derive(Default, GraphQLObject, arbitrary::Arbitrary)]
struct Query {
x: i32,
y: String,
z: Vec<bool>,
}

fuzz_target!(|input: &[u8]| {
let mut u = Unstructured::new(input);
let arbitrary_schema: arbitrary::Result<
juniper::RootNode<Query, EmptyMutation<()>, EmptySubscription<()>, DefaultScalarValue>,
> = u.arbitrary();

if let Ok(schema) = arbitrary_schema {
let _ = juniper::introspect(&schema, &(), IntrospectionFormat::default());
}
});
2 changes: 2 additions & 0 deletions juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ default = [
"url",
"uuid",
]
arbitrary1 = ["arbitrary", "smartstring/arbitrary"]
expose-test-schema = ["anyhow", "serde_json"]
graphql-parser-integration = ["graphql-parser"]
scalar-naivetime = []
Expand All @@ -35,6 +36,7 @@ schema-language = ["graphql-parser-integration"]
juniper_codegen = { version = "0.16.0-dev", path = "../juniper_codegen" }

anyhow = { version = "1.0.32", optional = true, default-features = false }
arbitrary = { version = "1.1", optional = true, features = ["derive"] }
async-trait = "0.1.39"
bson = { version = "2.0", features = ["chrono-0_4"], optional = true }
chrono = { version = "0.4", default-features = false, optional = true }
Expand Down
162 changes: 158 additions & 4 deletions juniper/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,36 @@ pub enum Type<'a> {
NonNullList(Box<Type<'a>>, Option<usize>),
}

#[cfg(feature = "arbitrary1")]
impl<'a> arbitrary::Arbitrary<'a> for Type<'a> {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let num_choices = 4;

let ty = match u.int_in_range::<u8>(1..=num_choices)? {
1 => Type::Named(u.arbitrary::<Cow<'a, str>>()?),
2 => Type::List(
u.arbitrary::<Box<Type<'a>>>()?,
u.arbitrary::<Option<usize>>()?,
),
3 => Type::NonNullNamed(u.arbitrary::<Cow<'a, str>>()?),
4 => Type::NonNullList(
u.arbitrary::<Box<Type<'a>>>()?,
u.arbitrary::<Option<usize>>()?,
),
_ => unreachable!(),
};
Ok(ty)
}
}

/// A JSON-like value that can be passed into the query execution, either
/// out-of-band, or in-band as default variable values. These are _not_ constant
/// and might contain variables.
///
/// Lists and objects variants are _spanned_, i.e. they contain a reference to
/// their position in the source file, if available.
#[derive(Clone, Debug, PartialEq)]
#[allow(missing_docs)]
#[cfg_attr(feature = "arbitrary1", derive(arbitrary::Arbitrary))]
pub enum InputValue<S = DefaultScalarValue> {
Null,
Scalar(S),
Expand All @@ -46,23 +68,67 @@ pub enum InputValue<S = DefaultScalarValue> {
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct VariableDefinition<'a, S> {
pub var_type: Spanning<Type<'a>>,
pub default_value: Option<Spanning<InputValue<S>>>,
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for VariableDefinition<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let var_type: Spanning<Type<'a>> = u.arbitrary()?;
let default_value: Option<Spanning<InputValue<S>>> = u.arbitrary()?;
let directives: Option<Vec<Spanning<Directive<'a, S>>>> = u.arbitrary()?;

Ok(Self {
var_type,
default_value,
directives,
})
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Arguments<'a, S> {
pub items: Vec<(Spanning<&'a str>, Spanning<InputValue<S>>)>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for Arguments<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let items: Vec<(Spanning<&'a str>, Spanning<InputValue<S>>)> = u.arbitrary()?;
Ok(Self { items })
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct VariableDefinitions<'a, S> {
pub items: Vec<(Spanning<&'a str>, VariableDefinition<'a, S>)>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for VariableDefinitions<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let items: Vec<(Spanning<&'a str>, VariableDefinition<'a, S>)> = u.arbitrary()?;
Ok(Self { items })
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Field<'a, S> {
pub alias: Option<Spanning<&'a str>>,
pub name: Spanning<&'a str>,
Expand All @@ -71,19 +137,74 @@ pub struct Field<'a, S> {
pub selection_set: Option<Vec<Selection<'a, S>>>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for Field<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let alias: Option<Spanning<&'a str>> = u.arbitrary()?;
let name: Spanning<&'a str> = u.arbitrary()?;
let arguments: Option<Spanning<Arguments<'a, S>>> = u.arbitrary()?;
let directives: Option<Vec<Spanning<Directive<'a, S>>>> = u.arbitrary()?;
let selection_set: Option<Vec<Selection<'a, S>>> = u.arbitrary()?;

Ok(Self {
alias,
name,
arguments,
directives,
selection_set,
})
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct FragmentSpread<'a, S> {
pub name: Spanning<&'a str>,
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for FragmentSpread<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let name: Spanning<&'a str> = u.arbitrary()?;
let directives: Option<Vec<Spanning<Directive<'a, S>>>> = u.arbitrary()?;

Ok(Self { name, directives })
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct InlineFragment<'a, S> {
pub type_condition: Option<Spanning<&'a str>>,
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
pub selection_set: Vec<Selection<'a, S>>,
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for InlineFragment<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let type_condition: Option<Spanning<&'a str>> = u.arbitrary()?;
let directives: Option<Vec<Spanning<Directive<'a, S>>>> = u.arbitrary()?;
let selection_set: Vec<Selection<'a, S>> = u.arbitrary()?;

Ok(Self {
type_condition,
directives,
selection_set,
})
}
}

/// Entry in a GraphQL selection set
///
/// This enum represents one of the three variants of a selection that exists
Expand All @@ -107,22 +228,54 @@ pub enum Selection<'a, S = DefaultScalarValue> {
InlineFragment(Spanning<InlineFragment<'a, S>>),
}

#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for Selection<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let num_choices = 3;

let ty = match u.int_in_range::<u8>(1..=num_choices)? {
1 => Selection::Field(u.arbitrary::<Spanning<Field<'a, S>>>()?),
2 => Selection::FragmentSpread(u.arbitrary::<Spanning<FragmentSpread<'a, S>>>()?),
3 => Selection::InlineFragment(u.arbitrary::<Spanning<InlineFragment<'a, S>>>()?),
_ => unreachable!(),
};
Ok(ty)
}
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Directive<'a, S> {
pub name: Spanning<&'a str>,
pub arguments: Option<Spanning<Arguments<'a, S>>>,
}

#[allow(missing_docs)]
#[cfg(feature = "arbitrary1")]
impl<'a, S> arbitrary::Arbitrary<'a> for Directive<'a, S>
where
S: arbitrary::Arbitrary<'a>,
{
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let name: Spanning<&'a str> = u.arbitrary()?;
let arguments: Option<Spanning<Arguments<'a, S>>> = u.arbitrary()?;
Ok(Self { name, arguments })
}
}

#[derive(Clone, PartialEq, Debug)]
#[cfg_attr(feature = "arbitrary1", derive(arbitrary::Arbitrary))]
#[allow(missing_docs)]
pub enum OperationType {
Query,
Mutation,
Subscription,
}

#[allow(missing_docs)]
#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Operation<'a, S> {
pub operation_type: OperationType,
pub name: Option<Spanning<&'a str>>,
Expand All @@ -132,15 +285,16 @@ pub struct Operation<'a, S> {
}

#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub struct Fragment<'a, S> {
pub name: Spanning<&'a str>,
pub type_condition: Spanning<&'a str>,
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
pub selection_set: Vec<Selection<'a, S>>,
}

#[doc(hidden)]
#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub enum Definition<'a, S> {
Operation(Spanning<Operation<'a, S>>),
Fragment(Spanning<Fragment<'a, S>>),
Expand Down
2 changes: 2 additions & 0 deletions juniper/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Lexer<'a> {
///
/// This is only used for tagging how the lexer has interpreted a value literal
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary1", derive(arbitrary::Arbitrary))]
#[allow(missing_docs)]
pub enum ScalarToken<'a> {
String(&'a str),
Expand All @@ -30,6 +31,7 @@ pub enum ScalarToken<'a> {

/// A single token in the input source
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary1", derive(arbitrary::Arbitrary))]
#[allow(missing_docs)]
pub enum Token<'a> {
Name(&'a str),
Expand Down
Loading