diff --git a/scripts/harnesses/gratetestreport.py b/scripts/harnesses/gratetestreport.py index 7745bbcb9..f08cec6ce 100644 --- a/scripts/harnesses/gratetestreport.py +++ b/scripts/harnesses/gratetestreport.py @@ -239,7 +239,11 @@ def run_subprocess(cmd: list[str], timeout: int | None = None, cwd: Path | None def compile_grate_test(test: GrateTestCase) -> tuple[bool, str]: - grate_compile_cmd = [GRATE_CLANG, "-s", "--compile-grate", "--output-dir", "grates", test.grate_source.name] + # Grates are always built as dynamically linked (no -s); that is the only + # supported grate build mode for the test suite. + grate_compile_cmd = ( + [GRATE_CLANG, "--compile-grate", "--output-dir", "grates", test.grate_source.name] + ) cage_compile_cmd = [GRATE_CLANG, test.cage_source.name] try: diff --git a/src/lind-boot/src/lind_wasmtime/execute.rs b/src/lind-boot/src/lind_wasmtime/execute.rs index 52d7cadca..47337eb4a 100644 --- a/src/lind-boot/src/lind_wasmtime/execute.rs +++ b/src/lind-boot/src/lind_wasmtime/execute.rs @@ -705,25 +705,80 @@ fn load_main_module( // This function will be called at either the first cage or exec-ed cages. set_vmctx_thread(cageid, THREAD_START_ID as u64, vmctx_wrapper); - // Grate calls only supports static linking for now, so we only initialize the grate pool and register - // grate workers when dylink is not enabled. - if !dylink_metadata.dylink_enabled { - // 4) register grate workers for this cage - let grate_template = GrateTemplate { - engine: module.engine().clone(), - module: module.clone(), - linker: linker_guard.clone(), + // 4) Register grate workers for this cage. Grates now work under both static and + // dynamic builds. + // + // Static grates: workers clone the template linker and instantiate the module directly + // (worker_builder = None). + // + // Dynamically linked grates: a worker is a separate Store sharing the cage's linear + // memory, and Wasmtime Table/Global objects are store-bound, so each worker must + // rebuild its own per-store linker, indirect function table, and GOT — exactly like a + // thread of the grate cage. We capture, from the fully-relocated main store, everything + // build_dylink_child_store needs and hand it to the worker pool as a worker_builder. + let worker_builder: Option> = if dylink_metadata.dylink_enabled { + let engine = module.engine().clone(); + let symbol_table = store.as_context_mut().get_library_symbol_table().clone(); + let (modules, dlopen_modules) = { + let ctx = store.data().lind_fork_ctx.as_ref().unwrap(); + (ctx.modules().to_vec(), ctx.dlopen_modules().to_vec()) }; - let host = store.data().clone(); + // Snapshot the parent linker's store-independent imports (GOT cells, host funcs, + // shared memory) and all global values AFTER GOT relocation has finalized them. + let snapshot = linker_guard.get_linker_snapshot_for_child(&mut *store, true); + let global_snapshots = store.as_context_mut().get_global_snapshot(); + + Some(Box::new( + move |cageid: u64, _worker_id: u64, host: HostCtx, slot_top: u32| { + let built = wasmtime::build_dylink_child_store( + &engine, + host, + symbol_table.clone(), + cageid, + &modules, + &dlopen_modules, + &snapshot, + &global_snapshots, + true, /* dylink_enabled */ + slot_top, + )?; + let wasmtime::LindDylinkChildStore { + mut store, + instance, + linker, + got, + stack_top, + .. + } = built; + // Give the worker's host context its own per-store linker and GOT so any + // later ctx.linker / ctx.got use stays consistent within this worker store. + { + let ctx = store.data_mut().lind_fork_ctx.as_mut().unwrap(); + ctx.attach_linker(linker); + ctx.attach_got_table(got); + } + Ok((store, instance, stack_top)) + }, + ) as WorkerBuilder) + } else { + None + }; - // initialize the grate pool for later use in grate calls and - // other syscalls that require re-entry into wasmtime runtime. - init_grate_pool(); - unregister_grate_handler(cageid); + let grate_template = GrateTemplate { + engine: module.engine().clone(), + module: module.clone(), + linker: linker_guard.clone(), + worker_builder, + }; + let host = store.data().clone(); - register_grate_handler_for_cage(&grate_template, host, cageid) - .with_context(|| format!("failed to register grate workers for cage {}", cageid))?; - } + // initialize the grate pool for later use in grate calls and + // other syscalls that require re-entry into wasmtime runtime. + init_grate_pool(); + unregister_grate_handler(cageid); + + register_grate_handler_for_cage(&grate_template, host, cageid) + .with_context(|| format!("failed to register grate workers for cage {}", cageid))?; // 5) Notify threei of the cage runtime type threei::set_cage_runtime(cageid, threei_const::RUNTIME_TYPE_WASMTIME); diff --git a/src/sysdefs/src/constants/lind_platform_const.rs b/src/sysdefs/src/constants/lind_platform_const.rs index bfe959f10..54ef8bfce 100644 --- a/src/sysdefs/src/constants/lind_platform_const.rs +++ b/src/sysdefs/src/constants/lind_platform_const.rs @@ -135,14 +135,45 @@ pub const FPCAST_FUNC_SIGNATURE: &str = "$fpcast_emu$"; /// instance reserves the same number of worker stack slots in linear memory. pub const MAX_GRATE_WORKERS: usize = 32; -/// Size in bytes of the usable stack region assigned to one grate worker. +/// Default size in bytes of the usable stack region assigned to one grate worker. /// /// Each worker executes in its own `Store + Instance` context, but workers may /// still attach to the same underlying linear memory. Therefore, every worker -/// must be given a disjoint stack slot inside the shared stack arena. -/// -/// This constant specifies the usable portion of that per-worker slot. -pub const GRATE_STACK_SLOT_SIZE: u32 = 8 * 1024 * 1024; +/// must be given a disjoint stack slot inside the shared stack arena. This is the +/// usable portion of that per-worker slot. +/// +/// Grate workers only ever run grate *handler* code (syscall interposition), which +/// is shallow compared to arbitrary user programs, so the default is modest (1 MiB). +/// Override at runtime with the `LIND_GRATE_STACK_SIZE` environment variable; see +/// [`grate_stack_slot_size`]. +pub const DEFAULT_GRATE_STACK_SLOT_SIZE: u32 = 1024 * 1024; + +/// Environment variable that overrides the per-worker grate stack slot size (bytes). +pub const GRATE_STACK_SIZE_ENV: &str = "LIND_GRATE_STACK_SIZE"; + +static GRATE_STACK_SLOT_SIZE_CACHED: OnceLock = OnceLock::new(); + +/// Per-worker grate stack slot size in bytes. +/// +/// Returns [`DEFAULT_GRATE_STACK_SLOT_SIZE`] (1 MiB) unless the `LIND_GRATE_STACK_SIZE` +/// environment variable is set to a positive integer number of bytes, in which case +/// that value (rounded up to a 4 KiB page) is used. +/// +/// The result is read from the environment once and cached for the process lifetime so +/// that every consumer agrees on the same arena geometry: the arena reservation in +/// `instance.rs` and the per-worker slot addressing in `lind-3i` must use the identical +/// value, otherwise worker stacks would not line up with the reserved region. +pub fn grate_stack_slot_size() -> u32 { + *GRATE_STACK_SLOT_SIZE_CACHED.get_or_init(|| { + std::env::var(GRATE_STACK_SIZE_ENV) + .ok() + .and_then(|v| v.trim().parse::().ok()) + .filter(|n| *n > 0) + // Keep slot boundaries page-aligned so the arena layout stays well-formed. + .map(|n| (n + 4095) & !4095) + .unwrap_or(DEFAULT_GRATE_STACK_SLOT_SIZE) + }) +} /// Size in bytes of the guard region placed before each grate-worker stack slot. /// diff --git a/src/wasmtime/crates/lind-3i/src/lib.rs b/src/wasmtime/crates/lind-3i/src/lib.rs index 8ea9b262f..a378ca019 100644 --- a/src/wasmtime/crates/lind-3i/src/lib.rs +++ b/src/wasmtime/crates/lind-3i/src/lib.rs @@ -80,7 +80,7 @@ use std::sync::{Condvar, Mutex, MutexGuard, OnceLock}; use sysdefs::constants::lind_platform_const; use sysdefs::constants::lind_platform_const::*; use wasmtime::error::Context as WasmtimeContext; -use wasmtime::{Engine, Global, Linker, Module, Store, TypedFunc, Val}; +use wasmtime::{Engine, Global, Instance, Linker, Module, Store, TypedFunc, Val}; type PassFptrTyped = TypedFunc< ( @@ -104,6 +104,24 @@ type PassFptrTyped = TypedFunc< type WorkerId = u64; +/// Optional per-worker store builder for dynamically linked grates. +/// +/// Static grates clone the template linker and instantiate the module directly (see the +/// `None` branch of [`create_worker`]). Dynamically linked grates cannot do that: each +/// worker owns a separate `Store`, and Wasmtime `Table`/`Global` objects are store-bound, +/// so a worker must rebuild its own per-store linker, indirect function table, and GOT — +/// exactly like a thread of the grate cage. That replay needs `LindGOT`/`LindCtx` +/// machinery that lives in crates above `lind-3i`, so it is injected here as an opaque +/// builder rather than depended upon directly. +/// +/// Arguments: `(cageid, worker_id, host, slot_top)`. Returns the worker's +/// `(Store, Instance, effective_stack_top)`, where `effective_stack_top` is the worker's +/// stack-slot top AFTER per-instance TLS has been reserved (TLS is carved downward from +/// `slot_top`). This is the value the worker resets `__stack_pointer` to before each call, +/// so it must sit below the TLS region — see [`GrateWorker::reset_worker_stack`]. +pub type WorkerBuilder = + Box anyhow::Result<(Store, Instance, u32)>>; + const DEFAULT_GRATE_WORKERS: usize = MAX_GRATE_WORKERS; const GRATE_WORKERS_ENV: &str = "LIND_GRATE_WORKERS"; @@ -127,7 +145,7 @@ pub enum ConcurrencyMode { /// construct worker-local execution contexts for the same grate module. /// Each worker clones or reuses these components to create its own /// `Store + Instance` runtime state. -pub struct GrateTemplate { +pub struct GrateTemplate { /// The Wasmtime engine used to create worker-local stores and instances. /// /// This is shared across all workers for the same grate. @@ -144,6 +162,14 @@ pub struct GrateTemplate { /// Each worker starts from this template linker and clones it during /// worker creation so that instantiation can proceed independently. pub linker: Linker, + + /// Per-worker store builder for dynamically linked grates. + /// + /// `None` for statically linked grates: `create_worker` clones [`Self::linker`] and + /// instantiates [`Self::module`] directly. `Some` for dynamically linked grates: each + /// worker store is rebuilt with full dynamic-linking replay (separate store sharing the + /// cage memory, fresh per-store linker/table/GOT). See [`WorkerBuilder`]. + pub worker_builder: Option>, } /// Marshalled arguments for one grate call. @@ -274,7 +300,8 @@ fn worker_stack_base(cageid: u64, workerid: WorkerId) -> u32 { panic!("STACK_ARENA_BASE is not initialized for cageid {}", cageid); }); stack_arena_base - + (workerid as u32 - 1) * (GRATE_STACK_GUARD_SIZE + GRATE_STACK_SLOT_SIZE) + + (workerid as u32 - 1) + * (GRATE_STACK_GUARD_SIZE + lind_platform_const::grate_stack_slot_size()) + GRATE_STACK_GUARD_SIZE } @@ -284,7 +311,7 @@ fn worker_stack_base(cageid: u64, workerid: WorkerId) -> u32 { /// starting a new grate call, ensuring that each invocation begins with a clean /// stack state inside that worker’s private stack slot. fn worker_stack_top(cageid: u64, workerid: WorkerId) -> u32 { - worker_stack_base(cageid, workerid) + GRATE_STACK_SLOT_SIZE + worker_stack_base(cageid, workerid) + lind_platform_const::grate_stack_slot_size() } fn configured_grate_workers() -> usize { @@ -681,13 +708,32 @@ pub fn create_worker( where T: Clone + 'static, { - let mut store = Store::new(&template.engine, host); - - let linker: Linker = template.linker.clone(); - - let (instance, _, _) = linker - .instantiate_with_lind_thread(&mut store, &template.module, false) - .context("failed to instantiate grate module")?; + // Acquire this worker's store + instance + effective stack top. + // + // Static grates (no dylink section): clone the template linker and instantiate the + // module directly. All workers are byte-identical because globals are baked in. The + // worker's stack top is simply the slot top. + // + // Dynamically linked grates: delegate to the injected worker builder, which rebuilds a + // separate per-store linker/table/GOT (sharing the cage's linear memory) just like a + // thread of the grate cage. It returns the effective stack top AFTER per-instance TLS + // has been carved out of the slot, which the worker must use as its stack reset target. + let (mut store, instance, stack_top) = match &template.worker_builder { + Some(build_worker) => { + let slot_top = worker_stack_top(cageid, worker_id); + build_worker(cageid, worker_id, host, slot_top) + .context("failed to build dynamically linked grate worker")? + } + None => { + let mut store = Store::new(&template.engine, host); + let linker: Linker = template.linker.clone(); + let (instance, _, _) = linker + .instantiate_with_lind_thread(&mut store, &template.module, false) + .context("failed to instantiate grate module")?; + let stack_top = worker_stack_top(cageid, worker_id); + (store, instance, stack_top) + } + }; let pass_fptr_func = match instance.get_export(&mut store, "pass_fptr_to_wt") { Some(_) => Some(instance.get_typed_func::<( @@ -709,8 +755,9 @@ where None => None, }; + // `stack_top` is already determined above (slot top for static, post-TLS slot top for + // dynamic); only the slot base is computed here. let stack_base = worker_stack_base(cageid, worker_id); - let stack_top = worker_stack_top(cageid, worker_id); let stack_pointer = instance .get_global(&mut store, "__stack_pointer") .ok_or_else(|| anyhow::anyhow!("missing __stack_pointer"))?; diff --git a/src/wasmtime/crates/lind-multi-process/src/lib.rs b/src/wasmtime/crates/lind-multi-process/src/lib.rs index 3ee6c5aa3..908984371 100644 --- a/src/wasmtime/crates/lind-multi-process/src/lib.rs +++ b/src/wasmtime/crates/lind-multi-process/src/lib.rs @@ -25,8 +25,9 @@ use std::sync::{Arc, Barrier, Mutex}; use std::thread; use wasmtime::{ AsContext, AsContextMut, AsyncifyState, Caller, ChildLibraryType, Engine, ExternType, - InstanceId, InstantiateType, Linker, Module, OnCalledAction, SharedMemory, Store, StoreOpaque, - VMContext, VMOpaqueContext, Val, ValRaw, ValType, + InstanceId, InstantiateType, LindDylinkChildStore, Linker, Module, OnCalledAction, + SharedMemory, Store, StoreOpaque, VMContext, VMOpaqueContext, Val, ValRaw, ValType, + build_dylink_child_store, }; use cage::alloc_cage_id; @@ -210,6 +211,18 @@ impl &[(String, String, Module)] { + &self.modules + } + + /// The cage's dlopen'd module list. Exposed for the same reason as [`Self::modules`]. + pub fn dlopen_modules(&self) -> &[(String, String, Module)] { + &self.dlopen_modules + } + // Attach a LindGOT (Global Offset Table) to this context, wrapping it in // Arc> for shared, thread-safe access. The GOT maps symbol names to // the addresses of their GOT cells; it is shared across all modules within @@ -704,7 +717,7 @@ impl> = if dylink_enabled { + let wb_engine = engine.clone(); + // `symbol_table` was already consumed into the child store; read it + // back from the child store for the worker builder. + let wb_symbol_table = + store.as_context_mut().get_library_symbol_table().clone(); + let wb_modules = modules.clone(); + let wb_dlopen_modules = dlopen_modules.clone(); + let wb_global_snapshots = global_snapshots.clone(); + let wb_get_cx = get_cx.clone(); + let wb_snapshot = + cloned_linker.get_linker_snapshot_for_child(&mut store, true); + + Some(Box::new( + move |cageid: u64, _worker_id: u64, host: T, slot_top: u32| { + let built = build_dylink_child_store( + &wb_engine, + host, + wb_symbol_table.clone(), + cageid, + &wb_modules, + &wb_dlopen_modules, + &wb_snapshot, + &wb_global_snapshots, + true, /* dylink_enabled */ + slot_top, + )?; + let LindDylinkChildStore { + mut store, + instance, + linker, + got, + stack_top, + .. + } = built; + { + let ctx = wb_get_cx(store.data_mut()); + ctx.attach_linker(linker); + ctx.attach_got_table(got); + } + Ok((store, instance, stack_top)) + }, + ) as WorkerBuilder) + } else { + None + }; + + let grate_template = GrateTemplate { + engine: module.engine().clone(), + module: module.clone(), + linker: cloned_linker, + worker_builder, + }; + + // register grate workers for this cage + create_handler_for_cage( + &grate_template, + store.data().clone(), + child_cageid, + ConcurrencyMode::Parallel, + ) + .with_context(|| { + format!("failed to register grate workers for cage {}", child_cageid) + }) + .expect("create_handler_for_cage failed"); // get the asyncify_rewind_start and module start function let child_rewind_start; @@ -1512,17 +1575,18 @@ impl Linker { } } +/// Result of [`build_dylink_child_store`]: a fully set-up child Wasmtime store that +/// shares the parent cage's linear memory. +/// +/// This bundles everything a caller needs to finish wiring a *thread* or a *grate +/// worker* (both are separate `Store`s that share the cage's single linear memory): +/// the child [`Store`](crate::Store), its main-module [`Instance`], the per-store +/// [`Linker`] and [`LindGOT`] (which the caller attaches to its host context), the +/// epoch global handler, and the post-TLS stack top. +#[allow(missing_docs)] +pub struct LindDylinkChildStore { + pub store: crate::Store, + pub instance: Instance, + pub instance_id: InstanceId, + pub linker: Linker, + pub got: Option, + pub epoch_handler: Option<*mut u64>, + /// Stack top after per-instance TLS has been carved out of the input `stack_addr`. + /// Threads set `__stack_pointer` relative to this; grate workers use it as the + /// per-call stack-reset target. + pub stack_top: u32, +} + +/// Build a child Wasmtime store that shares the parent cage's linear memory, set up for +/// dynamic linking (or static, when `dylink_enabled` is `false`). +/// +/// This is the per-store dynamic-linking replay used to create **grate workers** (each +/// worker is a separate `Store` that shares the cage's single linear memory). It mirrors, +/// step for step, the thread-creation path in `lind-multi-process::pthread_create_call` +/// (each thread is likewise a separate `Store` sharing memory). That path currently keeps +/// its own inline copy of this logic; unifying it to call this helper is a deferred DRY +/// cleanup (kept separate for now to avoid destabilizing the proven thread path). If you +/// change one, change the other. Wasmtime `Table`/`Global` +/// objects are store-bound and cannot be reused across stores, so each child must: +/// rebuild a fresh per-store linker from `snapshot`, allocate a fresh indirect function +/// table, replay the preloaded and dlopen'd libraries into the new store (re-growing the +/// table in the same order and applying the parent's global snapshots so GOT cell values +/// and memory bases match), instantiate the main module, and carve per-instance TLS out +/// of `stack_addr` (growing downward). +/// +/// It deliberately does NOT touch the lind `LindCtx` (cageid/tid bookkeeping, +/// `attach_linker`/`attach_got_table`), perform signal init, register the vmctx, or run +/// `_start`/asyncify rewind — those are caller-specific and stay in `pthread_create` / +/// the grate worker builder. Keeping them out lets this helper live in the wasmtime crate +/// (where all the involved types are in scope) and remain free of lind-multi-process types. +/// +/// `stack_addr` is the high address of the child's stack region (stacks grow down); for a +/// thread it is the thread stack top, for a grate worker it is the worker's stack-slot top. +/// The returned `stack_top` is `stack_addr` after TLS has been reserved. +pub fn build_dylink_child_store( + engine: &Engine, + child_host: T, + symbol_table: SymbolTable, + cageid: u64, + modules: &[(String, String, Module)], + dlopen_modules: &[(String, String, Module)], + snapshot: &( + Vec<(String, String, GlobalType, Val)>, + Vec<(String, String, Arc)>, + Option<(String, String, ClonedMemory)>, + ), + global_snapshots: &HashMap>, + dylink_enabled: bool, + mut stack_addr: u32, +) -> Result> { + // The main module is the first entry in the module list. + let module = modules + .get(0) + .expect("module list must contain the main module") + .2 + .clone(); + + let store_inner = crate::Store::::new_inner(engine, symbol_table)?; + let mut store = crate::Store::new_with_inner(engine, child_host, store_inner)?; + + let mut child_got = if dylink_enabled { + Some(LindGOT::new()) + } else { + None + }; + + // Rebuild a fresh per-store linker from the parent's snapshot: store-local Globals + // (re-registering GOT cell pointers), the shared memory re-defined into this store, + // and store-independent host functions re-inserted. + let (mut linker, memory_base_table, epoch_handler, _) = Linker::new_child_linker( + &mut store, + engine, + &mut child_got, + &snapshot.0, + &snapshot.1, + &snapshot.2, + )?; + + let child_table = if dylink_enabled { + // The main module declares the minimal indirect-function-table size via its + // table import; start the fresh table there. + let mut table_size: u64 = 0; + for import in module.imports() { + if let ExternType::Table(table) = import.ty() { + table_size = table.minimum(); + } + } + let mut child_table = linker.attach_function_table(&mut store, table_size as u32)?; + linker.attach_asyncify(&mut store)?; + + // Replay each preloaded library into this store, re-growing the shared table in + // the SAME order as the parent so cloned GOT indices remain valid. + for (name, _path, lib_module) in modules.iter().skip(1) { + let dylink_info = lib_module.dylink_meminfo(); + let dylink_info = dylink_info.as_ref().unwrap(); + let table_start = child_table.size(&mut store) as i32; + + lind_log!( + DYLINK, + "[debug] library table_start: {}, grow: {}", + table_start, + dylink_info.table_size + ); + child_table.grow( + &mut store, + dylink_info.table_size as u64, + crate::Ref::Func(None), + )?; + + let module_name = lib_module.name().unwrap_or(""); + let module_memory_base = *memory_base_table + .get(module_name) + .expect("memory base not found for library"); + + linker.allow_shadowing(true); + linker.module_with_child( + &mut store, + cageid, + name, + lib_module, + &mut child_table, + table_start, + module_memory_base, + ChildLibraryType::Thread(&mut stack_addr), + global_snapshots + .get(module_name) + .map(Vec::as_slice) + .unwrap_or(&[]), + )?; + linker.allow_shadowing(false); + } + + Some(child_table) + } else { + None + }; + + // Workers/threads share the cage's linear memory. `is_thread` is only consulted on + // fork paths (which neither a freshly-created thread child nor a grate worker takes + // here), so this is a safe, behavior-neutral label that also matches the thread path + // this routine is modeled on. + store.set_is_thread(true); + + let (instance, _stack_arena, instance_id) = + linker.instantiate_with_lind_thread(&mut store, &module, false)?; + + // Register the main-module instance by name so global-snapshot lookups resolve it, + // then restore the parent's globals (GOT cell addresses, stack pointer, memory base). + let main_module_name = module.name().unwrap_or(""); + store + .as_context_mut() + .register_named_instance(main_module_name.to_string(), instance_id); + instance.apply_global_snapshots( + &mut store, + global_snapshots + .get(main_module_name) + .map(Vec::as_slice) + .unwrap_or(&[]), + ); + + if dylink_enabled { + let mut child_table = child_table.unwrap(); + let fpcast_enabled = engine.fpcast_enabled(); + // got=None: the table is rebuilt for this store but GOT cell values come from the + // applied global snapshot, so they must not be re-derived here. + instance.apply_GOT_relocs(&mut store, None, &child_table, None, fpcast_enabled)?; + + // Expose the main module's exports to this store's linker (skip signal_callback). + linker.instance_dylink(&mut store, "env", instance, vec!["signal_callback"])?; + + // Replay dlopen'd libraries the same way as preloaded ones. + for (name, _path, lib_module) in dlopen_modules.iter() { + let dylink_info = lib_module.dylink_meminfo(); + let dylink_info = dylink_info.as_ref().unwrap(); + let table_start = child_table.size(&mut store) as i32; + + lind_log!( + DYLINK, + "[debug] dlopen library table_start: {}, grow: {}", + table_start, + dylink_info.table_size + ); + child_table.grow( + &mut store, + dylink_info.table_size as u64, + crate::Ref::Func(None), + )?; + + let module_name = lib_module.name().unwrap_or(""); + let module_memory_base = *memory_base_table + .get(module_name) + .expect("memory base not found for library"); + + linker.allow_shadowing(true); + linker.module_with_child( + &mut store, + cageid, + name, + lib_module, + &mut child_table, + table_start, + module_memory_base, + ChildLibraryType::Thread(&mut stack_addr), + global_snapshots + .get(module_name) + .map(Vec::as_slice) + .unwrap_or(&[]), + )?; + linker.allow_shadowing(false); + } + } + + // Carve per-instance TLS out of the stack (grows downward). The remaining stack_addr + // becomes the effective stack top for this child. + if let Ok(init_tls) = + instance.get_typed_func::(store.as_context_mut(), "__wasm_init_tls") + { + let get_tls_size = + instance.get_typed_func::<(), i32>(store.as_context_mut(), "__get_aligned_tls_size")?; + let tls_size = get_tls_size.call(store.as_context_mut(), ())?; + stack_addr -= tls_size as u32; + init_tls.call(store.as_context_mut(), stack_addr as i32)?; + } + + Ok(LindDylinkChildStore { + store, + instance, + instance_id, + linker, + got: child_got, + epoch_handler, + stack_top: stack_addr, + }) +} + /// Additional APIs for attaching common imports used by the Lind runtime and toolchain. #[allow(missing_docs)] impl Linker { diff --git a/tests/unit-tests/memory_tests/fail/invalid_access_direct.c b/tests/unit-tests/memory_tests/fail/invalid_access_direct.c index c16ebcaa1..ce46cc34c 100644 --- a/tests/unit-tests/memory_tests/fail/invalid_access_direct.c +++ b/tests/unit-tests/memory_tests/fail/invalid_access_direct.c @@ -8,7 +8,16 @@ #include int main(void) { - volatile int *addr = (volatile int *)0x1234567; + /* + * 1 GiB. This sits comfortably above everything rawposix maps for this cage + * -- the stack, the module data region, the grate worker stack arena (which + * scales with the worker count and per-worker stack size), and the (tiny) + * heap -- yet remains well within the 4 GiB wasm linear memory. The page is + * therefore reserved PROT_NONE and the access faults instead of reading + * mapped memory. (A lower address like 0x1234567 can fall inside the grate + * stack arena and read as valid.) + */ + volatile int *addr = (volatile int *)0x40000000; int val = *addr; /* expected to trap / fault */ printf("val=%d\n", val); return 0; diff --git a/tests/unit-tests/memory_tests/fail/invalid_access_fork.c b/tests/unit-tests/memory_tests/fail/invalid_access_fork.c index ab1ca5930..de4e74e5e 100644 --- a/tests/unit-tests/memory_tests/fail/invalid_access_fork.c +++ b/tests/unit-tests/memory_tests/fail/invalid_access_fork.c @@ -18,8 +18,15 @@ int main(void) { } if (pid == 0) { - /* child: dereference an unmapped address */ - volatile int *addr = (volatile int *)0x1234567; + /* + * child: dereference an unmapped address. 1 GiB sits above everything + * rawposix maps for this cage -- stack, data region, grate worker stack + * arena (scales with worker count and per-worker stack size), and the + * tiny heap -- yet stays within the 4 GiB wasm linear memory, so the + * page is reserved PROT_NONE and the access faults. (A lower address + * like 0x1234567 can fall inside the grate stack arena and read as valid.) + */ + volatile int *addr = (volatile int *)0x40000000; int val = *addr; /* expected to trap / fault */ printf("val=%d\n", val); exit(0);