/src/wasm-tools/fuzz/src/roundtrip_wit.rs
Line | Count | Source |
1 | | use arbitrary::{Result, Unstructured}; |
2 | | use std::path::Path; |
3 | | use wasm_encoder::reencode::{self, Reencode}; |
4 | | use wasm_encoder::{ImportSection, Module}; |
5 | | use wit_component::*; |
6 | | use wit_parser::{LiftLowerAbi, ManglingAndAbi, PackageId, Resolve}; |
7 | | |
8 | 2.71k | pub fn run(u: &mut Unstructured<'_>) -> Result<()> { |
9 | 2.71k | let wasm = u.arbitrary().and_then(|config| { |
10 | 2.71k | log::debug!("config: {config:#?}"); |
11 | 2.71k | wit_smith::smith(&config, u) |
12 | 2.71k | })?; |
13 | 2.71k | write_file("doc1.wasm", &wasm); |
14 | 2.71k | let (resolve, pkg) = match wit_component::decode(&wasm).unwrap() { |
15 | 2.71k | DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg), |
16 | 0 | DecodedWasm::Component(..) => unreachable!(), |
17 | | }; |
18 | 2.71k | resolve.assert_valid(); |
19 | | |
20 | 2.71k | roundtrip_through_printing("doc1", &resolve, pkg, &wasm); |
21 | | |
22 | 2.71k | let (resolve2, pkg2) = match wit_component::decode(&wasm).unwrap() { |
23 | 2.71k | DecodedWasm::WitPackage(resolve, pkgs) => (resolve, pkgs), |
24 | 0 | DecodedWasm::Component(..) => unreachable!(), |
25 | | }; |
26 | 2.71k | resolve2.assert_valid(); |
27 | | |
28 | 2.71k | let wasm2 = wit_component::encode(&resolve2, pkg2).expect("failed to encode WIT document"); |
29 | 2.71k | write_file("doc2.wasm", &wasm2); |
30 | 2.71k | roundtrip_through_printing("doc2", &resolve2, pkg2, &wasm2); |
31 | | |
32 | 2.71k | if wasm != wasm2 { |
33 | 0 | panic!("roundtrip wasm didn't match"); |
34 | 2.71k | } |
35 | | |
36 | | // If there's hundreds or thousands of worlds only work with the first few |
37 | | // to avoid timing out this fuzzer with asan enabled. |
38 | 2.71k | let mut decoded_bindgens = Vec::new(); |
39 | 4.03k | for (id, world) in resolve.worlds.iter().take(20) { |
40 | 4.03k | let mangling = match u.int_in_range(0..=3)? { |
41 | 3.23k | 0 => ManglingAndAbi::Legacy(LiftLowerAbi::Sync), |
42 | 236 | 1 => ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback), |
43 | 360 | 2 => ManglingAndAbi::Legacy(LiftLowerAbi::AsyncStackful), |
44 | 206 | 3 => ManglingAndAbi::Standard32, |
45 | 0 | _ => unreachable!(), |
46 | | }; |
47 | 4.03k | log::debug!( |
48 | 0 | "embedding world {} as in a dummy module with abi {mangling:?}", |
49 | | world.name |
50 | | ); |
51 | 4.03k | let mut dummy = wit_component::dummy_module(&resolve, id, mangling); |
52 | 4.03k | if u.arbitrary()? { |
53 | 454 | let mut dst = Module::default(); |
54 | 454 | let mut reencode = RemoveImports { |
55 | 454 | u, |
56 | 454 | removed_funcs: 0, |
57 | 454 | }; |
58 | 454 | if reencode |
59 | 454 | .parse_core_module(&mut dst, Default::default(), &dummy) |
60 | 454 | .is_ok() |
61 | 454 | { |
62 | 454 | dummy = dst.finish(); |
63 | 454 | } |
64 | 3.57k | } |
65 | 4.03k | wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8) |
66 | 4.03k | .unwrap(); |
67 | 4.03k | write_file("dummy.wasm", &dummy); |
68 | | |
69 | 4.03k | log::debug!("... componentizing the world into a binary component"); |
70 | 4.03k | let wasm = wit_component::ComponentEncoder::default() |
71 | 4.03k | .module(&dummy) |
72 | 4.03k | .unwrap() |
73 | 4.03k | .encode() |
74 | 4.03k | .unwrap(); |
75 | 4.03k | write_file("dummy.component.wasm", &wasm); |
76 | 4.03k | wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all()) |
77 | 4.03k | .validate_all(&wasm) |
78 | 4.03k | .unwrap(); |
79 | | |
80 | | // Decode what was just created and record it later for testing merging |
81 | | // worlds together. |
82 | 4.03k | let (_, decoded) = wit_component::metadata::decode(&dummy).unwrap(); |
83 | 4.03k | decoded_bindgens.push((decoded, dummy, world.name.clone())); |
84 | | |
85 | 4.03k | log::debug!("... decoding the component itself"); |
86 | 4.03k | wit_component::decode(&wasm).unwrap(); |
87 | | |
88 | | // Test out importizing the world and then assert the world is still |
89 | | // valid. |
90 | 4.03k | log::debug!("... importizing this world"); |
91 | 4.03k | let mut resolve2 = resolve.clone(); |
92 | 4.03k | let _ = resolve2.importize(id, None); |
93 | | } |
94 | | |
95 | 2.71k | if decoded_bindgens.len() < 2 { |
96 | 1.99k | return Ok(()); |
97 | 721 | } |
98 | | |
99 | 721 | let i = u.choose_index(decoded_bindgens.len())?; |
100 | 721 | let (mut b1, wasm1, world1) = decoded_bindgens.swap_remove(i); |
101 | | |
102 | 721 | if u.arbitrary()? { |
103 | 98 | let i = u.choose_index(decoded_bindgens.len())?; |
104 | 98 | let (b2, wasm2, world2) = decoded_bindgens.swap_remove(i); |
105 | | |
106 | 98 | log::debug!("merging bindgens world {world1} <- world {world2}"); |
107 | | |
108 | 98 | write_file("bindgen1.wasm", &wasm1); |
109 | 98 | write_file("bindgen2.wasm", &wasm2); |
110 | | |
111 | | // Merging worlds may fail but if successful then a `Resolve` is asserted |
112 | | // to be valid which is what we're interested in here. Note that failure |
113 | | // here can be due to the structure of worlds which aren't reasonable to |
114 | | // control in this generator, so it's just done to see what happens and try |
115 | | // to trigger panics in `Resolve::assert_valid`. |
116 | 98 | let _ = b1.merge(b2); |
117 | | } else { |
118 | 623 | log::debug!("merging world imports based on semver {world1}"); |
119 | 623 | write_file("bindgen1.wasm", &wasm1); |
120 | 623 | let _ = b1.resolve.merge_world_imports_based_on_semver(b1.world); |
121 | | } |
122 | 721 | Ok(()) |
123 | 2.71k | } |
124 | | |
125 | 5.43k | fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, wasm: &[u8]) { |
126 | | // Print to a single string, using nested `package ... { .. }` statements, |
127 | | // and then parse that in a new `Resolve`. |
128 | 5.43k | let mut new_resolve = Resolve::default(); |
129 | 5.43k | new_resolve.all_features = true; |
130 | 5.43k | let package_deps = resolve |
131 | 5.43k | .packages |
132 | 5.43k | .iter() |
133 | 5.43k | .map(|p| p.0) |
134 | 5.92k | .filter(|k| *k != pkg) |
135 | 5.43k | .collect::<Vec<_>>(); |
136 | 5.43k | let mut printer = WitPrinter::default(); |
137 | 5.43k | printer.print(resolve, pkg, &package_deps).unwrap(); |
138 | 5.43k | let doc = printer.output.to_string(); |
139 | 5.43k | let new_pkg = new_resolve |
140 | 5.43k | .push_str(&format!("printed-{file}.wit"), &doc) |
141 | 5.43k | .unwrap(); |
142 | | |
143 | | // Finally encode the `new_resolve` which should be the exact same as |
144 | | // before. |
145 | 5.43k | let wasm2 = wit_component::encode(&new_resolve, new_pkg).unwrap(); |
146 | 5.43k | write_file(&format!("{file}-reencoded.wasm"), &wasm2); |
147 | 5.43k | if wasm != wasm2 { |
148 | 0 | panic!("failed to roundtrip through text printing"); |
149 | 5.43k | } |
150 | 5.43k | } |
151 | | |
152 | 19.7k | fn write_file(path: &str, contents: impl AsRef<[u8]>) { |
153 | 19.7k | if !log::log_enabled!(log::Level::Debug) { |
154 | 19.7k | return; |
155 | 0 | } |
156 | 0 | log::debug!("writing file {path}"); |
157 | 0 | let contents = contents.as_ref(); |
158 | 0 | let path = Path::new(path); |
159 | 0 | std::fs::write(path, contents).unwrap(); |
160 | 0 | if path.extension().and_then(|s| s.to_str()) == Some("wasm") { |
161 | 0 | let path = path.with_extension("wat"); |
162 | 0 | log::debug!("writing file {}", path.display()); |
163 | 0 | std::fs::write(path, wasmprinter::print_bytes(&contents).unwrap()).unwrap(); |
164 | 0 | } |
165 | 19.7k | } |
166 | | |
167 | | struct RemoveImports<'a, 'b> { |
168 | | u: &'a mut Unstructured<'b>, |
169 | | removed_funcs: u32, |
170 | | } |
171 | | |
172 | | impl Reencode for RemoveImports<'_, '_> { |
173 | | type Error = std::convert::Infallible; |
174 | | |
175 | 1.30k | fn function_index(&mut self, idx: u32) -> Result<u32, reencode::Error<Self::Error>> { |
176 | 1.30k | Ok(idx - self.removed_funcs) |
177 | 1.30k | } |
178 | | |
179 | 4.07k | fn parse_import( |
180 | 4.07k | &mut self, |
181 | 4.07k | imports: &mut ImportSection, |
182 | 4.07k | import: wasmparser::Import<'_>, |
183 | 4.07k | ) -> Result<(), reencode::Error<Self::Error>> { |
184 | 4.07k | if self.u.arbitrary().unwrap_or(false) { |
185 | 2.69k | self.removed_funcs += 1; |
186 | 2.69k | Ok(()) |
187 | | } else { |
188 | 1.38k | reencode::utils::parse_import(self, imports, import) |
189 | | } |
190 | 4.07k | } |
191 | | } |
192 | | |
193 | | #[test] |
194 | | fn smoke() { |
195 | | super::test::test_n_times(100, run); |
196 | | } |