Skip to content

implement fuzzing for component types#4537

Merged
alexcrichton merged 1 commit intobytecodealliance:mainfrom
dicej:component-fuzz-generator
Aug 4, 2022
Merged

implement fuzzing for component types#4537
alexcrichton merged 1 commit intobytecodealliance:mainfrom
dicej:component-fuzz-generator

Conversation

@dicej
Copy link
Copy Markdown
Contributor

@dicej dicej commented Jul 26, 2022

This addresses #4307.

For the static API we generate 100 arbitrary test cases at build time, each of
which includes 0-5 parameter types, a result type, and a WAT fragment containing
an imported function and an exported function. The exported function calls the
imported function, which is implemented by the host. At runtime, the fuzz test
selects a test case at random and feeds it zero or more sets of arbitrary
parameters and results, checking that values which flow host-to-guest and
guest-to-host make the transition unchanged.

The fuzz test for the dynamic API follows a similar pattern, the only difference
being that test cases are generated at runtime.

Signed-off-by: Joel Dice joel.dice@fermyon.com

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 26, 2022

I need to add doc comments to component_types.rs (and remove the #![allow(missing_docs)] at the top. I'll do that tomorrow, but I wanted to open this PR now to give people a chance to look at it in the meantime.

@github-actions github-actions bot added the fuzzing Issues related to our fuzzing infrastructure label Jul 27, 2022
@github-actions
Copy link
Copy Markdown

Subscribe to Label Action

cc @fitzgen

Details This issue or pull request has been labeled: "fuzzing"

Thus the following users have been cc'd because of the following labels:

  • fitzgen: fuzzing

To subscribe or unsubscribe from this label, edit the .github/subscribe-to-label.json configuration file.

Learn more.

@dicej dicej force-pushed the component-fuzz-generator branch 2 times, most recently from 8fe0fdb to 8872259 Compare July 27, 2022 16:16
@alexcrichton
Copy link
Copy Markdown
Member

This is looking awesome, thanks again for pushing on this! I've got a few thoughts of how to simplify this and possibly extend it a bit more as well:

  • Currently this is setup as a unit-test generator but I think that the current support you've got may be better modeled as a fuzzer instead. For example you've got a "convert a value to Rust source code" baked in which I think would be fine to remove if this were a fuzzer. The fuzzer I'm imagining would look something like:
    1. Use the Unstructured to pick a test to run (each has a 1/N chance of running in theory)
    2. Use the input data to generate input values
    3. Run the test per-input-value to ensure the round-trip happens successfully
  • Currently this is exercising same-type-both-in-and-out but could this be extended to support multiple parameters and a different return type? I think that could help improve our test case coverage by mixing things up more often. I'm imagining that there are 0-5 inputs and 1 output. In the fuzzer scheme above the fuzzer would generate the inputs and outputs and then feed the inputs into the component, asserting that the imported function is called with the same inputs. The imported function then returns the canned output and asserts that the same output pops out the other side. That should test a variety of nontrivial signatures as well as lifting/lowering in all the various positions
  • During fuzz/test case runs I think it might be good to print out the seed at the top which in case a failure happens we should have the seed on-hand to reproduce with. In general though I think we want CI to be deterministic so for general fuzzing and CI we should draw the seed from a deterministic source (maybe like the git rev? or something like that?)

One day in the future we can also work on deduplicating all the size/alignment/etc calculations. I feel like you and I have written the same size/align/flatten/etc in so many different places now we can do it while we dream. Don't worry about it this PR though, the logic is fairly trivial and not the easiest to get wrong so I think this is something we should look to do later. I also don't think it will be easy to share things since it's needed in ever-so-slightly-different contexts everywhere, so it may also just be a pipe dream.

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 27, 2022

@alexcrichton I'm having trouble imagining how this would work as a fuzz test and still use the TypedFunc API. A fuzz test based on the dynamic Func API is certainly reasonable, and I've been working on that today. Can you elaborate on what a TypedFunc-based fuzz test would look like? Specifically, I'm not sure how we would construct instances of arbitrary types at runtime.

Regarding the suggestion to generalize the tests to accept arbitrary numbers of parameters and return other instances of other types: yes, that should be easy to do.

I'll add some code to print the seed to stderr, and I'll see what I can do to derive the seed from the git rev in CI.

@alexcrichton
Copy link
Copy Markdown
Member

This is what I have in mind:

fn test<'a, P, R>(data: &mut Unstructured<'a>, component: &str) -> arbitrary::Result<()>
where
    P: Lift + Lower + Clone + PartialEq + Debug + Arbitrary<'a>,
    R: Lift + Lower + Clone + PartialEq + Debug + Arbitrary<'a>,
{
    let mut config = Config::new();
    config.wasm_component_model(true);
    let engine = Engine::new(&config).unwrap();
    let component = Component::new(&engine, component).unwrap();
    let mut linker = Linker::new(&engine);
    linker
        .root()
        .func_wrap(
            "host",
            |cx: StoreContextMut<'_, (Option<P>, Option<R>)>, param: P| -> Result<R> {
                let (expected_params, result) = cx.data();
                assert_eq!(param, *expected_params.as_ref().unwrap());
                Ok(result.as_ref().unwrap().clone())
            },
        )
        .unwrap();
    let mut store = Store::new(&engine, (None, None));
    let instance = linker.instantiate(&mut store, &component).unwrap();
    let func = instance
        .get_typed_func::<(P,), R, _>(&mut store, "run")
        .unwrap();

    while data.arbitrary()? {
        let params = data.arbitrary::<P>()?;
        let result = data.arbitrary::<R>()?;
        *store.data_mut() = (Some(params.clone()), Some(result.clone()));

        assert_eq!(func.call(&mut store, (params,)).unwrap(), result);
        func.post_return(&mut store).unwrap();
    }

    Ok(())
}

