Skip to main content

threatflux_binary_analysis/formats/
wasm.rs

1//! WebAssembly (Wasm) format parser
2
3use crate::{
4    BinaryFormatParser, BinaryFormatTrait, Result,
5    types::{
6        Architecture, BinaryFormat as Format, BinaryMetadata, Endianness, Export, Import, Section,
7        SectionPermissions, SectionType, SecurityFeatures, Symbol,
8    },
9};
10
11use wasmparser::{Parser, Payload, Validator};
12
13/// WebAssembly format parser
14pub struct WasmParser;
15
16impl BinaryFormatParser for WasmParser {
17    fn parse(data: &[u8]) -> Result<Box<dyn BinaryFormatTrait>> {
18        Ok(Box::new(WasmBinary::parse(data)?))
19    }
20
21    fn can_parse(data: &[u8]) -> bool {
22        data.len() >= 4 && &data[0..4] == b"\0asm"
23    }
24}
25
26/// Parsed WebAssembly binary
27pub struct WasmBinary {
28    metadata: BinaryMetadata,
29    sections: Vec<Section>,
30    imports: Vec<Import>,
31    exports: Vec<Export>,
32}
33
34fn preflight_wasm_structure(data: &[u8]) -> Result<()> {
35    let mut budget = super::ParseOutputBudget::default();
36
37    for payload in Parser::new(0).parse_all(data) {
38        let payload = payload?;
39        let records = match &payload {
40            Payload::TypeSection(section) => usize::try_from(section.count()).unwrap_or(usize::MAX),
41            Payload::ImportSection(section) => {
42                usize::try_from(section.count()).unwrap_or(usize::MAX)
43            }
44            Payload::FunctionSection(section) => {
45                usize::try_from(section.count()).unwrap_or(usize::MAX)
46            }
47            Payload::TableSection(section) => {
48                usize::try_from(section.count()).unwrap_or(usize::MAX)
49            }
50            Payload::MemorySection(section) => {
51                usize::try_from(section.count()).unwrap_or(usize::MAX)
52            }
53            Payload::TagSection(section) => usize::try_from(section.count()).unwrap_or(usize::MAX),
54            Payload::GlobalSection(section) => {
55                usize::try_from(section.count()).unwrap_or(usize::MAX)
56            }
57            Payload::ExportSection(section) => {
58                usize::try_from(section.count()).unwrap_or(usize::MAX)
59            }
60            Payload::ElementSection(section) => {
61                usize::try_from(section.count()).unwrap_or(usize::MAX)
62            }
63            Payload::DataSection(section) => usize::try_from(section.count()).unwrap_or(usize::MAX),
64            Payload::DataCountSection { count, .. } | Payload::CodeSectionStart { count, .. } => {
65                usize::try_from(*count).unwrap_or(usize::MAX)
66            }
67            Payload::StartSection { .. }
68            | Payload::CustomSection(_)
69            | Payload::UnknownSection { .. } => 1,
70            Payload::Version { .. } | Payload::CodeSectionEntry(_) | Payload::End(_) => 0,
71            _ => 1,
72        };
73        budget.claim_records(records, "WebAssembly structural records")?;
74    }
75
76    Ok(())
77}
78
79impl WasmBinary {
80    fn parse(data: &[u8]) -> Result<Self> {
81        preflight_wasm_structure(data)?;
82
83        // `Parser::parse_all` outlines function bodies without validating their
84        // instructions or cross-section semantics. Validate the complete module
85        // before publishing structural metadata as a successful parse.
86        Validator::new().validate_all(data)?;
87
88        let parser = Parser::new(0);
89        let mut output_budget = super::ParseOutputBudget::default();
90        let mut sections = Vec::new();
91        let mut imports = Vec::new();
92        let mut exports = Vec::new();
93        for payload in parser.parse_all(data) {
94            let payload = payload?;
95            match payload {
96                Payload::Version { .. } => {}
97                // A WebAssembly start value is a function index, not a virtual
98                // address, so it must not be reported through `entry_point`.
99                Payload::StartSection { .. } => {}
100                Payload::ImportSection(s) => {
101                    let range = s.range();
102                    for import in s.into_imports() {
103                        let import = import?;
104                        output_budget.reserve_record(&mut imports, "WebAssembly imports")?;
105                        imports.push(Import {
106                            name: output_budget
107                                .copy_name(import.name, "WebAssembly import name")?,
108                            library: Some(
109                                output_budget
110                                    .copy_name(import.module, "WebAssembly import module")?,
111                            ),
112                            address: None,
113                            ordinal: None,
114                        });
115                    }
116                    output_budget.reserve_record(&mut sections, "WebAssembly sections")?;
117                    sections.push(Section {
118                        name: output_budget.copy_name("import", "WebAssembly section name")?,
119                        address: 0,
120                        size: (range.end - range.start) as u64,
121                        offset: range.start as u64,
122                        file_size: (range.end - range.start) as u64,
123                        permissions: SectionPermissions {
124                            read: true,
125                            write: false,
126                            execute: false,
127                        },
128                        section_type: SectionType::Other(
129                            output_budget.copy_name("Import", "WebAssembly section type")?,
130                        ),
131                        data: None,
132                    });
133                }
134                Payload::ExportSection(s) => {
135                    let range = s.range();
136                    for export in s {
137                        let export = export?;
138                        output_budget.reserve_record(&mut exports, "WebAssembly exports")?;
139                        exports.push(Export {
140                            name: output_budget
141                                .copy_name(export.name, "WebAssembly export name")?,
142                            address: 0,
143                            ordinal: None,
144                            forwarded_name: None,
145                        });
146                    }
147                    output_budget.reserve_record(&mut sections, "WebAssembly sections")?;
148                    sections.push(Section {
149                        name: output_budget.copy_name("export", "WebAssembly section name")?,
150                        address: 0,
151                        size: (range.end - range.start) as u64,
152                        offset: range.start as u64,
153                        file_size: (range.end - range.start) as u64,
154                        permissions: SectionPermissions {
155                            read: true,
156                            write: false,
157                            execute: false,
158                        },
159                        section_type: SectionType::Other(
160                            output_budget.copy_name("Export", "WebAssembly section type")?,
161                        ),
162                        data: None,
163                    });
164                }
165                Payload::CodeSectionStart { range, .. } => {
166                    output_budget.reserve_record(&mut sections, "WebAssembly sections")?;
167                    sections.push(Section {
168                        name: output_budget.copy_name("code", "WebAssembly section name")?,
169                        address: 0,
170                        size: (range.end - range.start) as u64,
171                        offset: range.start as u64,
172                        file_size: (range.end - range.start) as u64,
173                        permissions: SectionPermissions {
174                            read: true,
175                            write: false,
176                            execute: true,
177                        },
178                        section_type: SectionType::Code,
179                        data: None,
180                    });
181                }
182                Payload::DataSection(s) => {
183                    let range = s.range();
184                    // Consume section entries
185                    for entry in s {
186                        entry?;
187                    }
188                    output_budget.reserve_record(&mut sections, "WebAssembly sections")?;
189                    sections.push(Section {
190                        name: output_budget.copy_name("data", "WebAssembly section name")?,
191                        address: 0,
192                        size: (range.end - range.start) as u64,
193                        offset: range.start as u64,
194                        file_size: (range.end - range.start) as u64,
195                        permissions: SectionPermissions {
196                            read: true,
197                            write: true,
198                            execute: false,
199                        },
200                        section_type: SectionType::Data,
201                        data: None,
202                    });
203                }
204                Payload::CustomSection(section) => {
205                    output_budget.reserve_record(&mut sections, "WebAssembly sections")?;
206                    let name = output_budget
207                        .copy_name(section.name(), "WebAssembly custom-section name")?;
208                    let section_type_name = output_budget
209                        .copy_name(section.name(), "WebAssembly custom-section type")?;
210                    sections.push(Section {
211                        name,
212                        address: 0,
213                        size: section.data().len() as u64,
214                        offset: section.data_offset() as u64,
215                        file_size: section.data().len() as u64,
216                        permissions: SectionPermissions {
217                            read: true,
218                            write: false,
219                            execute: false,
220                        },
221                        section_type: SectionType::Other(section_type_name),
222                        data: None,
223                    });
224                }
225                _ => {}
226            }
227        }
228
229        let metadata = BinaryMetadata {
230            size: data.len(),
231            format: Format::Wasm,
232            architecture: Architecture::Wasm,
233            entry_point: None,
234            base_address: None,
235            timestamp: None,
236            compiler_info: None,
237            endian: Endianness::Little,
238            security_features: SecurityFeatures::default(),
239        };
240
241        Ok(Self {
242            metadata,
243            sections,
244            imports,
245            exports,
246        })
247    }
248}
249
250impl BinaryFormatTrait for WasmBinary {
251    fn format_type(&self) -> Format {
252        Format::Wasm
253    }
254
255    fn architecture(&self) -> Architecture {
256        Architecture::Wasm
257    }
258
259    fn entry_point(&self) -> Option<u64> {
260        self.metadata.entry_point
261    }
262
263    fn sections(&self) -> &[Section] {
264        &self.sections
265    }
266
267    fn symbols(&self) -> &[Symbol] {
268        &[]
269    }
270
271    fn imports(&self) -> &[Import] {
272        &self.imports
273    }
274
275    fn exports(&self) -> &[Export] {
276        &self.exports
277    }
278
279    fn metadata(&self) -> &BinaryMetadata {
280        &self.metadata
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    fn push_u32_leb(mut value: u32, output: &mut Vec<u8>) {
289        loop {
290            let mut byte = (value & 0x7f) as u8;
291            value >>= 7;
292            if value != 0 {
293                byte |= 0x80;
294            }
295            output.push(byte);
296            if value == 0 {
297                break;
298            }
299        }
300    }
301
302    #[test]
303    fn malformed_data_entries_are_not_silently_ignored() {
304        let malformed = b"\0asm\x01\0\0\0\x0b\x02\x01\xff";
305        assert!(WasmParser::parse(malformed).is_err());
306    }
307
308    #[test]
309    fn malformed_function_body_is_not_silently_ignored() {
310        // Structurally complete type/function/code sections whose single body
311        // contains an unknown opcode and no terminating `end` instruction.
312        let malformed =
313            b"\0asm\x01\0\0\0\x01\x04\x01\x60\x00\x00\x03\x02\x01\x00\x0a\x04\x01\x02\x00\xff";
314        assert!(WasmParser::parse(malformed).is_err());
315    }
316
317    #[test]
318    fn oversized_structural_count_is_rejected_before_validation() {
319        let mut count = Vec::new();
320        push_u32_leb((super::super::MAX_PARSED_RECORDS + 1) as u32, &mut count);
321
322        let mut module = b"\0asm\x01\0\0\0".to_vec();
323        module.push(2); // import section
324        push_u32_leb(count.len() as u32, &mut module);
325        module.extend_from_slice(&count);
326
327        let error = WasmParser::parse(&module)
328            .err()
329            .expect("oversized structural count must fail");
330        assert!(error.to_string().contains("parser record limit"));
331    }
332
333    #[test]
334    fn oversized_owned_name_is_rejected() {
335        let name = "x".repeat(super::super::MAX_NAME_BYTES + 1);
336        let module = wat::parse_str(format!(r#"(module (import "m" "{name}" (func)))"#)).unwrap();
337
338        let error = WasmParser::parse(&module)
339            .err()
340            .expect("oversized output name must fail");
341        assert!(error.to_string().contains("per-name limit"));
342    }
343
344    #[test]
345    fn start_function_index_is_not_reported_as_an_address() {
346        let module = wat::parse_str("(module (func) (start 0))").unwrap();
347        let parsed = WasmParser::parse(&module).unwrap();
348
349        assert_eq!(parsed.entry_point(), None);
350        assert_eq!(parsed.metadata().entry_point, None);
351    }
352}