Implement bulk memory's partial failure instantiation semantics

Essentially, table and memory out of bounds errors are no longer link errors,
but traps after linking. This means that the partail writes / inits are visible.
This commit is contained in:
Nick Fitzgerald
2020-02-20 16:19:36 -08:00
parent 44c28612fb
commit 81227892da
6 changed files with 88 additions and 15 deletions

View File

@@ -186,7 +186,8 @@ fn ignore(testsuite: &str, testname: &str, strategy: &str) -> bool {
| ("bulk_memory_operations", "table_init") | ("bulk_memory_operations", "table_init")
| ("bulk_memory_operations", "elem") | ("bulk_memory_operations", "elem")
| ("bulk_memory_operations", "memory_copy") | ("bulk_memory_operations", "memory_copy")
| ("bulk_memory_operations", "memory_fill") => return false, | ("bulk_memory_operations", "memory_fill")
| ("bulk_memory_operations", "linking") => return false,
("bulk_memory_operations", _) => return true, ("bulk_memory_operations", _) => return true,
_ => {} _ => {}

View File

@@ -26,11 +26,15 @@ fn instantiate(
let mut resolver = SimpleResolver { imports }; let mut resolver = SimpleResolver { imports };
unsafe { unsafe {
let instance = compiled_module let instance = compiled_module
.instantiate(&mut resolver) .instantiate(
config.validating_config.operator_config.enable_bulk_memory,
&mut resolver,
)
.map_err(|e| -> Error { .map_err(|e| -> Error {
match e { match e {
InstantiationError::StartTrap(trap) => Trap::from_jit(trap).into(), InstantiationError::StartTrap(trap) => Trap::from_jit(trap).into(),
e @ InstantiationError::TableOutOfBounds(_) => { e @ InstantiationError::TableOutOfBounds(_)
| e @ InstantiationError::MemoryOutOfBounds(_) => {
let msg = e.to_string(); let msg = e.to_string();
if config.validating_config.operator_config.enable_bulk_memory { if config.validating_config.operator_config.enable_bulk_memory {
Trap::new(msg).into() Trap::new(msg).into()

View File

@@ -42,6 +42,12 @@ pub(crate) fn create_handle(
&data_initializers, &data_initializers,
signatures.into_boxed_slice(), signatures.into_boxed_slice(),
None, None,
store
.engine()
.config()
.validating_config
.operator_config
.enable_bulk_memory,
state, state,
)?) )?)
} }

View File

@@ -205,6 +205,7 @@ impl CompiledModule {
/// See `InstanceHandle::new` /// See `InstanceHandle::new`
pub unsafe fn instantiate( pub unsafe fn instantiate(
&self, &self,
is_bulk_memory: bool,
resolver: &mut dyn Resolver, resolver: &mut dyn Resolver,
) -> Result<InstanceHandle, InstantiationError> { ) -> Result<InstanceHandle, InstantiationError> {
let data_initializers = self let data_initializers = self
@@ -224,6 +225,7 @@ impl CompiledModule {
&data_initializers, &data_initializers,
self.signatures.clone(), self.signatures.clone(),
self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)), self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)),
is_bulk_memory,
Box::new(()), Box::new(()),
) )
} }
@@ -277,9 +279,10 @@ pub unsafe fn instantiate(
data: &[u8], data: &[u8],
resolver: &mut dyn Resolver, resolver: &mut dyn Resolver,
debug_info: bool, debug_info: bool,
is_bulk_memory: bool,
profiler: Option<&Arc<Mutex<Box<dyn ProfilingAgent + Send>>>>, profiler: Option<&Arc<Mutex<Box<dyn ProfilingAgent + Send>>>>,
) -> Result<InstanceHandle, SetupError> { ) -> Result<InstanceHandle, SetupError> {
let instance = let instance = CompiledModule::new(compiler, data, debug_info, profiler)?
CompiledModule::new(compiler, data, debug_info, profiler)?.instantiate(resolver)?; .instantiate(is_bulk_memory, resolver)?;
Ok(instance) Ok(instance)
} }

View File

@@ -21,6 +21,7 @@ use more_asserts::assert_lt;
use std::alloc::{self, Layout}; use std::alloc::{self, Layout};
use std::any::Any; use std::any::Any;
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::cmp;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::TryFrom; use std::convert::TryFrom;
@@ -810,6 +811,7 @@ impl InstanceHandle {
data_initializers: &[DataInitializer<'_>], data_initializers: &[DataInitializer<'_>],
vmshared_signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>, vmshared_signatures: BoxedSlice<SignatureIndex, VMSharedSignatureIndex>,
dbg_jit_registration: Option<Rc<GdbJitImageRegistration>>, dbg_jit_registration: Option<Rc<GdbJitImageRegistration>>,
is_bulk_memory: bool,
host_state: Box<dyn Any>, host_state: Box<dyn Any>,
) -> Result<Self, InstantiationError> { ) -> Result<Self, InstantiationError> {
let tables = create_tables(&module); let tables = create_tables(&module);
@@ -904,9 +906,14 @@ impl InstanceHandle {
VMBuiltinFunctionsArray::initialized(), VMBuiltinFunctionsArray::initialized(),
); );
// Check initializer bounds before initializing anything. // Check initializer bounds before initializing anything. Only do this
check_table_init_bounds(instance)?; // when bulk memory is disabled, since the bulk memory proposal changes
check_memory_init_bounds(instance, data_initializers)?; // instantiation such that the intermediate results of failed
// initializations are visible.
if !is_bulk_memory {
check_table_init_bounds(instance)?;
check_memory_init_bounds(instance, data_initializers)?;
}
// Apply the initializers. // Apply the initializers.
initialize_tables(instance)?; initialize_tables(instance)?;
@@ -1133,9 +1140,9 @@ fn check_memory_init_bounds(
unsafe { unsafe {
let mem_slice = get_memory_slice(init, instance); let mem_slice = get_memory_slice(init, instance);
if mem_slice.get_mut(start..start + init.data.len()).is_none() { if mem_slice.get_mut(start..start + init.data.len()).is_none() {
return Err(InstantiationError::Link(LinkError( return Err(InstantiationError::MemoryOutOfBounds(
"data segment does not fit".to_owned(), "data segment does not fit".into(),
))); ));
} }
} }
} }
@@ -1179,11 +1186,29 @@ fn initialize_tables(instance: &Instance) -> Result<(), InstantiationError> {
let start = get_table_init_start(init, instance); let start = get_table_init_start(init, instance);
let table = instance.get_table(init.table_index); let table = instance.get_table(init.table_index);
// Even if there aren't any elements being initialized, if its out of
// bounds, then we report an error. This is required by the bulk memory
// proposal and the way that it uses `table.init` during instantiation.
if start > table.size() as usize {
return Err(InstantiationError::TableOutOfBounds(
"active element segment does not fit in table".into(),
));
}
for (i, func_idx) in init.elements.iter().enumerate() { for (i, func_idx) in init.elements.iter().enumerate() {
let anyfunc = instance.get_caller_checked_anyfunc(*func_idx); let anyfunc = instance.get_caller_checked_anyfunc(*func_idx);
table table
.set(u32::try_from(start + i).unwrap(), anyfunc) .set(u32::try_from(start + i).unwrap(), anyfunc)
.unwrap(); // Note that when multi-value is disabled, this will never fail
// since we bounds check table element initialization before
// doing any table slot writes. However when multi-value is
// enabled, these become runtime traps and the intermediate
// table slot writes are visible.
.map_err(|()| {
InstantiationError::TableOutOfBounds(
"active element segment does not fit in table".into(),
)
})?;
} }
} }
@@ -1239,8 +1264,25 @@ fn initialize_memories(
let start = get_memory_init_start(init, instance); let start = get_memory_init_start(init, instance);
unsafe { unsafe {
let mem_slice = get_memory_slice(init, instance); let mem_slice = get_memory_slice(init, instance);
let to_init = &mut mem_slice[start..start + init.data.len()];
to_init.copy_from_slice(init.data); let end = start.saturating_add(init.data.len());
let actual_end = cmp::min(mem_slice.len(), end);
let num_uncopied = end - actual_end;
let to_init = &mut mem_slice[start..actual_end];
to_init.copy_from_slice(&init.data[..init.data.len() - num_uncopied]);
// Note that if bulk memory is disabled, we'll never hit this case
// because we did an eager bounds check in
// `check_memory_init_bounds`. However when bulk memory is enabled,
// we need the incremental writes up to the OOB trap to be visible,
// since this is dictated by the updated spec for the bulk memory
// proposal.
if num_uncopied > 0 {
return Err(InstantiationError::MemoryOutOfBounds(
"active data segment does not fit in memory".into(),
));
}
} }
} }
@@ -1307,6 +1349,13 @@ pub enum InstantiationError {
#[error("Table out of bounds error: {0}")] #[error("Table out of bounds error: {0}")]
TableOutOfBounds(String), TableOutOfBounds(String),
/// A memory out of bounds error.
///
/// Depending on whether bulk memory is enabled or not, this is either a
/// link error or a trap raised during instantiation.
#[error("Memory out of bounds error: {0}")]
MemoryOutOfBounds(String),
/// A wasm link error occured. /// A wasm link error occured.
#[error("Failed to link module")] #[error("Failed to link module")]
Link(#[from] LinkError), Link(#[from] LinkError),

View File

@@ -24,7 +24,17 @@ fn test_environ_translate() {
let cache_config = CacheConfig::new_cache_disabled(); let cache_config = CacheConfig::new_cache_disabled();
let mut compiler = Compiler::new(isa, CompilationStrategy::Auto, cache_config); let mut compiler = Compiler::new(isa, CompilationStrategy::Auto, cache_config);
unsafe { unsafe {
let instance = instantiate(&mut compiler, &data, &mut resolver, false, None); let instance = instantiate(
&mut compiler,
&data,
&mut resolver,
// Bulk memory.
false,
// Debug info.
false,
// Profiler.
None,
);
assert!(instance.is_ok()); assert!(instance.is_ok());
} }
} }