As I wrote this out supporting more than one parameter is actually nontrivial given that in the closure it needs to be a: A, b: B, c: C, ... and in the get_typed_func it needs to be (A, B, C). There could be test0/test1/testN, however.

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 27, 2022

@alexcrichton Thanks for elaborating. That makes sense.

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 28, 2022

@alexcrichton At the moment, I'm still focusing on the dynamic API oracle (having realized that #4310 did not cover dynamic host function wrapping, so I'm implementing that), but I could use some more clarification about what you have in mind for the static API tests.

What I'm envisioning now is code in build.rs which generates 1000 arbitrary test cases, each of which includes 0-5 arbitrary types to use as parameters and one type to use as the result. Each of those tests will (at runtime) call one of six functions (one for each arity) which are generated by a simple declarative macro, following the pattern described by your test function above. Thus we'll generate types and test{0-5} calls using Unstructured at build time, and use Unstructured again at runtime to generate values of those types.

I'm not sure that matches your vision, though, since it doesn't include the "Use the Unstructured to pick a test to run (each has a 1/N chance of running in theory)" step you mentioned above. Can you clarify what you meant by that?

Thanks for the help as always.

@alexcrichton
Copy link
Copy Markdown
Member

Oh sure yeah. I personally think this is still best modeled as a fuzz target so I don't think that this will be part of unit tests run on CI, but I'm imagining that the fuzz target looks something like:

let mut u = Unstructured::new(raw_libfuzzer_input);
match u.int_in_range(0..=100)? {
    0 => test0::<u32>(&mut u, echo_component_0_wat),
    1 => test5::<f32, Option<u32>, ...>(&mut u, echo_component_1_wat),
    // ...
}

the 1000 tests are 1000 different type signatures that could be invoked, and the fuzz input indicates which type signature should be tested for that particular fuzz input. Mutation in libfuzzer should then help hit all of the signatures.

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 28, 2022

Another question (probably not the last): Is there a precedent for generated fuzz tests? I think all the current fuzz tests are hand-written, so I'm not sure how generated code would fit in. My first thought is to add a build.rs to wasmtime-fuzz which generates a fuzz_targets/static_component_api.rs and add lines to the Cargo.toml to reference it, e.g.:

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

(plus add a line to .gitignore to ignore fuzz_targets/static_component_api.rs)

Is there a better way?

@alexcrichton
Copy link
Copy Markdown
Member

No worries!

Currently we have no procedurally generated fuzz tests. That being said we don't want to generate files that are present in the source tree. To get around that though the build.rs can output a file in OUT_DIR and then fuzz_targets/static_component_api.rs can contain:

include!(concat!(env!("OUT_DIR"), "/your_generated_file.rs"));

and that should suffice

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 28, 2022

Another question: How should I handle arbitrary::Error::NotEnoughData in these tests? Just return normally and let libFuzzer call again with a new byte slice? Is there a better (i.e. more efficient) way to handle it?

@alexcrichton
Copy link
Copy Markdown
Member

Yeah that's the general scheme for what we do with libfuzzer, basically just "discard" the fuzz input and libfuzzer will realize that other mutated inputs have more coverage and will probably eventually itself discard the original one from its corpus

@dicej dicej force-pushed the component-fuzz-generator branch from a40dc86 to 6c412dc Compare July 29, 2022 00:02
@github-actions github-actions bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Jul 29, 2022
@github-actions
Copy link
Copy Markdown

Subscribe to Label Action

cc @peterhuene

Details This issue or pull request has been labeled: "wasmtime:api"

