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 let instance_group_state = Arc::new(Mutex::new(Some(instance_group)));
761
762 let linker = Self {
763 shared: linker_shared.clone(),
764 instance_group_state: instance_group_state.clone(),
765 };
766
767 let module_handles = WasiModuleTreeHandles::Dynamic {
768 linker: linker.clone(),
769 main_module_instance_handles: WasiModuleInstanceHandles::new(
770 memory.clone(),
771 store,
772 main_instance.clone(),
773 Some(indirect_function_table.clone()),
774 ),
775 };
776
777 func_env
778 .initialize_handles_and_layout(
779 store,
780 main_instance.clone(),
781 module_handles,
782 None,
783 false,
784 )
785 .map_err(LinkError::MainModuleHandleInitFailed)?;
786
787 let side_module_handles: Vec<ModuleHandle> =
788 ls_write.side_modules.keys().copied().collect();
789 for module_handle in side_module_handles {
790 trace!(?module_handle, "Instantiating existing side module");
791 let prepared = {
792 let mut guard = instance_group_state.lock().unwrap();
793 let group = guard
794 .as_mut()
795 .expect("Internal error: instance group state was cleared during spawn");
796 group.prepare_side_module_from_linker(
797 &ls_write,
798 store,
799 &func_env.env,
800 module_handle,
801 &mut pending_resolutions,
802 )?
803 };
804
805 // Guest code may reenter the linker (e.g. via sched_yield); do not hold the
806 // instance-group mutex across __wasix_init_tls.
807 let tls_base =
808 call_initialization_function::<i32>(&prepared.instance, store, "__wasix_init_tls")?
809 .map(|v| v as u64);
810
811 {
812 let mut guard = instance_group_state.lock().unwrap();
813 let group = guard
814 .as_mut()
815 .expect("Internal error: instance group state was cleared during spawn");
816 group.complete_side_module_from_linker(prepared, tls_base, store)?;
817 }
818 }
819
820 trace!("Finalizing pending functions");
821 {
822 let guard = instance_group_state.lock().unwrap();
823 let group = guard
824 .as_ref()
825 .expect("Internal error: instance group state was cleared during spawn");
826 group.finalize_pending_resolutions_from_linker(&pending_resolutions, store)?;
827 }
828
829 trace!("Applying externally-requested function table entries");
830 {
831 let guard = instance_group_state.lock().unwrap();
832 let group = guard
833 .as_ref()
834 .expect("Internal error: instance group state was cleared during spawn");
835 group.apply_requested_symbols_from_linker(store, &ls_write)?;
836 }
837
838 drop(ls_write);
839 drop(topology_hold);
840
841 trace!("Instance group spawned successfully");
842
843 Ok((
844 linker,
845 LinkedMainModule {
846 instance: main_instance,
847 memory,
848 indirect_function_table,
849 stack_low,
850 stack_high,
851 },
852 ))
853 }
854
855 pub fn shutdown_instance_group(
856 &self,
857 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
858 ) -> Result<(), LinkError> {
859 trace!("Shutting instance group down");
860
861 let mut guard = self.instance_group_state.lock().unwrap();
862 match guard.as_mut() {
863 None => Ok(()),
864 Some(group_state) => {
865 // We need to do this even if the results of an incoming dl op will be thrown away;
866 // this is because the instigating group will have counted us and we need to hit the
867 // barrier twice to unblock everybody else.
868 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
869 guard.take();
870 drop(linker_state);
871
872 trace!("Instance group shut down");
873
874 Ok(())
875 }
876 }
877 }
878
879 /// Allocate a index for a closure in the indirect function table
880 pub fn allocate_closure_index(
881 &self,
882 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
883 ) -> Result<u32, LinkError> {
884 lock_instance_group_state!(
885 group_state_guard,
886 group_state,
887 self,
888 LinkError::InstanceGroupIsDead
889 );
890 let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
891
892 // Use a previously allocated slot if possible
893 if let Some(function_index) = linker_state.available_closure_functions.pop() {
894 linker_state
895 .allocated_closure_functions
896 .insert(function_index, true);
897 return Ok(function_index);
898 }
899
900 drop(linker_state);
901
902 let (topology_token, mut linker_state) = self
903 .shared
904 .write_linker_state_with_topology(group_state, ctx)?;
905
906 let mut store = ctx.as_store_mut();
907
908 // Another group may have refilled slots while we released the linker lock.
909 if let Some(function_index) = linker_state.available_closure_functions.pop() {
910 linker_state
911 .allocated_closure_functions
912 .insert(function_index, true);
913 drop(linker_state);
914 drop(topology_token);
915 return Ok(function_index);
916 }
917
918 // Allocate more closures than we need to reduce the number of sync operations
919 const CLOSURE_ALLOCATION_SIZE: u32 = 100;
920
921 let function_index = group_state
922 .allocate_function_table(&mut store, CLOSURE_ALLOCATION_SIZE, 0)
923 .map_err(LinkError::TableAllocationError)? as u32;
924
925 linker_state
926 .available_closure_functions
927 .reserve(CLOSURE_ALLOCATION_SIZE as usize - 1);
928 for i in 1..CLOSURE_ALLOCATION_SIZE {
929 linker_state
930 .available_closure_functions
931 .push(function_index + i);
932 linker_state
933 .allocated_closure_functions
934 .insert(function_index + i, false);
935 }
936 linker_state
937 .allocated_closure_functions
938 .insert(function_index, true);
939
940 self.shared.synchronize_link_operation(
941 topology_token,
942 DlOperation::AllocateFunctionTable {
943 index: function_index,
944 size: CLOSURE_ALLOCATION_SIZE,
945 },
946 linker_state,
947 group_state,
948 &ctx.data().process,
949 ctx.data().tid(),
950 );
951
952 Ok(function_index)
953 }
954
955 /// Remove a previously allocated slot for a closure in the indirect function table
956 ///
957 /// After calling this it is undefined behavior to call the function at the given index.
958 pub fn free_closure_index(
959 &self,
960 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
961 function_id: u32,
962 ) -> Result<(), LinkError> {
963 lock_instance_group_state!(
964 group_state_guard,
965 group_state,
966 self,
967 LinkError::InstanceGroupIsDead
968 );
969 let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
970
971 let Some(entry) = linker_state
972 .allocated_closure_functions
973 .get_mut(&function_id)
974 else {
975 // Not allocated
976 return Ok(());
977 };
978 if !*entry {
979 // Not used
980 return Ok(());
981 }
982
983 *entry = false;
984 linker_state.available_closure_functions.push(function_id);
985 Ok(())
986 }
987
988 /// Check if an indirect_function_table entry is reserved for closures.
989 /// Returns false if the entry is not reserved for closures.
990 /// Requires a FunctionEnvMut because pending DL operations should always
991 /// be processed before acquiring any lock on the linker.
992 // TODO: we can cache this information within the group state so we don't
993 // need a write lock on the linker state here
994 pub fn is_closure(
995 &self,
996 function_id: u32,
997 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
998 ) -> Result<bool, LinkError> {
999 // If we can get a read lock on the linker state, do it
1000 if let Ok(linker_state) = self.shared.try_read_linker_state() {
1001 return Ok(linker_state
1002 .allocated_closure_functions
1003 .contains_key(&function_id));
1004 }
1005
1006 // Otherwise, fall back to the path where we apply DL ops and acquire
1007 // a write lock afterwards
1008 lock_instance_group_state!(
1009 group_state_guard,
1010 group_state,
1011 self,
1012 LinkError::InstanceGroupIsDead
1013 );
1014 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
1015 Ok(linker_state
1016 .allocated_closure_functions
1017 .contains_key(&function_id))
1018 }
1019
1020 /// Loads a side module from the given path, linking it against the existing module tree
1021 /// and instantiating it. Symbols from the module can then be retrieved by calling
1022 /// [`Linker::resolve_export`].
1023 pub fn load_module(
1024 &self,
1025 module_spec: DlModuleSpec,
1026 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1027 ) -> Result<ModuleHandle, LinkError> {
1028 trace!(?module_spec, "Loading module");
1029
1030 lock_instance_group_state!(
1031 group_state_guard,
1032 group_state,
1033 self,
1034 LinkError::InstanceGroupIsDead
1035 );
1036
1037 // TODO: differentiate between an actual link error and an error that occurs as the
1038 // result of a pending operation that needs to be applied first. Currently, errors
1039 // from pending ops are treated as link errors and just reported to guest code rather
1040 // than terminating the process.
1041 let (topology_token, mut linker_state) = self
1042 .shared
1043 .write_linker_state_with_topology(group_state, ctx)?;
1044
1045 let mut link_state = InProgressLinkState::default();
1046 let env = ctx.as_ref();
1047 let mut store = ctx.as_store_mut();
1048
1049 trace!("Loading module tree for requested module");
1050 let wasi_env = env.as_ref(&store);
1051 let runtime_path: &[String] = &[];
1052 let module_handle = linker_state.load_module_tree(
1053 module_spec,
1054 &mut link_state,
1055 &wasi_env.runtime,
1056 &wasi_env.state,
1057 runtime_path, // No runtime path when loading a module via dlopen
1058 Option::<&Path>::None, // Empty runtime path means we don't need the module's path either
1059 )?;
1060
1061 let new_modules = link_state
1062 .new_modules
1063 .iter()
1064 .map(|m| m.handle)
1065 .collect::<Vec<_>>();
1066
1067 for handle in &new_modules {
1068 trace!(?module_handle, "Instantiating module");
1069 group_state.instantiate_side_module_from_link_state(
1070 &mut linker_state,
1071 &mut store,
1072 &env,
1073 &mut link_state,
1074 *handle,
1075 )?;
1076 }
1077
1078 trace!("Finalizing link");
1079 self.finalize_link_operation(group_state_guard, &mut linker_state, &mut store, link_state)?;
1080
1081 if !new_modules.is_empty() {
1082 // The group state is unlocked for stub functions, now lock it again
1083 lock_instance_group_state!(
1084 group_state_guard,
1085 group_state,
1086 self,
1087 LinkError::InstanceGroupIsDead
1088 );
1089
1090 self.shared.synchronize_link_operation(
1091 topology_token,
1092 DlOperation::LoadModules(new_modules),
1093 linker_state,
1094 group_state,
1095 &ctx.data().process,
1096 ctx.data().tid(),
1097 );
1098 }
1099
1100 // FIXME: If we fail at an intermediate step, we should reset the linker's state, a la:
1101 // if result.is_err() {
1102 // let mut guard = self.state.lock().unwrap();
1103 // let memory = guard.memory.clone();
1104
1105 // for module_handle in link_state.module_handles.iter().cloned() {
1106 // let module = guard.side_modules.remove(&module_handle).unwrap();
1107 // guard
1108 // .side_module_names
1109 // .retain(|_, handle| *handle != module_handle);
1110 // // We already have an error we need to report, so ignore memory deallocation errors
1111 // _ = guard
1112 // .memory_allocator
1113 // .deallocate(&memory, store, module.memory_base);
1114 // }
1115 // }
1116
1117 trace!("Module load complete");
1118
1119 Ok(module_handle)
1120 }
1121
1122 fn finalize_link_operation(
1123 &self,
1124 // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1125 mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1126 linker_state: &mut LinkerState,
1127 store: &mut impl AsStoreMut,
1128 link_state: InProgressLinkState,
1129 ) -> Result<(), LinkError> {
1130 let group_state = group_state_guard.as_mut().unwrap();
1131
1132 trace!(?link_state, "Finalizing link operation");
1133
1134 group_state.finalize_pending_globals(
1135 linker_state,
1136 store,
1137 &link_state.unresolved_globals,
1138 )?;
1139
1140 self.initialize_new_modules(group_state_guard, store, link_state)
1141 }
1142
1143 fn initialize_new_modules(
1144 &self,
1145 // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1146 mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1147 store: &mut impl AsStoreMut,
1148 link_state: InProgressLinkState,
1149 ) -> Result<(), LinkError> {
1150 let group_state = group_state_guard.as_mut().unwrap();
1151
1152 let new_instances = link_state
1153 .new_modules
1154 .iter()
1155 .map(|m| group_state.side_instances[&m.handle].instance.clone())
1156 .collect::<Vec<_>>();
1157
1158 // The instance group must be unlocked for the next step, since modules may need to resolve
1159 // stub functions and that requires a lock on the instance group's state
1160 drop(group_state_guard);
1161
1162 // These functions are exported from PIE executables, and need to be run before calling
1163 // _initialize or _start. More info:
1164 // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
1165 trace!("Calling data relocation functions");
1166 for instance in &new_instances {
1167 call_initialization_function::<()>(instance, store, "__wasm_apply_data_relocs")?;
1168 call_initialization_function::<()>(instance, store, "__wasm_apply_tls_relocs")?;
1169 }
1170
1171 trace!("Calling ctor functions");
1172 for instance in &new_instances {
1173 call_initialization_function::<()>(instance, store, "__wasm_call_ctors")?;
1174 }
1175
1176 Ok(())
1177 }
1178
1179 // TODO: Support RTLD_NEXT
1180 /// Resolves an export from the module corresponding to the given module handle.
1181 /// Only functions and globals can be resolved.
1182 ///
1183 /// If the symbol is a global, the returned value will be the absolute address of
1184 /// the data corresponding to that global within the shared linear memory.
1185 ///
1186 /// If it's a function, it'll be placed into the indirect function table,
1187 /// which creates a "function pointer" that can be used from WASM code.
1188 pub fn resolve_export(
1189 &self,
1190 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1191 module_handle: Option<ModuleHandle>,
1192 symbol: &str,
1193 ) -> Result<ResolvedExport, ResolveError> {
1194 trace!(?module_handle, symbol, "Resolving symbol");
1195
1196 let resolution_key = SymbolResolutionKey::Requested {
1197 resolve_from: module_handle,
1198 name: symbol.to_string(),
1199 };
1200
1201 lock_instance_group_state!(guard, group_state, self, ResolveError::InstanceGroupIsDead);
1202
1203 if let Ok(linker_state) = self.shared.try_read_linker_state()
1204 && let Some(resolution) = linker_state.symbol_resolution_records.get(&resolution_key)
1205 {
1206 trace!(?resolution, "Already have a resolution for this symbol");
1207 match resolution {
1208 SymbolResolutionResult::FunctionPointer {
1209 function_table_index: addr,
1210 ..
1211 } => {
1212 return Ok(ResolvedExport::Function {
1213 func_ptr: *addr as u64,
1214 });
1215 }
1216 SymbolResolutionResult::Memory(addr) => {
1217 return Ok(ResolvedExport::Global { data_ptr: *addr });
1218 }
1219 SymbolResolutionResult::Tls {
1220 resolved_from,
1221 offset,
1222 } => {
1223 let Some(tls_base) = group_state.tls_base(*resolved_from) else {
1224 return Err(ResolveError::NoTlsBaseGlobalExport);
1225 };
1226 return Ok(ResolvedExport::Global {
1227 data_ptr: tls_base + offset,
1228 });
1229 }
1230 r => panic!(
1231 "Internal error: unexpected symbol resolution \
1232 {r:?} for requested symbol {symbol}"
1233 ),
1234 }
1235 }
1236
1237 let (topology_token, mut linker_state) = self
1238 .shared
1239 .write_linker_state_with_topology(group_state, ctx)?;
1240
1241 let mut store = ctx.as_store_mut();
1242
1243 trace!("Resolving export");
1244 let (export, resolved_from) =
1245 group_state.resolve_export(&linker_state, &mut store, module_handle, symbol, false)?;
1246
1247 trace!(?export, ?resolved_from, "Resolved export");
1248
1249 match export {
1250 PartiallyResolvedExport::Global(addr) => {
1251 linker_state
1252 .symbol_resolution_records
1253 .insert(resolution_key, SymbolResolutionResult::Memory(addr));
1254
1255 Ok(ResolvedExport::Global { data_ptr: addr })
1256 }
1257 PartiallyResolvedExport::Tls { offset, final_addr } => {
1258 linker_state.symbol_resolution_records.insert(
1259 resolution_key,
1260 SymbolResolutionResult::Tls {
1261 resolved_from,
1262 offset,
1263 },
1264 );
1265
1266 Ok(ResolvedExport::Global {
1267 data_ptr: final_addr,
1268 })
1269 }
1270 PartiallyResolvedExport::Function(func) => {
1271 let func_ptr = group_state
1272 .append_to_function_table(&mut store, func.clone())
1273 .map_err(ResolveError::TableAllocationError)?;
1274 trace!(
1275 ?func_ptr,
1276 table_size = group_state.indirect_function_table.size(&store),
1277 "Placed resolved function into table"
1278 );
1279 linker_state.symbol_resolution_records.insert(
1280 resolution_key,
1281 SymbolResolutionResult::FunctionPointer {
1282 resolved_from,
1283 function_table_index: func_ptr,
1284 },
1285 );
1286
1287 self.shared.synchronize_link_operation(
1288 topology_token,
1289 DlOperation::ResolveFunction {
1290 name: symbol.to_string(),
1291 resolved_from,
1292 function_table_index: func_ptr,
1293 },
1294 linker_state,
1295 group_state,
1296 &ctx.data().process,
1297 ctx.data().tid(),
1298 );
1299
1300 Ok(ResolvedExport::Function {
1301 func_ptr: func_ptr as u64,
1302 })
1303 }
1304 }
1305 }
1306
1307 pub fn is_handle_valid(
1308 &self,
1309 handle: ModuleHandle,
1310 ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1311 ) -> Result<bool, LinkError> {
1312 // If we can get a read lock on the linker state, do it
1313 if let Ok(linker_state) = self.shared.try_read_linker_state() {
1314 return Ok(linker_state.side_modules.contains_key(&handle));
1315 }
1316
1317 // Otherwise, fall back to the path where we apply DL ops and acquire
1318 // a write lock afterwards
1319 lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
1320 let linker_state = self.shared.write_linker_state(group_state, ctx)?;
1321 Ok(linker_state.side_modules.contains_key(&handle))
1322 }
1323}