wasmer_wasix/state/linker/mod.rs
1// TODO: The linker *can* exist in the runtime, since technically, there's nothing that
2// prevents us from having a non-WASIX linker. However, there is currently no use-case
3// for a non-WASIX linker, so we'll refrain from making it generic for the time being.
4
5//! Linker for loading and linking dynamic modules at runtime. The linker is designed to
6//! work with output from clang (version 19 was used at the time of creating this code).
7//! Note that dynamic linking of WASM modules is considered unstable in clang/LLVM, so
8//! this code may need to be updated for future versions of clang.
9//!
10//! The linker doesn't care about where code exists and how modules call each other, but
11//! the way we have found to be most effective is:
12//! * The main module carries with it all of wasix-libc, and exports everything
13//! * Side module don't link wasix-libc in, instead importing it from the main module
14//!
15//! This way, we only need one instance of wasix-libc, and one instance of all the static
16//! data that it requires to function. Indeed, if there were multiple instances of its
17//! static data, it would more than likely just break completely; one needs only imagine
18//! what would happen if there were multiple memory allocators (malloc) running at the same
19//! time. Emscripten (the only WASM runtime that supports dynamic linking, at the time of
20//! this writing) takes the same approach.
21//!
22//! While locating modules by relative or absolute paths is possible, it is recommended
23//! to put every side module into /lib, where they can be located by name as well as by
24//! path.
25//!
26//! The linker starts from a dynamically-linked main module. It scans the dylink.0 section
27//! for memory and table-related information and the list of needed modules. The module
28//! tree requires a memory, an indirect function table, and stack-related parameters
29//! (including the __stack_pointer global), which are created. Since dynamically-linked
30//! modules use PIC (position-independent code), the stack is not fixed and can be resized
31//! at runtime.
32//!
33//! After the memory, function table and stack are created, the linker proceeds to load in
34//! needed modules. Needed modules are always loaded in and initialized before modules that
35//! asked for them, since it is expected that the needed module needs to be usable before
36//! the module that needs it can be initialized.
37//!
38//! However, we also need to support circular dependencies between the modules; the most
39//! common case is when the main needs a side module and imports function from it, and the
40//! side imports wasix-libc functions from the main. To support this, the linker generates
41//! stub functions for all the imports that cannot be resolved when a module is being
42//! loaded in. The stub functions will then resolve the function once (and only once) at
43//! runtime when they're first called. This *does*, however, mean that link errors can happen
44//! at runtime, after the linker has reported successful linking of the modules. Such errors
45//! are turned into a [`WasiError::DlSymbolResolutionFailed`] error and will terminate
46//! execution completely.
47//!
48//! # Threading Support
49//!
50//! The linker supports the concept of "Instance Groups", which are multiple instances
51//! of the same module tree. This corresponds very closely to WASIX threads, but is
52//! named an instance group so as to keep the logic decoupled from the threading logic
53//! in WASIX.
54//!
55//! Each instance group has its own store, indirect function table, and stack pointer,
56//! but shares its memory with every other instance group. Note that even though the
57//! underlying memory is the same, we need to create a new [`Memory`] instance
58//! for each group via [`Memory::share_and_detach`] +
59//! [`DetachedMemory::attach`](wasmer::DetachedMemory::attach). Also, when placing a symbol
60//! in the function table, the linker always updates all function tables at the same
61//! time. This is because function "pointers" can be passed across instance groups
62//! (read: sent to other threads) by the guest code, so all function tables should
63//! have exactly the same content at all times.
64//!
65//! One important aspect of instance groups is that they do *not* share the same store;
66//! this lets us put different instance groups on different OS threads. However, this
67//! also means that one call to [`Linker::load_module`], etc. cannot update every
68//! instance group as each one has its own function table. To make the linker work
69//! across threads, we need a "stop-the-world" lock on every instance group. The group
70//! the load/resolve request originates from sets a flag, which other instance
71//! groups are required to check periodically by calling [`Linker::do_pending_link_operations`].
72//! Once all instance groups are stopped in that function, the original can proceed to
73//! perform the operation, and report its results to all other instance groups so they
74//! can make the same changes to their function table as well.
75//!
76//! In WASIX, the periodic check is performed at the start of most (but not all) syscalls.
77//! This means a thread that doesn't make any syscalls can potentially block all other
78//! threads if a DL operation is performed. This also means that two instance groups
79//! cannot co-exist on the same OS thread, as the first one will block the OS thread
80//! and the second can't enter the "lock" again to let the first continue its work.
81//!
82//! To also get cooperation from threads that are waiting in a syscall, a
83//! [`Signal::Sigwakeup`](wasmer_wasix_types::wasi::Signal::Sigwakeup) signal is sent to
84//! all threads when a DL operation needs to be synchronized.
85//!
86//! # About TLS
87//!
88//! Each instance of each group gets its own TLS area, so there are 4 cases to consider:
89//! * Main instance of main module: TLS area will be allocated by the compiler, and be
90//! placed at the start of the memory region requested by the `dylink.0` section.
91//! * Main instance of side modules: Almost same as main module, but tls_base will be
92//! non-zero because side modules get a non-zero memory_base. It is very important
93//! to note that the main instance of a side module lives in the instance group
94//! that initially loads it in. This **does not** have to be the main instance
95//! group.
96//! * Other instances of main module: Each worker thread gets its TLS area
97//! allocated by the code in pthread_create, and a pointer to the TLS area is passed
98//! through the thread start args. This pointer is read by the code in thread_spawn,
99//! and passed through to us as part of the environment's memory layout.
100//! * Other instances of side modules: This is where the linker comes in. When the
101//! new instance is created, the linker will call its `__wasix_init_tls` function,
102//! which is responsible for setting up the TLS area for the thread.
103//!
104//! Since we only want to call `__wasix_init_tls` for non-main instances of side modules,
105//! it is enough to call it only within [`InstanceGroupState::instantiate_side_module_from_linker`].
106//!
107//! # Module Loading
108//!
109//! Module loading happens as an orchestrated effort between the shared linker state, the
110//! state of the instance group that started (or "instigated") the operation, and other
111//! instance groups. Access to a set of instances is required for resolution of exports,
112//! which is why the linker state alone (which only stores modules) is not enough.
113//!
114//! Even though most (if not all) operations require access to both the shared linker state
115//! and a/the instance group state, they're separated into three sets:
116//! * Operations that deal with metadata exist as impls on [`LinkerState`]. These take
117//! a (read-only) instance group state for export resolution, as well as a
118//! [`StoreRef`](wasmer::StoreRef). They're guaranteed not to alter the store or the
119//! instance group state.
120//! * Operations that deal with the actual instances (instantiating, putting symbols in the
121//! function table, etc.) and are started by the instigating group exist as impls on
122//! [`InstanceGroupState`] that also take a mutable reference to the shared linker state, and
123//! require it to be locked for writing. These operations can and will update the linker state,
124//! mainly to store symbol resolution records.
125//! * Operations that deal with replicating changes to instances from another thread also exits
126//! as impls on [`InstanceGroupState`], but take a read-only reference to the shared linker
127//! state. This is important because all the information needed for replicating the change to
128//! the instigating group's instances should already be in the linker state. See
129//! [`InstanceGroupState::populate_imports_from_linker`] and
130//! [`InstanceGroupState::instantiate_side_module_from_linker`] for the two most important ones.
131//!
132//! Module loading generally works by going through these steps:
133//! * [`LinkerState::load_module_tree`] loads modules (and their needed modules) and assigns
134//! module handles
135//! * Then, for each new module:
136//! * Memory and table space is allocated
137//! * Imports are resolved (see next section)
138//! * The module is instantiated
139//! * After all modules have been instantiated, pending imports (resulting from circular
140//! dependencies) are resolved
141//! * Finally, module initializers are called
142//!
143//! ## Symbol resolution
144//!
145//! To support replicating operations from the instigating group to other groups, symbol resolution
146//! happens in 3 steps:
147//! * [`LinkerState::resolve_symbols`] goes through the imports of a soon-to-be-loaded module,
148//! recording the imports as [`NeededSymbolResolutionKey`]s and creating
149//! [`InProgressSymbolResolution`]s in response to each one.
150//! * [`InstanceGroupState::populate_imports_from_link_state`] then goes through the results
151//! and resolves each import to its final value, while also recording enough information (in the
152//! shape of [`SymbolResolutionResult`]s) for other groups to resolve the symbol from their own
153//! instances.
154//! * Finally, instances are created and finalized, and initializers are called.
155//!
156//! ## Stub functions
157//!
158//! As noted above, stub functions are generated in response to circular dependencies. The stub
159//! functions do take previous symbol resolution records into account, so that the stub corresponding
160//! to a single import cannot resolve to different exports in different groups. If no such record is
161//! found, then a new record is created by the stub function. However, there's a catch.
162//!
163//! It must be noted that, during initialization, the shared linker state has to remain write-locked
164//! so as to prevent other threads from starting another operation (the replication logic only works
165//! with one active operation at a time). Stub functions need a write lock on the shared linker state
166//! to store new resolution records, and as such, they can't store resolution records if they're
167//! called in response to a module's initialization routines. This can happen easily if:
168//! * A side module is needed by the main
169//! * That side module accesses any libc functions, such as printing something to stdout.
170//!
171//! To work around this, stub functions only *try* to lock the shared linker state, and if they can't,
172//! they won't store anything. A follow-up call to the stub function can resolve the symbol again,
173//! store it for use by further calls to the function, and also create a resolution record. This does
174//! create a few hard-to-reach edge cases:
175//! * If the symbol happens to resolve differently between the two calls to the stub, unpredictable
176//! behavior can happen; however, this is impossible in the current implementation.
177//! * If the shared state is locked by a different instance group, then the stub won't store its
178//! lookup results anyway, even though it could have if it had waited.
179//!
180//! ## Locating side modules
181//!
182//! Side modules are located according to these steps:
183//! * If the name contains a slash (/), it is treated as a relative or absolute path.
184//! * Otherwise, the name is searched for in `/lib`, `/usr/lib` and `/usr/local/lib`.
185//! LD_LIBRARY_PATH is not supported yet.
186//!
187//! # Building dynamically-linked modules
188//!
189//! Note that building modules that conform the specific requirements of this linker requires
190//! careful configuration of clang. A PIC sysroot is required. The steps to build a main
191//! module are:
192//!
193//! ```bash
194//! clang-19 \
195//! --target=wasm32-wasi --sysroot=/path/to/sysroot32-pic \
196//! -matomics -mbulk-memory -mmutable-globals -pthread \
197//! -mthread-model posix -ftls-model=local-exec \
198//! -fno-trapping-math -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_SIGNAL \
199//! -D_WASI_EMULATED_PROCESS_CLOCKS \
200//! # PIC is required for all modules, main and side
201//! -fPIC \
202//! # We need to compile to an object file we can manually link in the next step
203//! -c main.c -o main.o
204//!
205//! wasm-ld-19 \
206//! # To link needed side modules, assuming `libsidewasm.so` exists in the current directory:
207//! -L. -lsidewasm \
208//! -L/path/to/sysroot32-pic/lib \
209//! -L/path/to/sysroot32-pic/lib/wasm32-wasi \
210//! # Make wasm-ld search everywhere and export everything, needed for wasix-libc functions to
211//! # be exported correctly from the main module
212//! --whole-archive --export-all \
213//! # The object file from the last step
214//! main.o \
215//! # The crt1.o file contains the _start and _main_void functions
216//! /path/to/sysroot32-pic/lib/wasm32-wasi/crt1.o \
217//! # Statically link the sysroot's libraries
218//! -lc -lresolv -lrt -lm -lpthread -lwasi-emulated-mman \
219//! # The usual linker config for wasix modules
220//! --import-memory --shared-memory --extra-features=atomics,bulk-memory,mutable-globals \
221//! --export=__wasm_signal --export=__tls_size --export=__tls_align \
222//! --export=__tls_base --export=__wasm_call_ctors --export-if-defined=__wasm_apply_data_relocs \
223//! # Again, PIC is very important, as well as producing a location-independent executable with -pie
224//! --experimental-pic -pie \
225//! -o main.wasm
226//! ```
227//!
228//! And the steps to build a side module are:
229//!
230//! ```bash
231//! clang-19 \
232//! --target=wasm32-wasi --sysroot=/path/to/sysroot32-pic \
233//! -matomics -mbulk-memory -mmutable-globals -pthread \
234//! -mthread-model posix -ftls-model=local-exec \
235//! -fno-trapping-math -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_SIGNAL \
236//! -D_WASI_EMULATED_PROCESS_CLOCKS \
237//! # We need PIC
238//! -fPIC \
239//! # Make it export everything that's not hidden explicitly
240//! -fvisibility=default \
241//! -c side.c -o side.o
242//!
243//! wasm-ld-19 \
244//! # Note: we don't link against wasix-libc, so no -lc etc., because we want
245//! # those symbols to be imported.
246//! --extra-features=atomics,bulk-memory,mutable-globals \
247//! --export=__wasm_call_ctors --export-if-defined=__wasm_apply_data_relocs \
248//! # Need PIC
249//! --experimental-pic \
250//! # Import everything that's undefined, including wasix-libc functions
251//! --unresolved-symbols=import-dynamic \
252//! # build a shared library
253//! -shared \
254//! # Import a shared memory
255//! --shared-memory \
256//! # Conform to the libxxx.so naming so clang can find it via -lxxx
257//! -o libsidewasm.so side.o
258//! ```
259
260#![allow(clippy::result_large_err)]
261
262mod dylink;
263mod error;
264mod instance_group;
265mod internal_types;
266mod linker_state;
267mod locator;
268mod memory_allocator;
269mod sync;
270mod types;
271mod wasm_utils;
272
273pub use dylink::*;
274pub use error::*;
275pub use types::*;
276
277use instance_group::*;
278use internal_types::*;
279use linker_state::*;
280use locator::*;
281use memory_allocator::*;
282use sync::*;
283use wasm_utils::*;
284
285use std::{
286 collections::{BTreeMap, HashMap},
287 ops::DerefMut,
288 path::Path,
289 sync::{Arc, Mutex, MutexGuard, atomic::Ordering},
290};
291
292use bus::Bus;
293use tracing::trace;
294use wasmer::{AsStoreMut, Engine, FunctionEnvMut, Instance, Memory, Module, StoreMut, Tag, Type};
295use wasmer_wasix_types::wasix::WasiMemoryLayout;
296
297use crate::{WasiEnv, WasiFunctionEnv, WasiModuleTreeHandles, import_object_for_all_wasi_versions};
298
299use super::WasiModuleInstanceHandles;
300
301// Module handle 1 is always the main module. Side modules get handles starting from the next one after the main module.
302pub static MAIN_MODULE_HANDLE: ModuleHandle = ModuleHandle(1);
303static INVALID_MODULE_HANDLE: ModuleHandle = ModuleHandle(u32::MAX);
304
305// Need to keep the zeroth index null to catch null function pointers at runtime
306static MAIN_MODULE_TABLE_BASE: u64 = 1;
307
308/// The linker is responsible for loading and linking dynamic modules at runtime,
309/// and managing the shared memory and indirect function table.
310/// Each linker instance represents a specific instance group. Cloning a linker
311/// instance does *not* create a new instance group though; the clone will refer
312/// to the same group as the original.
313#[derive(Clone)]
314pub struct Linker {
315 shared: LinkerShared,
316 instance_group_state: Arc<Mutex<Option<InstanceGroupState>>>,
317}
318
319impl std::fmt::Debug for Linker {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 f.debug_struct("Linker").finish()
322 }
323}
324
325impl Linker {
326 /// Creates a new linker for the given main module. The module is expected to be a
327 /// PIE executable. Imports for the module will be fulfilled, so that it can start
328 /// running, and a Linker instance is returned which can then be used for the
329 /// loading/linking of further side modules.
330 pub fn new(
331 engine: Engine,
332 main_module: &Module,
333 store: &mut StoreMut<'_>,
334 memory: Option<Memory>,
335 func_env: &mut WasiFunctionEnv,
336 stack_size: u64,
337 ld_library_path: &[&Path],
338 ) -> Result<(Self, LinkedMainModule), LinkError> {
339 let dylink_section = parse_dylink0_section(main_module)?;
340
341 trace!(?dylink_section, "Loading main module");
342
343 let mut imports = import_object_for_all_wasi_versions(main_module, store, &func_env.env);
344
345 let function_table_type = main_module_function_table_type(main_module)?;
346
347 let expected_table_length =
348 dylink_section.mem_info.table_size + MAIN_MODULE_TABLE_BASE as u32;
349 let indirect_function_table =
350 create_indirect_function_table(store, function_table_type, expected_table_length)?;
351
352 // Give modules a non-zero memory base, since we don't want
353 // any valid pointers to point to the zero address
354 let memory_base = 2u64.pow(dylink_section.mem_info.memory_alignment);
355
356 let memory_type = main_module_memory_type(main_module)?;
357
358 let memory = match memory {
359 Some(m) => m,
360 None => Memory::new(store, memory_type)?,
361 };
362
363 let stack_low = {
364 let data_end = memory_base + dylink_section.mem_info.memory_size as u64;
365 if !data_end.is_multiple_of(1024) {
366 data_end + 1024 - (data_end % 1024)
367 } else {
368 data_end
369 }
370 };
371
372 if !stack_size.is_multiple_of(1024) {
373 panic!("Stack size must be 1024-bit aligned");
374 }
375
376 let stack_high = stack_low + stack_size;
377
378 // Allocate memory for the stack. This does not need to go through the memory allocator
379 // because it's always placed directly after the main module's data
380 memory.grow_at_least(store, stack_high)?;
381
382 trace!(
383 memory_pages = ?memory.grow(store, 0).unwrap(),
384 memory_base,
385 stack_low,
386 stack_high,
387 "Memory layout"
388 );
389
390 let stack_pointer = create_main_stack_pointer_global(store, main_module, stack_high)?;
391
392 let c_longjmp = Tag::new(store, vec![Type::I32]);
393 let cpp_exception = Tag::new(store, vec![Type::I32]);
394
395 let mut barrier_tx = Bus::new(1);
396 let barrier_rx = barrier_tx.add_rx();
397 let mut operation_tx = Bus::new(1);
398 let operation_rx = operation_tx.add_rx();
399
400 let mut instance_group = InstanceGroupState {
401 main_instance: None,
402 // The TLS base for the main instance is determined by reading the
403 // `__tls_base` global export from the instance after instantiation.
404 main_instance_tls_base: None,
405 side_instances: HashMap::new(),
406 stack_pointer,
407 memory: memory.clone(),
408 indirect_function_table: indirect_function_table.clone(),
409 c_longjmp,
410 cpp_exception,
411 recv_pending_operation_barrier: barrier_rx,
412 recv_pending_operation: operation_rx,
413 };
414
415 let mut linker_state = LinkerState {
416 engine,
417 main_module: main_module.clone(),
418 main_module_dylink_info: dylink_section,
419 main_module_memory_base: memory_base,
420 side_modules: BTreeMap::new(),
421 side_modules_by_name: HashMap::new(),
422 next_module_handle: MAIN_MODULE_HANDLE.0 + 1,
423 memory_allocator: MemoryAllocator::new(),
424 allocated_closure_functions: BTreeMap::new(),
425 available_closure_functions: Vec::new(),
426 heap_base: stack_high,
427 symbol_resolution_records: HashMap::new(),
428 send_pending_operation_barrier: barrier_tx,
429 send_pending_operation: operation_tx,
430 };
431
432 let mut link_state = InProgressLinkState::default();
433
434 let well_known_imports = [
435 ("env", "__memory_base", memory_base),
436 ("env", "__table_base", MAIN_MODULE_TABLE_BASE),
437 ("GOT.mem", "__stack_high", stack_high),
438 ("GOT.mem", "__stack_low", stack_low),
439 ("GOT.mem", "__heap_base", stack_high),
440 ];
441
442 trace!("Resolving main module's symbols");
443 linker_state.resolve_symbols(
444 &instance_group,
445 store,
446 main_module,
447 MAIN_MODULE_HANDLE,
448 &mut link_state,
449 &well_known_imports,
450 )?;
451
452 trace!("Populating main module's imports object");
453 instance_group.populate_imports_from_link_state(
454 MAIN_MODULE_HANDLE,
455 &mut linker_state,
456 &mut link_state,
457 store,
458 main_module,
459 &mut imports,
460 &func_env.env,
461 &well_known_imports,
462 )?;
463
464 // TODO: figure out which way is faster (stubs in main or stubs in sides),
465 // use that ordering. My *guess* is that, since main exports all the libc
466 // functions and those are called frequently by basically any code, then giving
467 // stubs to main will be faster, but we need numbers before we decide this.
468 let main_instance = Instance::new(store, main_module, &imports)?;
469 instance_group.main_instance = Some(main_instance.clone());
470
471 let tls_base = get_tls_base_export(&main_instance, store)?;
472 instance_group.main_instance_tls_base = tls_base;
473
474 let runtime_path = linker_state.main_module_dylink_info.runtime_path.clone();
475 for needed in linker_state.main_module_dylink_info.needed.clone() {
476 // A successful load_module will add the module to the side_modules list,
477 // from which symbols can be resolved in the following call to
478 // guard.resolve_imports.
479 trace!(name = needed, "Loading module needed by main");
480 let wasi_env = func_env.data(store);
481 linker_state.load_module_tree(
482 DlModuleSpec::FileSystem {
483 module_spec: Path::new(needed.as_str()),
484 ld_library_path,
485 },
486 &mut link_state,
487 &wasi_env.runtime,
488 &wasi_env.state,
489 runtime_path.as_ref(),
490 // HACK: The main module doesn't have to exist in the virtual FS at all; e.g.
491 // if one runs `wasmer ../module.wasm --volume .`, we won't have access to the
492 // main module's folder within the virtual FS. This is why we're picking PWD
493 // as the $ORIGIN of the main module, which should at least be slightly
494 // sensible. The `main.wasm` file name will be stripped and only the `./`
495 // will be taken into account by `locate_module`.
496 Some(Path::new("./main.wasm")),
497 )?;
498 }
499
500 for module_handle in link_state
501 .new_modules
502 .iter()
503 .map(|m| m.handle)
504 .collect::<Vec<_>>()
505 {
506 trace!(?module_handle, "Instantiating module");
507 instance_group.instantiate_side_module_from_link_state(
508 &mut linker_state,
509 store,
510 &func_env.env,
511 &mut link_state,
512 module_handle,
513 )?;
514 }
515
516 let linker = Self {
517 shared: LinkerShared::new(linker_state),
518 instance_group_state: Arc::new(Mutex::new(Some(instance_group))),
519 };
520
521 let stack_layout = WasiMemoryLayout {
522 stack_lower: stack_low,
523 stack_upper: stack_high,
524 stack_size: stack_high - stack_low,
525 guard_size: 0,
526 tls_base,
527 };
528 let module_handles = WasiModuleTreeHandles::Dynamic {
529 linker: linker.clone(),
530 main_module_instance_handles: WasiModuleInstanceHandles::new(
531 memory.clone(),
532 store,
533 main_instance.clone(),
534 Some(indirect_function_table.clone()),
535 ),
536 };
537
538 func_env
539 .initialize_handles_and_layout(
540 store,
541 main_instance.clone(),
542 module_handles,
543 Some(stack_layout),
544 true,
545 )
546 .map_err(LinkError::MainModuleHandleInitFailed)?;
547
548 {
549 trace!(?link_state, "Finalizing linking of main module");
550
551 let mut group_guard = linker.instance_group_state.lock().unwrap();
552 unsafe {
553 linker.shared.bootstrap_exclusive_write_then(|ls| {
554 let group_state = group_guard.as_mut().unwrap();
555 group_state.finalize_pending_globals(
556 ls,
557 store,
558 &link_state.unresolved_globals,
559 )?;
560
561 trace!("Calling data relocator function for main module");
562 call_initialization_function::<()>(
563 &main_instance,
564 store,
565 "__wasm_apply_data_relocs",
566 )?;
567 call_initialization_function::<()>(
568 &main_instance,
569 store,
570 "__wasm_apply_tls_relocs",
571 )?;
572
573 linker.initialize_new_modules(group_guard, store, link_state)
574 })?;
575 }
576 }
577
578 trace!("Calling main module's _initialize function");
579 call_initialization_function::<()>(&main_instance, store, "_initialize")?;
580
581 trace!("Link complete");
582
583 Ok((
584 linker,
585 LinkedMainModule {
586 instance: main_instance,
587 memory,
588 indirect_function_table,
589 stack_low,
590 stack_high,
591 },
592 ))
593 }
594
595 /// This method gathers all necessary data from a parent thread's
596 /// environment, so a child thread can later call [`Self::create_instance_group`]
597 /// and have its own instance group, letting it take part in dynamic linking.
598 /// This two-part process is needed because the parent and child each have
599 /// their own [`Store`], and [`Store`]s are not `Send`.
600 pub fn prepare_for_instance_group(
601 &self,
602 parent_ctx: &mut FunctionEnvMut<'_, WasiEnv>,
603 ) -> Result<PreparedInstanceGroupData, LinkError> {
604 trace!("Preparing for new instance group");
605
606 lock_instance_group_state!(
607 parent_group_state_guard,
608 parent_group_state,
609 self,
610 LinkError::InstanceGroupIsDead
611 );
612
613 // Lease topology only: parent does not mutate shared `LinkerState` here; the child takes
614 // the blocking write in `create_instance_group` while holding the moved token.
615 let env = parent_ctx.as_ref();
616 let mut store = parent_ctx.as_store_mut();
617 let topology_token =
618 self.shared
619 .acquire_topology_token(parent_group_state, &mut store, &env)?;
620
621 let parent_store = parent_ctx.as_store_mut();
622
623 let memory = parent_group_state
624 .memory
625 .as_shared(&parent_store)
626 .ok_or_else(|| LinkError::MemoryNotShared)?;
627
628 let indirect_function_table_type =
629 parent_group_state.indirect_function_table.ty(&parent_store);
630
631 let expected_table_length = parent_group_state
632 .indirect_function_table
633 .size(&parent_store);
634
635 Ok(PreparedInstanceGroupData {
636 linker_shared: self.shared.clone(),
637 topology_token,
638 memory,
639 indirect_function_table_type,
640 expected_table_length,
641 })
642 }
643
644 pub(crate) fn do_pending_link_operations(
645 &self,
646 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
647 fast: bool,
648 ) -> Result<(), LinkError> {
649 if !self.shared.dl_operation_pending_load(if fast {
650 Ordering::Relaxed
651 } else {
652 Ordering::SeqCst
653 }) {
654 return Ok(());
655 }
656
657 lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
658
659 let env = ctx.as_ref();
660 let mut store = ctx.as_store_mut();
661 self.shared
662 .do_pending_link_operations_internal(group_state, &mut store, &env)
663 }
664
665 pub fn create_instance_group(
666 prepared_instance_group_data: PreparedInstanceGroupData,
667 store: &mut StoreMut<'_>,
668 func_env: &mut WasiFunctionEnv,
669 ) -> Result<(Self, LinkedMainModule), LinkError> {
670 trace!("Spawning new instance group");
671
672 let PreparedInstanceGroupData {
673 linker_shared,
674 topology_token,
675 memory,
676 indirect_function_table_type,
677 expected_table_length,
678 } = prepared_instance_group_data;
679
680 let (topology_hold, mut ls_write) =
681 linker_shared.write_linker_state_blocking_holding_topology(topology_token);
682
683 let main_module = ls_write.main_module.clone();
684
685 let mut imports = import_object_for_all_wasi_versions(&main_module, store, &func_env.env);
686
687 let memory = memory.attach(store);
688
689 let indirect_function_table = create_indirect_function_table(
690 store,
691 indirect_function_table_type,
692 expected_table_length,
693 )?;
694
695 // Since threads initialize their own stack space, we can only rely on the layout being
696 // initialized beforehand, which is the case with the thread_spawn syscall.
697 // FIXME: this needs to become a parameter if we ever decouple the linker from WASIX
698 let (stack_low, stack_high, tls_base) = {
699 let layout = &func_env.env.as_ref(store).layout;
700 (
701 layout.stack_lower,
702 layout.stack_upper,
703 layout.tls_base.expect(
704 "tls_base must be set in memory layout of new instance group's main instance",
705 ),
706 )
707 };
708
709 trace!(stack_low, stack_high, "Memory layout");
710
711 // WASIX threads initialize their own stack pointer global in wasi_thread_start,
712 // so no need to initialize it to a value here.
713 let stack_pointer = create_main_stack_pointer_global(store, &main_module, 0)?;
714
715 let c_longjmp = Tag::new(store, vec![Type::I32]);
716 let cpp_exception = Tag::new(store, vec![Type::I32]);
717
718 let barrier_rx = ls_write.send_pending_operation_barrier.add_rx();
719 let operation_rx = ls_write.send_pending_operation.add_rx();
720
721 let mut instance_group = InstanceGroupState {
722 main_instance: None,
723 main_instance_tls_base: Some(tls_base),
724 side_instances: HashMap::new(),
725 stack_pointer,
726 memory: memory.clone(),
727 indirect_function_table: indirect_function_table.clone(),
728 c_longjmp,
729 cpp_exception,
730 recv_pending_operation_barrier: barrier_rx,
731 recv_pending_operation: operation_rx,
732 };
733
734 let mut pending_resolutions = PendingResolutionsFromLinker::default();
735
736 let well_known_imports = [
737 ("env", "__memory_base", ls_write.main_module_memory_base),
738 ("env", "__table_base", MAIN_MODULE_TABLE_BASE),
739 ("GOT.mem", "__stack_high", stack_high),
740 ("GOT.mem", "__stack_low", stack_low),
741 ("GOT.mem", "__heap_base", ls_write.heap_base),
742 ];
743
744 trace!("Populating imports object for new instance group's main instance");
745 instance_group.populate_imports_from_linker(
746 MAIN_MODULE_HANDLE,
747 &ls_write,
748 store,
749 &main_module,
750 &mut imports,
751 &func_env.env,
752 &well_known_imports,
753 &mut pending_resolutions,
754 )?;
755
756 let main_instance = Instance::new(store, &main_module, &imports)?;
757
758 instance_group.main_instance = Some(main_instance.clone());
759
760 for side in &ls_write.side_modules {
761 trace!(module_handle = ?side.0, "Instantiating existing side module");
762 instance_group.instantiate_side_module_from_linker(
763 &ls_write,
764 store,
765 &func_env.env,
766 *side.0,
767 &mut pending_resolutions,
768 )?;
769 }
770
771 trace!("Finalizing pending functions");
772 instance_group.finalize_pending_resolutions_from_linker(&pending_resolutions, store)?;
773
774 trace!("Applying externally-requested function table entries");
775 instance_group.apply_requested_symbols_from_linker(store, &ls_write)?;
776
777 drop(ls_write);
778 drop(topology_hold);
779
780 let linker = Self {
781 shared: linker_shared,
782 instance_group_state: Arc::new(Mutex::new(Some(instance_group))),
783 };
784
785 let module_handles = WasiModuleTreeHandles::Dynamic {
786 linker: linker.clone(),
787 main_module_instance_handles: WasiModuleInstanceHandles::new(
788 memory.clone(),
789 store,
790 main_instance.clone(),
791 Some(indirect_function_table.clone()),
792 ),
793 };
794
795 func_env
796 .initialize_handles_and_layout(
797 store,
798 main_instance.clone(),
799 module_handles,
800 None,
801 false,
802 )
803 .map_err(LinkError::MainModuleHandleInitFailed)?;
804
805 trace!("Instance group spawned successfully");
806
807 Ok((
808 linker,
809 LinkedMainModule {
810 instance: main_instance,
811 memory,
812 indirect_function_table,
813 stack_low,
814 stack_high,
815 },
816 ))
817 }
818
819 pub fn shutdown_instance_group(
820 &self,
821 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
822 ) -> Result<(), LinkError> {
823 trace!("Shutting instance group down");
824
825 let mut guard = self.instance_group_state.lock().unwrap();
826 match guard.as_mut() {
827 None => Ok(()),
828 Some(group_state) => {
829 // We need to do this even if the results of an incoming dl op will be thrown away;
830 // this is because the instigating group will have counted us and we need to hit the
831 // barrier twice to unblock everybody else.
832 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
833 guard.take();
834 drop(linker_state);
835
836 trace!("Instance group shut down");
837
838 Ok(())
839 }
840 }
841 }
842
843 /// Allocate a index for a closure in the indirect function table
844 pub fn allocate_closure_index(
845 &self,
846 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
847 ) -> Result<u32, LinkError> {
848 lock_instance_group_state!(
849 group_state_guard,
850 group_state,
851 self,
852 LinkError::InstanceGroupIsDead
853 );
854 let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
855
856 // Use a previously allocated slot if possible
857 if let Some(function_index) = linker_state.available_closure_functions.pop() {
858 linker_state
859 .allocated_closure_functions
860 .insert(function_index, true);
861 return Ok(function_index);
862 }
863
864 drop(linker_state);
865
866 let (topology_token, mut linker_state) = self
867 .shared
868 .write_linker_state_with_topology(group_state, ctx)?;
869
870 let mut store = ctx.as_store_mut();
871
872 // Another group may have refilled slots while we released the linker lock.
873 if let Some(function_index) = linker_state.available_closure_functions.pop() {
874 linker_state
875 .allocated_closure_functions
876 .insert(function_index, true);
877 drop(linker_state);
878 drop(topology_token);
879 return Ok(function_index);
880 }
881
882 // Allocate more closures than we need to reduce the number of sync operations
883 const CLOSURE_ALLOCATION_SIZE: u32 = 100;
884
885 let function_index = group_state
886 .allocate_function_table(&mut store, CLOSURE_ALLOCATION_SIZE, 0)
887 .map_err(LinkError::TableAllocationError)? as u32;
888
889 linker_state
890 .available_closure_functions
891 .reserve(CLOSURE_ALLOCATION_SIZE as usize - 1);
892 for i in 1..CLOSURE_ALLOCATION_SIZE {
893 linker_state
894 .available_closure_functions
895 .push(function_index + i);
896 linker_state
897 .allocated_closure_functions
898 .insert(function_index + i, false);
899 }
900 linker_state
901 .allocated_closure_functions
902 .insert(function_index, true);
903
904 self.shared.synchronize_link_operation(
905 topology_token,
906 DlOperation::AllocateFunctionTable {
907 index: function_index,
908 size: CLOSURE_ALLOCATION_SIZE,
909 },
910 linker_state,
911 group_state,
912 &ctx.data().process,
913 ctx.data().tid(),
914 );
915
916 Ok(function_index)
917 }
918
919 /// Remove a previously allocated slot for a closure in the indirect function table
920 ///
921 /// After calling this it is undefined behavior to call the function at the given index.
922 pub fn free_closure_index(
923 &self,
924 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
925 function_id: u32,
926 ) -> Result<(), LinkError> {
927 lock_instance_group_state!(
928 group_state_guard,
929 group_state,
930 self,
931 LinkError::InstanceGroupIsDead
932 );
933 let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
934
935 let Some(entry) = linker_state
936 .allocated_closure_functions
937 .get_mut(&function_id)
938 else {
939 // Not allocated
940 return Ok(());
941 };
942 if !*entry {
943 // Not used
944 return Ok(());
945 }
946
947 *entry = false;
948 linker_state.available_closure_functions.push(function_id);
949 Ok(())
950 }
951
952 /// Check if an indirect_function_table entry is reserved for closures.
953 /// Returns false if the entry is not reserved for closures.
954 /// Requires a FunctionEnvMut because pending DL operations should always
955 /// be processed before acquiring any lock on the linker.
956 // TODO: we can cache this information within the group state so we don't
957 // need a write lock on the linker state here
958 pub fn is_closure(
959 &self,
960 function_id: u32,
961 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
962 ) -> Result<bool, LinkError> {
963 // If we can get a read lock on the linker state, do it
964 if let Ok(linker_state) = self.shared.try_read_linker_state() {
965 return Ok(linker_state
966 .allocated_closure_functions
967 .contains_key(&function_id));
968 }
969
970 // Otherwise, fall back to the path where we apply DL ops and acquire
971 // a write lock afterwards
972 lock_instance_group_state!(
973 group_state_guard,
974 group_state,
975 self,
976 LinkError::InstanceGroupIsDead
977 );
978 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
979 Ok(linker_state
980 .allocated_closure_functions
981 .contains_key(&function_id))
982 }
983
984 /// Loads a side module from the given path, linking it against the existing module tree
985 /// and instantiating it. Symbols from the module can then be retrieved by calling
986 /// [`Linker::resolve_export`].
987 pub fn load_module(
988 &self,
989 module_spec: DlModuleSpec,
990 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
991 ) -> Result<ModuleHandle, LinkError> {
992 trace!(?module_spec, "Loading module");
993
994 lock_instance_group_state!(
995 group_state_guard,
996 group_state,
997 self,
998 LinkError::InstanceGroupIsDead
999 );
1000
1001 // TODO: differentiate between an actual link error and an error that occurs as the
1002 // result of a pending operation that needs to be applied first. Currently, errors
1003 // from pending ops are treated as link errors and just reported to guest code rather
1004 // than terminating the process.
1005 let (topology_token, mut linker_state) = self
1006 .shared
1007 .write_linker_state_with_topology(group_state, ctx)?;
1008
1009 let mut link_state = InProgressLinkState::default();
1010 let env = ctx.as_ref();
1011 let mut store = ctx.as_store_mut();
1012
1013 trace!("Loading module tree for requested module");
1014 let wasi_env = env.as_ref(&store);
1015 let runtime_path: &[String] = &[];
1016 let module_handle = linker_state.load_module_tree(
1017 module_spec,
1018 &mut link_state,
1019 &wasi_env.runtime,
1020 &wasi_env.state,
1021 runtime_path, // No runtime path when loading a module via dlopen
1022 Option::<&Path>::None, // Empty runtime path means we don't need the module's path either
1023 )?;
1024
1025 let new_modules = link_state
1026 .new_modules
1027 .iter()
1028 .map(|m| m.handle)
1029 .collect::<Vec<_>>();
1030
1031 for handle in &new_modules {
1032 trace!(?module_handle, "Instantiating module");
1033 group_state.instantiate_side_module_from_link_state(
1034 &mut linker_state,
1035 &mut store,
1036 &env,
1037 &mut link_state,
1038 *handle,
1039 )?;
1040 }
1041
1042 trace!("Finalizing link");
1043 self.finalize_link_operation(group_state_guard, &mut linker_state, &mut store, link_state)?;
1044
1045 if !new_modules.is_empty() {
1046 // The group state is unlocked for stub functions, now lock it again
1047 lock_instance_group_state!(
1048 group_state_guard,
1049 group_state,
1050 self,
1051 LinkError::InstanceGroupIsDead
1052 );
1053
1054 self.shared.synchronize_link_operation(
1055 topology_token,
1056 DlOperation::LoadModules(new_modules),
1057 linker_state,
1058 group_state,
1059 &ctx.data().process,
1060 ctx.data().tid(),
1061 );
1062 }
1063
1064 // FIXME: If we fail at an intermediate step, we should reset the linker's state, a la:
1065 // if result.is_err() {
1066 // let mut guard = self.state.lock().unwrap();
1067 // let memory = guard.memory.clone();
1068
1069 // for module_handle in link_state.module_handles.iter().cloned() {
1070 // let module = guard.side_modules.remove(&module_handle).unwrap();
1071 // guard
1072 // .side_module_names
1073 // .retain(|_, handle| *handle != module_handle);
1074 // // We already have an error we need to report, so ignore memory deallocation errors
1075 // _ = guard
1076 // .memory_allocator
1077 // .deallocate(&memory, store, module.memory_base);
1078 // }
1079 // }
1080
1081 trace!("Module load complete");
1082
1083 Ok(module_handle)
1084 }
1085
1086 fn finalize_link_operation(
1087 &self,
1088 // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1089 mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1090 linker_state: &mut LinkerState,
1091 store: &mut impl AsStoreMut,
1092 link_state: InProgressLinkState,
1093 ) -> Result<(), LinkError> {
1094 let group_state = group_state_guard.as_mut().unwrap();
1095
1096 trace!(?link_state, "Finalizing link operation");
1097
1098 group_state.finalize_pending_globals(
1099 linker_state,
1100 store,
1101 &link_state.unresolved_globals,
1102 )?;
1103
1104 self.initialize_new_modules(group_state_guard, store, link_state)
1105 }
1106
1107 fn initialize_new_modules(
1108 &self,
1109 // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1110 mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1111 store: &mut impl AsStoreMut,
1112 link_state: InProgressLinkState,
1113 ) -> Result<(), LinkError> {
1114 let group_state = group_state_guard.as_mut().unwrap();
1115
1116 let new_instances = link_state
1117 .new_modules
1118 .iter()
1119 .map(|m| group_state.side_instances[&m.handle].instance.clone())
1120 .collect::<Vec<_>>();
1121
1122 // The instance group must be unlocked for the next step, since modules may need to resolve
1123 // stub functions and that requires a lock on the instance group's state
1124 drop(group_state_guard);
1125
1126 // These functions are exported from PIE executables, and need to be run before calling
1127 // _initialize or _start. More info:
1128 // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
1129 trace!("Calling data relocation functions");
1130 for instance in &new_instances {
1131 call_initialization_function::<()>(instance, store, "__wasm_apply_data_relocs")?;
1132 call_initialization_function::<()>(instance, store, "__wasm_apply_tls_relocs")?;
1133 }
1134
1135 trace!("Calling ctor functions");
1136 for instance in &new_instances {
1137 call_initialization_function::<()>(instance, store, "__wasm_call_ctors")?;
1138 }
1139
1140 Ok(())
1141 }
1142
1143 // TODO: Support RTLD_NEXT
1144 /// Resolves an export from the module corresponding to the given module handle.
1145 /// Only functions and globals can be resolved.
1146 ///
1147 /// If the symbol is a global, the returned value will be the absolute address of
1148 /// the data corresponding to that global within the shared linear memory.
1149 ///
1150 /// If it's a function, it'll be placed into the indirect function table,
1151 /// which creates a "function pointer" that can be used from WASM code.
1152 pub fn resolve_export(
1153 &self,
1154 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1155 module_handle: Option<ModuleHandle>,
1156 symbol: &str,
1157 ) -> Result<ResolvedExport, ResolveError> {
1158 trace!(?module_handle, symbol, "Resolving symbol");
1159
1160 let resolution_key = SymbolResolutionKey::Requested {
1161 resolve_from: module_handle,
1162 name: symbol.to_string(),
1163 };
1164
1165 lock_instance_group_state!(guard, group_state, self, ResolveError::InstanceGroupIsDead);
1166
1167 if let Ok(linker_state) = self.shared.try_read_linker_state()
1168 && let Some(resolution) = linker_state.symbol_resolution_records.get(&resolution_key)
1169 {
1170 trace!(?resolution, "Already have a resolution for this symbol");
1171 match resolution {
1172 SymbolResolutionResult::FunctionPointer {
1173 function_table_index: addr,
1174 ..
1175 } => {
1176 return Ok(ResolvedExport::Function {
1177 func_ptr: *addr as u64,
1178 });
1179 }
1180 SymbolResolutionResult::Memory(addr) => {
1181 return Ok(ResolvedExport::Global { data_ptr: *addr });
1182 }
1183 SymbolResolutionResult::Tls {
1184 resolved_from,
1185 offset,
1186 } => {
1187 let Some(tls_base) = group_state.tls_base(*resolved_from) else {
1188 return Err(ResolveError::NoTlsBaseGlobalExport);
1189 };
1190 return Ok(ResolvedExport::Global {
1191 data_ptr: tls_base + offset,
1192 });
1193 }
1194 r => panic!(
1195 "Internal error: unexpected symbol resolution \
1196 {r:?} for requested symbol {symbol}"
1197 ),
1198 }
1199 }
1200
1201 let (topology_token, mut linker_state) = self
1202 .shared
1203 .write_linker_state_with_topology(group_state, ctx)?;
1204
1205 let mut store = ctx.as_store_mut();
1206
1207 trace!("Resolving export");
1208 let (export, resolved_from) =
1209 group_state.resolve_export(&linker_state, &mut store, module_handle, symbol, false)?;
1210
1211 trace!(?export, ?resolved_from, "Resolved export");
1212
1213 match export {
1214 PartiallyResolvedExport::Global(addr) => {
1215 linker_state
1216 .symbol_resolution_records
1217 .insert(resolution_key, SymbolResolutionResult::Memory(addr));
1218
1219 Ok(ResolvedExport::Global { data_ptr: addr })
1220 }
1221 PartiallyResolvedExport::Tls { offset, final_addr } => {
1222 linker_state.symbol_resolution_records.insert(
1223 resolution_key,
1224 SymbolResolutionResult::Tls {
1225 resolved_from,
1226 offset,
1227 },
1228 );
1229
1230 Ok(ResolvedExport::Global {
1231 data_ptr: final_addr,
1232 })
1233 }
1234 PartiallyResolvedExport::Function(func) => {
1235 let func_ptr = group_state
1236 .append_to_function_table(&mut store, func.clone())
1237 .map_err(ResolveError::TableAllocationError)?;
1238 trace!(
1239 ?func_ptr,
1240 table_size = group_state.indirect_function_table.size(&store),
1241 "Placed resolved function into table"
1242 );
1243 linker_state.symbol_resolution_records.insert(
1244 resolution_key,
1245 SymbolResolutionResult::FunctionPointer {
1246 resolved_from,
1247 function_table_index: func_ptr,
1248 },
1249 );
1250
1251 self.shared.synchronize_link_operation(
1252 topology_token,
1253 DlOperation::ResolveFunction {
1254 name: symbol.to_string(),
1255 resolved_from,
1256 function_table_index: func_ptr,
1257 },
1258 linker_state,
1259 group_state,
1260 &ctx.data().process,
1261 ctx.data().tid(),
1262 );
1263
1264 Ok(ResolvedExport::Function {
1265 func_ptr: func_ptr as u64,
1266 })
1267 }
1268 }
1269 }
1270
1271 pub fn is_handle_valid(
1272 &self,
1273 handle: ModuleHandle,
1274 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1275 ) -> Result<bool, LinkError> {
1276 // If we can get a read lock on the linker state, do it
1277 if let Ok(linker_state) = self.shared.try_read_linker_state() {
1278 return Ok(linker_state.side_modules.contains_key(&handle));
1279 }
1280
1281 // Otherwise, fall back to the path where we apply DL ops and acquire
1282 // a write lock afterwards
1283 lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
1284 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
1285 Ok(linker_state.side_modules.contains_key(&handle))
1286 }
1287}