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}