threatflux_binary_analysis/formats/
wasm.rs1use 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
13pub 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
26pub 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 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 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 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 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); 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}