Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion scripts/harnesses/gratetestreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
87 changes: 71 additions & 16 deletions src/lind-boot/src/lind_wasmtime/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkerBuilder<HostCtx>> = 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<HostCtx>)
} 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);
Expand Down
41 changes: 36 additions & 5 deletions src/sysdefs/src/constants/lind_platform_const.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Comment on lines +145 to +152

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if making this configurable through CLI makes more sense


static GRATE_STACK_SLOT_SIZE_CACHED: OnceLock<u32> = 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::<u32>().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)
})
}
Comment on lines +166 to +176

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIND_GRATE_STACK_SIZE page rounding can overflow and produce a zero/small stack slot


/// Size in bytes of the guard region placed before each grate-worker stack slot.
///
Expand Down
71 changes: 59 additions & 12 deletions src/wasmtime/crates/lind-3i/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<
(
Expand All @@ -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<T> =
Box<dyn Fn(u64, u64, T, u32) -> anyhow::Result<(Store<T>, Instance, u32)>>;

const DEFAULT_GRATE_WORKERS: usize = MAX_GRATE_WORKERS;
const GRATE_WORKERS_ENV: &str = "LIND_GRATE_WORKERS";

Expand All @@ -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<T> {
pub struct GrateTemplate<T: 'static> {
/// The Wasmtime engine used to create worker-local stores and instances.
///
/// This is shared across all workers for the same grate.
Expand All @@ -144,6 +162,14 @@ pub struct GrateTemplate<T> {
/// Each worker starts from this template linker and clones it during
/// worker creation so that instantiation can proceed independently.
pub linker: Linker<T>,

/// 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<WorkerBuilder<T>>,
}

/// Marshalled arguments for one grate call.
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -681,13 +708,32 @@ pub fn create_worker<T>(
where
T: Clone + 'static,
{
let mut store = Store::new(&template.engine, host);

let linker: Linker<T> = 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<T> = 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::<(
Expand All @@ -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"))?;
Expand Down
Loading