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

[Merged by Bors] - VM Fuzzer #2401

Closed
wants to merge 8 commits into from
Closed
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
17 changes: 17 additions & 0 deletions boa_engine/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ pub struct Context {
#[cfg(feature = "intl")]
icu: icu::Icu,

/// Number of instructions remaining before a forced exit
#[cfg(feature = "fuzz")]
pub(crate) instructions_remaining: usize,

pub(crate) vm: Vm,

pub(crate) promise_job_queue: VecDeque<JobCallback>,
Expand Down Expand Up @@ -593,6 +597,8 @@ pub struct ContextBuilder {
interner: Option<Interner>,
#[cfg(feature = "intl")]
icu: Option<icu::Icu>,
#[cfg(feature = "fuzz")]
instructions_remaining: usize,
}

impl ContextBuilder {
Expand All @@ -615,6 +621,15 @@ impl ContextBuilder {
Ok(self)
}

/// Specifies the number of instructions remaining to the [`Context`].
///
/// This function is only available if the `fuzz` feature is enabled.
#[cfg(feature = "fuzz")]
pub fn instructions_remaining(mut self, instructions_remaining: usize) -> Self {
self.instructions_remaining = instructions_remaining;
self
}

/// Creates a new [`ContextBuilder`] with a default empty [`Interner`]
/// and a default [`BoaProvider`] if the `intl` feature is enabled.
pub fn new() -> Self {
Expand Down Expand Up @@ -643,6 +658,8 @@ impl ContextBuilder {
icu::Icu::new(Box::new(icu_testdata::get_provider()))
.expect("Failed to initialize default icu data.")
}),
#[cfg(feature = "fuzz")]
instructions_remaining: self.instructions_remaining,
promise_job_queue: VecDeque::new(),
};

Expand Down
23 changes: 23 additions & 0 deletions boa_engine/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,17 @@ impl JsNativeError {
Self::new(JsNativeErrorKind::Uri, Box::default(), None)
}

/// Creates a new `JsNativeError` that indicates that the context hit its execution limit. This
/// is only used in a fuzzing context.
#[cfg(feature = "fuzz")]
pub fn no_instructions_remain() -> Self {
Self::new(
JsNativeErrorKind::NoInstructionsRemain,
Box::default(),
None,
)
}

/// Sets the message of this error.
///
/// # Examples
Expand Down Expand Up @@ -619,6 +630,12 @@ impl JsNativeError {
}
JsNativeErrorKind::Type => (constructors.type_error().prototype(), ErrorKind::Type),
JsNativeErrorKind::Uri => (constructors.uri_error().prototype(), ErrorKind::Uri),
#[cfg(feature = "fuzz")]
JsNativeErrorKind::NoInstructionsRemain => {
unreachable!(
"The NoInstructionsRemain native error cannot be converted to an opaque type."
)
}
};

let o = JsObject::from_proto_and_data(prototype, ObjectData::error(tag));
Expand Down Expand Up @@ -747,6 +764,10 @@ pub enum JsNativeErrorKind {
/// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
/// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI
Uri,
/// Error thrown when no instructions remain. Only used in a fuzzing context; not a valid JS
/// error variant.
#[cfg(feature = "fuzz")]
NoInstructionsRemain,
addisoncrump marked this conversation as resolved.
Show resolved Hide resolved
}

impl std::fmt::Display for JsNativeErrorKind {
Expand All @@ -760,6 +781,8 @@ impl std::fmt::Display for JsNativeErrorKind {
JsNativeErrorKind::Syntax => "SyntaxError",
JsNativeErrorKind::Type => "TypeError",
JsNativeErrorKind::Uri => "UriError",
#[cfg(feature = "fuzz")]
JsNativeErrorKind::NoInstructionsRemain => "NoInstructionsRemain",
}
.fmt(f)
}
Expand Down
9 changes: 9 additions & 0 deletions boa_engine/src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use crate::{
vm::{call_frame::CatchAddresses, code_block::Readable},
Context, JsResult, JsValue,
};
#[cfg(feature = "fuzz")]
use crate::{JsError, JsNativeError};
use boa_interner::ToInternedString;
use boa_profiler::Profiler;
use std::{convert::TryInto, mem::size_of, time::Instant};
Expand Down Expand Up @@ -179,6 +181,13 @@ impl Context {
});

