1//! Support for parsing and analyzing [dynamic
2//! library](https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md) modules.
3
4use {
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)]
18pub enum ValueType {
19 I32,
20 I64,
21 F32,
22 F64,
23}
24
25impl 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
39impl 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)]
52pub struct FunctionType {
53 pub parameters: Vec<ValueType>,
54 pub results: Vec<ValueType>,
55}
56
57impl fmt::Display for FunctionType {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 write!(f, "{:?} -> {:?}", self.parameters, self.results)
60 }
61}
62
63impl 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)]
84pub struct GlobalType {
85 pub ty: ValueType,
86 pub mutable: bool,
87 pub shared: bool,
88}
89
90impl 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)]
101pub enum Type {
102 Function(FunctionType),
103 Global(GlobalType),
104}
105
106impl 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
115impl 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)]
126pub 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)]
135pub struct ExportKey<'a> {
136 pub name: &'a str,
137 pub ty: Type,
138}
139
140impl<'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)]
148pub 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)]
155pub 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
210impl<'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