Coverage Report

Created: 2025-12-09 07:28

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
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
}