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}