Thus the following users have been cc'd because of the following labels:

  • peterhuene: wasmtime:api

To subscribe or unsubscribe from this label, edit the .github/subscribe-to-label.json configuration file.

Learn more.

@dicej dicej force-pushed the component-fuzz-generator branch from 6c412dc to 1514bd0 Compare July 29, 2022 03:42
@dicej dicej changed the title implement fuzzing for statically-defined component types implement fuzzing for component types Jul 29, 2022
@dicej dicej force-pushed the component-fuzz-generator branch from 7027323 to 9f53dc9 Compare July 29, 2022 16:49
@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 29, 2022

This is ready for another review. Per #4307 (comment), CI will be broken until we've enabled the component-model feature unconditionally when testing.

Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great, and I'm excited that it's already catching a few bugs here and there! I think the biggest thing that needs resolution is not pulling in all of wasmtime-fuzzing and its dependency tree to a build dependency. Eventually I want to deduplicate the Arbitrary type bits if we can between the fact-valid-module fuzzer, the size/alignment calculations, and ideally the type hierarchy as well.

@alexcrichton
Copy link
Copy Markdown
Member

Also it looks like you may have had to deal with a lot of Cargo features and conditional compilation in this PR to get fiddly bits to work. I think it's fine to say that during testing we can assume that the component model is unconditionally enabled and hopefully avoid too much of a maze of optional dependencies and such.

alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 29, 2022
This commit implements one of the two remaining types for adapter
fusion, lists. This implementation is particularly tricky for a number
of reasons:

* Lists have a number of validity checks which need to be carefully
  implemented. For example the byte length of the list passed to
  allocation in the destination module could overflow the 32-bit index
  space. Additionally lists in 32-bit memories need a check that their
  final address is in-bounds in the address space.

* In the effort to go ahead and support memory64 at the lowest layers
  this is where much of the magic happens. Lists are naturally always
  stored in memory and shifting between 64/32-bit address spaces
  is done here. This notably required plumbing an `Options` around
  during flattening/size/alignment calculations due to the size/types of
  lists changing depending on the memory configuration.

I've also added a small `factc` program in this commit which should
hopefully assist in exploring and debugging adapter modules. This takes
as input a component (text or binary format) and then generates an
adapter module for all component function signatures found internally.

This commit notably does not include tests for lists. I tried to figure
out a good way to add these but I felt like there were too many cases to
test and the tests would otherwise be extremely verbose. Instead I think
the best testing strategy for this commit will be through bytecodealliance#4537 which
should be relatively extensible to testing adapters between modules in
addition to host-based lifting/lowering.
@dicej dicej force-pushed the component-fuzz-generator branch from 8fceba5 to be75789 Compare July 29, 2022 21:37
@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Jul 29, 2022

I've addressed a few of the issues above. Planning to work on the rest on Monday.


