feat: add a knob for reset stack (#4813)
* feat: add a knob for reset stack * Touch up documentation of `async_stack_zeroing` Co-authored-by: Alex Crichton <alex@alexcrichton.com>
This commit is contained in:
@@ -181,7 +181,8 @@ impl Config {
|
|||||||
self.wasmtime.memory_guaranteed_dense_image_size,
|
self.wasmtime.memory_guaranteed_dense_image_size,
|
||||||
))
|
))
|
||||||
.allocation_strategy(self.wasmtime.strategy.to_wasmtime())
|
.allocation_strategy(self.wasmtime.strategy.to_wasmtime())
|
||||||
.generate_address_map(self.wasmtime.generate_address_map);
|
.generate_address_map(self.wasmtime.generate_address_map)
|
||||||
|
.async_stack_zeroing(self.wasmtime.async_stack_zeroing);
|
||||||
|
|
||||||
self.wasmtime.codegen.configure(&mut cfg);
|
self.wasmtime.codegen.configure(&mut cfg);
|
||||||
|
|
||||||
@@ -386,6 +387,7 @@ pub struct WasmtimeConfig {
|
|||||||
padding_between_functions: Option<u16>,
|
padding_between_functions: Option<u16>,
|
||||||
generate_address_map: bool,
|
generate_address_map: bool,
|
||||||
native_unwind_info: bool,
|
native_unwind_info: bool,
|
||||||
|
async_stack_zeroing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmtimeConfig {
|
impl WasmtimeConfig {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ cfg_if::cfg_if! {
|
|||||||
use imp::{commit_memory_pages, commit_table_pages, decommit_memory_pages, decommit_table_pages};
|
use imp::{commit_memory_pages, commit_table_pages, decommit_memory_pages, decommit_table_pages};
|
||||||
|
|
||||||
#[cfg(all(feature = "async", unix))]
|
#[cfg(all(feature = "async", unix))]
|
||||||
use imp::{commit_stack_pages, decommit_stack_pages};
|
use imp::{commit_stack_pages, reset_stack_pages_to_zero};
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
use super::FiberStackError;
|
use super::FiberStackError;
|
||||||
@@ -887,11 +887,16 @@ struct StackPool {
|
|||||||
max_instances: usize,
|
max_instances: usize,
|
||||||
page_size: usize,
|
page_size: usize,
|
||||||
index_allocator: Mutex<PoolingAllocationState>,
|
index_allocator: Mutex<PoolingAllocationState>,
|
||||||
|
async_stack_zeroing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "async", unix))]
|
#[cfg(all(feature = "async", unix))]
|
||||||
impl StackPool {
|
impl StackPool {
|
||||||
fn new(instance_limits: &InstanceLimits, stack_size: usize) -> Result<Self> {
|
fn new(
|
||||||
|
instance_limits: &InstanceLimits,
|
||||||
|
stack_size: usize,
|
||||||
|
async_stack_zeroing: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
use rustix::mm::{mprotect, MprotectFlags};
|
use rustix::mm::{mprotect, MprotectFlags};
|
||||||
|
|
||||||
let page_size = crate::page_size();
|
let page_size = crate::page_size();
|
||||||
@@ -931,6 +936,7 @@ impl StackPool {
|
|||||||
stack_size,
|
stack_size,
|
||||||
max_instances,
|
max_instances,
|
||||||
page_size,
|
page_size,
|
||||||
|
async_stack_zeroing,
|
||||||
// We always use a `NextAvailable` strategy for stack
|
// We always use a `NextAvailable` strategy for stack
|
||||||
// allocation. We don't want or need an affinity policy
|
// allocation. We don't want or need an affinity policy
|
||||||
// here: stacks do not benefit from being allocated to the
|
// here: stacks do not benefit from being allocated to the
|
||||||
@@ -997,7 +1003,9 @@ impl StackPool {
|
|||||||
let index = (start_of_stack - base) / self.stack_size;
|
let index = (start_of_stack - base) / self.stack_size;
|
||||||
assert!(index < self.max_instances);
|
assert!(index < self.max_instances);
|
||||||
|
|
||||||
decommit_stack_pages(bottom_of_stack as _, stack_size).unwrap();
|
if self.async_stack_zeroing {
|
||||||
|
reset_stack_pages_to_zero(bottom_of_stack as _, stack_size).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
self.index_allocator.lock().unwrap().free(SlotId(index));
|
self.index_allocator.lock().unwrap().free(SlotId(index));
|
||||||
}
|
}
|
||||||
@@ -1024,6 +1032,7 @@ impl PoolingInstanceAllocator {
|
|||||||
instance_limits: InstanceLimits,
|
instance_limits: InstanceLimits,
|
||||||
stack_size: usize,
|
stack_size: usize,
|
||||||
tunables: &Tunables,
|
tunables: &Tunables,
|
||||||
|
async_stack_zeroing: bool,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
if instance_limits.count == 0 {
|
if instance_limits.count == 0 {
|
||||||
bail!("the instance count limit cannot be zero");
|
bail!("the instance count limit cannot be zero");
|
||||||
@@ -1032,11 +1041,12 @@ impl PoolingInstanceAllocator {
|
|||||||
let instances = InstancePool::new(strategy, &instance_limits, tunables)?;
|
let instances = InstancePool::new(strategy, &instance_limits, tunables)?;
|
||||||
|
|
||||||
drop(stack_size); // suppress unused warnings w/o async feature
|
drop(stack_size); // suppress unused warnings w/o async feature
|
||||||
|
drop(async_stack_zeroing); // suppress unused warnings w/o async feature
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
instances: instances,
|
instances: instances,
|
||||||
#[cfg(all(feature = "async", unix))]
|
#[cfg(all(feature = "async", unix))]
|
||||||
stacks: StackPool::new(&instance_limits, stack_size)?,
|
stacks: StackPool::new(&instance_limits, stack_size, async_stack_zeroing)?,
|
||||||
#[cfg(all(feature = "async", windows))]
|
#[cfg(all(feature = "async", windows))]
|
||||||
stack_size,
|
stack_size,
|
||||||
})
|
})
|
||||||
@@ -1332,6 +1342,7 @@ mod test {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
1,
|
1,
|
||||||
|
true,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let native_page_size = crate::page_size();
|
let native_page_size = crate::page_size();
|
||||||
@@ -1408,6 +1419,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
4096,
|
4096,
|
||||||
&Tunables::default(),
|
&Tunables::default(),
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
.expect_err("expected a failure constructing instance allocator"),
|
.expect_err("expected a failure constructing instance allocator"),
|
||||||
@@ -1430,6 +1442,7 @@ mod test {
|
|||||||
static_memory_bound: 1,
|
static_memory_bound: 1,
|
||||||
..Tunables::default()
|
..Tunables::default()
|
||||||
},
|
},
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
.expect_err("expected a failure constructing instance allocator"),
|
.expect_err("expected a failure constructing instance allocator"),
|
||||||
@@ -1453,6 +1466,7 @@ mod test {
|
|||||||
static_memory_offset_guard_size: 0,
|
static_memory_offset_guard_size: 0,
|
||||||
..Tunables::default()
|
..Tunables::default()
|
||||||
},
|
},
|
||||||
|
true
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
.expect_err("expected a failure constructing instance allocator"),
|
.expect_err("expected a failure constructing instance allocator"),
|
||||||
@@ -1473,12 +1487,13 @@ mod test {
|
|||||||
memories: 0,
|
memories: 0,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
4096,
|
128,
|
||||||
&Tunables::default(),
|
&Tunables::default(),
|
||||||
|
true,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
for _ in 0..10 {
|
for _ in 0..255 {
|
||||||
let stack = allocator.allocate_fiber_stack()?;
|
let stack = allocator.allocate_fiber_stack()?;
|
||||||
|
|
||||||
// The stack pointer is at the top, so decrement it first
|
// The stack pointer is at the top, so decrement it first
|
||||||
@@ -1493,4 +1508,39 @@ mod test {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(unix, target_pointer_width = "64", feature = "async"))]
|
||||||
|
#[test]
|
||||||
|
fn test_stack_unzeroed() -> Result<()> {
|
||||||
|
let allocator = PoolingInstanceAllocator::new(
|
||||||
|
PoolingAllocationStrategy::NextAvailable,
|
||||||
|
InstanceLimits {
|
||||||
|
count: 1,
|
||||||
|
table_elements: 0,
|
||||||
|
memory_pages: 0,
|
||||||
|
tables: 0,
|
||||||
|
memories: 0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
128,
|
||||||
|
&Tunables::default(),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
for i in 0..255 {
|
||||||
|
let stack = allocator.allocate_fiber_stack()?;
|
||||||
|
|
||||||
|
// The stack pointer is at the top, so decrement it first
|
||||||
|
let addr = stack.top().unwrap().sub(1);
|
||||||
|
|
||||||
|
assert_eq!(*addr, i);
|
||||||
|
*addr = i + 1;
|
||||||
|
|
||||||
|
allocator.deallocate_fiber_stack(&stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,6 @@ pub fn commit_stack_pages(_addr: *mut u8, _len: usize) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async")]
|
#[cfg(feature = "async")]
|
||||||
pub fn decommit_stack_pages(addr: *mut u8, len: usize) -> Result<()> {
|
pub fn reset_stack_pages_to_zero(addr: *mut u8, len: usize) -> Result<()> {
|
||||||
decommit(addr, len, false)
|
decommit(addr, len, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ pub struct Config {
|
|||||||
pub(crate) memory_init_cow: bool,
|
pub(crate) memory_init_cow: bool,
|
||||||
pub(crate) memory_guaranteed_dense_image_size: u64,
|
pub(crate) memory_guaranteed_dense_image_size: u64,
|
||||||
pub(crate) force_memory_init_memfd: bool,
|
pub(crate) force_memory_init_memfd: bool,
|
||||||
|
pub(crate) async_stack_zeroing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User-provided configuration for the compiler.
|
/// User-provided configuration for the compiler.
|
||||||
@@ -196,6 +197,7 @@ impl Config {
|
|||||||
memory_init_cow: true,
|
memory_init_cow: true,
|
||||||
memory_guaranteed_dense_image_size: 16 << 20,
|
memory_guaranteed_dense_image_size: 16 << 20,
|
||||||
force_memory_init_memfd: false,
|
force_memory_init_memfd: false,
|
||||||
|
async_stack_zeroing: false,
|
||||||
};
|
};
|
||||||
#[cfg(compiler)]
|
#[cfg(compiler)]
|
||||||
{
|
{
|
||||||
@@ -341,6 +343,35 @@ impl Config {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures whether or not stacks used for async futures are reset to
|
||||||
|
/// zero after usage.
|
||||||
|
///
|
||||||
|
/// When the [`async_support`](Config::async_support) method is enabled for
|
||||||
|
/// Wasmtime and the [`call_async`] variant
|
||||||
|
/// of calling WebAssembly is used then Wasmtime will create a separate
|
||||||
|
/// runtime execution stack for each future produced by [`call_async`].
|
||||||
|
/// When using the pooling instance allocator
|
||||||
|
/// ([`InstanceAllocationStrategy::Pooling`]) this allocation will happen
|
||||||
|
/// from a pool of stacks and additionally deallocation will simply release
|
||||||
|
/// the stack back to the pool. During the deallocation process Wasmtime
|
||||||
|
/// won't by default reset the contents of the stack back to zero.
|
||||||
|
///
|
||||||
|
/// When this option is enabled it can be seen as a defense-in-depth
|
||||||
|
/// mechanism to reset a stack back to zero. This is not required for
|
||||||
|
/// correctness and can be a costly operation in highly concurrent
|
||||||
|
/// environments due to modifications of the virtual address space requiring
|
||||||
|
/// process-wide synchronization.
|
||||||
|
///
|
||||||
|
/// This option defaults to `false`.
|
||||||
|
///
|
||||||
|
/// [`call_async`]: crate::TypedFunc::call_async
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
#[cfg_attr(nightlydoc, doc(cfg(feature = "async")))]
|
||||||
|
pub fn async_stack_zeroing(&mut self, enable: bool) -> &mut Self {
|
||||||
|
self.async_stack_zeroing = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Configures whether DWARF debug information will be emitted during
|
/// Configures whether DWARF debug information will be emitted during
|
||||||
/// compilation.
|
/// compilation.
|
||||||
///
|
///
|
||||||
@@ -1432,6 +1463,7 @@ impl Config {
|
|||||||
instance_limits,
|
instance_limits,
|
||||||
stack_size,
|
stack_size,
|
||||||
&self.tunables,
|
&self.tunables,
|
||||||
|
self.async_stack_zeroing,
|
||||||
)?)),
|
)?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user