feat: add dependency

This commit is contained in:
2023-01-20 22:36:19 +08:00
parent 68e8d103b4
commit cf8e579f27
644 changed files with 150099 additions and 14 deletions

View File

@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

View File

@@ -0,0 +1,41 @@
[package]
name = "boa_fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
boa_ast = { path = "../boa_ast", features = ["fuzz"] }
boa_engine = { path = "../boa_engine", features = ["fuzz"] }
boa_interner = { path = "../boa_interner", features = ["fuzz"] }
boa_parser = { path = "../boa_parser" }
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[profile.release]
debug = 1
[[bin]]
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

View File

@@ -0,0 +1,56 @@
# boa_engine-fuzz
This directory contains fuzzers which can be used to automatically identify faults present in Boa. All the fuzzers in
this directory are [grammar-aware](https://www.fuzzingbook.org/html/Grammars.html) (based on
[Arbitrary](https://docs.rs/arbitrary/latest/arbitrary/)) and coverage-guided. See [common.rs](fuzz/fuzz_targets/common.rs)
for implementation specifics.
You can run any fuzzer you wish with the following command (replacing `your-fuzzer` with a fuzzer availble in
fuzz_targets, e.g. `parser-idempotency`):
```bash
cargo fuzz run -s none your-fuzzer
```
Note that you may wish to use a different sanitizer option (`-s`) according to what kind of issue you're looking for.
Refer to the [cargo-fuzz book](https://rust-fuzz.github.io/book/cargo-fuzz.html) for details on how to select a
sanitizer and other flags.
## Parser Fuzzer
The parser fuzzer, located in [parser-idempotency.rs](fuzz/fuzz_targets/parser-idempotency.rs), identifies
correctness issues in both the parser and the AST-to-source conversion process (e.g., via `to_interned_string`) by
searching for inputs which are not idempotent over parsing and conversion back to source. It does this by doing the
following:
1. Generate an arbitrary AST
2. Convert that AST to source code with `to_interned_string`; we'll call this the "original source"
3. Parse the original source into an AST; we'll call this the "first AST"
- Arbitrary ASTs aren't guaranteed to be parseable; to avoid errors caused by this, we discard errors here.
4. Convert the first AST to source code with `to_interned_string`; we'll call this the "first source"
5. Parse the first source into an AST; we'll call this the "second AST"
- Since the original source was parseable, the first source must be parseable; emit any errors parsing produces.
6. Compare the first AST and the second AST. If they are not equal, emit an error.
- An error here indicates that either the parser or the AST-to-source conversion lost information or added incorrect
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).

View File

@@ -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) });

View File

@@ -0,0 +1,96 @@
use boa_ast::{
visitor::{VisitWith, VisitorMut},
Expression, StatementList,
};
use boa_interner::{Interner, Sym, ToInternedString};
use libfuzzer_sys::arbitrary;
use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured};
use std::fmt::{Debug, Formatter};
use std::ops::ControlFlow;
/// Context for performing fuzzing. This structure contains both the generated AST as well as the
/// context used to resolve the symbols therein.
pub struct FuzzData {
pub interner: Interner,
pub ast: StatementList,
}
impl<'a> Arbitrary<'a> for FuzzData {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let mut interner = Interner::with_capacity(8);
let mut syms_available = Vec::with_capacity(8);
for c in 'a'..='h' {
syms_available.push(interner.get_or_intern(&*String::from(c)));
}
let mut ast = StatementList::arbitrary(u)?;
struct FuzzReplacer<'a, 's, 'u> {
syms: &'s [Sym],
u: &'u mut Unstructured<'a>,
}
impl<'a, 's, 'u, 'ast> VisitorMut<'ast> for FuzzReplacer<'a, 's, 'u> {
type BreakTy = arbitrary::Error;
// TODO arbitrary strings literals?
fn visit_expression_mut(
&mut self,
node: &'ast mut Expression,
) -> ControlFlow<Self::BreakTy> {
if matches!(node, Expression::FormalParameterList(_)) {
match self.u.arbitrary() {
Ok(id) => *node = Expression::Identifier(id),
Err(e) => return ControlFlow::Break(e),
}
}
node.visit_with_mut(self)
}
fn visit_sym_mut(&mut self, node: &'ast mut Sym) -> ControlFlow<Self::BreakTy> {
*node = self.syms[node.get() % self.syms.len()];
ControlFlow::Continue(())
}
}
let mut replacer = FuzzReplacer {
syms: &syms_available,
u,
};
if let ControlFlow::Break(e) = replacer.visit_statement_list_mut(&mut ast) {
Err(e)
} else {
Ok(Self { interner, ast })
}
}
}
impl Debug for FuzzData {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FuzzData")
.field("ast", &self.ast)
.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))
}
}

View File

@@ -0,0 +1,74 @@
#![no_main]
mod common;
use crate::common::FuzzData;
use boa_interner::ToInternedString;
use boa_parser::Parser;
use libfuzzer_sys::fuzz_target;
use libfuzzer_sys::Corpus;
use std::error::Error;
use std::io::Cursor;
/// Fuzzer test harness. This function accepts the arbitrary AST and performs the fuzzing operation.
///
/// See [README.md](../README.md) for details on the design of this fuzzer.
fn do_fuzz(mut data: FuzzData) -> Result<(), Box<dyn Error>> {
let original = data.ast.to_interned_string(&data.interner);
let mut parser = Parser::new(Cursor::new(&original));
let before = data.interner.len();
// For a variety of reasons, we may not actually produce valid code here (e.g., nameless function).
// Fail fast and only make the next checks if we were valid.
if let Ok(first) = parser.parse_all(&mut data.interner) {
let after_first = data.interner.len();
let first_interned = first.to_interned_string(&data.interner);
assert_eq!(
before,
after_first,
"The number of interned symbols changed; a new string was read.\nBefore:\n{}\nAfter:\n{}\nBefore (AST):\n{:#?}\nAfter (AST):\n{:#?}",
original,
first_interned,
data.ast,
first
);
let mut parser = Parser::new(Cursor::new(&first_interned));
// Now, we most assuredly should produce valid code. It has already gone through a first pass.
let second = parser
.parse_all(&mut data.interner)
.expect("Could not parse the first-pass interned copy.");
let second_interned = second.to_interned_string(&data.interner);
let after_second = data.interner.len();
assert_eq!(
after_first,
after_second,
"The number of interned symbols changed; a new string was read.\nBefore:\n{}\nAfter:\n{}\nBefore (AST):\n{:#?}\nAfter (AST):\n{:#?}",
first_interned,
second_interned,
first,
second
);
assert_eq!(
first,
second,
"Expected the same AST after two intern passes, but found dissimilar.\nOriginal:\n{}\nFirst:\n{}\nSecond:\n{}",
original,
first_interned,
second_interned,
);
}
Ok(())
}
// Fuzz harness wrapper to expose it to libfuzzer (and thus cargo-fuzz)
// See: https://rust-fuzz.github.io/book/cargo-fuzz.html
fuzz_target!(|data: FuzzData| -> Corpus {
if do_fuzz(data).is_ok() {
Corpus::Keep
} else {
Corpus::Reject
}
});

View File

@@ -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);
});