// Implement for functions with a leading `StoreContextMut` parameter
#[allow(non_snake_case)]
impl<T, F> IntoComponentFunc<T, (StoreContextMut<'_, T>, &[Val]), Val> for F
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry I forgot to review this in the initial first pass. We won't want to have this implementation though because the IntoComponentFunc impl is only intended for host functions where the component-type of the function can be statically inferred. As you have probably surmised with the lock/etc in typechecking for these functions the "dynamic" interface here doesn't really match the interface for static functions.

Instead what we'll want to do is something along the lines of the difference between wasmtime::Func::{wrap,new} where here you're defining the new equivalent. That will grow a new function component::Linker::func_new which will take the type of the function as its argument as well as a closure. The return values of the closure are then type-checked against the original type at runtime before yielding the result back to the original component.

This may be worth splitting to its own PR if you'd prefer. I'm also happy to review it here as well though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind sketching out an example of how your proposed API would be used? I'm specifically interested in where the user would get the type of the function to pass to component::Linker::func_new. Would they query the Component for the things it imports, find the function of interest, and get the type from that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that is indeed a good question. In core wasm users would do FuncType::new(...) and create their own function type. In the fullness of time I'd expect the same operation to be available to components where function types could be manufactured in addition to being learned from components. That's a big missing piece of the embedding API for components right now though so, yeah, getting it from a component seems the most reasonable for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcrichton I've just pushed an update per your suggested API. Let me know what you think.

@dicej dicej force-pushed the component-fuzz-generator branch from 5dda2f7 to 73a18aa Compare August 1, 2022 15:58
@alexcrichton
Copy link
Copy Markdown
Member

Also to get a head start on deduplicating internally within the codebase, could you update the fact-valid-module fuzzer to using this new enum Type in the component-fuzz-util crate?

During the intern phase of that fuzzer not all types are supported yet so I think it would be fine to return a Result from interning and "throw out" test cases with unsupported types (e.g. lists/strings at the time of this writing)

@dicej dicej force-pushed the component-fuzz-generator branch from 73a18aa to 8800b14 Compare August 1, 2022 17:53
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Aug 1, 2022
This commit implements one of the two remaining types for adapter
fusion, lists. This implementation is particularly tricky for a number
of reasons:

* Lists have a number of validity checks which need to be carefully
  implemented. For example the byte length of the list passed to
  allocation in the destination module could overflow the 32-bit index
  space. Additionally lists in 32-bit memories need a check that their
  final address is in-bounds in the address space.

* In the effort to go ahead and support memory64 at the lowest layers
  this is where much of the magic happens. Lists are naturally always
  stored in memory and shifting between 64/32-bit address spaces
  is done here. This notably required plumbing an `Options` around
  during flattening/size/alignment calculations due to the size/types of
  lists changing depending on the memory configuration.

I've also added a small `factc` program in this commit which should
hopefully assist in exploring and debugging adapter modules. This takes
as input a component (text or binary format) and then generates an
adapter module for all component function signatures found internally.

This commit notably does not include tests for lists. I tried to figure
out a good way to add these but I felt like there were too many cases to
test and the tests would otherwise be extremely verbose. Instead I think
the best testing strategy for this commit will be through bytecodealliance#4537 which
should be relatively extensible to testing adapters between modules in
addition to host-based lifting/lowering.
alexcrichton added a commit that referenced this pull request Aug 1, 2022
* Implement fused adapters for `(list T)` types

This commit implements one of the two remaining types for adapter
fusion, lists. This implementation is particularly tricky for a number
of reasons:

* Lists have a number of validity checks which need to be carefully
  implemented. For example the byte length of the list passed to
  allocation in the destination module could overflow the 32-bit index
  space. Additionally lists in 32-bit memories need a check that their
  final address is in-bounds in the address space.

* In the effort to go ahead and support memory64 at the lowest layers
  this is where much of the magic happens. Lists are naturally always
  stored in memory and shifting between 64/32-bit address spaces
  is done here. This notably required plumbing an `Options` around
  during flattening/size/alignment calculations due to the size/types of
  lists changing depending on the memory configuration.

I've also added a small `factc` program in this commit which should
hopefully assist in exploring and debugging adapter modules. This takes
as input a component (text or binary format) and then generates an
adapter module for all component function signatures found internally.

This commit notably does not include tests for lists. I tried to figure
out a good way to add these but I felt like there were too many cases to
test and the tests would otherwise be extremely verbose. Instead I think
the best testing strategy for this commit will be through #4537 which
should be relatively extensible to testing adapters between modules in
addition to host-based lifting/lowering.

* Improve handling of lists of 0-size types

* Skip overflow checks on byte sizes for 0-size types
* Skip the copy loop entirely when src/dst are both 0
* Skip the increments of src/dst pointers if either is 0-size

* Update semantics for zero-sized lists/strings

When a list/string has a 0-byte-size the base pointer is no longer
verified to be in-bounds to match the supposedly desired adapter
semantics where no trap happens because no turn of the loop happens.
@dicej dicej force-pushed the component-fuzz-generator branch from 8800b14 to 8cc607f Compare August 1, 2022 23:45
Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks! I don't think this is the final shape we'll want of func_new but for now I think it's good to leave it as-is to get this landed and it can be iterated on further later.

Also, to confirm, it looks like a few bugs have been found already but have you been able to run this fuzzer locally for a few minutes to ensure that there's no other "easy" fuzz bugs to find?

@dicej
Copy link
Copy Markdown
Contributor Author

dicej commented Aug 2, 2022

@alexcrichton I believe I've addressed all your latest feedback (deferring func_new API changes for later). And yes, I've run the fuzz tests for an hour or so without issue.

Copy link
Copy Markdown
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, thanks again! With a rebase I think this should be good to go.

This addresses bytecodealliance#4307.

For the static API we generate 100 arbitrary test cases at build time, each of
which includes 0-5 parameter types, a result type, and a WAT fragment containing
an imported function and an exported function.  The exported function calls the
imported function, which is implemented by the host.  At runtime, the fuzz test
selects a test case at random and feeds it zero or more sets of arbitrary
parameters and results, checking that values which flow host-to-guest and
guest-to-host make the transition unchanged.

The fuzz test for the dynamic API follows a similar pattern, the only difference
being that test cases are generated at runtime.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej dicej force-pushed the component-fuzz-generator branch from 0dee43c to 68dc79e Compare August 3, 2022 14:57
@alexcrichton alexcrichton merged commit ed8908e into bytecodealliance:main Aug 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fuzzing Issues related to our fuzzing infrastructure wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants