Coverage Report

Created: 2026-08-15 07:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
1.96k
pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
9
1.96k
    let wasm = u.arbitrary().and_then(|config| {
10
1.96k
        log::debug!("config: {config:#?}");
11
1.96k
        wit_smith::smith(&config, u)
12
1.96k
    })?;
13
1.93k
    write_file("doc1.wasm", &wasm);
14
1.93k
    let (resolve, pkg) = match wit_component::decode(&wasm).unwrap() {
15
1.93k
        DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg),
16
0
        DecodedWasm::Component(..) => unreachable!(),
17
    };
18
1.93k
    resolve.assert_valid();
19
20
1.93k
    roundtrip_through_printing("doc1", &resolve, pkg, &wasm);
21
22
1.93k
    let (resolve2, pkg2) = match wit_component::decode(&wasm).unwrap() {
23
1.93k
        DecodedWasm::WitPackage(resolve, pkgs) => (resolve, pkgs),
24
0
        DecodedWasm::Component(..) => unreachable!(),
25
    };
26
1.93k
    resolve2.assert_valid();
27
28
1.93k
    let wasm2 = wit_component::encode(&resolve2, pkg2).expect("failed to encode WIT document");
29
1.93k
    write_file("doc2.wasm", &wasm2);
30
1.93k
    roundtrip_through_printing("doc2", &resolve2, pkg2, &wasm2);
31
32
1.93k
    if wasm != wasm2 {
33
0
        panic!("roundtrip wasm didn't match");
34
1.93k
    }
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
1.93k
    let mut decoded_bindgens = Vec::new();
39
2.89k
    for (id, world) in resolve.worlds.iter().take(20) {
40
2.89k
        let mangling = match u.int_in_range(0..=3)? {
41
2.31k
            0 => ManglingAndAbi::Legacy(LiftLowerAbi::Sync),
42
212
            1 => ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback),
43
221
            2 => ManglingAndAbi::Legacy(LiftLowerAbi::AsyncStackful),
44
143
            3 => ManglingAndAbi::Standard32,
45
0
            _ => unreachable!(),
46
        };
47
2.89k
        log::debug!(
48
            "embedding world {} as in a dummy module with abi {mangling:?}",
49
            world.name
50
        );
51
2.89k
        let mut dummy = wit_component::dummy_module(&resolve, id, mangling);
52
2.89k
        if u.arbitrary()? {
53
377
            let mut dst = Module::default();
54
377
            let mut reencode = RemoveImports {
55
377
                u,
56
377
                removed_funcs: 0,
57
377
            };
58
377
            if reencode
59
377
                .parse_core_module(&mut dst, Default::default(), &dummy)
60
377
                .is_ok()
61
377
            {
62
377
                dummy = dst.finish();
63
377
            }
64
2.51k
        }
65
2.89k
        wit_component::embed_component_metadata(&mut dummy, &resolve, id, StringEncoding::UTF8)
66
2.89k
            .unwrap();
67
2.89k
        write_file("dummy.wasm", &dummy);
68
69
2.89k
        log::debug!("... componentizing the world into a binary component");
70
2.89k
        let wasm = wit_component::ComponentEncoder::default()
71
2.89k
            .shim_return_call_ref(u.arbitrary()?)
72
2.89k
            .debug_names(u.arbitrary()?)
73
2.89k
            .realloc_via_memory_grow(u.arbitrary()?)
74
2.89k
            .module(&dummy)
75
2.89k
            .unwrap()
76
2.89k
            .encode()
77
2.89k
            .unwrap();
78
2.89k
        write_file("dummy.component.wasm", &wasm);
79
2.89k
        wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all())
80
2.89k
            .validate_all(&wasm)
81
2.89k
            .unwrap();
82
83
        // Decode what was just created and record it later for testing merging
84
        // worlds together.
85
2.89k
        let (dresolve, dworldid) = match wit_component::decode(&wasm).unwrap() {
86
2.89k
            wit_component::DecodedWasm::Component(r, w) => (r, w),
87
0
            _ => unreachable!(),
88
        };
89
2.89k
        decoded_bindgens.push((dresolve, dworldid, dummy, world.name.clone()));
90
91
2.89k
        log::debug!("... decoding the component itself");
92
2.89k
        wit_component::decode(&wasm).unwrap();
93
94
        // Test out importizing the world and then assert the world is still
95
        // valid.
96
2.89k
        log::debug!("... importizing this world");
97
2.89k
        let mut resolve2 = resolve.clone();
98
2.89k
        let _ = resolve2.importize(id, None);
99
100
        // Test out `generate_nominal_type_ids`
101
2.89k
        log::debug!("... calling `generate_nominal_type_ids`");
102
2.89k
        let mut resolve2 = resolve.clone();
103
2.89k
        resolve2.generate_nominal_type_ids(id);
104
    }
105
106
1.93k
    if decoded_bindgens.len() < 2 {
107
1.44k
        return Ok(());
108
483
    }
109
110
483
    let i = u.choose_index(decoded_bindgens.len())?;
111
483
    let (mut b1, worldid1, wasm1, world1) = decoded_bindgens.swap_remove(i);
112
113
483
    if u.arbitrary()? {
114
78
        let i = u.choose_index(decoded_bindgens.len())?;
115
78
        let (mut b2, worldid2, wasm2, world2) = decoded_bindgens.swap_remove(i);
116
117
78
        log::debug!("merging bindgens world {world1} <- world {world2}");
118
119
78
        write_file("bindgen1.wasm", &wasm1);
120
78
        write_file("bindgen2.wasm", &wasm2);
121
122
78
        let pkg2 = b2.worlds[worldid2].package.unwrap();
123
78
        let mut name = b2.packages[pkg2].name.clone();
124
78
        b2.package_names.swap_remove(&name).unwrap();
125
78
        name.name.push_str("2");
126
78
        let prev = b2.package_names.insert(name.clone(), pkg2);
127
78
        assert!(prev.is_none());
128
78
        b2.packages[pkg2].name = name;
129
130
78
        let only_interfaces = b1.worlds[worldid1]
131
78
            .imports
132
78
            .iter()
133
78
            .chain(b1.worlds[worldid1].exports.iter())
134
78
            .chain(b2.worlds[worldid2].imports.iter())
135
78
            .chain(b2.worlds[worldid2].exports.iter())
136
78
            .all(|(id, _)| matches!(id, wit_parser::WorldKey::Interface(_)));
137
138
        // Merging worlds may fail but if successful then a `Resolve` is asserted
139
        // to be valid which is what we're interested in here. Note that failure
140
        // here can be due to the structure of worlds which aren't reasonable to
141
        // control in this generator, so it's just done to see what happens and try
142
        // to trigger panics in `Resolve::assert_valid`.
143
78
        let merge_result = b1.merge(b2);
144
145
78
        if only_interfaces {
146
16
            merge_result.unwrap();
147
62
        }
148
    } else {
149
405
        log::debug!("merging world imports based on semver {world1}");
150
405
        write_file("bindgen1.wasm", &wasm1);
151
405
        let _ = b1.merge_world_imports_based_on_semver(worldid1);
152
    }
153
483
    Ok(())
154
1.96k
}
155
156
3.86k
fn roundtrip_through_printing(file: &str, resolve: &Resolve, pkg: PackageId, wasm: &[u8]) {
157
    // Print to a single string, using nested `package ... { .. }` statements,
158
    // and then parse that in a new `Resolve`.
159
3.86k
    let mut new_resolve = Resolve::default();
160
3.86k
    new_resolve.all_features = true;
161
3.86k
    let package_deps = resolve
162
3.86k
        .packages
163
3.86k
        .iter()
164
3.86k
        .map(|p| p.0)
165
4.41k
        .filter(|k| *k != pkg)
166
3.86k
        .collect::<Vec<_>>();
167
3.86k
    let mut printer = WitPrinter::default();
168
3.86k
    printer.print(resolve, pkg, &package_deps).unwrap();
169
3.86k
    let doc = printer.output.to_string();
170
3.86k
    let new_pkg = new_resolve
171
3.86k
        .push_str(&format!("printed-{file}.wit"), &doc)
172
3.86k
        .unwrap();
173
174
    // Finally encode the `new_resolve` which should be the exact same as
175
    // before.
176
3.86k
    let wasm2 = wit_component::encode(&new_resolve, new_pkg).unwrap();
177
3.86k
    write_file(&format!("{file}-reencoded.wasm"), &wasm2);
178
3.86k
    if wasm != wasm2 {
179
0
        panic!("failed to roundtrip through text printing");
180
3.86k
    }
181
3.86k
}
182
183
14.0k
fn write_file(path: &str, contents: impl AsRef<[u8]>) {
184
14.0k
    if !log::log_enabled!(log::Level::Debug) {
185
14.0k
        return;
186
0
    }
187
0
    log::debug!("writing file {path}");
188
0
    let contents = contents.as_ref();
189
0
    let path = Path::new(path);
190
0
    std::fs::write(path, contents).unwrap();
191
0
    if path.extension().and_then(|s| s.to_str()) == Some("wasm") {
192
0
        let path = path.with_extension("wat");
193
0
        log::debug!("writing file {}", path.display());
194
0
        std::fs::write(path, wasmprinter::print_bytes(&contents).unwrap()).unwrap();
195
0
    }
196
14.0k
}
197
198
struct RemoveImports<'a, 'b> {
199
    u: &'a mut Unstructured<'b>,
200
    removed_funcs: u32,
201
}
202
203
impl Reencode for RemoveImports<'_, '_> {
204
    type Error = std::convert::Infallible;
205
206
2.35k
    fn function_index(&mut self, idx: u32) -> Result<u32, reencode::Error<Self::Error>> {
207
2.35k
        Ok(idx - self.removed_funcs)
208
2.35k
    }
209
210
0
    fn parse_import(
211
0
        &mut self,
212
0
        imports: &mut ImportSection,
213
0
        import: wasmparser::Import<'_>,
214
0
    ) -> Result<(), reencode::Error<Self::Error>> {
215
0
        if self.u.arbitrary().unwrap_or(false) {
216
0
            self.removed_funcs += 1;
217
0
            Ok(())
218
        } else {
219
0
            reencode::utils::parse_import(self, imports, import)
220
        }
221
0
    }
222
}
223
224
#[test]
225
fn smoke() {
226
    super::test::test_n_times(100, run);
227
}