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 runtime_hooks;
270mod sync;
271mod types;
272mod wasm_utils;
273
274pub use dylink::*;
275pub use error::*;
276pub use types::*;
277
278use instance_group::*;
279use internal_types::*;
280use linker_state::*;
281use locator::*;
282use memory_allocator::*;
283use runtime_hooks::instantiate_with_runtime_hooks;
284use sync::*;
285use wasm_utils::*;
286
287use std::{
288    collections::{BTreeMap, HashMap},
289    ops::DerefMut,
290    path::Path,
291    sync::{Arc, Mutex, MutexGuard, atomic::Ordering},
292};
293
294use bus::Bus;
295use tracing::trace;
296use wasmer::{AsStoreMut, Engine, FunctionEnvMut, Memory, Module, StoreMut, Tag, Type};
297use wasmer_wasix_types::wasix::WasiMemoryLayout;
298
299use crate::{WasiEnv, WasiFunctionEnv, WasiModuleTreeHandles, import_object_for_all_wasi_versions};
300
301use super::WasiModuleInstanceHandles;
302
303// Module handle 1 is always the main module. Side modules get handles starting from the next one after the main module.
304pub static MAIN_MODULE_HANDLE: ModuleHandle = ModuleHandle(1);
305static INVALID_MODULE_HANDLE: ModuleHandle = ModuleHandle(u32::MAX);
306
307// Need to keep the zeroth index null to catch null function pointers at runtime
308static MAIN_MODULE_TABLE_BASE: u64 = 1;
309
310/// The linker is responsible for loading and linking dynamic modules at runtime,
311/// and managing the shared memory and indirect function table.
312/// Each linker instance represents a specific instance group. Cloning a linker
313/// instance does *not* create a new instance group though; the clone will refer
314/// to the same group as the original.
315#[derive(Clone)]
316pub struct Linker {
317    shared: LinkerShared,
318    instance_group_state: Arc<Mutex<Option<InstanceGroupState>>>,
319}
320
321impl std::fmt::Debug for Linker {
322    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323        f.debug_struct("Linker").finish()
324    }
325}
326
327impl Linker {
328    /// Creates a new linker for the given main module. The module is expected to be a
329    /// PIE executable. Imports for the module will be fulfilled, so that it can start
330    /// running, and a Linker instance is returned which can then be used for the
331    /// loading/linking of further side modules.
332    pub fn new(
333        engine: Engine,
334        main_module: &Module,
335        store: &mut StoreMut<'_>,
336        memory: Option<Memory>,
337        func_env: &mut WasiFunctionEnv,
338        stack_size: u64,
339        ld_library_path: &[&Path],
340    ) -> Result<(Self, LinkedMainModule), LinkError> {
341        let dylink_section = parse_dylink0_section(main_module)?;
342
343        trace!(?dylink_section, "Loading main module");
344
345        let mut imports = import_object_for_all_wasi_versions(main_module, store, &func_env.env);
346
347        let function_table_type = main_module_function_table_type(main_module)?;
348
349        let expected_table_length =
350            dylink_section.mem_info.table_size + MAIN_MODULE_TABLE_BASE as u32;
351        let indirect_function_table =
352            create_indirect_function_table(store, function_table_type, expected_table_length)?;
353
354        // Give modules a non-zero memory base, since we don't want
355        // any valid pointers to point to the zero address
356        let memory_base = 2u64.pow(dylink_section.mem_info.memory_alignment);
357
358        let memory_type = main_module_memory_type(main_module)?;
359
360        let memory = match memory {
361            Some(m) => m,
362            None => Memory::new(store, memory_type)?,
363        };
364
365        let stack_low = {
366            let data_end = memory_base + dylink_section.mem_info.memory_size as u64;
367            if !data_end.is_multiple_of(1024) {
368                data_end + 1024 - (data_end % 1024)
369            } else {
370                data_end
371            }
372        };
373
374        if !stack_size.is_multiple_of(1024) {
375            panic!("Stack size must be 1024-bit aligned");
376        }
377
378        let stack_high = stack_low + stack_size;
379
380        // Allocate memory for the stack. This does not need to go through the memory allocator
381        // because it's always placed directly after the main module's data
382        memory.grow_at_least(store, stack_high)?;
383
384        trace!(
385            memory_pages = ?memory.grow(store, 0).unwrap(),
386            memory_base,
387            stack_low,
388            stack_high,
389            "Memory layout"
390        );
391
392        let stack_pointer = create_main_stack_pointer_global(store, main_module, stack_high)?;
393
394        let c_longjmp = Tag::new(store, vec![Type::I32]);
395        let cpp_exception = Tag::new(store, vec![Type::I32]);
396
397        let mut barrier_tx = Bus::new(1);
398        let barrier_rx = barrier_tx.add_rx();
399        let mut operation_tx = Bus::new(1);
400        let operation_rx = operation_tx.add_rx();
401
402        let mut instance_group = InstanceGroupState {
403            main_instance: None,
404            // The TLS base for the main instance is determined by reading the
405            // `__tls_base` global export from the instance after instantiation.
406            main_instance_tls_base: None,
407            side_instances: HashMap::new(),
408            stack_pointer,
409            memory: memory.clone(),
410            indirect_function_table: indirect_function_table.clone(),
411            c_longjmp,
412            cpp_exception,
413            recv_pending_operation_barrier: barrier_rx,
414            recv_pending_operation: operation_rx,
415        };
416
417        let mut linker_state = LinkerState {
418            engine,
419            main_module: main_module.clone(),
420            main_module_dylink_info: dylink_section,
421            main_module_memory_base: memory_base,
422            side_modules: BTreeMap::new(),
423            side_modules_by_name: HashMap::new(),
424            next_module_handle: MAIN_MODULE_HANDLE.0 + 1,
425            memory_allocator: MemoryAllocator::new(),
426            allocated_closure_functions: BTreeMap::new(),
427            available_closure_functions: Vec::new(),
428            heap_base: stack_high,
429            symbol_resolution_records: HashMap::new(),
430            send_pending_operation_barrier: barrier_tx,
431            send_pending_operation: operation_tx,
432        };
433
434        let mut link_state = InProgressLinkState::default();
435
436        let well_known_imports = [
437            ("env", "__memory_base", memory_base),
438            ("env", "__table_base", MAIN_MODULE_TABLE_BASE),
439            ("GOT.mem", "__stack_high", stack_high),
440            ("GOT.mem", "__stack_low", stack_low),
441            ("GOT.mem", "__heap_base", stack_high),
442        ];
443
444        trace!("Resolving main module's symbols");
445        linker_state.resolve_symbols(
446            &instance_group,
447            store,
448            main_module,
449            MAIN_MODULE_HANDLE,
450            &mut link_state,
451            &well_known_imports,
452        )?;
453
454        trace!("Populating main module's imports object");
455        instance_group.populate_imports_from_link_state(
456            MAIN_MODULE_HANDLE,
457            &mut linker_state,
458            &mut link_state,
459            store,
460            main_module,
461            &mut imports,
462            &func_env.env,
463            &well_known_imports,
464        )?;
465
466        // TODO: figure out which way is faster (stubs in main or stubs in sides),
467        // use that ordering. My *guess* is that, since main exports all the libc
468        // functions and those are called frequently by basically any code, then giving
469        // stubs to main will be faster, but we need numbers before we decide this.
470        let main_instance = instantiate_with_runtime_hooks(
471            &func_env.env,
472            store,
473            main_module,
474            &mut imports,
475            &memory,
476        )?;
477        instance_group.main_instance = Some(main_instance.clone());
478
479        let tls_base = get_tls_base_export(&main_instance, store)?;
480        instance_group.main_instance_tls_base = tls_base;
481
482        let runtime_path = linker_state.main_module_dylink_info.runtime_path.clone();
483        for needed in linker_state.main_module_dylink_info.needed.clone() {
484            // A successful load_module will add the module to the side_modules list,
485            // from which symbols can be resolved in the following call to
486            // guard.resolve_imports.
487            trace!(name = needed, "Loading module needed by main");
488            let wasi_env = func_env.data(store);
489            linker_state.load_module_tree(
490                DlModuleSpec::FileSystem {
491                    module_spec: Path::new(needed.as_str()),
492                    ld_library_path,
493                },
494                &mut link_state,
495                &wasi_env.runtime,
496                &wasi_env.state,
497                runtime_path.as_ref(),
498                // HACK: The main module doesn't have to exist in the virtual FS at all; e.g.
499                // if one runs `wasmer ../module.wasm --volume .`, we won't have access to the
500                // main module's folder within the virtual FS. This is why we're picking PWD
501                // as the $ORIGIN of the main module, which should at least be slightly
502                // sensible. The `main.wasm` file name will be stripped and only the `./`
503                // will be taken into account by `locate_module`.
504                Some(Path::new("./main.wasm")),
505            )?;
506        }
507
508        for module_handle in link_state
509            .new_modules
510            .iter()
511            .map(|m| m.handle)
512            .collect::<Vec<_>>()
513        {
514            trace!(?module_handle, "Instantiating module");
515            instance_group.instantiate_side_module_from_link_state(
516                &mut linker_state,
517                store,
518                &func_env.env,
519                &mut link_state,
520                module_handle,
521            )?;
522        }
523
524        let linker = Self {
525            shared: LinkerShared::new(linker_state),
526            instance_group_state: Arc::new(Mutex::new(Some(instance_group))),
527        };
528
529        let stack_layout = WasiMemoryLayout {
530            stack_lower: stack_low,
531            stack_upper: stack_high,
532            stack_size: stack_high - stack_low,
533            guard_size: 0,
534            tls_base,
535        };
536        let module_handles = WasiModuleTreeHandles::Dynamic {
537            linker: linker.clone(),
538            main_module_instance_handles: WasiModuleInstanceHandles::new(
539                memory.clone(),
540                store,
541                main_instance.clone(),
542                Some(indirect_function_table.clone()),
543            ),
544        };
545
546        func_env
547            .initialize_handles_and_layout(
548                store,
549                main_instance.clone(),
550                module_handles,
551                Some(stack_layout),
552                true,
553            )
554            .map_err(LinkError::MainModuleHandleInitFailed)?;
555
556        {
557            trace!(?link_state, "Finalizing linking of main module");
558
559            let mut group_guard = linker.instance_group_state.lock().unwrap();
560            unsafe {
561                linker.shared.bootstrap_exclusive_write_then(|ls| {
562                    let group_state = group_guard.as_mut().unwrap();
563                    group_state.finalize_pending_globals(
564                        ls,
565                        store,
566                        &link_state.unresolved_globals,
567                    )?;
568
569                    trace!("Calling data relocator function for main module");
570                    call_initialization_function::<()>(
571                        &main_instance,
572                        store,
573                        "__wasm_apply_data_relocs",
574                    )?;
575                    call_initialization_function::<()>(
576                        &main_instance,
577                        store,
578                        "__wasm_apply_tls_relocs",
579                    )?;
580
581                    linker.initialize_new_modules(group_guard, store, link_state)
582                })?;
583            }
584        }
585
586        trace!("Calling main module's _initialize function");
587        call_initialization_function::<()>(&main_instance, store, "_initialize")?;
588
589        trace!("Link complete");
590
591        Ok((
592            linker,
593            LinkedMainModule {
594                instance: main_instance,
595                memory,
596                indirect_function_table,
597                stack_low,
598                stack_high,
599            },
600        ))
601    }
602
603    /// This method gathers all necessary data from a parent thread's
604    /// environment, so a child thread can later call [`Self::create_instance_group`]
605    /// and have its own instance group, letting it take part in dynamic linking.
606    /// This two-part process is needed because the parent and child each have
607    /// their own [`Store`], and [`Store`]s are not `Send`.
608    pub fn prepare_for_instance_group(
609        &self,
610        parent_ctx: &mut FunctionEnvMut<'_, WasiEnv>,
611    ) -> Result<PreparedInstanceGroupData, LinkError> {
612        trace!("Preparing for new instance group");
613
614        lock_instance_group_state!(
615            parent_group_state_guard,
616            parent_group_state,
617            self,
618            LinkError::InstanceGroupIsDead
619        );
620
621        // Lease topology only: parent does not mutate shared `LinkerState` here; the child takes
622        // the blocking write in `create_instance_group` while holding the moved token.
623        let env = parent_ctx.as_ref();
624        let mut store = parent_ctx.as_store_mut();
625        let topology_token =
626            self.shared
627                .acquire_topology_token(parent_group_state, &mut store, &env)?;
628
629        let parent_store = parent_ctx.as_store_mut();
630
631        let memory = parent_group_state
632            .memory
633            .as_shared(&parent_store)
634            .ok_or_else(|| LinkError::MemoryNotShared)?;
635
636        let indirect_function_table_type =
637            parent_group_state.indirect_function_table.ty(&parent_store);
638
639        let expected_table_length = parent_group_state
640            .indirect_function_table
641            .size(&parent_store);
642
643        Ok(PreparedInstanceGroupData {
644            linker_shared: self.shared.clone(),
645            topology_token,
646            memory,
647            indirect_function_table_type,
648            expected_table_length,
649        })
650    }
651
652    pub(crate) fn do_pending_link_operations(
653        &self,
654        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
655        fast: bool,
656    ) -> Result<(), LinkError> {
657        if !self.shared.dl_operation_pending_load(if fast {
658            Ordering::Relaxed
659        } else {
660            Ordering::SeqCst
661        }) {
662            return Ok(());
663        }
664
665        lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
666
667        let env = ctx.as_ref();
668        let mut store = ctx.as_store_mut();
669        self.shared
670            .do_pending_link_operations_internal(group_state, &mut store, &env)
671    }
672
673    pub fn create_instance_group(
674        prepared_instance_group_data: PreparedInstanceGroupData,
675        store: &mut StoreMut<'_>,
676        func_env: &mut WasiFunctionEnv,
677    ) -> Result<(Self, LinkedMainModule), LinkError> {
678        trace!("Spawning new instance group");
679
680        let PreparedInstanceGroupData {
681            linker_shared,
682            topology_token,
683            memory,
684            indirect_function_table_type,
685            expected_table_length,
686        } = prepared_instance_group_data;
687
688        let (topology_hold, mut ls_write) =
689            linker_shared.write_linker_state_blocking_holding_topology(topology_token);
690
691        let main_module = ls_write.main_module.clone();
692
693        let mut imports = import_object_for_all_wasi_versions(&main_module, store, &func_env.env);
694
695        let memory = memory.attach(store);
696
697        let indirect_function_table = create_indirect_function_table(
698            store,
699            indirect_function_table_type,
700            expected_table_length,
701        )?;
702
703        // Since threads initialize their own stack space, we can only rely on the layout being
704        // initialized beforehand, which is the case with the thread_spawn syscall.
705        // FIXME: this needs to become a parameter if we ever decouple the linker from WASIX
706        let (stack_low, stack_high, tls_base) = {
707            let layout = &func_env.env.as_ref(store).layout;
708            (
709                layout.stack_lower,
710                layout.stack_upper,
711                layout.tls_base.expect(
712                    "tls_base must be set in memory layout of new instance group's main instance",
713                ),
714            )
715        };
716
717        trace!(stack_low, stack_high, "Memory layout");
718
719        // WASIX threads initialize their own stack pointer global in wasi_thread_start,
720        // so no need to initialize it to a value here.
721        let stack_pointer = create_main_stack_pointer_global(store, &main_module, 0)?;
722
723        let c_longjmp = Tag::new(store, vec![Type::I32]);
724        let cpp_exception = Tag::new(store, vec![Type::I32]);
725
726        let barrier_rx = ls_write.send_pending_operation_barrier.add_rx();
727        let operation_rx = ls_write.send_pending_operation.add_rx();
728
729        let mut instance_group = InstanceGroupState {
730            main_instance: None,
731            main_instance_tls_base: Some(tls_base),
732            side_instances: HashMap::new(),
733            stack_pointer,
734            memory: memory.clone(),
735            indirect_function_table: indirect_function_table.clone(),
736            c_longjmp,
737            cpp_exception,
738            recv_pending_operation_barrier: barrier_rx,
739            recv_pending_operation: operation_rx,
740        };
741
742        let mut pending_resolutions = PendingResolutionsFromLinker::default();
743
744        let well_known_imports = [
745            ("env", "__memory_base", ls_write.main_module_memory_base),
746            ("env", "__table_base", MAIN_MODULE_TABLE_BASE),
747            ("GOT.mem", "__stack_high", stack_high),
748            ("GOT.mem", "__stack_low", stack_low),
749            ("GOT.mem", "__heap_base", ls_write.heap_base),
750        ];
751
752        trace!("Populating imports object for new instance group's main instance");
753        instance_group.populate_imports_from_linker(
754            MAIN_MODULE_HANDLE,
755            &ls_write,
756            store,
757            &main_module,
758            &mut imports,
759            &func_env.env,
760            &well_known_imports,
761            &mut pending_resolutions,
762        )?;
763
764        let main_instance = instantiate_with_runtime_hooks(
765            &func_env.env,
766            store,
767            &main_module,
768            &mut imports,
769            &memory,
770        )?;
771
772        instance_group.main_instance = Some(main_instance.clone());
773
774        let instance_group_state = Arc::new(Mutex::new(Some(instance_group)));
775
776        let linker = Self {
777            shared: linker_shared.clone(),
778            instance_group_state: instance_group_state.clone(),
779        };
780
781        let module_handles = WasiModuleTreeHandles::Dynamic {
782            linker: linker.clone(),
783            main_module_instance_handles: WasiModuleInstanceHandles::new(
784                memory.clone(),
785                store,
786                main_instance.clone(),
787                Some(indirect_function_table.clone()),
788            ),
789        };
790
791        func_env
792            .initialize_handles_and_layout(
793                store,
794                main_instance.clone(),
795                module_handles,
796                None,
797                false,
798            )
799            .map_err(LinkError::MainModuleHandleInitFailed)?;
800
801        let side_module_handles: Vec<ModuleHandle> =
802            ls_write.side_modules.keys().copied().collect();
803        for module_handle in side_module_handles {
804            trace!(?module_handle, "Instantiating existing side module");
805            let prepared = {
806                let mut guard = instance_group_state.lock().unwrap();
807                let group = guard
808                    .as_mut()
809                    .expect("Internal error: instance group state was cleared during spawn");
810                group.prepare_side_module_from_linker(
811                    &ls_write,
812                    store,
813                    &func_env.env,
814                    module_handle,
815                    &mut pending_resolutions,
816                )?
817            };
818
819            // Guest code may reenter the linker (e.g. via sched_yield); do not hold the
820            // instance-group mutex across __wasix_init_tls.
821            let tls_base =
822                call_initialization_function::<i32>(&prepared.instance, store, "__wasix_init_tls")?
823                    .map(|v| v as u64);
824
825            {
826                let mut guard = instance_group_state.lock().unwrap();
827                let group = guard
828                    .as_mut()
829                    .expect("Internal error: instance group state was cleared during spawn");
830                group.complete_side_module_from_linker(prepared, tls_base, store)?;
831            }
832        }
833
834        trace!("Finalizing pending functions");
835        {
836            let guard = instance_group_state.lock().unwrap();
837            let group = guard
838                .as_ref()
839                .expect("Internal error: instance group state was cleared during spawn");
840            group.finalize_pending_resolutions_from_linker(&pending_resolutions, store)?;
841        }
842
843        trace!("Applying externally-requested function table entries");
844        {
845            let guard = instance_group_state.lock().unwrap();
846            let group = guard
847                .as_ref()
848                .expect("Internal error: instance group state was cleared during spawn");
849            group.apply_requested_symbols_from_linker(store, &ls_write)?;
850        }
851
852        drop(ls_write);
853        drop(topology_hold);
854
855        trace!("Instance group spawned successfully");
856
857        Ok((
858            linker,
859            LinkedMainModule {
860                instance: main_instance,
861                memory,
862                indirect_function_table,
863                stack_low,
864                stack_high,
865            },
866        ))
867    }
868
869    pub fn shutdown_instance_group(
870        &self,
871        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
872    ) -> Result<(), LinkError> {
873        trace!("Shutting instance group down");
874
875        let mut guard = self.instance_group_state.lock().unwrap();
876        match guard.as_mut() {
877            None => Ok(()),
878            Some(group_state) => {
879                // We need to do this even if the results of an incoming dl op will be thrown away;
880                // this is because the instigating group will have counted us and we need to hit the
881                // barrier twice to unblock everybody else.
882                let linker_state = self.shared.write_linker_state(group_state, ctx)?;
883                guard.take();
884                drop(linker_state);
885
886                trace!("Instance group shut down");
887
888                Ok(())
889            }
890        }
891    }
892
893    /// Allocate a index for a closure in the indirect function table
894    pub fn allocate_closure_index(
895        &self,
896        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
897    ) -> Result<u32, LinkError> {
898        lock_instance_group_state!(
899            group_state_guard,
900            group_state,
901            self,
902            LinkError::InstanceGroupIsDead
903        );
904        let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
905
906        // Use a previously allocated slot if possible
907        if let Some(function_index) = linker_state.available_closure_functions.pop() {
908            linker_state
909                .allocated_closure_functions
910                .insert(function_index, true);
911            return Ok(function_index);
912        }
913
914        drop(linker_state);
915
916        let (topology_token, mut linker_state) = self
917            .shared
918            .write_linker_state_with_topology(group_state, ctx)?;
919
920        let mut store = ctx.as_store_mut();
921
922        // Another group may have refilled slots while we released the linker lock.
923        if let Some(function_index) = linker_state.available_closure_functions.pop() {
924            linker_state
925                .allocated_closure_functions
926                .insert(function_index, true);
927            drop(linker_state);
928            drop(topology_token);
929            return Ok(function_index);
930        }
931
932        // Allocate more closures than we need to reduce the number of sync operations
933        const CLOSURE_ALLOCATION_SIZE: u32 = 100;
934
935        let function_index = group_state
936            .allocate_function_table(&mut store, CLOSURE_ALLOCATION_SIZE, 0)
937            .map_err(LinkError::TableAllocationError)? as u32;
938
939        linker_state
940            .available_closure_functions
941            .reserve(CLOSURE_ALLOCATION_SIZE as usize - 1);
942        for i in 1..CLOSURE_ALLOCATION_SIZE {
943            linker_state
944                .available_closure_functions
945                .push(function_index + i);
946            linker_state
947                .allocated_closure_functions
948                .insert(function_index + i, false);
949        }
950        linker_state
951            .allocated_closure_functions
952            .insert(function_index, true);
953
954        self.shared.synchronize_link_operation(
955            topology_token,
956            DlOperation::AllocateFunctionTable {
957                index: function_index,
958                size: CLOSURE_ALLOCATION_SIZE,
959            },
960            linker_state,
961            group_state,
962            &ctx.data().process,
963            ctx.data().tid(),
964        );
965
966        Ok(function_index)
967    }
968
969    /// Remove a previously allocated slot for a closure in the indirect function table
970    ///
971    /// After calling this it is undefined behavior to call the function at the given index.
972    pub fn free_closure_index(
973        &self,
974        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
975        function_id: u32,
976    ) -> Result<(), LinkError> {
977        lock_instance_group_state!(
978            group_state_guard,
979            group_state,
980            self,
981            LinkError::InstanceGroupIsDead
982        );
983        let mut linker_state = self.shared.write_linker_state(group_state, ctx)?;
984
985        let Some(entry) = linker_state
986            .allocated_closure_functions
987            .get_mut(&function_id)
988        else {
989            // Not allocated
990            return Ok(());
991        };
992        if !*entry {
993            // Not used
994            return Ok(());
995        }
996
997        *entry = false;
998        linker_state.available_closure_functions.push(function_id);
999        Ok(())
1000    }
1001
1002    /// Check if an indirect_function_table entry is reserved for closures.
1003    /// Returns false if the entry is not reserved for closures.
1004    /// Requires a FunctionEnvMut because pending DL operations should always
1005    /// be processed before acquiring any lock on the linker.
1006    // TODO: we can cache this information within the group state so we don't
1007    // need a write lock on the linker state here
1008    pub fn is_closure(
1009        &self,
1010        function_id: u32,
1011        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1012    ) -> Result<bool, LinkError> {
1013        // If we can get a read lock on the linker state, do it
1014        if let Ok(linker_state) = self.shared.try_read_linker_state() {
1015            return Ok(linker_state
1016                .allocated_closure_functions
1017                .contains_key(&function_id));
1018        }
1019
1020        // Otherwise, fall back to the path where we apply DL ops and acquire
1021        // a write lock afterwards
1022        lock_instance_group_state!(
1023            group_state_guard,
1024            group_state,
1025            self,
1026            LinkError::InstanceGroupIsDead
1027        );
1028        let linker_state = self.shared.write_linker_state(group_state, ctx)?;
1029        Ok(linker_state
1030            .allocated_closure_functions
1031            .contains_key(&function_id))
1032    }
1033
1034    /// Loads a side module from the given path, linking it against the existing module tree
1035    /// and instantiating it. Symbols from the module can then be retrieved by calling
1036    /// [`Linker::resolve_export`].
1037    pub fn load_module(
1038        &self,
1039        module_spec: DlModuleSpec,
1040        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1041    ) -> Result<ModuleHandle, LinkError> {
1042        trace!(?module_spec, "Loading module");
1043
1044        lock_instance_group_state!(
1045            group_state_guard,
1046            group_state,
1047            self,
1048            LinkError::InstanceGroupIsDead
1049        );
1050
1051        // TODO: differentiate between an actual link error and an error that occurs as the
1052        // result of a pending operation that needs to be applied first. Currently, errors
1053        // from pending ops are treated as link errors and just reported to guest code rather
1054        // than terminating the process.
1055        let (topology_token, mut linker_state) = self
1056            .shared
1057            .write_linker_state_with_topology(group_state, ctx)?;
1058
1059        let mut link_state = InProgressLinkState::default();
1060        let env = ctx.as_ref();
1061        let mut store = ctx.as_store_mut();
1062
1063        trace!("Loading module tree for requested module");
1064        let wasi_env = env.as_ref(&store);
1065        let runtime_path: &[String] = &[];
1066        let module_handle = linker_state.load_module_tree(
1067            module_spec,
1068            &mut link_state,
1069            &wasi_env.runtime,
1070            &wasi_env.state,
1071            runtime_path,          // No runtime path when loading a module via dlopen
1072            Option::<&Path>::None, // Empty runtime path means we don't need the module's path either
1073        )?;
1074
1075        let new_modules = link_state
1076            .new_modules
1077            .iter()
1078            .map(|m| m.handle)
1079            .collect::<Vec<_>>();
1080
1081        for handle in &new_modules {
1082            trace!(?module_handle, "Instantiating module");
1083            group_state.instantiate_side_module_from_link_state(
1084                &mut linker_state,
1085                &mut store,
1086                &env,
1087                &mut link_state,
1088                *handle,
1089            )?;
1090        }
1091
1092        trace!("Finalizing link");
1093        self.finalize_link_operation(group_state_guard, &mut linker_state, &mut store, link_state)?;
1094
1095        if !new_modules.is_empty() {
1096            // The group state is unlocked for stub functions, now lock it again
1097            lock_instance_group_state!(
1098                group_state_guard,
1099                group_state,
1100                self,
1101                LinkError::InstanceGroupIsDead
1102            );
1103
1104            self.shared.synchronize_link_operation(
1105                topology_token,
1106                DlOperation::LoadModules(new_modules),
1107                linker_state,
1108                group_state,
1109                &ctx.data().process,
1110                ctx.data().tid(),
1111            );
1112        }
1113
1114        // FIXME: If we fail at an intermediate step, we should reset the linker's state, a la:
1115        // if result.is_err() {
1116        //     let mut guard = self.state.lock().unwrap();
1117        //     let memory = guard.memory.clone();
1118
1119        //     for module_handle in link_state.module_handles.iter().cloned() {
1120        //         let module = guard.side_modules.remove(&module_handle).unwrap();
1121        //         guard
1122        //             .side_module_names
1123        //             .retain(|_, handle| *handle != module_handle);
1124        //         // We already have an error we need to report, so ignore memory deallocation errors
1125        //         _ = guard
1126        //             .memory_allocator
1127        //             .deallocate(&memory, store, module.memory_base);
1128        //     }
1129        // }
1130
1131        trace!("Module load complete");
1132
1133        Ok(module_handle)
1134    }
1135
1136    fn finalize_link_operation(
1137        &self,
1138        // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1139        mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1140        linker_state: &mut LinkerState,
1141        store: &mut impl AsStoreMut,
1142        link_state: InProgressLinkState,
1143    ) -> Result<(), LinkError> {
1144        let group_state = group_state_guard.as_mut().unwrap();
1145
1146        trace!(?link_state, "Finalizing link operation");
1147
1148        group_state.finalize_pending_globals(
1149            linker_state,
1150            store,
1151            &link_state.unresolved_globals,
1152        )?;
1153
1154        self.initialize_new_modules(group_state_guard, store, link_state)
1155    }
1156
1157    fn initialize_new_modules(
1158        &self,
1159        // Take ownership of the guard and drop it ourselves to ensure no deadlock can happen
1160        mut group_state_guard: MutexGuard<'_, Option<InstanceGroupState>>,
1161        store: &mut impl AsStoreMut,
1162        link_state: InProgressLinkState,
1163    ) -> Result<(), LinkError> {
1164        let group_state = group_state_guard.as_mut().unwrap();
1165
1166        let new_instances = link_state
1167            .new_modules
1168            .iter()
1169            .map(|m| group_state.side_instances[&m.handle].instance.clone())
1170            .collect::<Vec<_>>();
1171
1172        // The instance group must be unlocked for the next step, since modules may need to resolve
1173        // stub functions and that requires a lock on the instance group's state
1174        drop(group_state_guard);
1175
1176        // These functions are exported from PIE executables, and need to be run before calling
1177        // _initialize or _start. More info:
1178        // https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
1179        trace!("Calling data relocation functions");
1180        for instance in &new_instances {
1181            call_initialization_function::<()>(instance, store, "__wasm_apply_data_relocs")?;
1182            call_initialization_function::<()>(instance, store, "__wasm_apply_tls_relocs")?;
1183        }
1184
1185        trace!("Calling ctor functions");
1186        for instance in &new_instances {
1187            call_initialization_function::<()>(instance, store, "__wasm_call_ctors")?;
1188        }
1189
1190        Ok(())
1191    }
1192
1193    // TODO: Support RTLD_NEXT
1194    /// Resolves an export from the module corresponding to the given module handle.
1195    /// Only functions and globals can be resolved.
1196    ///
1197    /// If the symbol is a global, the returned value will be the absolute address of
1198    /// the data corresponding to that global within the shared linear memory.
1199    ///
1200    /// If it's a function, it'll be placed into the indirect function table,
1201    /// which creates a "function pointer" that can be used from WASM code.
1202    pub fn resolve_export(
1203        &self,
1204        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1205        module_handle: Option<ModuleHandle>,
1206        symbol: &str,
1207    ) -> Result<ResolvedExport, ResolveError> {
1208        trace!(?module_handle, symbol, "Resolving symbol");
1209
1210        let resolution_key = SymbolResolutionKey::Requested {
1211            resolve_from: module_handle,
1212            name: symbol.to_string(),
1213        };
1214
1215        lock_instance_group_state!(guard, group_state, self, ResolveError::InstanceGroupIsDead);
1216
1217        if let Ok(linker_state) = self.shared.try_read_linker_state()
1218            && let Some(resolution) = linker_state.symbol_resolution_records.get(&resolution_key)
1219        {
1220            trace!(?resolution, "Already have a resolution for this symbol");
1221            match resolution {
1222                SymbolResolutionResult::FunctionPointer {
1223                    function_table_index: addr,
1224                    ..
1225                } => {
1226                    return Ok(ResolvedExport::Function {
1227                        func_ptr: *addr as u64,
1228                    });
1229                }
1230                SymbolResolutionResult::Memory(addr) => {
1231                    return Ok(ResolvedExport::Global { data_ptr: *addr });
1232                }
1233                SymbolResolutionResult::Tls {
1234                    resolved_from,
1235                    offset,
1236                } => {
1237                    let Some(tls_base) = group_state.tls_base(*resolved_from) else {
1238                        return Err(ResolveError::NoTlsBaseGlobalExport);
1239                    };
1240                    return Ok(ResolvedExport::Global {
1241                        data_ptr: tls_base + offset,
1242                    });
1243                }
1244                r => panic!(
1245                    "Internal error: unexpected symbol resolution \
1246                        {r:?} for requested symbol {symbol}"
1247                ),
1248            }
1249        }
1250
1251        let (topology_token, mut linker_state) = self
1252            .shared
1253            .write_linker_state_with_topology(group_state, ctx)?;
1254
1255        let mut store = ctx.as_store_mut();
1256
1257        trace!("Resolving export");
1258        let (export, resolved_from) =
1259            group_state.resolve_export(&linker_state, &mut store, module_handle, symbol, false)?;
1260
1261        trace!(?export, ?resolved_from, "Resolved export");
1262
1263        match export {
1264            PartiallyResolvedExport::Global(addr) => {
1265                linker_state
1266                    .symbol_resolution_records
1267                    .insert(resolution_key, SymbolResolutionResult::Memory(addr));
1268
1269                Ok(ResolvedExport::Global { data_ptr: addr })
1270            }
1271            PartiallyResolvedExport::Tls { offset, final_addr } => {
1272                linker_state.symbol_resolution_records.insert(
1273                    resolution_key,
1274                    SymbolResolutionResult::Tls {
1275                        resolved_from,
1276                        offset,
1277                    },
1278                );
1279
1280                Ok(ResolvedExport::Global {
1281                    data_ptr: final_addr,
1282                })
1283            }
1284            PartiallyResolvedExport::Function(func) => {
1285                let func_ptr = group_state
1286                    .append_to_function_table(&mut store, func.clone())
1287                    .map_err(ResolveError::TableAllocationError)?;
1288                trace!(
1289                    ?func_ptr,
1290                    table_size = group_state.indirect_function_table.size(&store),
1291                    "Placed resolved function into table"
1292                );
1293                linker_state.symbol_resolution_records.insert(
1294                    resolution_key,
1295                    SymbolResolutionResult::FunctionPointer {
1296                        resolved_from,
1297                        function_table_index: func_ptr,
1298                    },
1299                );
1300
1301                self.shared.synchronize_link_operation(
1302                    topology_token,
1303                    DlOperation::ResolveFunction {
1304                        name: symbol.to_string(),
1305                        resolved_from,
1306                        function_table_index: func_ptr,
1307                    },
1308                    linker_state,
1309                    group_state,
1310                    &ctx.data().process,
1311                    ctx.data().tid(),
1312                );
1313
1314                Ok(ResolvedExport::Function {
1315                    func_ptr: func_ptr as u64,
1316                })
1317            }
1318        }
1319    }
1320
1321    pub fn is_handle_valid(
1322        &self,
1323        handle: ModuleHandle,
1324        ctx: &mut FunctionEnvMut<'_, WasiEnv>,
1325    ) -> Result<bool, LinkError> {
1326        // If we can get a read lock on the linker state, do it
1327        if let Ok(linker_state) = self.shared.try_read_linker_state() {
1328            return Ok(linker_state.side_modules.contains_key(&handle));
1329        }
1330
1331        // Otherwise, fall back to the path where we apply DL ops and acquire
1332        // a write lock afterwards
1333        lock_instance_group_state!(guard, group_state, self, LinkError::InstanceGroupIsDead);
1334        let linker_state = self.shared.write_linker_state(group_state, ctx)?;
1335        Ok(linker_state.side_modules.contains_key(&handle))
1336    }
1337}