while self.vm.frame().pc < self.vm.frame().code.code.len() {
#[cfg(feature = "fuzz")]
if self.instructions_remaining == 0 {
return Err(JsError::from_native(JsNativeError::no_instructions_remain()));
} else {
self.instructions_remaining -= 1;
}

let result = if self.vm.trace {
let mut pc = self.vm.frame().pc;
let opcode: Opcode = self
Expand Down
31 changes: 6 additions & 25 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ name = "parser-idempotency"
path = "fuzz_targets/parser-idempotency.rs"
test = false
doc = false

[[bin]]
name = "vm-implied"
path = "fuzz_targets/vm-implied.rs"
test = false
doc = false

[[bin]]
name = "bytecompiler-implied"
path = "fuzz_targets/bytecompiler-implied.rs"
test = false
doc = false
19 changes: 19 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,22 @@ following:
information, as the inputs parsed between the two should be the same.

In this way, this fuzzer can identify correctness issues present in the parser.

## Bytecompiler Fuzzer

The bytecompiler fuzzer, located in [bytecompiler-implied.rs](fuzz_targets/bytecompiler-implied.rs), identifies cases
which cause an assertion failure in the bytecompiler. These crashes can cause denial of service issues and may block the
discovery of crash cases in the VM fuzzer.

## VM Fuzzer

The VM fuzzer, located in [vm-implied.rs](fuzz_targets/vm-implied.rs), identifies crash cases in the VM. It does so by
generating an arbitrary AST, converting it to source code (to remove invalid inputs), then executing that source code.
Because we are not comparing against any invariants other than "does it crash", this fuzzer will only discover faults
which cause the VM to terminate unexpectedly, e.g. as a result of a panic. It will not discover logic errors present in
the VM.

To ensure that the VM does not attempt to execute an infinite loop, Boa is restricted to a finite number of instructions
before the VM is terminated. If a program takes more than a second or so to execute, it likely indicates an issue in the
VM (as we expect the fuzzer to execute only a certain amount of instructions, which should take significantly less
time).
25 changes: 25 additions & 0 deletions fuzz/fuzz_targets/bytecompiler-implied.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![no_main]

mod common;

use crate::common::FuzzSource;
use boa_engine::Context;
use boa_parser::Parser;
use libfuzzer_sys::{fuzz_target, Corpus};
use std::io::Cursor;

fn do_fuzz(original: FuzzSource) -> Corpus {
let mut ctx = Context::builder()
.interner(original.interner)
.instructions_remaining(0)
.build();
let mut parser = Parser::new(Cursor::new(&original.source));
if let Ok(parsed) = parser.parse_all(ctx.interner_mut()) {
let _ = ctx.compile(&parsed);
Corpus::Keep
} else {
Corpus::Reject
}
}

fuzz_target!(|original: FuzzSource| -> Corpus { do_fuzz(original) });
24 changes: 23 additions & 1 deletion fuzz/fuzz_targets/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use boa_ast::{
visitor::{VisitWith, VisitorMut},
Expression, StatementList,
};
use boa_interner::{Interner, Sym};
use boa_interner::{Interner, Sym, ToInternedString};
use libfuzzer_sys::arbitrary;
use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured};
use std::fmt::{Debug, Formatter};
Expand Down Expand Up @@ -72,3 +72,25 @@ impl Debug for FuzzData {
.finish_non_exhaustive()
}
}

pub struct FuzzSource {
pub interner: Interner,
pub source: String,
}

impl<'a> Arbitrary<'a> for FuzzSource {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let data = FuzzData::arbitrary(u)?;
let source = data.ast.to_interned_string(&data.interner);
Ok(Self {
interner: data.interner,
source,
})
}
}

impl Debug for FuzzSource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("Fuzzed source:\n{}", self.source))
}
}
19 changes: 19 additions & 0 deletions fuzz/fuzz_targets/vm-implied.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#![no_main]

mod common;

use crate::common::FuzzSource;
use boa_engine::{Context, JsResult, JsValue};
use libfuzzer_sys::fuzz_target;

fn do_fuzz(original: FuzzSource) -> JsResult<JsValue> {
let mut ctx = Context::builder()
.interner(original.interner)
.instructions_remaining(1 << 16)
.build();
ctx.eval(&original.source)
}

fuzz_target!(|original: FuzzSource| {
let _ = do_fuzz(original);
});