1 | //! Support for parsing and analyzing [dynamic |
2 | //! library](https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md) modules. |
3 | |
4 | use { |
5 | anyhow::{bail, Context, Error, Result}, |
6 | std::{ |
7 | collections::{BTreeSet, HashMap, HashSet}, |
8 | fmt, |
9 | }, |
10 | wasmparser::{ |
11 | Dylink0Subsection, ExternalKind, FuncType, KnownCustom, MemInfo, Parser, Payload, RefType, |
12 | SymbolFlags, TableType, TypeRef, ValType, |
13 | }, |
14 | }; |
15 | |
16 | /// Represents a core Wasm value type (not including V128 or reference types, which are not yet supported) |
17 | #[derive (Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
18 | pub enum ValueType { |
19 | I32, |
20 | I64, |
21 | F32, |
22 | F64, |
23 | } |
24 | |
25 | impl TryFrom<ValType> for ValueType { |
26 | type Error = Error; |
27 | |
28 | fn try_from(value: ValType) -> Result<Self> { |
29 | Ok(match value { |
30 | ValType::I32 => Self::I32, |
31 | ValType::I64 => Self::I64, |
32 | ValType::F32 => Self::F32, |
33 | ValType::F64 => Self::F64, |
34 | _ => bail!(" {value:?} not yet supported" ), |
35 | }) |
36 | } |
37 | } |
38 | |
39 | impl From<ValueType> for wasm_encoder::ValType { |
40 | fn from(value: ValueType) -> Self { |
41 | match value { |
42 | ValueType::I32 => Self::I32, |
43 | ValueType::I64 => Self::I64, |
44 | ValueType::F32 => Self::F32, |
45 | ValueType::F64 => Self::F64, |
46 | } |
47 | } |
48 | } |
49 | |
50 | /// Represents a core Wasm function type |
51 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
52 | pub struct FunctionType { |
53 | pub parameters: Vec<ValueType>, |
54 | pub results: Vec<ValueType>, |
55 | } |
56 | |
57 | impl fmt::Display for FunctionType { |
58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
59 | write!(f, " {:?} -> {:?}" , self.parameters, self.results) |
60 | } |
61 | } |
62 | |
63 | impl TryFrom<&FuncType> for FunctionType { |
64 | type Error = Error; |
65 | |
66 | fn try_from(value: &FuncType) -> Result<Self> { |
67 | Ok(Self { |
68 | parameters: valueimpl Iterator- >
|
69 | .params() |
70 | .iter() |
71 | .map(|&v: ValType| ValueType::try_from(v)) |
72 | .collect::<Result<_>>()?, |
73 | results: valueimpl Iterator- >
|
74 | .results() |
75 | .iter() |
76 | .map(|&v: ValType| ValueType::try_from(v)) |
77 | .collect::<Result<_>>()?, |
78 | }) |
79 | } |
80 | } |
81 | |
82 | /// Represents a core Wasm global variable type |
83 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
84 | pub struct GlobalType { |
85 | pub ty: ValueType, |
86 | pub mutable: bool, |
87 | pub shared: bool, |
88 | } |
89 | |
90 | impl fmt::Display for GlobalType { |
91 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
92 | if self.mutable { |
93 | write!(f, "mut " )?; |
94 | } |
95 | write!(f, " {:?}" , self.ty) |
96 | } |
97 | } |
98 | |
99 | /// Represents a core Wasm export or import type |
100 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
101 | pub enum Type { |
102 | Function(FunctionType), |
103 | Global(GlobalType), |
104 | } |
105 | |
106 | impl fmt::Display for Type { |
107 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
108 | match self { |
109 | Self::Function(ty: &FunctionType) => write!(f, "function {ty}" ), |
110 | Self::Global(ty: &GlobalType) => write!(f, "global {ty}" ), |
111 | } |
112 | } |
113 | } |
114 | |
115 | impl From<&Type> for wasm_encoder::ExportKind { |
116 | fn from(value: &Type) -> Self { |
117 | match value { |
118 | Type::Function(_) => wasm_encoder::ExportKind::Func, |
119 | Type::Global(_) => wasm_encoder::ExportKind::Global, |
120 | } |
121 | } |
122 | } |
123 | |
124 | /// Represents a core Wasm import |
125 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
126 | pub struct Import<'a> { |
127 | pub module: &'a str, |
128 | pub name: &'a str, |
129 | pub ty: Type, |
130 | pub flags: SymbolFlags, |
131 | } |
132 | |
133 | /// Represents a core Wasm export |
134 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
135 | pub struct ExportKey<'a> { |
136 | pub name: &'a str, |
137 | pub ty: Type, |
138 | } |
139 | |
140 | impl<'a> fmt::Display for ExportKey<'a> { |
141 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
142 | write!(f, " {} ( {})" , self.name, self.ty) |
143 | } |
144 | } |
145 | |
146 | /// Represents a core Wasm export, including dylink.0 flags |
147 | #[derive (Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] |
148 | pub struct Export<'a> { |
149 | pub key: ExportKey<'a>, |
150 | pub flags: SymbolFlags, |
151 | } |
152 | |
153 | /// Metadata extracted from a dynamic library module |
154 | #[derive (Debug)] |
155 | pub struct Metadata<'a> { |
156 | /// The name of the module |
157 | /// |
158 | /// This is currently not part of the file itself and must be provided separately, but the plan is to add |
159 | /// something like a `WASM_DYLINK_SO_NAME` field to the dynamic linking tool convention so we can parse it |
160 | /// along with everything else. |
161 | pub name: &'a str, |
162 | |
163 | /// Whether this module should be resolvable via `dlopen` |
164 | pub dl_openable: bool, |
165 | |
166 | /// The `WASM_DYLINK_MEM_INFO` value (or all zeros if not found) |
167 | pub mem_info: MemInfo, |
168 | |
169 | /// The `WASM_DYLINK_NEEDED` values, if any |
170 | pub needed_libs: Vec<&'a str>, |
171 | |
172 | /// Whether this module exports `__wasm_apply_data_relocs` |
173 | pub has_data_relocs: bool, |
174 | |
175 | /// Whether this module exports `__wasm_call_ctors` |
176 | pub has_ctors: bool, |
177 | |
178 | /// Whether this module exports `_initialize` |
179 | pub has_initialize: bool, |
180 | |
181 | /// Whether this module exports `_start` |
182 | pub has_wasi_start: bool, |
183 | |
184 | /// Whether this module exports `__wasm_set_libraries` |
185 | pub has_set_libraries: bool, |
186 | |
187 | /// Whether this module includes any `component-type*` custom sections which include exports |
188 | pub has_component_exports: bool, |
189 | |
190 | /// Whether this module imports `__asyncify_state` or `__asyncify_data`, indicating that it is |
191 | /// asyncified with `--pass-arg=asyncify-relocatable` option. |
192 | pub is_asyncified: bool, |
193 | |
194 | /// The functions imported from the `env` module, if any |
195 | pub env_imports: BTreeSet<(&'a str, (FunctionType, SymbolFlags))>, |
196 | |
197 | /// The memory addresses imported from `GOT.mem`, if any |
198 | pub memory_address_imports: BTreeSet<&'a str>, |
199 | |
200 | /// The table addresses imported from `GOT.func`, if any |
201 | pub table_address_imports: BTreeSet<&'a str>, |
202 | |
203 | /// The symbols exported by this module, if any |
204 | pub exports: BTreeSet<Export<'a>>, |
205 | |
206 | /// The symbols imported by this module (and not accounted for in the above fields), if any |
207 | pub imports: BTreeSet<Import<'a>>, |
208 | } |
209 | |
210 | impl<'a> Metadata<'a> { |
211 | /// Parse the specified module and extract its metadata. |
212 | pub fn try_new( |
213 | name: &'a str, |
214 | dl_openable: bool, |
215 | module: &'a [u8], |
216 | adapter_names: &HashSet<&str>, |
217 | ) -> Result<Self> { |
218 | let bindgen = crate::metadata::decode(module)?.1; |
219 | let has_component_exports = !bindgen.resolve.worlds[bindgen.world].exports.is_empty(); |
220 | |
221 | let mut result = Self { |
222 | name, |
223 | dl_openable, |
224 | mem_info: MemInfo { |
225 | memory_size: 0, |
226 | memory_alignment: 1, |
227 | table_size: 0, |
228 | table_alignment: 1, |
229 | }, |
230 | needed_libs: Vec::new(), |
231 | has_data_relocs: false, |
232 | has_ctors: false, |
233 | has_initialize: false, |
234 | has_wasi_start: false, |
235 | has_set_libraries: false, |
236 | has_component_exports, |
237 | is_asyncified: false, |
238 | env_imports: BTreeSet::new(), |
239 | memory_address_imports: BTreeSet::new(), |
240 | table_address_imports: BTreeSet::new(), |
241 | exports: BTreeSet::new(), |
242 | imports: BTreeSet::new(), |
243 | }; |
244 | let mut types = Vec::new(); |
245 | let mut function_types = Vec::new(); |
246 | let mut global_types = Vec::new(); |
247 | let mut import_info = HashMap::new(); |
248 | let mut export_info = HashMap::new(); |
249 | |
250 | for payload in Parser::new(0).parse_all(module) { |
251 | match payload? { |
252 | Payload::CustomSection(section) => { |
253 | if let KnownCustom::Dylink0(reader) = section.as_known() { |
254 | for subsection in reader { |
255 | match subsection.context("failed to parse `dylink.0` subsection" )? { |
256 | Dylink0Subsection::MemInfo(info) => result.mem_info = info, |
257 | Dylink0Subsection::Needed(needed) => { |
258 | result.needed_libs = needed.clone() |
259 | } |
260 | Dylink0Subsection::ExportInfo(info) => { |
261 | export_info |
262 | .extend(info.iter().map(|info| (info.name, info.flags))); |
263 | } |
264 | Dylink0Subsection::ImportInfo(info) => { |
265 | import_info.extend( |
266 | info.iter() |
267 | .map(|info| ((info.module, info.field), info.flags)), |
268 | ); |
269 | } |
270 | Dylink0Subsection::Unknown { ty, .. } => { |
271 | bail!("unrecognized `dylink.0` subsection: {ty}" ) |
272 | } |
273 | } |
274 | } |
275 | } |
276 | } |
277 | |
278 | Payload::TypeSection(reader) => { |
279 | types = reader |
280 | .into_iter_err_on_gc_types() |
281 | .collect::<Result<Vec<_>, _>>()?; |
282 | } |
283 | |
284 | Payload::ImportSection(reader) => { |
285 | for import in reader { |
286 | let import = import?; |
287 | |
288 | match import.ty { |
289 | TypeRef::Func(ty) => function_types.push(usize::try_from(ty).unwrap()), |
290 | TypeRef::Global(ty) => global_types.push(ty), |
291 | _ => (), |
292 | } |
293 | |
294 | let type_error = || { |
295 | bail!( |
296 | "unexpected type for {}: {}: {:?}" , |
297 | import.module, |
298 | import.name, |
299 | import.ty |
300 | ) |
301 | }; |
302 | |
303 | match (import.module, import.name) { |
304 | ("env" , "memory" ) => { |
305 | if !matches!(import.ty, TypeRef::Memory(_)) { |
306 | return type_error(); |
307 | } |
308 | } |
309 | ("env" , "__asyncify_data" | "__asyncify_state" ) => { |
310 | result.is_asyncified = true; |
311 | if !matches!( |
312 | import.ty, |
313 | TypeRef::Global(wasmparser::GlobalType { |
314 | content_type: ValType::I32, |
315 | .. |
316 | }) |
317 | ) { |
318 | return type_error(); |
319 | } |
320 | } |
321 | ("env" , "__memory_base" | "__table_base" | "__stack_pointer" ) => { |
322 | if !matches!( |
323 | import.ty, |
324 | TypeRef::Global(wasmparser::GlobalType { |
325 | content_type: ValType::I32, |
326 | .. |
327 | }) |
328 | ) { |
329 | return type_error(); |
330 | } |
331 | } |
332 | ("env" , "__indirect_function_table" ) => { |
333 | if let TypeRef::Table(TableType { |
334 | element_type, |
335 | maximum: None, |
336 | .. |
337 | }) = import.ty |
338 | { |
339 | if element_type != RefType::FUNCREF { |
340 | return type_error(); |
341 | } |
342 | } else { |
343 | return type_error(); |
344 | } |
345 | } |
346 | ("env" , name) => { |
347 | if let TypeRef::Func(ty) = import.ty { |
348 | result.env_imports.insert(( |
349 | name, |
350 | ( |
351 | FunctionType::try_from( |
352 | &types[usize::try_from(ty).unwrap()], |
353 | )?, |
354 | import_info |
355 | .get(&("env" , name)) |
356 | .copied() |
357 | .unwrap_or_default(), |
358 | ), |
359 | )); |
360 | } else { |
361 | return type_error(); |
362 | } |
363 | } |
364 | ("GOT.mem" , name) => { |
365 | if let TypeRef::Global(wasmparser::GlobalType { |
366 | content_type: ValType::I32, |
367 | .. |
368 | }) = import.ty |
369 | { |
370 | match name { |
371 | "__heap_base" | "__heap_end" => (), |
372 | _ => { |
373 | result.memory_address_imports.insert(name); |
374 | } |
375 | } |
376 | } else { |
377 | return type_error(); |
378 | } |
379 | } |
380 | ("GOT.func" , name) => { |
381 | if let TypeRef::Global(wasmparser::GlobalType { |
382 | content_type: ValType::I32, |
383 | .. |
384 | }) = import.ty |
385 | { |
386 | result.table_address_imports.insert(name); |
387 | } else { |
388 | return type_error(); |
389 | } |
390 | } |
391 | (module, name) if adapter_names.contains(module) => { |
392 | let ty = match import.ty { |
393 | TypeRef::Global(wasmparser::GlobalType { |
394 | content_type, |
395 | mutable, |
396 | shared, |
397 | }) => Type::Global(GlobalType { |
398 | ty: content_type.try_into()?, |
399 | mutable, |
400 | shared, |
401 | }), |
402 | TypeRef::Func(ty) => Type::Function(FunctionType::try_from( |
403 | &types[usize::try_from(ty).unwrap()], |
404 | )?), |
405 | ty => { |
406 | bail!("unsupported import kind for {module}. {name}: {ty:?}" ,) |
407 | } |
408 | }; |
409 | let flags = import_info |
410 | .get(&(module, name)) |
411 | .copied() |
412 | .unwrap_or_default(); |
413 | result.imports.insert(Import { |
414 | module, |
415 | name, |
416 | ty, |
417 | flags, |
418 | }); |
419 | } |
420 | _ => { |
421 | if !matches!(import.ty, TypeRef::Func(_) | TypeRef::Global(_)) { |
422 | return type_error(); |
423 | } |
424 | } |
425 | } |
426 | } |
427 | } |
428 | |
429 | Payload::FunctionSection(reader) => { |
430 | for function in reader { |
431 | function_types.push(usize::try_from(function?).unwrap()); |
432 | } |
433 | } |
434 | |
435 | Payload::GlobalSection(reader) => { |
436 | for global in reader { |
437 | global_types.push(global?.ty); |
438 | } |
439 | } |
440 | |
441 | Payload::ExportSection(reader) => { |
442 | for export in reader { |
443 | let export = export?; |
444 | |
445 | match export.name { |
446 | "__wasm_apply_data_relocs" => result.has_data_relocs = true, |
447 | "__wasm_call_ctors" => result.has_ctors = true, |
448 | "_initialize" => result.has_initialize = true, |
449 | "_start" => result.has_wasi_start = true, |
450 | "__wasm_set_libraries" => result.has_set_libraries = true, |
451 | _ => { |
452 | let ty = match export.kind { |
453 | ExternalKind::Func => Type::Function(FunctionType::try_from( |
454 | &types[function_types |
455 | [usize::try_from(export.index).unwrap()]], |
456 | )?), |
457 | ExternalKind::Global => { |
458 | let ty = |
459 | global_types[usize::try_from(export.index).unwrap()]; |
460 | Type::Global(GlobalType { |
461 | ty: ValueType::try_from(ty.content_type)?, |
462 | mutable: ty.mutable, |
463 | shared: ty.shared, |
464 | }) |
465 | } |
466 | kind => { |
467 | bail!( |
468 | "unsupported export kind for {}: {kind:?}" , |
469 | export.name |
470 | ) |
471 | } |
472 | }; |
473 | let flags = |
474 | export_info.get(&export.name).copied().unwrap_or_default(); |
475 | result.exports.insert(Export { |
476 | key: ExportKey { |
477 | name: export.name, |
478 | ty, |
479 | }, |
480 | flags, |
481 | }); |
482 | } |
483 | } |
484 | } |
485 | } |
486 | |
487 | _ => {} |
488 | } |
489 | } |
490 | |
491 | Ok(result) |
492 | } |
493 | } |
494 | |