implement fuzzing for component types#4537
implement fuzzing for component types#4537alexcrichton merged 1 commit intobytecodealliance:mainfrom
Conversation
|
I need to add doc comments to |
Subscribe to Label Actioncc @fitzgen DetailsThis issue or pull request has been labeled: "fuzzing"Thus the following users have been cc'd because of the following labels:
To subscribe or unsubscribe from this label, edit the |
8fe0fdb to
8872259
Compare
|
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:
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. |
|
@alexcrichton I'm having trouble imagining how this would work as a fuzz test and still use the 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. |
|
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 |
|
@alexcrichton Thanks for elaborating. That makes sense. |
|
@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 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. |
|
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. |
|
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 [[bin]]
name = "static_component_api"
path = "fuzz_targets/static_component_api.rs"
test = false
doc = false(plus add a line to .gitignore to ignore Is there a better way? |
|
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 include!(concat!(env!("OUT_DIR"), "/your_generated_file.rs"));and that should suffice |
|
Another question: How should I handle |
|
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 |
a40dc86 to
6c412dc
Compare
Subscribe to Label Actioncc @peterhuene DetailsThis issue or pull request has been labeled: "wasmtime:api"Thus the following users have been cc'd because of the following labels:
To subscribe or unsubscribe from this label, edit the |
6c412dc to
1514bd0
Compare
7027323 to
9f53dc9
Compare
|
This is ready for another review. Per #4307 (comment), CI will be broken until we've enabled the |
alexcrichton
left a comment
There was a problem hiding this comment.
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.
|
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. |
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.
8fceba5 to
be75789
Compare
|
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@alexcrichton I've just pushed an update per your suggested API. Let me know what you think.
5dda2f7 to
73a18aa
Compare
|
Also to get a head start on deduplicating internally within the codebase, could you update the During the intern phase of that fuzzer not all types are supported yet so I think it would be fine to return a |
73a18aa to
8800b14
Compare
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.
* 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.
8800b14 to
8cc607f
Compare
alexcrichton
left a comment
There was a problem hiding this comment.
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?
|
@alexcrichton I believe I've addressed all your latest feedback (deferring |
alexcrichton
left a comment
There was a problem hiding this comment.
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>
0dee43c to
68dc79e
Compare
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