diff --git a/Cargo.lock b/Cargo.lock index 518b7d8b0c29..b952743ceae2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,15 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -3333,6 +3342,7 @@ dependencies = [ "async-trait", "bincode", "cfg-if", + "encoding_rs", "indexmap", "libc", "log", @@ -3653,6 +3663,7 @@ dependencies = [ "anyhow", "cc", "cfg-if", + "encoding_rs", "indexmap", "libc", "log", diff --git a/crates/component-util/src/lib.rs b/crates/component-util/src/lib.rs index 409ba551c8f4..2c4a2a461dc9 100644 --- a/crates/component-util/src/lib.rs +++ b/crates/component-util/src/lib.rs @@ -88,6 +88,8 @@ pub const REALLOC_AND_FREE: &str = r#" (param $new_size i32) (result i32) + (local $ret i32) + ;; Test if the old pointer is non-null local.get $old_ptr if @@ -101,8 +103,8 @@ pub const REALLOC_AND_FREE: &str = r#" return end - ;; ... otherwise this is unimplemented - unreachable + ;; otherwise fall through to allocate a new chunk which will later + ;; copy data over end ;; align up `$last` @@ -121,6 +123,7 @@ pub const REALLOC_AND_FREE: &str = r#" ;; save the current value of `$last` as the return value global.get $last + local.tee $ret ;; ensure anything necessary is set to valid data by spraying a bit ;; pattern that is invalid @@ -129,6 +132,16 @@ pub const REALLOC_AND_FREE: &str = r#" local.get $new_size memory.fill + ;; If the old pointer is present then that means this was a reallocation + ;; of an existing chunk which means the existing data must be copied. + local.get $old_ptr + if + local.get $ret ;; destination + local.get $old_ptr ;; source + local.get $old_size ;; size + memory.copy + end + ;; bump our pointer (global.set $last (i32.add diff --git a/crates/cranelift/src/compiler/component.rs b/crates/cranelift/src/compiler/component.rs index 318764d11e56..33bd9d74cdbc 100644 --- a/crates/cranelift/src/compiler/component.rs +++ b/crates/cranelift/src/compiler/component.rs @@ -10,8 +10,9 @@ use object::write::Object; use std::any::Any; use std::ops::Range; use wasmtime_environ::component::{ - AlwaysTrapInfo, CanonicalOptions, Component, ComponentCompiler, ComponentTypes, FunctionInfo, - LowerImport, LoweredIndex, RuntimeAlwaysTrapIndex, VMComponentOffsets, + AlwaysTrapInfo, CanonicalOptions, Component, ComponentCompiler, ComponentTypes, FixedEncoding, + FunctionInfo, LowerImport, LoweredIndex, RuntimeAlwaysTrapIndex, RuntimeMemoryIndex, + RuntimeTranscoderIndex, Transcode, Transcoder, VMComponentOffsets, }; use wasmtime_environ::{PrimaryMap, PtrSize, SignatureIndex, Trampoline, TrapCode, WasmFuncType}; @@ -47,42 +48,7 @@ impl ComponentCompiler for Compiler { let vmctx = builder.func.dfg.block_params(block0)[0]; // Save the exit FP and return address for stack walking purposes. - // - // First we need to get the `VMRuntimeLimits`. - let limits = builder.ins().load( - pointer_type, - MemFlags::trusted(), - vmctx, - i32::try_from(offsets.limits()).unwrap(), - ); - // Then save the exit Wasm FP to the limits. We dereference the current - // FP to get the previous FP because the current FP is the trampoline's - // FP, and we want the Wasm function's FP, which is the caller of this - // trampoline. - let trampoline_fp = builder.ins().get_frame_pointer(pointer_type); - let wasm_fp = builder.ins().load( - pointer_type, - MemFlags::trusted(), - trampoline_fp, - // The FP always points to the next older FP for all supported - // targets. See assertion in - // `crates/runtime/src/traphandlers/backtrace.rs`. - 0, - ); - builder.ins().store( - MemFlags::trusted(), - wasm_fp, - limits, - offsets.ptr.vmruntime_limits_last_wasm_exit_fp(), - ); - // Finally save the Wasm return address to the limits. - let wasm_pc = builder.ins().get_return_address(pointer_type); - builder.ins().store( - MemFlags::trusted(), - wasm_pc, - limits, - offsets.ptr.vmruntime_limits_last_wasm_exit_pc(), - ); + self.save_last_wasm_fp_and_pc(&mut builder, &offsets, vmctx); // Below this will incrementally build both the signature of the host // function we're calling as well as the list of arguments since the @@ -219,15 +185,53 @@ impl ComponentCompiler for Compiler { Ok(Box::new(func)) } + fn compile_transcoder( + &self, + component: &Component, + transcoder: &Transcoder, + types: &ComponentTypes, + ) -> Result> { + let ty = &types[transcoder.signature]; + let isa = &*self.isa; + let offsets = VMComponentOffsets::new(isa.pointer_bytes(), component); + + let CompilerContext { + mut func_translator, + codegen_context: mut context, + } = self.take_context(); + + context.func = ir::Function::with_name_signature( + ir::ExternalName::user(0, 0), + crate::indirect_signature(isa, ty), + ); + + let mut builder = FunctionBuilder::new(&mut context.func, func_translator.context()); + let block0 = builder.create_block(); + builder.append_block_params_for_function_params(block0); + builder.switch_to_block(block0); + builder.seal_block(block0); + + self.translate_transcode(&mut builder, &offsets, transcoder, block0); + + let func: CompiledFunction = self.finish_trampoline(&mut context, isa)?; + self.save_context(CompilerContext { + func_translator, + codegen_context: context, + }); + Ok(Box::new(func)) + } + fn emit_obj( &self, lowerings: PrimaryMap>, always_trap: PrimaryMap>, + transcoders: PrimaryMap>, trampolines: Vec<(SignatureIndex, Box)>, obj: &mut Object<'static>, ) -> Result<( PrimaryMap, PrimaryMap, + PrimaryMap, Vec, )> { let module = Default::default(); @@ -264,6 +268,16 @@ impl ComponentCompiler for Compiler { }) .collect(); + let ret_transcoders = transcoders + .iter() + .map(|(i, func)| { + let func = func.downcast_ref::().unwrap(); + let name = format!("_wasmtime_transcoder{}", i.as_u32()); + let range = text.named_func(&name, func); + range2info(range) + }) + .collect(); + let ret_trampolines = trampolines .iter() .map(|(i, func)| { @@ -275,6 +289,313 @@ impl ComponentCompiler for Compiler { text.finish()?; - Ok((ret_lowerings, ret_always_trap, ret_trampolines)) + Ok(( + ret_lowerings, + ret_always_trap, + ret_transcoders, + ret_trampolines, + )) + } +} + +impl Compiler { + fn save_last_wasm_fp_and_pc( + &self, + builder: &mut FunctionBuilder<'_>, + offsets: &VMComponentOffsets, + vmctx: ir::Value, + ) { + let pointer_type = self.isa.pointer_type(); + // First we need to get the `VMRuntimeLimits`. + let limits = builder.ins().load( + pointer_type, + MemFlags::trusted(), + vmctx, + i32::try_from(offsets.limits()).unwrap(), + ); + // Then save the exit Wasm FP to the limits. We dereference the current + // FP to get the previous FP because the current FP is the trampoline's + // FP, and we want the Wasm function's FP, which is the caller of this + // trampoline. + let trampoline_fp = builder.ins().get_frame_pointer(pointer_type); + let wasm_fp = builder.ins().load( + pointer_type, + MemFlags::trusted(), + trampoline_fp, + // The FP always points to the next older FP for all supported + // targets. See assertion in + // `crates/runtime/src/traphandlers/backtrace.rs`. + 0, + ); + builder.ins().store( + MemFlags::trusted(), + wasm_fp, + limits, + offsets.ptr.vmruntime_limits_last_wasm_exit_fp(), + ); + // Finally save the Wasm return address to the limits. + let wasm_pc = builder.ins().get_return_address(pointer_type); + builder.ins().store( + MemFlags::trusted(), + wasm_pc, + limits, + offsets.ptr.vmruntime_limits_last_wasm_exit_pc(), + ); + } + + fn translate_transcode( + &self, + builder: &mut FunctionBuilder<'_>, + offsets: &VMComponentOffsets, + transcoder: &Transcoder, + block: ir::Block, + ) { + let pointer_type = self.isa.pointer_type(); + let vmctx = builder.func.dfg.block_params(block)[0]; + + // Save the exit FP and return address for stack walking purposes. This + // is used when an invalid encoding is encountered and a trap is raised. + self.save_last_wasm_fp_and_pc(builder, &offsets, vmctx); + + // Determine the static signature of the host libcall for this transcode + // operation and additionally calculate the static offset within the + // transode libcalls array. + let func = &mut builder.func; + let (sig, offset) = match transcoder.op { + Transcode::Copy(FixedEncoding::Utf8) => host::utf8_to_utf8(self, func), + Transcode::Copy(FixedEncoding::Utf16) => host::utf16_to_utf16(self, func), + Transcode::Copy(FixedEncoding::Latin1) => host::latin1_to_latin1(self, func), + Transcode::Latin1ToUtf16 => host::latin1_to_utf16(self, func), + Transcode::Latin1ToUtf8 => host::latin1_to_utf8(self, func), + Transcode::Utf16ToCompactProbablyUtf16 => { + host::utf16_to_compact_probably_utf16(self, func) + } + Transcode::Utf16ToCompactUtf16 => host::utf16_to_compact_utf16(self, func), + Transcode::Utf16ToLatin1 => host::utf16_to_latin1(self, func), + Transcode::Utf16ToUtf8 => host::utf16_to_utf8(self, func), + Transcode::Utf8ToCompactUtf16 => host::utf8_to_compact_utf16(self, func), + Transcode::Utf8ToLatin1 => host::utf8_to_latin1(self, func), + Transcode::Utf8ToUtf16 => host::utf8_to_utf16(self, func), + }; + + // Load the host function pointer for this transcode which comes from a + // function pointer within the VMComponentContext's libcall array. + let transcode_libcalls_array = builder.ins().load( + pointer_type, + MemFlags::trusted(), + vmctx, + i32::try_from(offsets.transcode_libcalls()).unwrap(), + ); + let transcode_libcall = builder.ins().load( + pointer_type, + MemFlags::trusted(), + transcode_libcalls_array, + i32::try_from(offset * u32::from(offsets.ptr.size())).unwrap(), + ); + + // Load the base pointers for the from/to linear memories. + let from_base = self.load_runtime_memory_base(builder, vmctx, offsets, transcoder.from); + let to_base = self.load_runtime_memory_base(builder, vmctx, offsets, transcoder.to); + + // Helper function to cast a core wasm input to a host pointer type + // which will go into the host libcall. + let cast_to_pointer = |builder: &mut FunctionBuilder<'_>, val: ir::Value, is64: bool| { + let host64 = pointer_type == ir::types::I64; + if is64 == host64 { + val + } else if !is64 { + assert!(host64); + builder.ins().uextend(pointer_type, val) + } else { + assert!(!host64); + builder.ins().ireduce(pointer_type, val) + } + }; + + // Helper function to cast an input parameter to the host pointer type. + let len_param = |builder: &mut FunctionBuilder<'_>, param: usize, is64: bool| { + let val = builder.func.dfg.block_params(block)[2 + param]; + cast_to_pointer(builder, val, is64) + }; + + // Helper function to interpret an input parameter as a pointer into + // linear memory. This will cast the input parameter to the host integer + // type and then add that value to the base. + // + // Note that bounds-checking happens in adapter modules, and this + // trampoline is simply calling the host libcall. + let ptr_param = + |builder: &mut FunctionBuilder<'_>, param: usize, is64: bool, base: ir::Value| { + let val = len_param(builder, param, is64); + builder.ins().iadd(base, val) + }; + + let Transcoder { to64, from64, .. } = *transcoder; + let mut args = Vec::new(); + + // Most transcoders share roughly the same signature despite doing very + // different things internally, so most libcalls are lumped together + // here. + match transcoder.op { + Transcode::Copy(_) + | Transcode::Latin1ToUtf16 + | Transcode::Utf16ToCompactProbablyUtf16 + | Transcode::Utf8ToLatin1 + | Transcode::Utf16ToLatin1 + | Transcode::Utf8ToUtf16 => { + args.push(ptr_param(builder, 0, from64, from_base)); + args.push(len_param(builder, 1, from64)); + args.push(ptr_param(builder, 2, to64, to_base)); + } + + Transcode::Utf16ToUtf8 | Transcode::Latin1ToUtf8 => { + args.push(ptr_param(builder, 0, from64, from_base)); + args.push(len_param(builder, 1, from64)); + args.push(ptr_param(builder, 2, to64, to_base)); + args.push(len_param(builder, 3, to64)); + } + + Transcode::Utf8ToCompactUtf16 | Transcode::Utf16ToCompactUtf16 => { + args.push(ptr_param(builder, 0, from64, from_base)); + args.push(len_param(builder, 1, from64)); + args.push(ptr_param(builder, 2, to64, to_base)); + args.push(len_param(builder, 3, to64)); + args.push(len_param(builder, 4, to64)); + } + }; + let call = builder.ins().call_indirect(sig, transcode_libcall, &args); + let results = builder.func.dfg.inst_results(call).to_vec(); + let mut raw_results = Vec::new(); + + // Helper to cast a host pointer integer type to the destination type. + let cast_from_pointer = |builder: &mut FunctionBuilder<'_>, val: ir::Value, is64: bool| { + let host64 = pointer_type == ir::types::I64; + if is64 == host64 { + val + } else if !is64 { + assert!(host64); + builder.ins().ireduce(ir::types::I32, val) + } else { + assert!(!host64); + builder.ins().uextend(ir::types::I64, val) + } + }; + + // Like the arguments the results are fairly similar across libcalls, so + // they're lumped into various buckets here. + match transcoder.op { + Transcode::Copy(_) | Transcode::Latin1ToUtf16 => {} + + Transcode::Utf8ToUtf16 + | Transcode::Utf16ToCompactProbablyUtf16 + | Transcode::Utf8ToCompactUtf16 + | Transcode::Utf16ToCompactUtf16 => { + raw_results.push(cast_from_pointer(builder, results[0], to64)); + } + + Transcode::Latin1ToUtf8 + | Transcode::Utf16ToUtf8 + | Transcode::Utf8ToLatin1 + | Transcode::Utf16ToLatin1 => { + raw_results.push(cast_from_pointer(builder, results[0], from64)); + raw_results.push(cast_from_pointer(builder, results[1], to64)); + } + }; + + builder.ins().return_(&raw_results); + builder.finalize(); + } + + fn load_runtime_memory_base( + &self, + builder: &mut FunctionBuilder<'_>, + vmctx: ir::Value, + offsets: &VMComponentOffsets, + mem: RuntimeMemoryIndex, + ) -> ir::Value { + let pointer_type = self.isa.pointer_type(); + let from_vmmemory_definition = builder.ins().load( + pointer_type, + MemFlags::trusted(), + vmctx, + i32::try_from(offsets.runtime_memory(mem)).unwrap(), + ); + builder.ins().load( + pointer_type, + MemFlags::trusted(), + from_vmmemory_definition, + i32::from(offsets.ptr.vmmemory_definition_base()), + ) + } +} + +/// Module with macro-generated contents that will return the signature and +/// offset for each of the host transcoder functions. +/// +/// Note that a macro is used here to keep this in sync with the actual +/// transcoder functions themselves which are also defined via a macro. +#[allow(unused_mut)] +mod host { + use crate::compiler::Compiler; + use cranelift_codegen::ir::{self, AbiParam}; + + macro_rules! host_transcode { + ( + $( + $( #[$attr:meta] )* + $name:ident( $( $pname:ident: $param:ident ),* ) $( -> $result:ident )?; + )* + ) => { + $( + pub(super) fn $name(compiler: &Compiler, func: &mut ir::Function) -> (ir::SigRef, u32) { + let pointer_type = compiler.isa.pointer_type(); + let params = vec![ + $( AbiParam::new(host_transcode!(@ty pointer_type $param)) ),* + ]; + let mut returns = Vec::new(); + $(host_transcode!(@push_return pointer_type params returns $result);)? + let sig = func.import_signature(ir::Signature { + params, + returns, + call_conv: crate::wasmtime_call_conv(&*compiler.isa), + }); + + (sig, offsets::$name) + } + )* + }; + + (@ty $ptr:ident size) => ($ptr); + (@ty $ptr:ident ptr_u8) => ($ptr); + (@ty $ptr:ident ptr_u16) => ($ptr); + + (@push_return $ptr:ident $params:ident $returns:ident size) => ($returns.push(AbiParam::new($ptr));); + (@push_return $ptr:ident $params:ident $returns:ident size_pair) => ({ + $returns.push(AbiParam::new($ptr)); + $returns.push(AbiParam::new($ptr)); + }); + } + + wasmtime_environ::foreach_transcoder!(host_transcode); + + mod offsets { + macro_rules! offsets { + ( + $( + $( #[$attr:meta] )* + $name:ident($($t:tt)*) $( -> $result:ident )?; + )* + ) => { + offsets!(@declare (0) $($name)*); + }; + + (@declare ($n:expr)) => (); + (@declare ($n:expr) $name:ident $($rest:tt)*) => ( + pub static $name: u32 = $n; + offsets!(@declare ($n + 1) $($rest)*); + ); + } + + wasmtime_environ::foreach_transcoder!(offsets); } } diff --git a/crates/cranelift/src/func_environ.rs b/crates/cranelift/src/func_environ.rs index 7d5e053b02eb..7576f423c081 100644 --- a/crates/cranelift/src/func_environ.rs +++ b/crates/cranelift/src/func_environ.rs @@ -1387,9 +1387,9 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m global_type: pointer_type, readonly: true, }); - let base_offset = i32::from(self.offsets.vmmemory_definition_base()); + let base_offset = i32::from(self.offsets.ptr.vmmemory_definition_base()); let current_length_offset = - i32::from(self.offsets.vmmemory_definition_current_length()); + i32::from(self.offsets.ptr.vmmemory_definition_current_length()); (memory, base_offset, current_length_offset) } else { let owned_index = self.module.owned_memory_index(def_index); @@ -1410,9 +1410,9 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m global_type: pointer_type, readonly: true, }); - let base_offset = i32::from(self.offsets.vmmemory_definition_base()); + let base_offset = i32::from(self.offsets.ptr.vmmemory_definition_base()); let current_length_offset = - i32::from(self.offsets.vmmemory_definition_current_length()); + i32::from(self.offsets.ptr.vmmemory_definition_current_length()); (memory, base_offset, current_length_offset) } }; @@ -1726,7 +1726,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m pos.ins() .load(pointer_type, ir::MemFlags::trusted(), base, offset); let vmmemory_definition_offset = - i64::from(self.offsets.vmmemory_definition_current_length()); + i64::from(self.offsets.ptr.vmmemory_definition_current_length()); let vmmemory_definition_ptr = pos.ins().iadd_imm(vmmemory_ptr, vmmemory_definition_offset); // This atomic access of the @@ -1758,7 +1758,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m .load(pointer_type, ir::MemFlags::trusted(), base, offset); if is_shared { let vmmemory_definition_offset = - i64::from(self.offsets.vmmemory_definition_current_length()); + i64::from(self.offsets.ptr.vmmemory_definition_current_length()); let vmmemory_definition_ptr = pos.ins().iadd_imm(vmmemory_ptr, vmmemory_definition_offset); pos.ins().atomic_load( @@ -1771,7 +1771,7 @@ impl<'module_environment> cranelift_wasm::FuncEnvironment for FuncEnvironment<'m pointer_type, ir::MemFlags::trusted(), vmmemory_ptr, - i32::from(self.offsets.vmmemory_definition_current_length()), + i32::from(self.offsets.ptr.vmmemory_definition_current_length()), ) } } diff --git a/crates/environ/fuzz/fuzz_targets/fact-valid-module.rs b/crates/environ/fuzz/fuzz_targets/fact-valid-module.rs index 58f2488cf174..5c7114a4a348 100644 --- a/crates/environ/fuzz/fuzz_targets/fact-valid-module.rs +++ b/crates/environ/fuzz/fuzz_targets/fact-valid-module.rs @@ -45,9 +45,11 @@ enum GenStringEncoding { CompactUtf16, } -fuzz_target!(|module: GenAdapterModule| { drop(target(module)) }); +fuzz_target!(|module: GenAdapterModule| { + target(module); +}); -fn target(module: GenAdapterModule) -> Result<(), ()> { +fn target(module: GenAdapterModule) { drop(env_logger::try_init()); let mut types = ComponentTypesBuilder::default(); @@ -57,7 +59,7 @@ fn target(module: GenAdapterModule) -> Result<(), ()> { let mut next_def = 0; let mut dummy_def = || { next_def += 1; - CoreDef::Adapter(AdapterIndex::from_u32(next_def)) + dfg::CoreDef::Adapter(dfg::AdapterId::from_u32(next_def)) }; // Manufactures a `CoreExport` for a memory with the shape specified. Note @@ -80,8 +82,8 @@ fn target(module: GenAdapterModule) -> Result<(), ()> { } else { dst[0] }; - CoreExport { - instance: RuntimeInstanceIndex::from_u32(idx), + dfg::CoreExport { + instance: dfg::InstanceId::from_u32(idx), item: ExportItem::Name(String::new()), } }; @@ -90,9 +92,9 @@ fn target(module: GenAdapterModule) -> Result<(), ()> { for adapter in module.adapters.iter() { let mut params = Vec::new(); for param in adapter.ty.params.iter() { - params.push((None, intern(&mut types, param)?)); + params.push((None, intern(&mut types, param))); } - let result = intern(&mut types, &adapter.ty.result)?; + let result = intern(&mut types, &adapter.ty.result); let signature = types.add_func_type(TypeFunc { params: params.into(), result, @@ -143,7 +145,7 @@ fn target(module: GenAdapterModule) -> Result<(), ()> { .validate_all(&wasm); let err = match result { - Ok(_) => return Ok(()), + Ok(_) => return, Err(e) => e, }; eprintln!("invalid wasm module: {err:?}"); @@ -159,8 +161,8 @@ fn target(module: GenAdapterModule) -> Result<(), ()> { panic!() } -fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result { - Ok(match ty { +fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> InterfaceType { + match ty { ValType::Unit => InterfaceType::Unit, ValType::Bool => InterfaceType::Bool, ValType::U8 => InterfaceType::U8, @@ -174,8 +176,9 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result InterfaceType::Float32, ValType::Float64 => InterfaceType::Float64, ValType::Char => InterfaceType::Char, + ValType::String => InterfaceType::String, ValType::List(ty) => { - let ty = intern(types, ty)?; + let ty = intern(types, ty); InterfaceType::List(types.add_interface_type(ty)) } ValType::Record(tys) => { @@ -183,13 +186,11 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result>()?, + .collect(), }; InterfaceType::Record(types.add_record_type(ty)) } @@ -201,10 +202,7 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result { let ty = TypeTuple { - types: tys - .iter() - .map(|ty| intern(types, ty)) - .collect::>()?, + types: tys.iter().map(|ty| intern(types, ty)).collect(), }; InterfaceType::Tuple(types.add_tuple_type(ty)) } @@ -213,22 +211,17 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result>()?, + .collect(), }; InterfaceType::Variant(types.add_variant_type(ty)) } ValType::Union(tys) => { let ty = TypeUnion { - types: tys - .iter() - .map(|ty| intern(types, ty)) - .collect::>()?, + types: tys.iter().map(|ty| intern(types, ty)).collect(), }; InterfaceType::Union(types.add_union_type(ty)) } @@ -239,16 +232,15 @@ fn intern(types: &mut ComponentTypesBuilder, ty: &ValType) -> Result { - let ty = intern(types, ty)?; + let ty = intern(types, ty); InterfaceType::Option(types.add_interface_type(ty)) } ValType::Expected { ok, err } => { - let ok = intern(types, ok)?; - let err = intern(types, err)?; + let ok = intern(types, ok); + let err = intern(types, err); InterfaceType::Expected(types.add_expected_type(TypeExpected { ok, err })) } - ValType::String => return Err(()), - }) + } } impl From for StringEncoding { diff --git a/crates/environ/src/component.rs b/crates/environ/src/component.rs index d6c1c2838492..0961b356e9a1 100644 --- a/crates/environ/src/component.rs +++ b/crates/environ/src/component.rs @@ -48,3 +48,25 @@ pub use self::info::*; pub use self::translate::*; pub use self::types::*; pub use self::vmcomponent_offsets::*; + +/// Helper macro to iterate over the transcoders that the host will provide +/// adapter modules through libcalls. +#[macro_export] +macro_rules! foreach_transcoder { + ($mac:ident) => { + $mac! { + utf8_to_utf8(src: ptr_u8, len: size, dst: ptr_u8); + utf16_to_utf16(src: ptr_u16, len: size, dst: ptr_u16); + latin1_to_latin1(src: ptr_u8, len: size, dst: ptr_u8); + latin1_to_utf16(src: ptr_u8, len: size, dst: ptr_u16); + utf8_to_utf16(src: ptr_u8, len: size, dst: ptr_u16) -> size; + utf16_to_utf8(src: ptr_u16, src_len: size, dst: ptr_u8, dst_len: size) -> size_pair; + latin1_to_utf8(src: ptr_u8, src_len: size, dst: ptr_u8, dst_len: size) -> size_pair; + utf16_to_compact_probably_utf16(src: ptr_u16, len: size, dst: ptr_u16) -> size; + utf8_to_latin1(src: ptr_u8, len: size, dst: ptr_u8) -> size_pair; + utf16_to_latin1(src: ptr_u16, len: size, dst: ptr_u8) -> size_pair; + utf8_to_compact_utf16(src: ptr_u8, src_len: size, dst: ptr_u16, dst_len: size, bytes_so_far: size) -> size; + utf16_to_compact_utf16(src: ptr_u16, src_len: size, dst: ptr_u16, dst_len: size, bytes_so_far: size) -> size; + } + }; +} diff --git a/crates/environ/src/component/compiler.rs b/crates/environ/src/component/compiler.rs index ac07a3c1d540..212b6ca3c265 100644 --- a/crates/environ/src/component/compiler.rs +++ b/crates/environ/src/component/compiler.rs @@ -1,5 +1,6 @@ use crate::component::{ Component, ComponentTypes, LowerImport, LoweredIndex, RuntimeAlwaysTrapIndex, + RuntimeTranscoderIndex, Transcoder, }; use crate::{PrimaryMap, SignatureIndex, Trampoline, WasmFuncType}; use anyhow::Result; @@ -61,6 +62,24 @@ pub trait ComponentCompiler: Send + Sync { /// `canon lift`'d function immediately being `canon lower`'d. fn compile_always_trap(&self, ty: &WasmFuncType) -> Result>; + /// Compiles a trampoline to implement string transcoding from adapter + /// modules. + /// + /// The generated trampoline will invoke the `transcoder.op` libcall with + /// the various memory configuration provided in `transcoder`. This is used + /// to pass raw pointers to host functions to avoid the host having to deal + /// with base pointers, offsets, memory32-vs-64, etc. + /// + /// Note that all bounds checks for memories are present in adapters + /// themselves, and the host libcalls simply assume that the pointers are + /// valid. + fn compile_transcoder( + &self, + component: &Component, + transcoder: &Transcoder, + types: &ComponentTypes, + ) -> Result>; + /// Emits the `lowerings` and `trampolines` specified into the in-progress /// ELF object specified by `obj`. /// @@ -73,11 +92,13 @@ pub trait ComponentCompiler: Send + Sync { &self, lowerings: PrimaryMap>, always_trap: PrimaryMap>, + transcoders: PrimaryMap>, tramplines: Vec<(SignatureIndex, Box)>, obj: &mut Object<'static>, ) -> Result<( PrimaryMap, PrimaryMap, + PrimaryMap, Vec, )>; } diff --git a/crates/environ/src/component/dfg.rs b/crates/environ/src/component/dfg.rs index 7aa35d603d13..ae986b55a2cd 100644 --- a/crates/environ/src/component/dfg.rs +++ b/crates/environ/src/component/dfg.rs @@ -71,6 +71,9 @@ pub struct ComponentDfg { /// out of the inlining pass of translation. pub adapters: Intern, + /// Metadata about string transcoders needed by adapter modules. + pub transcoders: Intern, + /// Metadata about all known core wasm instances created. /// /// This is mostly an ordered list and is not deduplicated based on contents @@ -125,6 +128,7 @@ id! { pub struct PostReturnId(u32); pub struct AlwaysTrapId(u32); pub struct AdapterModuleId(u32); + pub struct TranscoderId(u32); } /// Same as `info::InstantiateModule` @@ -158,6 +162,7 @@ pub enum CoreDef { Lowered(LowerImportId), AlwaysTrap(AlwaysTrapId), InstanceFlags(RuntimeComponentInstanceIndex), + Transcoder(TranscoderId), /// This is a special variant not present in `info::CoreDef` which /// represents that this definition refers to a fused adapter function. This @@ -220,6 +225,18 @@ pub struct CanonicalOptions { pub post_return: Option, } +/// Same as `info::Transcoder` +#[derive(Clone, Hash, Eq, PartialEq)] +#[allow(missing_docs)] +pub struct Transcoder { + pub op: Transcode, + pub from: MemoryId, + pub from64: bool, + pub to: MemoryId, + pub to64: bool, + pub signature: SignatureIndex, +} + /// A helper structure to "intern" and deduplicate values of type `V` with an /// identifying key `K`. /// @@ -292,6 +309,7 @@ impl ComponentDfg { runtime_instances: Default::default(), runtime_always_trap: Default::default(), runtime_lowerings: Default::default(), + runtime_transcoders: Default::default(), }; // First the instances are all processed for instantiation. This will, @@ -324,6 +342,7 @@ impl ComponentDfg { num_runtime_instances: linearize.runtime_instances.len() as u32, num_always_trap: linearize.runtime_always_trap.len() as u32, num_lowerings: linearize.runtime_lowerings.len() as u32, + num_transcoders: linearize.runtime_transcoders.len() as u32, imports: self.imports, import_types: self.import_types, @@ -342,6 +361,7 @@ struct LinearizeDfg<'a> { runtime_instances: HashMap, runtime_always_trap: HashMap, runtime_lowerings: HashMap, + runtime_transcoders: HashMap, } #[derive(Copy, Clone, Hash, Eq, PartialEq)] @@ -460,6 +480,7 @@ impl LinearizeDfg<'_> { CoreDef::Lowered(id) => info::CoreDef::Lowered(self.runtime_lowering(*id)), CoreDef::InstanceFlags(i) => info::CoreDef::InstanceFlags(*i), CoreDef::Adapter(id) => info::CoreDef::Export(self.adapter(*id)), + CoreDef::Transcoder(id) => info::CoreDef::Transcoder(self.runtime_transcoder(*id)), } } @@ -497,6 +518,35 @@ impl LinearizeDfg<'_> { ) } + fn runtime_transcoder(&mut self, id: TranscoderId) -> RuntimeTranscoderIndex { + self.intern( + id, + |me| &mut me.runtime_transcoders, + |me, id| { + let info = &me.dfg.transcoders[id]; + ( + info.op, + me.runtime_memory(info.from), + info.from64, + me.runtime_memory(info.to), + info.to64, + info.signature, + ) + }, + |index, (op, from, from64, to, to64, signature)| { + GlobalInitializer::Transcoder(info::Transcoder { + index, + op, + from, + from64, + to, + to64, + signature, + }) + }, + ) + } + fn core_export(&mut self, export: &CoreExport) -> info::CoreExport where T: Clone, diff --git a/crates/environ/src/component/info.rs b/crates/environ/src/component/info.rs index 63f8545b825a..d0c32bd0f1a8 100644 --- a/crates/environ/src/component/info.rs +++ b/crates/environ/src/component/info.rs @@ -147,6 +147,10 @@ pub struct Component { /// The number of functions which "always trap" used to implement /// `canon.lower` of `canon.lift`'d functions within the same component. pub num_always_trap: u32, + + /// The number of host transcoder functions needed for strings in adapter + /// modules. + pub num_transcoders: u32, } /// GlobalInitializer instructions to get processed when instantiating a component @@ -207,6 +211,11 @@ pub enum GlobalInitializer { /// Same as `SaveModuleUpvar`, but for imports. SaveModuleImport(RuntimeImportIndex), + + /// Similar to `ExtractMemory` and friends and indicates that a + /// `VMCallerCheckedAnyfunc` needs to be initialized for a transcoder + /// function and this will later be used to instantiate an adapter module. + Transcoder(Transcoder), } /// Metadata for extraction of a memory of what's being extracted and where it's @@ -316,6 +325,9 @@ pub enum CoreDef { /// This is a reference to a wasm global which represents the /// runtime-managed flags for a wasm instance. InstanceFlags(RuntimeComponentInstanceIndex), + /// This refers to a cranelift-generated trampoline which calls to a + /// host-defined transcoding function. + Transcoder(RuntimeTranscoderIndex), } impl From> for CoreDef @@ -433,3 +445,42 @@ pub enum StringEncoding { Utf16, CompactUtf16, } + +/// Information about a string transcoding function required by an adapter +/// module. +/// +/// A transcoder is used when strings are passed between adapter modules, +/// optionally changing string encodings at the same time. The transcoder is +/// implemented in a few different layers: +/// +/// * Each generated adapter module has some glue around invoking the transcoder +/// represented by this item. This involves bounds-checks and handling +/// `realloc` for example. +/// * Each transcoder gets a cranelift-generated trampoline which has the +/// appropriate signature for the adapter module in question. Existence of +/// this initializer indicates that this should be compiled by Cranelift. +/// * The cranelift-generated trampoline will invoke a "transcoder libcall" +/// which is implemented natively in Rust that has a signature independent of +/// memory64 configuration options for example. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct Transcoder { + /// The index of the transcoder being defined and initialized. + /// + /// This indicates which `VMCallerCheckedAnyfunc` slot is written to in a + /// `VMComponentContext`. + pub index: RuntimeTranscoderIndex, + /// The transcoding operation being performed. + pub op: Transcode, + /// The linear memory that the string is being read from. + pub from: RuntimeMemoryIndex, + /// Whether or not the source linear memory is 64-bit or not. + pub from64: bool, + /// The linear memory that the string is being written to. + pub to: RuntimeMemoryIndex, + /// Whether or not the destination linear memory is 64-bit or not. + pub to64: bool, + /// The wasm signature of the cranelift-generated trampoline. + pub signature: SignatureIndex, +} + +pub use crate::fact::{FixedEncoding, Transcode}; diff --git a/crates/environ/src/component/translate/adapt.rs b/crates/environ/src/component/translate/adapt.rs index 7289bb555881..1b8c6c6830be 100644 --- a/crates/environ/src/component/translate/adapt.rs +++ b/crates/environ/src/component/translate/adapt.rs @@ -116,7 +116,8 @@ //! created. use crate::component::translate::*; -use crate::fact::Module; +use crate::fact; +use crate::EntityType; use std::collections::HashSet; use wasmparser::WasmFeatures; @@ -183,7 +184,7 @@ impl<'data> Translator<'_, 'data> { // the module using standard core wasm translation, and then fills out // the dfg metadata for each adapter. for (module_id, adapter_module) in state.adapter_modules.iter() { - let mut module = Module::new( + let mut module = fact::Module::new( self.types.component_types(), self.tunables.debug_adapter_modules, ); @@ -194,7 +195,7 @@ impl<'data> Translator<'_, 'data> { names.push(name); } let wasm = module.encode(); - let args = module.imports().to_vec(); + let imports = module.imports().to_vec(); // Extend the lifetime of the owned `wasm: Vec` on the stack to // a higher scope defined by our original caller. That allows to @@ -240,6 +241,12 @@ impl<'data> Translator<'_, 'data> { // module is also recorded in the dfg. This metadata will be used // to generate `GlobalInitializer` entries during the linearization // final phase. + assert_eq!(imports.len(), translation.module.imports().len()); + let args = imports + .iter() + .zip(translation.module.imports()) + .map(|(arg, (_, _, ty))| fact_import_to_core_def(component, arg, ty)) + .collect::>(); let static_index = self.static_modules.push(translation); let id = component.adapter_modules.push((static_index, args.into())); assert_eq!(id, module_id); @@ -247,6 +254,47 @@ impl<'data> Translator<'_, 'data> { } } +fn fact_import_to_core_def( + dfg: &mut dfg::ComponentDfg, + import: &fact::Import, + ty: EntityType, +) -> dfg::CoreDef { + match import { + fact::Import::CoreDef(def) => def.clone(), + fact::Import::Transcode { + op, + from, + from64, + to, + to64, + } => { + fn unwrap_memory(def: &dfg::CoreDef) -> dfg::CoreExport { + match def { + dfg::CoreDef::Export(e) => e.clone().map_index(|i| match i { + EntityIndex::Memory(i) => i, + _ => unreachable!(), + }), + _ => unreachable!(), + } + } + + let from = dfg.memories.push_uniq(unwrap_memory(from)); + let to = dfg.memories.push_uniq(unwrap_memory(to)); + dfg::CoreDef::Transcoder(dfg.transcoders.push_uniq(dfg::Transcoder { + op: *op, + from, + from64: *from64, + to, + to64: *to64, + signature: match ty { + EntityType::Function(signature) => signature, + _ => unreachable!(), + }, + })) + } + } +} + #[derive(Default)] struct PartitionAdapterModules { /// The next adapter module that's being created. This may be empty. @@ -336,6 +384,9 @@ impl PartitionAdapterModules { dfg::CoreDef::Lowered(_) | dfg::CoreDef::AlwaysTrap(_) | dfg::CoreDef::InstanceFlags(_) => {} + + // should not be in the dfg yet + dfg::CoreDef::Transcoder(_) => unreachable!(), } } diff --git a/crates/environ/src/component/types.rs b/crates/environ/src/component/types.rs index 3f1ade9d7748..da13b7de5c19 100644 --- a/crates/environ/src/component/types.rs +++ b/crates/environ/src/component/types.rs @@ -166,6 +166,13 @@ indices! { /// Index that represents an exported module from a component since that's /// currently the only use for saving the entire module state at runtime. pub struct RuntimeModuleIndex(u32); + + /// Index into the list of transcoders identified during compilation. + /// + /// This is used to index the `VMCallerCheckedAnyfunc` slots reserved for + /// string encoders which reference linear memories defined within a + /// component. + pub struct RuntimeTranscoderIndex(u32); } // Reexport for convenience some core-wasm indices which are also used in the diff --git a/crates/environ/src/component/vmcomponent_offsets.rs b/crates/environ/src/component/vmcomponent_offsets.rs index 649aae50f9a4..8d5a1388673d 100644 --- a/crates/environ/src/component/vmcomponent_offsets.rs +++ b/crates/environ/src/component/vmcomponent_offsets.rs @@ -2,11 +2,13 @@ // // struct VMComponentContext { // magic: u32, +// transcode_libcalls: &'static VMBuiltinTranscodeArray, // store: *mut dyn Store, // limits: *const VMRuntimeLimits, // flags: [VMGlobalDefinition; component.num_runtime_component_instances], // lowering_anyfuncs: [VMCallerCheckedAnyfunc; component.num_lowerings], // always_trap_anyfuncs: [VMCallerCheckedAnyfunc; component.num_always_trap], +// transcoder_anyfuncs: [VMCallerCheckedAnyfunc; component.num_transcoders], // lowerings: [VMLowering; component.num_lowerings], // memories: [*mut VMMemoryDefinition; component.num_memories], // reallocs: [*mut VMCallerCheckedAnyfunc; component.num_reallocs], @@ -15,7 +17,7 @@ use crate::component::{ Component, LoweredIndex, RuntimeAlwaysTrapIndex, RuntimeComponentInstanceIndex, - RuntimeMemoryIndex, RuntimePostReturnIndex, RuntimeReallocIndex, + RuntimeMemoryIndex, RuntimePostReturnIndex, RuntimeReallocIndex, RuntimeTranscoderIndex, }; use crate::PtrSize; @@ -57,14 +59,18 @@ pub struct VMComponentOffsets

{ /// Number of "always trap" functions which have their /// `VMCallerCheckedAnyfunc` stored inline in the `VMComponentContext`. pub num_always_trap: u32, + /// Number of transcoders needed for string conversion. + pub num_transcoders: u32, // precalculated offsets of various member fields magic: u32, + transcode_libcalls: u32, store: u32, limits: u32, flags: u32, lowering_anyfuncs: u32, always_trap_anyfuncs: u32, + transcoder_anyfuncs: u32, lowerings: u32, memories: u32, reallocs: u32, @@ -93,12 +99,15 @@ impl VMComponentOffsets

{ .try_into() .unwrap(), num_always_trap: component.num_always_trap, + num_transcoders: component.num_transcoders, magic: 0, + transcode_libcalls: 0, store: 0, limits: 0, flags: 0, lowering_anyfuncs: 0, always_trap_anyfuncs: 0, + transcoder_anyfuncs: 0, lowerings: 0, memories: 0, reallocs: 0, @@ -133,6 +142,7 @@ impl VMComponentOffsets

{ fields! { size(magic) = 4u32, align(u32::from(ret.ptr.size())), + size(transcode_libcalls) = ret.ptr.size(), size(store) = cmul(2, ret.ptr.size()), size(limits) = ret.ptr.size(), align(16), @@ -140,6 +150,7 @@ impl VMComponentOffsets

{ align(u32::from(ret.ptr.size())), size(lowering_anyfuncs) = cmul(ret.num_lowerings, ret.ptr.size_of_vmcaller_checked_anyfunc()), size(always_trap_anyfuncs) = cmul(ret.num_always_trap, ret.ptr.size_of_vmcaller_checked_anyfunc()), + size(transcoder_anyfuncs) = cmul(ret.num_transcoders, ret.ptr.size_of_vmcaller_checked_anyfunc()), size(lowerings) = cmul(ret.num_lowerings, ret.ptr.size() * 2), size(memories) = cmul(ret.num_runtime_memories, ret.ptr.size()), size(reallocs) = cmul(ret.num_runtime_reallocs, ret.ptr.size()), @@ -168,6 +179,12 @@ impl VMComponentOffsets

{ self.magic } + /// The offset of the `transcode_libcalls` field. + #[inline] + pub fn transcode_libcalls(&self) -> u32 { + self.transcode_libcalls + } + /// The offset of the `flags` field. #[inline] pub fn instance_flags(&self, index: RuntimeComponentInstanceIndex) -> u32 { @@ -215,6 +232,20 @@ impl VMComponentOffsets

{ + index.as_u32() * u32::from(self.ptr.size_of_vmcaller_checked_anyfunc()) } + /// The offset of the `transcoder_anyfuncs` field. + #[inline] + pub fn transcoder_anyfuncs(&self) -> u32 { + self.transcoder_anyfuncs + } + + /// The offset of `VMCallerCheckedAnyfunc` for the `index` specified. + #[inline] + pub fn transcoder_anyfunc(&self, index: RuntimeTranscoderIndex) -> u32 { + assert!(index.as_u32() < self.num_transcoders); + self.transcoder_anyfuncs() + + index.as_u32() * u32::from(self.ptr.size_of_vmcaller_checked_anyfunc()) + } + /// The offset of the `lowerings` field. #[inline] pub fn lowerings(&self) -> u32 { diff --git a/crates/environ/src/fact.rs b/crates/environ/src/fact.rs index 2c9f7b73dd80..3cb16a68c800 100644 --- a/crates/environ/src/fact.rs +++ b/crates/environ/src/fact.rs @@ -20,7 +20,7 @@ use crate::component::dfg::CoreDef; use crate::component::{Adapter, AdapterOptions, ComponentTypes, StringEncoding, TypeFuncIndex}; -use crate::{FuncIndex, GlobalIndex, MemoryIndex}; +use crate::{FuncIndex, GlobalIndex, MemoryIndex, PrimaryMap}; use std::collections::HashMap; use std::mem; use wasm_encoder::*; @@ -28,8 +28,11 @@ use wasm_encoder::*; mod core_types; mod signature; mod trampoline; +mod transcode; mod traps; +pub use self::transcode::{FixedEncoding, Transcode}; + /// Representation of an adapter module. pub struct Module<'a> { /// Whether or not debug code is inserted into the adapters themselves. @@ -46,9 +49,10 @@ pub struct Module<'a> { core_imports: ImportSection, /// Final list of imports that this module ended up using, in the same order /// as the imports in the import section. - imports: Vec, + imports: Vec, /// Intern'd imports and what index they were assigned. imported: HashMap, + imported_memories: PrimaryMap, // Current status of index spaces from the imports generated so far. core_funcs: u32, @@ -100,6 +104,7 @@ impl<'a> Module<'a> { imported: Default::default(), adapters: Default::default(), imports: Default::default(), + imported_memories: PrimaryMap::new(), core_funcs: 0, core_memories: 0, core_globals: 0, @@ -246,19 +251,50 @@ impl<'a> Module<'a> { let ret = *cnt - 1; self.core_imports.import(module, name, ty); self.imported.insert(def.clone(), ret); - self.imports.push(def); + if let EntityType::Memory(_) = ty { + self.imported_memories.push(def.clone()); + } + self.imports.push(Import::CoreDef(def)); ret } /// Encodes this module into a WebAssembly binary. pub fn encode(&mut self) -> Vec { + let mut types = mem::take(&mut self.core_types); + let mut transcoders = transcode::Transcoders::new(self.core_funcs); + let mut adapter_funcs = Vec::new(); + for adapter in self.adapters.iter() { + adapter_funcs.push(trampoline::compile( + self, + &mut types, + &mut transcoders, + adapter, + )); + } + + // If any string transcoding imports were needed add imported items + // associated with them. + for (module, name, ty, transcoder) in transcoders.imports() { + self.core_imports.import(module, name, ty); + let from = self.imported_memories[transcoder.from_memory].clone(); + let to = self.imported_memories[transcoder.to_memory].clone(); + self.imports.push(Import::Transcode { + op: transcoder.op, + from, + from64: transcoder.from_memory64, + to, + to64: transcoder.to_memory64, + }); + self.core_funcs += 1; + } + + // Now that all functions are known as well as all imports the actual + // bodies of all adapters are assembled into a final module. let mut funcs = FunctionSection::new(); let mut code = CodeSection::new(); let mut exports = ExportSection::new(); let mut traps = traps::TrapSection::default(); - - let mut types = mem::take(&mut self.core_types); - for adapter in self.adapters.iter() { + for (adapter, (function, func_traps)) in self.adapters.iter().zip(adapter_funcs) { let idx = self.core_funcs + funcs.len(); exports.export(&adapter.name, ExportKind::Func, idx); @@ -266,7 +302,6 @@ impl<'a> Module<'a> { let ty = types.function(&signature.params, &signature.results); funcs.function(ty); - let (function, func_traps) = trampoline::compile(self, &mut types, adapter); code.raw(&function); traps.append(idx, func_traps); } @@ -290,11 +325,31 @@ impl<'a> Module<'a> { /// Returns the imports that were used, in order, to create this adapter /// module. - pub fn imports(&self) -> &[CoreDef] { + pub fn imports(&self) -> &[Import] { &self.imports } } +/// Possible imports into an adapter module. +#[derive(Clone)] +pub enum Import { + /// A definition required in the configuration of an `Adapter`. + CoreDef(CoreDef), + /// A transcoding function from the host to convert between string encodings. + Transcode { + /// The transcoding operation this performs. + op: Transcode, + /// The memory being read + from: CoreDef, + /// Whether or not `from` is a 64-bit memory + from64: bool, + /// The memory being written + to: CoreDef, + /// Whether or not `to` is a 64-bit memory + to64: bool, + }, +} + impl Options { fn ptr(&self) -> ValType { if self.memory64 { diff --git a/crates/environ/src/fact/trampoline.rs b/crates/environ/src/fact/trampoline.rs index 16e528dd1b4f..b5337229a2f8 100644 --- a/crates/environ/src/fact/trampoline.rs +++ b/crates/environ/src/fact/trampoline.rs @@ -16,12 +16,13 @@ //! can be somewhat arbitrary, an intentional decision. use crate::component::{ - InterfaceType, TypeEnumIndex, TypeExpectedIndex, TypeFlagsIndex, TypeInterfaceIndex, - TypeRecordIndex, TypeTupleIndex, TypeUnionIndex, TypeVariantIndex, FLAG_MAY_ENTER, - FLAG_MAY_LEAVE, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, + InterfaceType, StringEncoding, TypeEnumIndex, TypeExpectedIndex, TypeFlagsIndex, + TypeInterfaceIndex, TypeRecordIndex, TypeTupleIndex, TypeUnionIndex, TypeVariantIndex, + FLAG_MAY_ENTER, FLAG_MAY_LEAVE, MAX_FLAT_PARAMS, MAX_FLAT_RESULTS, }; use crate::fact::core_types::CoreTypes; use crate::fact::signature::{align_to, Signature}; +use crate::fact::transcode::{FixedEncoding as FE, Transcode, Transcoder, Transcoders}; use crate::fact::traps::Trap; use crate::fact::{AdapterData, Context, Module, Options}; use crate::GlobalIndex; @@ -31,6 +32,9 @@ use std::ops::Range; use wasm_encoder::{BlockType, Encode, Instruction, Instruction::*, MemArg, ValType}; use wasmtime_component_util::{DiscriminantSize, FlagsSize}; +const MAX_STRING_BYTE_LENGTH: u32 = 1 << 31; +const UTF16_TAG: u32 = 1 << 31; + struct Compiler<'a, 'b> { /// The module that the adapter will eventually be inserted into. module: &'a Module<'a>, @@ -38,6 +42,9 @@ struct Compiler<'a, 'b> { /// The type section of `module` types: &'b mut CoreTypes, + /// Imported functions to transcode between various string encodings. + transcoders: &'b mut Transcoders, + /// Metadata about the adapter that is being compiled. adapter: &'a AdapterData, @@ -71,6 +78,7 @@ struct Compiler<'a, 'b> { pub(super) fn compile( module: &Module<'_>, types: &mut CoreTypes, + transcoders: &mut Transcoders, adapter: &AdapterData, ) -> (Vec, Vec<(usize, Trap)>) { let lower_sig = &module.signature(&adapter.lower, Context::Lower); @@ -79,6 +87,7 @@ pub(super) fn compile( module, types, adapter, + transcoders, code: Vec::new(), locals: Vec::new(), nlocals: lower_sig.params.len() as u32, @@ -356,6 +365,7 @@ impl Compiler<'_, '_> { InterfaceType::Float32 => self.translate_f32(src, dst_ty, dst), InterfaceType::Float64 => self.translate_f64(src, dst_ty, dst), InterfaceType::Char => self.translate_char(src, dst_ty, dst), + InterfaceType::String => self.translate_string(src, dst_ty, dst), InterfaceType::List(t) => self.translate_list(*t, src, dst_ty, dst), InterfaceType::Record(t) => self.translate_record(*t, src, dst_ty, dst), InterfaceType::Flags(f) => self.translate_flags(*f, src, dst_ty, dst), @@ -365,13 +375,6 @@ impl Compiler<'_, '_> { InterfaceType::Enum(t) => self.translate_enum(*t, src, dst_ty, dst), InterfaceType::Option(t) => self.translate_option(*t, src, dst_ty, dst), InterfaceType::Expected(t) => self.translate_expected(*t, src, dst_ty, dst), - - InterfaceType::String => { - // consider this field used for now until this is fully - // implemented. - drop(&self.adapter.lift.string_encoding); - unimplemented!("don't know how to translate strings") - } } } @@ -636,6 +639,768 @@ impl Compiler<'_, '_> { } } + fn translate_string(&mut self, src: &Source<'_>, dst_ty: &InterfaceType, dst: &Destination) { + assert!(matches!(dst_ty, InterfaceType::String)); + let src_opts = src.opts(); + let dst_opts = dst.opts(); + + // Load the pointer/length of this string into temporary locals. These + // will be referenced a good deal so this just makes it easier to deal + // with them consistently below rather than trying to reload from memory + // for example. + let src_ptr = self.gen_local(src_opts.ptr()); + let src_len = self.gen_local(src_opts.ptr()); + match src { + Source::Stack(s) => { + assert_eq!(s.locals.len(), 2); + self.stack_get(&s.slice(0..1), src_opts.ptr()); + self.instruction(LocalSet(src_ptr)); + self.stack_get(&s.slice(1..2), src_opts.ptr()); + self.instruction(LocalSet(src_len)); + } + Source::Memory(mem) => { + self.ptr_load(mem); + self.instruction(LocalSet(src_ptr)); + self.ptr_load(&mem.bump(src_opts.ptr_size().into())); + self.instruction(LocalSet(src_len)); + } + } + let src_str = &WasmString { + ptr: src_ptr, + len: src_len, + opts: src_opts, + }; + + let dst_str = match src_opts.string_encoding { + StringEncoding::Utf8 => match dst_opts.string_encoding { + StringEncoding::Utf8 => self.string_copy(src_str, FE::Utf8, dst_opts, FE::Utf8), + StringEncoding::Utf16 => self.string_utf8_to_utf16(src_str, dst_opts), + StringEncoding::CompactUtf16 => self.string_to_compact(src_str, FE::Utf8, dst_opts), + }, + + StringEncoding::Utf16 => { + self.verify_aligned(src_opts, src_ptr, 2); + match dst_opts.string_encoding { + StringEncoding::Utf8 => { + self.string_deflate_to_utf8(src_str, FE::Utf16, dst_opts) + } + StringEncoding::Utf16 => { + self.string_copy(src_str, FE::Utf16, dst_opts, FE::Utf16) + } + StringEncoding::CompactUtf16 => { + self.string_to_compact(src_str, FE::Utf16, dst_opts) + } + } + } + + StringEncoding::CompactUtf16 => { + self.verify_aligned(src_opts, src_ptr, 2); + + // Test the tag big to see if this is a utf16 or a latin1 string + // at runtime... + self.instruction(LocalGet(src_len)); + self.ptr_uconst(src_opts, UTF16_TAG); + self.ptr_and(src_opts); + self.ptr_if(src_opts, BlockType::Empty); + + // In the utf16 block unset the upper bit from the length local + // so further calculations have the right value. Afterwards the + // string transcode proceeds assuming utf16. + self.instruction(LocalGet(src_len)); + self.ptr_uconst(src_opts, UTF16_TAG); + self.ptr_xor(src_opts); + self.instruction(LocalSet(src_len)); + let s1 = match dst_opts.string_encoding { + StringEncoding::Utf8 => { + self.string_deflate_to_utf8(src_str, FE::Utf16, dst_opts) + } + StringEncoding::Utf16 => { + self.string_copy(src_str, FE::Utf16, dst_opts, FE::Utf16) + } + StringEncoding::CompactUtf16 => { + self.string_compact_utf16_to_compact(src_str, dst_opts) + } + }; + + self.instruction(Else); + + // In the latin1 block the `src_len` local is already the number + // of code units, so the string transcoding is all that needs to + // happen. + let s2 = match dst_opts.string_encoding { + StringEncoding::Utf16 => { + self.string_copy(src_str, FE::Latin1, dst_opts, FE::Utf16) + } + StringEncoding::Utf8 => { + self.string_deflate_to_utf8(src_str, FE::Latin1, dst_opts) + } + StringEncoding::CompactUtf16 => { + self.string_copy(src_str, FE::Latin1, dst_opts, FE::Latin1) + } + }; + // Set our `s2` generated locals to the `s2` generated locals + // as the resulting pointer of this transcode. + self.instruction(LocalGet(s2.ptr)); + self.instruction(LocalSet(s1.ptr)); + self.instruction(LocalGet(s2.len)); + self.instruction(LocalSet(s1.len)); + self.instruction(End); + s1 + } + }; + + // Store the ptr/length in the desired destination + match dst { + Destination::Stack(s, _) => { + self.instruction(LocalGet(dst_str.ptr)); + self.stack_set(&s[..1], dst_opts.ptr()); + self.instruction(LocalGet(dst_str.len)); + self.stack_set(&s[1..], dst_opts.ptr()); + } + Destination::Memory(mem) => { + self.instruction(LocalGet(mem.addr_local)); + self.instruction(LocalGet(dst_str.ptr)); + self.ptr_store(mem); + self.instruction(LocalGet(mem.addr_local)); + self.instruction(LocalGet(dst_str.len)); + self.ptr_store(&mem.bump(dst_opts.ptr_size().into())); + } + } + } + + // Corresponding function for `store_string_copy` in the spec. + // + // This performs a transcoding of the string with a one-pass copy from + // the `src` encoding to the `dst` encoding. This is only possible for + // fixed encodings where the first allocation is guaranteed to be an + // appropriate fit so it's not suitable for all encodings. + // + // Imported host transcoding functions here take the src/dst pointers as + // well as the number of code units in the source (which always matches + // the number of code units in the destination). There is no return + // value from the transcode function since the encoding should always + // work on the first pass. + fn string_copy<'a>( + &mut self, + src: &WasmString<'_>, + src_enc: FE, + dst_opts: &'a Options, + dst_enc: FE, + ) -> WasmString<'a> { + assert!(dst_enc.width() >= src_enc.width()); + self.validate_string_length(src, dst_enc); + + // Calculate the source byte length given the size of each code + // unit. Note that this shouldn't overflow given + // `validate_string_length` above. + let src_byte_len = if src_enc.width() == 1 { + src.len + } else { + assert_eq!(src_enc.width(), 2); + let tmp = self.gen_local(src.opts.ptr()); + self.instruction(LocalGet(src.len)); + self.ptr_uconst(src.opts, 1); + self.ptr_shl(src.opts); + self.instruction(LocalSet(tmp)); + tmp + }; + + // Convert the source code units length to the destination byte + // length type. + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + let dst_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalTee(dst_len)); + if dst_enc.width() > 1 { + assert_eq!(dst_enc.width(), 2); + self.ptr_uconst(dst_opts, 1); + self.ptr_shl(dst_opts); + } + let dst_byte_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalSet(dst_byte_len)); + + // Allocate space in the destination using the calculated byte + // length. + let dst = { + let dst_mem = self.malloc( + dst_opts, + MallocSize::Local(dst_byte_len), + dst_enc.width().into(), + ); + WasmString { + ptr: dst_mem.addr_local, + len: dst_len, + opts: dst_opts, + } + }; + + // Validate that `src_len + src_ptr` and + // `dst_mem.addr_local + dst_byte_len` are both in-bounds. This + // is done by loading the last byte of the string and if that + // doesn't trap then it's known valid. + self.validate_string_inbounds(src, src_byte_len); + self.validate_string_inbounds(&dst, dst_byte_len); + + // If the validations pass then the host `transcode` intrinsic + // is invoked. This will either raise a trap or otherwise succeed + // in which case we're done. + let op = if src_enc == dst_enc { + Transcode::Copy(src_enc) + } else { + assert_eq!(src_enc, FE::Latin1); + assert_eq!(dst_enc, FE::Utf16); + Transcode::Latin1ToUtf16 + }; + let transcode = self.transcoder(src, &dst, op); + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(Call(transcode)); + + dst + } + // Corresponding function for `store_string_to_utf8` in the spec. + // + // This translation works by possibly performing a number of + // reallocations. First a buffer of size input-code-units is used to try + // to get the transcoding correct on the first try. If that fails the + // maximum worst-case size is used and then that is resized down if it's + // too large. + // + // The host transcoding function imported here will receive src ptr/len + // and dst ptr/len and return how many code units were consumed on both + // sides. The amount of code units consumed in the source dictates which + // branches are taken in this conversion. + fn string_deflate_to_utf8<'a>( + &mut self, + src: &WasmString<'_>, + src_enc: FE, + dst_opts: &'a Options, + ) -> WasmString<'a> { + self.validate_string_length(src, src_enc); + + // Optimistically assume that the code unit length of the source is + // all that's needed in the destination. Perform that allocaiton + // here and proceed to transcoding below. + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + let dst_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalTee(dst_len)); + let dst_byte_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalSet(dst_byte_len)); + + let dst = { + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len), 1); + WasmString { + ptr: dst_mem.addr_local, + len: dst_len, + opts: dst_opts, + } + }; + + // Ensure buffers are all in-bounds + let src_byte_len = match src_enc { + FE::Latin1 => src.len, + FE::Utf16 => { + let tmp = self.gen_local(src.opts.ptr()); + self.instruction(LocalGet(src.len)); + self.ptr_uconst(src.opts, 1); + self.ptr_shl(src.opts); + self.instruction(LocalSet(tmp)); + tmp + } + FE::Utf8 => unreachable!(), + }; + self.validate_string_inbounds(src, src_byte_len); + self.validate_string_inbounds(&dst, dst_byte_len); + + // Perform the initial transcode + let op = match src_enc { + FE::Latin1 => Transcode::Latin1ToUtf8, + FE::Utf16 => Transcode::Utf16ToUtf8, + FE::Utf8 => unreachable!(), + }; + let transcode = self.transcoder(src, &dst, op); + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(LocalGet(dst_byte_len)); + self.instruction(Call(transcode)); + self.instruction(LocalSet(dst.len)); + let src_len_tmp = self.gen_local(src.opts.ptr()); + self.instruction(LocalSet(src_len_tmp)); + + // Test if the source was entirely transcoded by comparing + // `src_len_tmp`, the number of code units transcoded from the + // source, with `src_len`, the original number of code units. + self.instruction(LocalGet(src_len_tmp)); + self.instruction(LocalGet(src.len)); + self.ptr_ne(src.opts); + self.instruction(If(BlockType::Empty)); + + // Here a worst-case reallocation is performed to grow `dst_mem`. + // In-line a check is also performed that the worst-case byte size + // fits within the maximum size of strings. + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 1); // align + let factor = match src_enc { + FE::Latin1 => 2, + FE::Utf16 => 3, + _ => unreachable!(), + }; + self.validate_string_length_u8(src, factor); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + self.ptr_uconst(dst_opts, factor.into()); + self.ptr_mul(dst_opts); + self.instruction(LocalTee(dst_byte_len)); + self.instruction(Call(dst_opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + + // Verify that the destination is still in-bounds + self.validate_string_inbounds(&dst, dst_byte_len); + + // Perform another round of transcoding that should be guaranteed + // to succeed. Note that all the parameters here are offset by the + // results of the first transcoding to only perform the remaining + // transcode on the final units. + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src_len_tmp)); + if let FE::Utf16 = src_enc { + self.ptr_uconst(src.opts, 1); + self.ptr_shl(src.opts); + } + self.ptr_add(src.opts); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(src_len_tmp)); + self.ptr_sub(src.opts); + self.instruction(LocalGet(dst.ptr)); + self.instruction(LocalGet(dst.len)); + self.ptr_add(dst.opts); + self.instruction(LocalGet(dst_byte_len)); + self.instruction(LocalGet(dst.len)); + self.ptr_sub(dst.opts); + self.instruction(Call(transcode)); + + // Add the second result, the amount of destination units encoded, + // to `dst_len` so it's an accurate reflection of the final size of + // the destination buffer. + self.instruction(LocalGet(dst.len)); + self.ptr_add(dst.opts); + self.instruction(LocalSet(dst.len)); + + // In debug mode verify the first result consumed the entire string, + // otherwise simply discard it. + if self.module.debug { + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(src_len_tmp)); + self.ptr_sub(src.opts); + self.ptr_ne(src.opts); + self.instruction(If(BlockType::Empty)); + self.trap(Trap::AssertFailed("should have finished encoding")); + self.instruction(End); + } else { + self.instruction(Drop); + } + + // Perform a downsizing if the worst-case size was too large + self.instruction(LocalGet(dst.len)); + self.instruction(LocalGet(dst_byte_len)); + self.ptr_ne(dst.opts); + self.instruction(If(BlockType::Empty)); + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 1); // align + self.instruction(LocalGet(dst.len)); // new_size + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + self.instruction(End); + + // If the first transcode was enough then assert that the returned + // amount of destination items written equals the byte size. + if self.module.debug { + self.instruction(Else); + + self.instruction(LocalGet(dst.len)); + self.instruction(LocalGet(dst_byte_len)); + self.ptr_ne(dst_opts); + self.instruction(If(BlockType::Empty)); + self.trap(Trap::AssertFailed("should have finished encoding")); + self.instruction(End); + } + + self.instruction(End); // end of "first transcode not enough" + + dst + } + + // Corresponds to the `store_utf8_to_utf16` function in the spec. + // + // When converting utf-8 to utf-16 a pessimistic allocation is + // done which is twice the byte length of the utf-8 string. + // The host then transcodes and returns how many code units were + // actually used during the transcoding and if it's beneath the + // pessimistic maximum then the buffer is reallocated down to + // a smaller amount. + // + // The host-imported transcoding function takes the src/dst pointer as + // well as the code unit size of both the source and destination. The + // destination should always be big enough to hold the result of the + // transcode and so the result of the host function is how many code + // units were written to the destination. + fn string_utf8_to_utf16<'a>( + &mut self, + src: &WasmString<'_>, + dst_opts: &'a Options, + ) -> WasmString<'a> { + self.validate_string_length(src, FE::Utf16); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + let dst_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalTee(dst_len)); + self.ptr_uconst(dst_opts, 1); + self.ptr_shl(dst_opts); + let dst_byte_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalSet(dst_byte_len)); + let dst = { + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len), 2); + WasmString { + ptr: dst_mem.addr_local, + len: dst_len, + opts: dst_opts, + } + }; + + self.validate_string_inbounds(src, src.len); + self.validate_string_inbounds(&dst, dst_byte_len); + + let transcode = self.transcoder(src, &dst, Transcode::Utf8ToUtf16); + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(Call(transcode)); + self.instruction(LocalSet(dst.len)); + + // If the number of code units returned by transcode is not + // equal to the original number of code units then + // the buffer must be shrunk. + // + // Note that the byte length of the final allocation we + // want is twice the code unit length returned by the + // transcoding function. + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst.opts.ptr()); + self.instruction(LocalGet(dst.len)); + self.ptr_ne(dst_opts); + self.instruction(If(BlockType::Empty)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(LocalGet(dst_byte_len)); + self.ptr_uconst(dst.opts, 2); + self.instruction(LocalGet(dst.len)); + self.ptr_uconst(dst.opts, 1); + self.ptr_shl(dst.opts); + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + self.instruction(End); // end of shrink-to-fit + + dst + } + + // Corresponds to `store_probably_utf16_to_latin1_or_utf16` in the spec. + // + // This will try to transcode the input utf16 string to utf16 in the + // destination. If utf16 isn't needed though and latin1 could be used + // then that's used instead and a reallocation to downsize occurs + // afterwards. + // + // The host transcode function here will take the src/dst pointers as + // well as src length. The destination byte length is twice the src code + // unit length. The return value is the tagged length of the returned + // string. If the upper bit is set then utf16 was used and the + // conversion is done. If the upper bit is not set then latin1 was used + // and a downsizing needs to happen. + fn string_compact_utf16_to_compact<'a>( + &mut self, + src: &WasmString<'_>, + dst_opts: &'a Options, + ) -> WasmString<'a> { + self.validate_string_length(src, FE::Utf16); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + let dst_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalTee(dst_len)); + self.ptr_uconst(dst_opts, 1); + self.ptr_shl(dst_opts); + let dst_byte_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalSet(dst_byte_len)); + let dst = { + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len), 2); + WasmString { + ptr: dst_mem.addr_local, + len: dst_len, + opts: dst_opts, + } + }; + + self.validate_string_inbounds(src, dst_byte_len); + self.validate_string_inbounds(&dst, dst_byte_len); + + let transcode = self.transcoder(src, &dst, Transcode::Utf16ToCompactProbablyUtf16); + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(Call(transcode)); + self.instruction(LocalSet(dst.len)); + + // Assert that the untagged code unit length is the same as the + // source code unit length. + if self.module.debug { + self.instruction(LocalGet(dst.len)); + self.ptr_uconst(dst.opts, !UTF16_TAG); + self.ptr_and(dst.opts); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst.opts.ptr()); + self.ptr_ne(dst.opts); + self.instruction(If(BlockType::Empty)); + self.trap(Trap::AssertFailed("expected equal code units")); + self.instruction(End); + } + + // If the UTF16_TAG is set then utf16 was used and the destination + // should be appropriately sized. Bail out of the "is this string + // empty" block and fall through otherwise to resizing. + self.instruction(LocalGet(dst.len)); + self.ptr_uconst(dst.opts, UTF16_TAG); + self.ptr_and(dst.opts); + self.ptr_br_if(dst.opts, 0); + + // Here `realloc` is used to downsize the string + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 2); // align + self.instruction(LocalGet(dst.len)); // new_size + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + + dst + } + + // Corresponds to `store_string_to_latin1_or_utf16` in the spec. + // + // This will attempt a first pass of transcoding to latin1 and on + // failure a larger buffer is allocated for utf16 and then utf16 is + // encoded in-place into the buffer. After either latin1 or utf16 the + // buffer is then resized to fit the final string allocation. + fn string_to_compact<'a>( + &mut self, + src: &WasmString<'_>, + src_enc: FE, + dst_opts: &'a Options, + ) -> WasmString<'a> { + self.validate_string_length(src, src_enc); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst_opts.ptr()); + let dst_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalTee(dst_len)); + let dst_byte_len = self.gen_local(dst_opts.ptr()); + self.instruction(LocalSet(dst_byte_len)); + let dst = { + let dst_mem = self.malloc(dst_opts, MallocSize::Local(dst_byte_len), 2); + WasmString { + ptr: dst_mem.addr_local, + len: dst_len, + opts: dst_opts, + } + }; + + self.validate_string_inbounds(src, src.len); + self.validate_string_inbounds(&dst, dst_byte_len); + + // Perform the initial latin1 transcode. This returns the number of + // source code units consumed and the number of destination code + // units (bytes) written. + let (latin1, utf16) = match src_enc { + FE::Utf8 => (Transcode::Utf8ToLatin1, Transcode::Utf8ToCompactUtf16), + FE::Utf16 => (Transcode::Utf16ToLatin1, Transcode::Utf16ToCompactUtf16), + FE::Latin1 => unreachable!(), + }; + let transcode_latin1 = self.transcoder(src, &dst, latin1); + let transcode_utf16 = self.transcoder(src, &dst, utf16); + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(dst.ptr)); + self.instruction(Call(transcode_latin1)); + self.instruction(LocalSet(dst.len)); + let src_len_tmp = self.gen_local(src.opts.ptr()); + self.instruction(LocalSet(src_len_tmp)); + + // If the source was entirely consumed then the transcode completed + // and all that's necessary is to optionally shrink the buffer. + self.instruction(LocalGet(src_len_tmp)); + self.instruction(LocalGet(src.len)); + self.ptr_eq(src.opts); + self.instruction(If(BlockType::Empty)); // if latin1-or-utf16 block + + // Test if the original byte length of the allocation is the same as + // the number of written bytes, and if not then shrink the buffer + // with a call to `realloc`. + self.instruction(LocalGet(dst_byte_len)); + self.instruction(LocalGet(dst.len)); + self.ptr_ne(dst.opts); + self.instruction(If(BlockType::Empty)); + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 2); // align + self.instruction(LocalGet(dst.len)); // new_size + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + self.instruction(End); + + // In this block the latin1 encoding failed. The host transcode + // returned how many units were consumed from the source and how + // many bytes were written to the destination. Here the buffer is + // inflated and sized and the second utf16 intrinsic is invoked to + // perform the final inflation. + self.instruction(Else); // else latin1-or-utf16 block + + // For utf8 validate that the inflated size is still within bounds. + if src_enc.width() == 1 { + self.validate_string_length_u8(src, 2); + } + + // Reallocate the buffer with twice the source code units in byte + // size. + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 2); // align + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst.opts.ptr()); + self.ptr_uconst(dst.opts, 1); + self.ptr_shl(dst.opts); + self.instruction(LocalTee(dst_byte_len)); + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + + // Call the host utf16 transcoding function. This will inflate the + // prior latin1 bytes and then encode the rest of the source string + // as utf16 into the remaining space in the destination buffer. + self.instruction(LocalGet(src.ptr)); + self.instruction(LocalGet(src_len_tmp)); + if let FE::Utf16 = src_enc { + self.ptr_uconst(src.opts, 1); + self.ptr_shl(src.opts); + } + self.ptr_add(src.opts); + self.instruction(LocalGet(src.len)); + self.instruction(LocalGet(src_len_tmp)); + self.ptr_sub(src.opts); + self.instruction(LocalGet(dst.ptr)); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst.opts.ptr()); + self.instruction(LocalGet(dst.len)); + self.instruction(Call(transcode_utf16)); + self.instruction(LocalSet(dst.len)); + + // If the returned number of code units written to the destination + // is not equal to the size of the allocation then the allocation is + // resized down to the appropriate size. + // + // Note that the byte size desired is `2*dst_len` and the current + // byte buffer size is `2*src_len` so the `2` factor isn't checked + // here, just the lengths. + self.instruction(LocalGet(dst.len)); + self.convert_src_len_to_dst(src.len, src.opts.ptr(), dst.opts.ptr()); + self.ptr_ne(dst.opts); + self.instruction(If(BlockType::Empty)); + self.instruction(LocalGet(dst.ptr)); // old_ptr + self.instruction(LocalGet(dst_byte_len)); // old_size + self.ptr_uconst(dst.opts, 2); // align + self.instruction(LocalGet(dst.len)); + self.ptr_uconst(dst.opts, 1); + self.ptr_shl(dst.opts); + self.instruction(Call(dst.opts.realloc.unwrap().as_u32())); + self.instruction(LocalSet(dst.ptr)); + self.instruction(End); + + // Tag the returned pointer as utf16 + self.instruction(LocalGet(dst.len)); + self.ptr_uconst(dst.opts, UTF16_TAG); + self.ptr_or(dst.opts); + self.instruction(LocalSet(dst.len)); + + self.instruction(End); // end latin1-or-utf16 block + + dst + } + + fn validate_string_length(&mut self, src: &WasmString<'_>, dst: FE) { + self.validate_string_length_u8(src, dst.width()) + } + + fn validate_string_length_u8(&mut self, s: &WasmString<'_>, dst: u8) { + // Check to see if the source byte length is out of bounds in + // which case a trap is generated. + self.instruction(LocalGet(s.len)); + let max = MAX_STRING_BYTE_LENGTH / u32::from(dst); + self.ptr_uconst(s.opts, max); + self.ptr_ge_u(s.opts); + self.instruction(If(BlockType::Empty)); + self.trap(Trap::StringLengthTooBig); + self.instruction(End); + } + + fn transcoder(&mut self, src: &WasmString<'_>, dst: &WasmString<'_>, op: Transcode) -> u32 { + self.transcoders.import( + self.types, + Transcoder { + from_memory: src.opts.memory.unwrap(), + from_memory64: src.opts.memory64, + to_memory: dst.opts.memory.unwrap(), + to_memory64: dst.opts.memory64, + op, + }, + ) + } + + fn validate_string_inbounds(&mut self, s: &WasmString<'_>, byte_len: u32) { + let extend_to_64 = |me: &mut Self| { + if !s.opts.memory64 { + me.instruction(I64ExtendI32U); + } + }; + + self.instruction(Block(BlockType::Empty)); + self.instruction(Block(BlockType::Empty)); + + // Calculate the full byte size of memory with `memory.size`. Note that + // arithmetic here is done always in 64-bits to accomodate 4G memories. + // Additionally it's assumed that 64-bit memories never fill up + // entirely. + self.instruction(MemorySize(s.opts.memory.unwrap().as_u32())); + extend_to_64(self); + self.instruction(I64Const(16)); + self.instruction(I64Shl); + + // Calculate the end address of the string. This is done by adding the + // base pointer to the byte length. For 32-bit memories there's no need + // to check for overflow since everything is extended to 64-bit, but for + // 64-bit memories overflow is checked. + self.instruction(LocalGet(s.ptr)); + extend_to_64(self); + self.instruction(LocalGet(byte_len)); + extend_to_64(self); + self.instruction(I64Add); + if s.opts.memory64 { + let tmp = self.gen_local(ValType::I64); + self.instruction(LocalTee(tmp)); + self.instruction(LocalGet(s.ptr)); + self.ptr_lt_u(s.opts); + self.ptr_br_if(s.opts, 0); + self.instruction(LocalGet(tmp)); + } + + // If the byte size of memory is greater than the final address of the + // string then the string is invalid. Note that if it's precisely equal + // then that's ok. + self.instruction(I64GtU); + self.instruction(BrIf(1)); + + self.instruction(End); + self.trap(Trap::StringLengthOverflow); + self.instruction(End); + } + fn translate_list( &mut self, src_ty: TypeInterfaceIndex, @@ -1467,17 +2232,17 @@ impl Compiler<'_, '_> { self.instruction(GlobalSet(flags_global.as_u32())); } - fn verify_aligned(&mut self, memory: &Memory, align: usize) { + fn verify_aligned(&mut self, opts: &Options, addr_local: u32, align: usize) { // If the alignment is 1 then everything is trivially aligned and the // check can be omitted. if align == 1 { return; } - self.instruction(LocalGet(memory.addr_local)); + self.instruction(LocalGet(addr_local)); assert!(align.is_power_of_two()); - self.ptr_uconst(memory.opts, u32::try_from(align - 1).unwrap()); - self.ptr_and(memory.opts); - self.ptr_if(memory.opts, BlockType::Empty); + self.ptr_uconst(opts, u32::try_from(align - 1).unwrap()); + self.ptr_and(opts); + self.ptr_if(opts, BlockType::Empty); self.trap(Trap::UnalignedPointer); self.instruction(End); } @@ -1527,7 +2292,7 @@ impl Compiler<'_, '_> { offset: 0, opts, }; - self.verify_aligned(&ret, align); + self.verify_aligned(opts, ret.addr_local, align); ret } @@ -1711,6 +2476,46 @@ impl Compiler<'_, '_> { } } + fn ptr_sub(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Sub); + } else { + self.instruction(I32Sub); + } + } + + fn ptr_mul(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Mul); + } else { + self.instruction(I32Mul); + } + } + + fn ptr_ge_u(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64GeU); + } else { + self.instruction(I32GeU); + } + } + + fn ptr_lt_u(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64LtU); + } else { + self.instruction(I32LtU); + } + } + + fn ptr_shl(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Shl); + } else { + self.instruction(I32Shl); + } + } + fn ptr_eqz(&mut self, opts: &Options) { if opts.memory64 { self.instruction(I64Eqz); @@ -1735,6 +2540,22 @@ impl Compiler<'_, '_> { } } + fn ptr_eq(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Eq); + } else { + self.instruction(I32Eq); + } + } + + fn ptr_ne(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Ne); + } else { + self.instruction(I32Ne); + } + } + fn ptr_and(&mut self, opts: &Options) { if opts.memory64 { self.instruction(I64And); @@ -1743,6 +2564,22 @@ impl Compiler<'_, '_> { } } + fn ptr_or(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Or); + } else { + self.instruction(I32Or); + } + } + + fn ptr_xor(&mut self, opts: &Options) { + if opts.memory64 { + self.instruction(I64Xor); + } else { + self.instruction(I32Xor); + } + } + fn ptr_if(&mut self, opts: &Options, ty: BlockType) { if opts.memory64 { self.instruction(I64Const(0)); @@ -1974,3 +2811,9 @@ enum MallocSize { Const(usize), Local(u32), } + +struct WasmString<'a> { + ptr: u32, + len: u32, + opts: &'a Options, +} diff --git a/crates/environ/src/fact/transcode.rs b/crates/environ/src/fact/transcode.rs new file mode 100644 index 000000000000..865fef316ed6 --- /dev/null +++ b/crates/environ/src/fact/transcode.rs @@ -0,0 +1,178 @@ +use crate::fact::core_types::CoreTypes; +use crate::MemoryIndex; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use wasm_encoder::{EntityType, ValType}; + +pub struct Transcoders { + imported: HashMap, + prev_func_imports: u32, + imports: Vec<(String, EntityType, Transcoder)>, +} + +#[derive(Copy, Clone, Hash, Eq, PartialEq)] +pub struct Transcoder { + pub from_memory: MemoryIndex, + pub from_memory64: bool, + pub to_memory: MemoryIndex, + pub to_memory64: bool, + pub op: Transcode, +} + +/// Possible transcoding operations that must be provided by the host. +/// +/// Note that each transcoding operation may have a unique signature depending +/// on the precise operation. +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub enum Transcode { + Copy(FixedEncoding), + Latin1ToUtf16, + Latin1ToUtf8, + Utf16ToCompactProbablyUtf16, + Utf16ToCompactUtf16, + Utf16ToLatin1, + Utf16ToUtf8, + Utf8ToCompactUtf16, + Utf8ToLatin1, + Utf8ToUtf16, +} + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[allow(missing_docs)] +pub enum FixedEncoding { + Utf8, + Utf16, + Latin1, +} + +impl Transcoders { + pub fn new(prev_func_imports: u32) -> Transcoders { + Transcoders { + imported: HashMap::new(), + prev_func_imports, + imports: Vec::new(), + } + } + + pub fn import(&mut self, types: &mut CoreTypes, transcoder: Transcoder) -> u32 { + *self.imported.entry(transcoder).or_insert_with(|| { + let idx = self.prev_func_imports + (self.imports.len() as u32); + self.imports + .push((transcoder.name(), transcoder.ty(types), transcoder)); + idx + }) + } + + pub fn imports(&self) -> impl Iterator { + self.imports + .iter() + .map(|(name, ty, transcoder)| ("transcode", &name[..], *ty, transcoder)) + } +} + +impl Transcoder { + fn name(&self) -> String { + format!( + "{} (mem{} => mem{})", + self.op.desc(), + self.from_memory.as_u32(), + self.to_memory.as_u32(), + ) + } + + fn ty(&self, types: &mut CoreTypes) -> EntityType { + let from_ptr = if self.from_memory64 { + ValType::I64 + } else { + ValType::I32 + }; + let to_ptr = if self.to_memory64 { + ValType::I64 + } else { + ValType::I32 + }; + + let ty = match self.op { + // These direct transcodings take the source pointer, the source + // code units, and the destination pointer. + // + // The memories being copied between are part of each intrinsic and + // the destination code units are the same as the source. + // Note that the pointers are dynamically guaranteed to be aligned + // and in-bounds for the code units length as defined by the string + // encoding. + Transcode::Copy(_) | Transcode::Latin1ToUtf16 => { + types.function(&[from_ptr, from_ptr, to_ptr], &[]) + } + + // Transcoding from utf8 to utf16 takes the from ptr/len as well as + // a destination. The destination is valid for len*2 bytes. The + // return value is how many code units were written to the + // destination. + Transcode::Utf8ToUtf16 => types.function(&[from_ptr, from_ptr, to_ptr], &[to_ptr]), + + // Transcoding to utf8 as a smaller format takes all the parameters + // and returns the amount of space consumed in the src/destination + Transcode::Utf16ToUtf8 | Transcode::Latin1ToUtf8 => { + types.function(&[from_ptr, from_ptr, to_ptr, to_ptr], &[from_ptr, to_ptr]) + } + + // The return type is a tagged length which indicates which was + // used + Transcode::Utf16ToCompactProbablyUtf16 => { + types.function(&[from_ptr, from_ptr, to_ptr], &[to_ptr]) + } + + // The initial step of transcoding from a fixed format to a compact + // format. Takes the ptr/len of the source the the destination + // pointer. The destination length is implicitly the same. Returns + // how many code units were consumed in the source, which is also + // how many bytes were written to the destination. + Transcode::Utf8ToLatin1 | Transcode::Utf16ToLatin1 => { + types.function(&[from_ptr, from_ptr, to_ptr], &[from_ptr, to_ptr]) + } + + // The final step of transcoding to a compact format when the fixed + // transcode has failed. This takes the ptr/len of the source that's + // remaining to transcode. Then this takes the destination ptr/len + // as well as the destination bytes written so far with latin1. + // Finally this returns the number of code units written to the + // destination. + Transcode::Utf8ToCompactUtf16 | Transcode::Utf16ToCompactUtf16 => { + types.function(&[from_ptr, from_ptr, to_ptr, to_ptr, to_ptr], &[to_ptr]) + } + }; + EntityType::Function(ty) + } +} + +impl Transcode { + /// Returns a human-readable description for this transcoding operation. + pub fn desc(&self) -> &'static str { + match self { + Transcode::Copy(FixedEncoding::Utf8) => "utf8-to-utf8", + Transcode::Copy(FixedEncoding::Utf16) => "utf16-to-utf16", + Transcode::Copy(FixedEncoding::Latin1) => "latin1-to-latin1", + Transcode::Latin1ToUtf16 => "latin1-to-utf16", + Transcode::Latin1ToUtf8 => "latin1-to-utf8", + Transcode::Utf16ToCompactProbablyUtf16 => "utf16-to-compact-probably-utf16", + Transcode::Utf16ToCompactUtf16 => "utf16-to-compact-utf16", + Transcode::Utf16ToLatin1 => "utf16-to-latin1", + Transcode::Utf16ToUtf8 => "utf16-to-utf8", + Transcode::Utf8ToCompactUtf16 => "utf8-to-compact-utf16", + Transcode::Utf8ToLatin1 => "utf8-to-latin1", + Transcode::Utf8ToUtf16 => "utf8-to-utf16", + } + } +} + +impl FixedEncoding { + pub(crate) fn width(&self) -> u8 { + match self { + FixedEncoding::Utf8 => 1, + FixedEncoding::Utf16 => 2, + FixedEncoding::Latin1 => 1, + } + } +} diff --git a/crates/environ/src/fact/traps.rs b/crates/environ/src/fact/traps.rs index 393194f1012a..e68bccfc13d5 100644 --- a/crates/environ/src/fact/traps.rs +++ b/crates/environ/src/fact/traps.rs @@ -30,6 +30,8 @@ pub enum Trap { InvalidDiscriminant, InvalidChar, ListByteLengthOverflow, + StringLengthTooBig, + StringLengthOverflow, AssertFailed(&'static str), } @@ -105,6 +107,8 @@ impl fmt::Display for Trap { Trap::InvalidDiscriminant => "invalid variant discriminant".fmt(f), Trap::InvalidChar => "invalid char value specified".fmt(f), Trap::ListByteLengthOverflow => "byte size of list too large for i32".fmt(f), + Trap::StringLengthTooBig => "string byte size exceeds maximum".fmt(f), + Trap::StringLengthOverflow => "string byte size overflows i32".fmt(f), Trap::AssertFailed(s) => write!(f, "assertion failure: {}", s), } } diff --git a/crates/environ/src/module.rs b/crates/environ/src/module.rs index c937af024bad..23d352f5b052 100644 --- a/crates/environ/src/module.rs +++ b/crates/environ/src/module.rs @@ -974,7 +974,7 @@ impl Module { /// Returns an iterator of all the imports in this module, along with their /// module name, field name, and type that's being imported. - pub fn imports(&self) -> impl Iterator { + pub fn imports(&self) -> impl ExactSizeIterator { self.initializers.iter().map(move |i| match i { Initializer::Import { name, field, index } => { (name.as_str(), field.as_str(), self.type_of(*index)) diff --git a/crates/environ/src/vmoffsets.rs b/crates/environ/src/vmoffsets.rs index d301765b40bd..de131eab4ad4 100644 --- a/crates/environ/src/vmoffsets.rs +++ b/crates/environ/src/vmoffsets.rs @@ -136,6 +136,8 @@ pub trait PtrSize { 16 } + // Offsets within `VMRuntimeLimits` + /// Return the offset of the `stack_limit` field of `VMRuntimeLimits` #[inline] fn vmruntime_limits_stack_limit(&self) -> u8 { @@ -168,6 +170,34 @@ pub trait PtrSize { fn vmruntime_limits_last_wasm_entry_sp(&self) -> u8 { self.vmruntime_limits_last_wasm_exit_pc() + self.size() } + + // Offsets within `VMMemoryDefinition` + + /// The offset of the `base` field. + #[allow(clippy::erasing_op)] + #[inline] + fn vmmemory_definition_base(&self) -> u8 { + 0 * self.size() + } + + /// The offset of the `current_length` field. + #[allow(clippy::identity_op)] + #[inline] + fn vmmemory_definition_current_length(&self) -> u8 { + 1 * self.size() + } + + /// Return the size of `VMMemoryDefinition`. + #[inline] + fn size_of_vmmemory_definition(&self) -> u8 { + 2 * self.size() + } + + /// Return the size of `*mut VMMemoryDefinition`. + #[inline] + fn size_of_vmmemory_pointer(&self) -> u8 { + self.size() + } } /// Type representing the size of a pointer for the current compilation host @@ -395,9 +425,9 @@ impl From> for VMOffsets

{ size(defined_tables) = cmul(ret.num_defined_tables, ret.size_of_vmtable_definition()), size(defined_memories) - = cmul(ret.num_defined_memories, ret.size_of_vmmemory_pointer()), + = cmul(ret.num_defined_memories, ret.ptr.size_of_vmmemory_pointer()), size(owned_memories) - = cmul(ret.num_owned_memories, ret.size_of_vmmemory_definition()), + = cmul(ret.num_owned_memories, ret.ptr.size_of_vmmemory_definition()), align(16), size(defined_globals) = cmul(ret.num_defined_globals, ret.ptr.size_of_vmglobal_definition()), @@ -523,35 +553,6 @@ impl VMOffsets

{ } } -/// Offsets for `VMMemoryDefinition`. -impl VMOffsets

{ - /// The offset of the `base` field. - #[allow(clippy::erasing_op)] - #[inline] - pub fn vmmemory_definition_base(&self) -> u8 { - 0 * self.pointer_size() - } - - /// The offset of the `current_length` field. - #[allow(clippy::identity_op)] - #[inline] - pub fn vmmemory_definition_current_length(&self) -> u8 { - 1 * self.pointer_size() - } - - /// Return the size of `VMMemoryDefinition`. - #[inline] - pub fn size_of_vmmemory_definition(&self) -> u8 { - 2 * self.pointer_size() - } - - /// Return the size of `*mut VMMemoryDefinition`. - #[inline] - pub fn size_of_vmmemory_pointer(&self) -> u8 { - self.pointer_size() - } -} - /// Offsets for `VMGlobalImport`. impl VMOffsets

{ /// The offset of the `from` field. @@ -733,7 +734,8 @@ impl VMOffsets

{ #[inline] pub fn vmctx_vmmemory_pointer(&self, index: DefinedMemoryIndex) -> u32 { assert!(index.as_u32() < self.num_defined_memories); - self.vmctx_memories_begin() + index.as_u32() * u32::from(self.size_of_vmmemory_pointer()) + self.vmctx_memories_begin() + + index.as_u32() * u32::from(self.ptr.size_of_vmmemory_pointer()) } /// Return the offset to the owned `VMMemoryDefinition` at index `index`. @@ -741,7 +743,7 @@ impl VMOffsets

{ pub fn vmctx_vmmemory_definition(&self, index: OwnedMemoryIndex) -> u32 { assert!(index.as_u32() < self.num_owned_memories); self.vmctx_owned_memories_begin() - + index.as_u32() * u32::from(self.size_of_vmmemory_definition()) + + index.as_u32() * u32::from(self.ptr.size_of_vmmemory_definition()) } /// Return the offset to the `VMGlobalDefinition` index `index`. @@ -807,13 +809,14 @@ impl VMOffsets

{ /// Return the offset to the `base` field in `VMMemoryDefinition` index `index`. #[inline] pub fn vmctx_vmmemory_definition_base(&self, index: OwnedMemoryIndex) -> u32 { - self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_base()) + self.vmctx_vmmemory_definition(index) + u32::from(self.ptr.vmmemory_definition_base()) } /// Return the offset to the `current_length` field in `VMMemoryDefinition` index `index`. #[inline] pub fn vmctx_vmmemory_definition_current_length(&self, index: OwnedMemoryIndex) -> u32 { - self.vmctx_vmmemory_definition(index) + u32::from(self.vmmemory_definition_current_length()) + self.vmctx_vmmemory_definition(index) + + u32::from(self.ptr.vmmemory_definition_current_length()) } /// Return the offset to the `from` field in `VMGlobalImport` index `index`. diff --git a/crates/misc/component-test-util/src/lib.rs b/crates/misc/component-test-util/src/lib.rs index 364708250941..eb79e0f96989 100644 --- a/crates/misc/component-test-util/src/lib.rs +++ b/crates/misc/component-test-util/src/lib.rs @@ -35,7 +35,7 @@ impl FuncExt for Func { } } -pub fn engine() -> Engine { +pub fn config() -> Config { drop(env_logger::try_init()); let mut config = Config::new(); @@ -48,7 +48,11 @@ pub fn engine() -> Engine { config.static_memory_maximum_size(0); config.dynamic_memory_guard_size(0); } - Engine::new(&config).unwrap() + config +} + +pub fn engine() -> Engine { + Engine::new(&config()).unwrap() } /// Newtype wrapper for `f32` whose `PartialEq` impl considers NaNs equal to each other. diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index f209a2459453..3121c35de351 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -25,6 +25,7 @@ rand = "0.8.3" anyhow = "1.0.38" memfd = { version = "0.6.1", optional = true } paste = "1.0.3" +encoding_rs = { version = "0.8.31", optional = true } [target.'cfg(target_os = "macos")'.dependencies] mach = "0.3.2" @@ -62,4 +63,7 @@ pooling-allocator = [] # need portable signal handling. posix-signals-on-macos = [] -component-model = ["wasmtime-environ/component-model"] +component-model = [ + "wasmtime-environ/component-model", + "dep:encoding_rs", +] diff --git a/crates/runtime/src/component.rs b/crates/runtime/src/component.rs index 96db80b1979a..f450caaf0a7b 100644 --- a/crates/runtime/src/component.rs +++ b/crates/runtime/src/component.rs @@ -18,13 +18,16 @@ use std::ops::Deref; use std::ptr::{self, NonNull}; use wasmtime_environ::component::{ Component, LoweredIndex, RuntimeAlwaysTrapIndex, RuntimeComponentInstanceIndex, - RuntimeMemoryIndex, RuntimePostReturnIndex, RuntimeReallocIndex, StringEncoding, - VMComponentOffsets, FLAG_MAY_ENTER, FLAG_MAY_LEAVE, FLAG_NEEDS_POST_RETURN, VMCOMPONENT_MAGIC, + RuntimeMemoryIndex, RuntimePostReturnIndex, RuntimeReallocIndex, RuntimeTranscoderIndex, + StringEncoding, VMComponentOffsets, FLAG_MAY_ENTER, FLAG_MAY_LEAVE, FLAG_NEEDS_POST_RETURN, + VMCOMPONENT_MAGIC, }; use wasmtime_environ::HostPtr; const INVALID_PTR: usize = 0xdead_dead_beef_beef_u64 as usize; +mod transcode; + /// Runtime representation of a component instance and all state necessary for /// the instance itself. /// @@ -255,6 +258,14 @@ impl ComponentInstance { unsafe { self.anyfunc(self.offsets.always_trap_anyfunc(idx)) } } + /// Same as `lowering_anyfunc` except for the transcoding functions. + pub fn transcoder_anyfunc( + &self, + idx: RuntimeTranscoderIndex, + ) -> NonNull { + unsafe { self.anyfunc(self.offsets.transcoder_anyfunc(idx)) } + } + unsafe fn anyfunc(&self, offset: u32) -> NonNull { let ret = self.vmctx_plus_offset::(offset); debug_assert!((*ret).func_ptr.as_ptr() as usize != INVALID_PTR); @@ -349,6 +360,16 @@ impl ComponentInstance { unsafe { self.set_anyfunc(self.offsets.always_trap_anyfunc(idx), func_ptr, type_index) } } + /// Same as `set_lowering` but for the transcoder functions. + pub fn set_transcoder( + &mut self, + idx: RuntimeTranscoderIndex, + func_ptr: NonNull, + type_index: VMSharedSignatureIndex, + ) { + unsafe { self.set_anyfunc(self.offsets.transcoder_anyfunc(idx), func_ptr, type_index) } + } + unsafe fn set_anyfunc( &mut self, offset: u32, @@ -366,6 +387,8 @@ impl ComponentInstance { unsafe fn initialize_vmctx(&mut self, store: *mut dyn Store) { *self.vmctx_plus_offset(self.offsets.magic()) = VMCOMPONENT_MAGIC; + *self.vmctx_plus_offset(self.offsets.transcode_libcalls()) = + &transcode::VMBuiltinTranscodeArray::INIT; *self.vmctx_plus_offset(self.offsets.store()) = store; *self.vmctx_plus_offset(self.offsets.limits()) = (*store).vmruntime_limits(); @@ -395,6 +418,11 @@ impl ComponentInstance { let offset = self.offsets.always_trap_anyfunc(i); *self.vmctx_plus_offset(offset) = INVALID_PTR; } + for i in 0..self.offsets.num_transcoders { + let i = RuntimeTranscoderIndex::from_u32(i); + let offset = self.offsets.transcoder_anyfunc(i); + *self.vmctx_plus_offset(offset) = INVALID_PTR; + } for i in 0..self.offsets.num_runtime_memories { let i = RuntimeMemoryIndex::from_u32(i); let offset = self.offsets.runtime_memory(i); @@ -522,6 +550,19 @@ impl OwnedComponentInstance { .set_always_trap(idx, func_ptr, type_index) } } + + /// See `ComponentInstance::set_transcoder` + pub fn set_transcoder( + &mut self, + idx: RuntimeTranscoderIndex, + func_ptr: NonNull, + type_index: VMSharedSignatureIndex, + ) { + unsafe { + self.instance_mut() + .set_transcoder(idx, func_ptr, type_index) + } + } } impl Deref for OwnedComponentInstance { diff --git a/crates/runtime/src/component/transcode.rs b/crates/runtime/src/component/transcode.rs new file mode 100644 index 000000000000..e5f77a7e245c --- /dev/null +++ b/crates/runtime/src/component/transcode.rs @@ -0,0 +1,446 @@ +//! Implementation of string transcoding required by the component model. + +use anyhow::{anyhow, Result}; +use std::cell::Cell; +use std::slice; + +const UTF16_TAG: usize = 1 << 31; + +/// Macro to define the `VMBuiltinTranscodeArray` type which contains all of the +/// function pointers to the actual transcoder functions. This structure is read +/// by Cranelift-generated code, hence the `repr(C)`. +/// +/// Note that this references the `trampolines` module rather than the functions +/// below as the `trampolines` module has the raw ABI. +/// +/// This is modeled after the similar macros and usages in `libcalls.rs` and +/// `vmcontext.rs` +macro_rules! define_transcoders { + ( + $( + $( #[$attr:meta] )* + $name:ident( $( $pname:ident: $param:ident ),* ) $( -> $result:ident )?; + )* + ) => { + /// An array that stores addresses of builtin functions. We translate code + /// to use indirect calls. This way, we don't have to patch the code. + #[repr(C)] + pub struct VMBuiltinTranscodeArray { + $( + $name: unsafe extern "C" fn( + $(define_transcoders!(@ty $param),)* + $(define_transcoders!(@retptr $result),)? + ) $( -> define_transcoders!(@ty $result))?, + )* + } + + impl VMBuiltinTranscodeArray { + pub const INIT: VMBuiltinTranscodeArray = VMBuiltinTranscodeArray { + $($name: trampolines::$name,)* + }; + } + }; + + (@ty size) => (usize); + (@ty size_pair) => (usize); + (@ty ptr_u8) => (*mut u8); + (@ty ptr_u16) => (*mut u16); + + (@retptr size_pair) => (*mut usize); + (@retptr size) => (()); +} + +wasmtime_environ::foreach_transcoder!(define_transcoders); + +/// Submodule with macro-generated constants which are the actual libcall +/// transcoders that are invoked by Cranelift. These functions have a specific +/// ABI defined by the macro itself and will defer to the actual bodies of each +/// implementation following this submodule. +#[allow(improper_ctypes_definitions)] +mod trampolines { + macro_rules! transcoders { + ( + $( + $( #[$attr:meta] )* + $name:ident( $( $pname:ident: $param:ident ),* ) $( -> $result:ident )?; + )* + ) => ( + $( + pub unsafe extern "C" fn $name( + $($pname : define_transcoders!(@ty $param),)* + // If a result is given then a `size_pair` results gets its + // second result value passed via a return pointer here, so + // optionally indicate a return pointer. + $(_retptr: define_transcoders!(@retptr $result))? + ) $( -> define_transcoders!(@ty $result))? { + $(transcoders!(@validate_param $pname $param);)* + + // Always catch panics to avoid trying to unwind from Rust + // into Cranelift-generated code which would lead to a Bad + // Time. + // + // Additionally assume that every function below returns a + // `Result` where errors turn into traps. + let result = std::panic::catch_unwind(|| { + super::$name($($pname),*) + }); + match result { + Ok(Ok(ret)) => transcoders!(@convert_ret ret _retptr $($result)?), + Ok(Err(err)) => crate::traphandlers::raise_trap(err.into()), + Err(panic) => crate::traphandlers::resume_panic(panic), + } + } + )* + ); + + (@convert_ret $ret:ident $retptr:ident) => ($ret); + (@convert_ret $ret:ident $retptr:ident size) => ($ret); + (@convert_ret $ret:ident $retptr:ident size_pair) => ({ + let (a, b) = $ret; + *$retptr = b; + a + }); + + (@validate_param $arg:ident ptr_u16) => ({ + // This should already be guaranteed by the canonical ABI and our + // adapter modules, but double-check here to be extra-sure. If this + // is a perf concern it can become a `debug_assert!`. + assert!(($arg as usize) % 2 == 0, "unaligned 16-bit pointer"); + }); + (@validate_param $arg:ident $ty:ident) => (); + } + + wasmtime_environ::foreach_transcoder!(transcoders); +} + +/// This property should already be guaranteed by construction in the component +/// model but assert it here to be extra sure. Nothing below is sound if regions +/// can overlap. +fn assert_no_overlap(a: &[T], b: &[U]) { + let a_start = a.as_ptr() as usize; + let a_end = a_start + (a.len() * std::mem::size_of::()); + let b_start = b.as_ptr() as usize; + let b_end = b_start + (b.len() * std::mem::size_of::()); + + if a_start < b_start { + assert!(a_end < b_start); + } else { + assert!(b_end < a_start); + } +} + +/// Converts a utf8 string to a utf8 string. +/// +/// The length provided is length of both the source and the destination +/// buffers. No value is returned other than whether an invalid string was +/// found. +unsafe fn utf8_to_utf8(src: *mut u8, len: usize, dst: *mut u8) -> Result<()> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + log::trace!("utf8-to-utf8 {len}"); + let src = std::str::from_utf8(src).map_err(|_| anyhow!("invalid utf8 encoding"))?; + dst.copy_from_slice(src.as_bytes()); + Ok(()) +} + +/// Converts a utf16 string to a utf16 string. +/// +/// The length provided is length of both the source and the destination +/// buffers. No value is returned other than whether an invalid string was +/// found. +unsafe fn utf16_to_utf16(src: *mut u16, len: usize, dst: *mut u16) -> Result<()> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + log::trace!("utf16-to-utf16 {len}"); + run_utf16_to_utf16(src, dst)?; + Ok(()) +} + +/// Transcodes utf16 to itself, returning whether all code points were inside of +/// the latin1 space. +fn run_utf16_to_utf16(src: &[u16], mut dst: &mut [u16]) -> Result { + let mut all_latin1 = true; + for ch in std::char::decode_utf16(src.iter().map(|i| u16::from_le(*i))) { + let ch = ch.map_err(|_| anyhow!("invalid utf16 encoding"))?; + all_latin1 = all_latin1 && u8::try_from(u32::from(ch)).is_ok(); + let result = ch.encode_utf16(dst); + let size = result.len(); + for item in result { + *item = item.to_le(); + } + dst = &mut dst[size..]; + } + Ok(all_latin1) +} + +/// Converts a latin1 string to a latin1 string. +/// +/// Given that all byte sequences are valid latin1 strings this is simply a +/// memory copy. +unsafe fn latin1_to_latin1(src: *mut u8, len: usize, dst: *mut u8) -> Result<()> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + log::trace!("latin1-to-latin1 {len}"); + dst.copy_from_slice(src); + Ok(()) +} + +/// Converts a latin1 string to a utf16 string. +/// +/// This simply inflates the latin1 characters to the u16 code points. The +/// length provided is the same length of the source and destination buffers. +unsafe fn latin1_to_utf16(src: *mut u8, len: usize, dst: *mut u16) -> Result<()> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + for (src, dst) in src.iter().zip(dst) { + *dst = u16::from(*src).to_le(); + } + log::trace!("latin1-to-utf16 {len}"); + Ok(()) +} + +/// Converts utf8 to utf16. +/// +/// The length provided is the same unit length of both buffers, and the +/// returned value from this function is how many u16 units were written. +unsafe fn utf8_to_utf16(src: *mut u8, len: usize, dst: *mut u16) -> Result { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + + let result = run_utf8_to_utf16(src, dst)?; + log::trace!("utf8-to-utf16 {len} => {result}"); + Ok(result) +} + +fn run_utf8_to_utf16(src: &[u8], dst: &mut [u16]) -> Result { + let src = std::str::from_utf8(src).map_err(|_| anyhow!("invalid utf8 encoding"))?; + let mut amt = 0; + for (i, dst) in src.encode_utf16().zip(dst) { + *dst = i.to_le(); + amt += 1; + } + Ok(amt) +} + +/// Converts utf16 to utf8. +/// +/// Each buffer is specified independently here and the returned value is a pair +/// of the number of code units read and code units written. This might perform +/// a partial transcode if the destination buffer is not large enough to hold +/// the entire contents. +unsafe fn utf16_to_utf8( + src: *mut u16, + src_len: usize, + dst: *mut u8, + dst_len: usize, +) -> Result<(usize, usize)> { + let src = slice::from_raw_parts(src, src_len); + let mut dst = slice::from_raw_parts_mut(dst, dst_len); + assert_no_overlap(src, dst); + + // This iterator will convert to native endianness and additionally count + // how many items have been read from the iterator so far. This + // count is used to return how many of the source code units were read. + let src_iter_read = Cell::new(0); + let src_iter = src.iter().map(|i| { + src_iter_read.set(src_iter_read.get() + 1); + u16::from_le(*i) + }); + + let mut src_read = 0; + let mut dst_written = 0; + + for ch in std::char::decode_utf16(src_iter) { + let ch = ch.map_err(|_| anyhow!("invalid utf16 encoding"))?; + + // If the destination doesn't have enough space for this character + // then the loop is ended and this function will be called later with a + // larger destination buffer. + if dst.len() < 4 && dst.len() < ch.len_utf8() { + break; + } + + // Record that characters were read and then convert the `char` to + // utf-8, advancing the destination buffer. + src_read = src_iter_read.get(); + let len = ch.encode_utf8(dst).len(); + dst_written += len; + dst = &mut dst[len..]; + } + + log::trace!("utf16-to-utf8 {src_len}/{dst_len} => {src_read}/{dst_written}"); + Ok((src_read, dst_written)) +} + +/// Converts latin1 to utf8. +/// +/// Receives the independent size of both buffers and returns the number of code +/// units read and code units written (both bytes in this case). +/// +/// This may perform a partial encoding if the destination is not large enough. +unsafe fn latin1_to_utf8( + src: *mut u8, + src_len: usize, + dst: *mut u8, + dst_len: usize, +) -> Result<(usize, usize)> { + let src = slice::from_raw_parts(src, src_len); + let dst = slice::from_raw_parts_mut(dst, dst_len); + assert_no_overlap(src, dst); + let (read, written) = encoding_rs::mem::convert_latin1_to_utf8_partial(src, dst); + log::trace!("latin1-to-utf8 {src_len}/{dst_len} => ({read}, {written})"); + Ok((read, written)) +} + +/// Converts utf16 to "latin1+utf16", probably using a utf16 encoding. +/// +/// The length specified is the length of both the source and destination +/// buffers. If the source string has any characters that don't fit in the +/// latin1 code space (0xff and below) then a utf16-tagged length will be +/// returned. Otherwise the string is "deflated" from a utf16 string to a latin1 +/// string and the latin1 length is returned. +unsafe fn utf16_to_compact_probably_utf16( + src: *mut u16, + len: usize, + dst: *mut u16, +) -> Result { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + let all_latin1 = run_utf16_to_utf16(src, dst)?; + if all_latin1 { + let (left, dst, right) = dst.align_to_mut::(); + assert!(left.is_empty()); + assert!(right.is_empty()); + for i in 0..len { + dst[i] = dst[2 * i]; + } + log::trace!("utf16-to-compact-probably-utf16 {len} => latin1 {len}"); + Ok(len) + } else { + log::trace!("utf16-to-compact-probably-utf16 {len} => utf16 {len}"); + Ok(len | UTF16_TAG) + } +} + +/// Converts a utf8 string to latin1. +/// +/// The length specified is the same length of both the input and the output +/// buffers. +/// +/// Returns the number of code units read from the source and the number of code +/// units written to the destination. +/// +/// Note that this may not convert the entire source into the destination if the +/// original utf8 string has usvs not representable in latin1. +unsafe fn utf8_to_latin1(src: *mut u8, len: usize, dst: *mut u8) -> Result<(usize, usize)> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + let read = encoding_rs::mem::utf8_latin1_up_to(src); + let written = encoding_rs::mem::convert_utf8_to_latin1_lossy(&src[..read], dst); + log::trace!("utf8-to-latin1 {len} => ({read}, {written})"); + Ok((read, written)) +} + +/// Converts a utf16 string to latin1 +/// +/// This is the same as `utf8_to_latin1` in terms of parameters/results. +unsafe fn utf16_to_latin1(src: *mut u16, len: usize, dst: *mut u8) -> Result<(usize, usize)> { + let src = slice::from_raw_parts(src, len); + let dst = slice::from_raw_parts_mut(dst, len); + assert_no_overlap(src, dst); + + let mut size = 0; + for (src, dst) in src.iter().zip(dst) { + let src = u16::from_le(*src); + match u8::try_from(src) { + Ok(src) => *dst = src, + Err(_) => break, + } + size += 1; + } + log::trace!("utf16-to-latin1 {len} => {size}"); + Ok((size, size)) +} + +/// Converts a utf8 string to a utf16 string which has been partially converted +/// as latin1 prior. +/// +/// The original string has already been partially transcoded with +/// `utf8_to_latin1` and that was determined to not be able to transcode the +/// entire string. The substring of the source that couldn't be encoded into +/// latin1 is passed here via `src` and `src_len`. +/// +/// The destination buffer is specified by `dst` and `dst_len`. The first +/// `latin1_bytes_so_far` bytes (not code units) of the `dst` buffer have +/// already been filled in with latin1 characters and need to be inflated +/// in-place to their utf16 equivalents. +/// +/// After the initial latin1 code units have been inflated the entirety of `src` +/// is then transcoded into the remaining space within `dst`. +unsafe fn utf8_to_compact_utf16( + src: *mut u8, + src_len: usize, + dst: *mut u16, + dst_len: usize, + latin1_bytes_so_far: usize, +) -> Result { + let src = slice::from_raw_parts(src, src_len); + let dst = slice::from_raw_parts_mut(dst, dst_len); + assert_no_overlap(src, dst); + + let dst = inflate_latin1_bytes(dst, latin1_bytes_so_far); + let result = run_utf8_to_utf16(src, dst)?; + log::trace!("utf8-to-compact-utf16 {src_len}/{dst_len}/{latin1_bytes_so_far} => {result}"); + Ok(result + latin1_bytes_so_far) +} + +/// Same as `utf8_to_compact_utf16` but for utf16 source strings. +unsafe fn utf16_to_compact_utf16( + src: *mut u16, + src_len: usize, + dst: *mut u16, + dst_len: usize, + latin1_bytes_so_far: usize, +) -> Result { + let src = slice::from_raw_parts(src, src_len); + let dst = slice::from_raw_parts_mut(dst, dst_len); + assert_no_overlap(src, dst); + + let dst = inflate_latin1_bytes(dst, latin1_bytes_so_far); + run_utf16_to_utf16(src, dst)?; + let result = src.len(); + log::trace!("utf16-to-compact-utf16 {src_len}/{dst_len}/{latin1_bytes_so_far} => {result}"); + Ok(result + latin1_bytes_so_far) +} + +/// Inflates the `latin1_bytes_so_far` number of bytes written to the beginning +/// of `dst` into u16 codepoints. +/// +/// Returns the remaining space in the destination that can be transcoded into, +/// slicing off the prefix of the string that was inflated from the latin1 +/// bytes. +fn inflate_latin1_bytes(dst: &mut [u16], latin1_bytes_so_far: usize) -> &mut [u16] { + // Note that `latin1_bytes_so_far` is a byte measure while `dst` is a region + // of u16 units. This `split_at_mut` uses the byte index as an index into + // the u16 unit because each of the latin1 bytes will become a whole code + // unit in the destination which is 2 bytes large. + let (to_inflate, rest) = dst.split_at_mut(latin1_bytes_so_far); + + // Use a byte-oriented view to inflate the original latin1 bytes. + let (left, mid, right) = unsafe { to_inflate.align_to_mut::() }; + assert!(left.is_empty()); + assert!(right.is_empty()); + for i in (0..latin1_bytes_so_far).rev() { + mid[2 * i] = mid[i]; + mid[2 * i + 1] = 0; + } + + return rest; +} diff --git a/crates/runtime/src/vmcontext.rs b/crates/runtime/src/vmcontext.rs index 73eb7bf8563a..c2ee49a9b775 100644 --- a/crates/runtime/src/vmcontext.rs +++ b/crates/runtime/src/vmcontext.rs @@ -255,7 +255,7 @@ mod test_vmmemory_definition { use super::VMMemoryDefinition; use memoffset::offset_of; use std::mem::size_of; - use wasmtime_environ::{Module, VMOffsets}; + use wasmtime_environ::{Module, PtrSize, VMOffsets}; #[test] fn check_vmmemory_definition_offsets() { @@ -263,15 +263,15 @@ mod test_vmmemory_definition { let offsets = VMOffsets::new(size_of::<*mut u8>() as u8, &module); assert_eq!( size_of::(), - usize::from(offsets.size_of_vmmemory_definition()) + usize::from(offsets.ptr.size_of_vmmemory_definition()) ); assert_eq!( offset_of!(VMMemoryDefinition, base), - usize::from(offsets.vmmemory_definition_base()) + usize::from(offsets.ptr.vmmemory_definition_base()) ); assert_eq!( offset_of!(VMMemoryDefinition, current_length), - usize::from(offsets.vmmemory_definition_current_length()) + usize::from(offsets.ptr.vmmemory_definition_current_length()) ); /* TODO: Assert that the size of `current_length` matches. assert_eq!( diff --git a/crates/wasmtime/Cargo.toml b/crates/wasmtime/Cargo.toml index 65223caab723..c0643f039a82 100644 --- a/crates/wasmtime/Cargo.toml +++ b/crates/wasmtime/Cargo.toml @@ -37,6 +37,7 @@ once_cell = "1.12.0" rayon = { version = "1.0", optional = true } object = { version = "0.29", default-features = false, features = ['read_core', 'elf'] } async-trait = { version = "0.1.51", optional = true } +encoding_rs = { version = "0.8.31", optional = true } [target.'cfg(target_os = "windows")'.dependencies.windows-sys] version = "0.36.0" @@ -116,4 +117,5 @@ component-model = [ "wasmtime-runtime/component-model", "dep:wasmtime-component-macro", "dep:wasmtime-component-util", + "dep:encoding_rs", ] diff --git a/crates/wasmtime/src/component/component.rs b/crates/wasmtime/src/component/component.rs index f60aa99fb29a..f4afc9789f73 100644 --- a/crates/wasmtime/src/component/component.rs +++ b/crates/wasmtime/src/component/component.rs @@ -11,7 +11,7 @@ use std::ptr::NonNull; use std::sync::Arc; use wasmtime_environ::component::{ AlwaysTrapInfo, ComponentTypes, FunctionInfo, GlobalInitializer, LoweredIndex, - RuntimeAlwaysTrapIndex, StaticModuleIndex, Translator, + RuntimeAlwaysTrapIndex, RuntimeTranscoderIndex, StaticModuleIndex, Translator, }; use wasmtime_environ::{PrimaryMap, ScopeVec, SignatureIndex, Trampoline, TrapCode}; use wasmtime_jit::CodeMemory; @@ -64,6 +64,10 @@ struct ComponentInner { /// These functions are "degenerate functions" here solely to implement /// functions that are `canon lift`'d then immediately `canon lower`'d. always_trap: PrimaryMap, + + /// Where all the cranelift-generated transcode functions are located in the + /// compiled image of this component. + transcoders: PrimaryMap, } impl Component { @@ -158,7 +162,7 @@ impl Component { || Component::compile_component(engine, &component, &types, &provided_trampolines), ); let static_modules = static_modules?; - let (lowerings, always_trap, trampolines, trampoline_obj) = trampolines?; + let (lowerings, always_trap, transcoders, trampolines, trampoline_obj) = trampolines?; let mut trampoline_obj = CodeMemory::new(trampoline_obj); let code = trampoline_obj.publish()?; let text = wasmtime_jit::subslice_range(code.text, code.mmap); @@ -218,6 +222,7 @@ impl Component { text, lowerings, always_trap, + transcoders, }), }) } @@ -231,6 +236,7 @@ impl Component { ) -> Result<( PrimaryMap, PrimaryMap, + PrimaryMap, Vec, wasmtime_runtime::MmapVec, )> { @@ -239,22 +245,31 @@ impl Component { || -> Result<_> { Ok(engine.join_maybe_parallel( || compile_always_trap(engine, component, types), - || compile_trampolines(engine, component, types, provided_trampolines), + || -> Result<_> { + Ok(engine.join_maybe_parallel( + || compile_transcoders(engine, component, types), + || compile_trampolines(engine, component, types, provided_trampolines), + )) + }, )) }, ); let (lowerings, other) = results; - let (always_trap, trampolines) = other?; + let (always_trap, other) = other?; + let (transcoders, trampolines) = other?; let mut obj = engine.compiler().object()?; - let (lower, traps, trampolines) = engine.compiler().component_compiler().emit_obj( - lowerings?, - always_trap?, - trampolines?, - &mut obj, - )?; + let (lower, traps, transcoders, trampolines) = + engine.compiler().component_compiler().emit_obj( + lowerings?, + always_trap?, + transcoders?, + trampolines?, + &mut obj, + )?; return Ok(( lower, traps, + transcoders, trampolines, wasmtime_jit::mmap_vec_from_obj(obj)?, )); @@ -307,6 +322,30 @@ impl Component { .collect()) } + fn compile_transcoders( + engine: &Engine, + component: &wasmtime_environ::component::Component, + types: &ComponentTypes, + ) -> Result>> { + let always_trap = component + .initializers + .iter() + .filter_map(|init| match init { + GlobalInitializer::Transcoder(i) => Some(i), + _ => None, + }) + .collect::>(); + Ok(engine + .run_maybe_parallel(always_trap, |info| { + engine + .compiler() + .component_compiler() + .compile_transcoder(component, info, types) + })? + .into_iter() + .collect()) + } + fn compile_trampolines( engine: &Engine, component: &wasmtime_environ::component::Component, @@ -376,6 +415,11 @@ impl Component { self.func(&info.info) } + pub(crate) fn transcoder_ptr(&self, index: RuntimeTranscoderIndex) -> NonNull { + let info = &self.inner.transcoders[index]; + self.func(info) + } + fn func(&self, info: &FunctionInfo) -> NonNull { let text = self.text(); let trampoline = &text[info.start as usize..][..info.length as usize]; diff --git a/crates/wasmtime/src/component/func/typed.rs b/crates/wasmtime/src/component/func/typed.rs index 0300ac4e72e6..ec65231f5b5d 100644 --- a/crates/wasmtime/src/component/func/typed.rs +++ b/crates/wasmtime/src/component/func/typed.rs @@ -1,7 +1,7 @@ use crate::component::func::{Func, Memory, MemoryMut, Options}; use crate::store::StoreOpaque; use crate::{AsContext, AsContextMut, StoreContext, StoreContextMut, ValRaw}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use std::borrow::Cow; use std::fmt; use std::marker; @@ -801,6 +801,10 @@ unsafe impl Lift for char { } } +// TODO: these probably need different constants for memory64 +const UTF16_TAG: usize = 1 << 31; +const MAX_STRING_BYTE_LENGTH: usize = (1 << 31) - 1; + // Note that this is similar to `ComponentType for WasmStr` except it can only // be used for lowering, not lifting. unsafe impl ComponentType for str { @@ -843,16 +847,51 @@ unsafe impl Lower for str { } fn lower_string(mem: &mut MemoryMut<'_, T>, string: &str) -> Result<(usize, usize)> { + // Note that in general the wasm module can't assume anything about what the + // host strings are encoded as. Additionally hosts are allowed to have + // differently-encoded strings at runtime. Finally when copying a string + // into wasm it's somewhat strict in the sense that the various patterns of + // allocation and such are already dictated for us. + // + // In general what this means is that when copying a string from the host + // into the destination we need to follow one of the cases of copying into + // WebAssembly. It doesn't particularly matter which case as long as it ends + // up in the right encoding. For example a destination encoding of + // latin1+utf16 has a number of ways to get copied into and we do something + // here that isn't the default "utf8 to latin1+utf16" since we have access + // to simd-accelerated helpers in the `encoding_rs` crate. This is ok though + // because we can fake that the host string was already stored in latin1 + // format and follow that copy pattern instead. match mem.string_encoding() { + // This corresponds to `store_string_copy` in the canonical ABI where + // the host's representation is utf-8 and the wasm module wants utf-8 so + // a copy is all that's needed (and the `realloc` can be precise for the + // initial memory allocation). StringEncoding::Utf8 => { + if string.len() > MAX_STRING_BYTE_LENGTH { + bail!( + "string length of {} too large to copy into wasm", + string.len() + ); + } let ptr = mem.realloc(0, 0, 1, string.len())?; if string.len() > 0 { mem.as_slice_mut()[ptr..][..string.len()].copy_from_slice(string.as_bytes()); } Ok((ptr, string.len())) } + + // This corresponds to `store_utf8_to_utf16` in the canonical ABI. Here + // an over-large allocation is performed and then shrunk afterwards if + // necessary. StringEncoding::Utf16 => { let size = string.len() * 2; + if size > MAX_STRING_BYTE_LENGTH { + bail!( + "string length of {} too large to copy into wasm", + string.len() + ); + } let mut ptr = mem.realloc(0, 0, 2, size)?; let mut copied = 0; if size > 0 { @@ -869,8 +908,60 @@ fn lower_string(mem: &mut MemoryMut<'_, T>, string: &str) -> Result<(usize, u } Ok((ptr, copied)) } + StringEncoding::CompactUtf16 => { - unimplemented!("compact-utf-16"); + // This corresponds to `store_string_to_latin1_or_utf16` + let bytes = string.as_bytes(); + let mut iter = string.char_indices(); + let mut ptr = mem.realloc(0, 0, 2, bytes.len())?; + let mut dst = &mut mem.as_slice_mut()[ptr..][..bytes.len()]; + let mut result = 0; + while let Some((i, ch)) = iter.next() { + // Test if this `char` fits into the latin1 encoding. + if let Ok(byte) = u8::try_from(u32::from(ch)) { + dst[result] = byte; + result += 1; + continue; + } + + // .. if utf16 is forced to be used then the allocation is + // bumped up to the maximum size. + let worst_case = bytes + .len() + .checked_mul(2) + .ok_or_else(|| anyhow!("byte length overflow"))?; + if worst_case > MAX_STRING_BYTE_LENGTH { + bail!("byte length too large"); + } + ptr = mem.realloc(ptr, bytes.len(), 2, worst_case)?; + dst = &mut mem.as_slice_mut()[ptr..][..worst_case]; + + // Previously encoded latin1 bytes are inflated to their 16-bit + // size for utf16 + for i in (0..result).rev() { + dst[2 * i] = dst[i]; + dst[2 * i + 1] = 0; + } + + // and then the remainder of the string is encoded. + for (u, bytes) in string[i..] + .encode_utf16() + .zip(dst[2 * result..].chunks_mut(2)) + { + let u_bytes = u.to_le_bytes(); + bytes[0] = u_bytes[0]; + bytes[1] = u_bytes[1]; + result += 1; + } + if worst_case > 2 * result { + ptr = mem.realloc(ptr, worst_case, 2, 2 * result)?; + } + return Ok((ptr, result | UTF16_TAG)); + } + if result < bytes.len() { + ptr = mem.realloc(ptr, bytes.len(), 2, result)?; + } + Ok((ptr, result)) } } } @@ -898,7 +989,13 @@ impl WasmStr { let byte_len = match memory.string_encoding() { StringEncoding::Utf8 => Some(len), StringEncoding::Utf16 => len.checked_mul(2), - StringEncoding::CompactUtf16 => unimplemented!(), + StringEncoding::CompactUtf16 => { + if len & UTF16_TAG == 0 { + Some(len) + } else { + (len ^ UTF16_TAG).checked_mul(2) + } + } }; match byte_len.and_then(|len| ptr.checked_add(len)) { Some(n) if n <= memory.as_slice().len() => {} @@ -939,8 +1036,14 @@ impl WasmStr { fn to_str_from_store<'a>(&self, store: &'a StoreOpaque) -> Result> { match self.options.string_encoding() { StringEncoding::Utf8 => self.decode_utf8(store), - StringEncoding::Utf16 => self.decode_utf16(store), - StringEncoding::CompactUtf16 => unimplemented!(), + StringEncoding::Utf16 => self.decode_utf16(store, self.len), + StringEncoding::CompactUtf16 => { + if self.len & UTF16_TAG == 0 { + self.decode_latin1(store) + } else { + self.decode_utf16(store, self.len ^ UTF16_TAG) + } + } } } @@ -952,10 +1055,10 @@ impl WasmStr { Ok(str::from_utf8(&memory[self.ptr..][..self.len])?.into()) } - fn decode_utf16<'a>(&self, store: &'a StoreOpaque) -> Result> { + fn decode_utf16<'a>(&self, store: &'a StoreOpaque, len: usize) -> Result> { let memory = self.options.memory(store); // See notes in `decode_utf8` for why this is panicking indexing. - let memory = &memory[self.ptr..][..self.len * 2]; + let memory = &memory[self.ptr..][..len * 2]; Ok(std::char::decode_utf16( memory .chunks(2) @@ -964,6 +1067,14 @@ impl WasmStr { .collect::>()? .into()) } + + fn decode_latin1<'a>(&self, store: &'a StoreOpaque) -> Result> { + // See notes in `decode_utf8` for why this is panicking indexing. + let memory = self.options.memory(store); + Ok(encoding_rs::mem::decode_latin1( + &memory[self.ptr..][..self.len], + )) + } } // Note that this is similar to `ComponentType for str` except it can only be @@ -1068,7 +1179,7 @@ where let size = list .len() .checked_mul(elem_size) - .ok_or_else(|| anyhow::anyhow!("size overflow copying a list"))?; + .ok_or_else(|| anyhow!("size overflow copying a list"))?; let ptr = mem.realloc(0, 0, T::ALIGN32, size)?; let mut cur = ptr; for item in list { diff --git a/crates/wasmtime/src/component/instance.rs b/crates/wasmtime/src/component/instance.rs index 1f836051d33c..7abc81bfeafa 100644 --- a/crates/wasmtime/src/component/instance.rs +++ b/crates/wasmtime/src/component/instance.rs @@ -10,9 +10,9 @@ use std::sync::Arc; use wasmtime_environ::component::{ AlwaysTrap, ComponentTypes, CoreDef, CoreExport, Export, ExportItem, ExtractMemory, ExtractPostReturn, ExtractRealloc, GlobalInitializer, InstantiateModule, LowerImport, - RuntimeImportIndex, RuntimeInstanceIndex, RuntimeModuleIndex, + RuntimeImportIndex, RuntimeInstanceIndex, RuntimeModuleIndex, Transcoder, }; -use wasmtime_environ::{EntityIndex, Global, GlobalInit, PrimaryMap, WasmType}; +use wasmtime_environ::{EntityIndex, EntityType, Global, GlobalInit, PrimaryMap, WasmType}; use wasmtime_runtime::component::{ComponentInstance, OwnedComponentInstance}; /// An instantiated component. @@ -142,6 +142,11 @@ impl InstanceData { }, }) } + CoreDef::Transcoder(idx) => { + wasmtime_runtime::Export::Function(wasmtime_runtime::ExportFunction { + anyfunc: self.state.transcoder_anyfunc(*idx), + }) + } } } @@ -287,6 +292,8 @@ impl<'a> Instantiator<'a> { _ => unreachable!(), }); } + + GlobalInitializer::Transcoder(e) => self.transcoder(e), } } Ok(()) @@ -328,6 +335,17 @@ impl<'a> Instantiator<'a> { ); } + fn transcoder(&mut self, transcoder: &Transcoder) { + self.data.state.set_transcoder( + transcoder.index, + self.component.transcoder_ptr(transcoder.index), + self.component + .signatures() + .shared_signature(transcoder.signature) + .expect("found unregistered signature"), + ); + } + fn extract_memory(&mut self, store: &mut StoreOpaque, memory: &ExtractMemory) { let mem = match self.data.lookup_export(store, &memory.export) { wasmtime_runtime::Export::Memory(m) => m, @@ -371,24 +389,16 @@ impl<'a> Instantiator<'a> { // core wasm instantiations internally within a component are // unnecessary and superfluous. Naturally though mistakes may be // made, so double-check this property of wasmtime in debug mode. + if cfg!(debug_assertions) { - let export = self.data.lookup_def(store, arg); let (_, _, expected) = imports.next().unwrap(); - let val = unsafe { crate::Extern::from_wasmtime_export(export, store) }; - crate::types::matching::MatchCx { - store, - engine: store.engine(), - signatures: module.signatures(), - types: module.types(), - } - .extern_(&expected, &val) - .expect("unexpected typecheck failure"); + self.assert_type_matches(store, module, arg, expected); } - let export = self.data.lookup_def(store, arg); // The unsafety here should be ok since the `export` is loaded // directly from an instance which should only give us valid export // items. + let export = self.data.lookup_def(store, arg); unsafe { self.core_imports.push_export(&export); } @@ -397,6 +407,41 @@ impl<'a> Instantiator<'a> { &self.core_imports } + + fn assert_type_matches( + &mut self, + store: &mut StoreOpaque, + module: &Module, + arg: &CoreDef, + expected: EntityType, + ) { + let export = self.data.lookup_def(store, arg); + + // If this value is a core wasm function then the type check is inlined + // here. This can otherwise fail `Extern::from_wasmtime_export` because + // there's no guarantee that there exists a trampoline for `f` so this + // can't fall through to the case below + if let wasmtime_runtime::Export::Function(f) = &export { + match expected { + EntityType::Function(expected) => { + let actual = unsafe { f.anyfunc.as_ref().type_index }; + assert_eq!(module.signatures().shared_signature(expected), Some(actual)); + return; + } + _ => panic!("function not expected"), + } + } + + let val = unsafe { crate::Extern::from_wasmtime_export(export, store) }; + crate::types::matching::MatchCx { + store, + engine: store.engine(), + signatures: module.signatures(), + types: module.types(), + } + .extern_(&expected, &val) + .expect("unexpected typecheck failure"); + } } /// A "pre-instantiated" [`Instance`] which has all of its arguments already diff --git a/crates/wasmtime/src/config.rs b/crates/wasmtime/src/config.rs index 2b3cf1202c07..6bb890be3ff5 100644 --- a/crates/wasmtime/src/config.rs +++ b/crates/wasmtime/src/config.rs @@ -1462,6 +1462,15 @@ impl Config { compiler.build() } + + /// Internal setting for whether adapter modules for components will have + /// extra WebAssembly instructions inserted performing more debug checks + /// then are necessary. + #[cfg(feature = "component-model")] + pub fn debug_adapter_modules(&mut self, debug: bool) -> &mut Self { + self.tunables.debug_adapter_modules = debug; + self + } } fn round_up_to_pages(val: u64) -> u64 { diff --git a/deny.toml b/deny.toml index 036c996d0ce8..ee47bb543e10 100644 --- a/deny.toml +++ b/deny.toml @@ -14,10 +14,9 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "Apache-2.0", "BSD-2-Clause", - "CC0-1.0", + "BSD-3-Clause", "ISC", "MIT", - "MPL-2.0", "Zlib", ] diff --git a/supply-chain/config.toml b/supply-chain/config.toml index f46040a05d00..e8e3985ae852 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -311,6 +311,10 @@ criteria = "safe-to-deploy" version = "0.3.6" criteria = "safe-to-deploy" +[[exemptions.encoding_rs]] +version = "0.8.31" +criteria = "safe-to-deploy" + [[exemptions.env_logger]] version = "0.7.1" criteria = "safe-to-deploy" diff --git a/tests/all/component_model.rs b/tests/all/component_model.rs index a9a2117ad151..325ad1f965e3 100644 --- a/tests/all/component_model.rs +++ b/tests/all/component_model.rs @@ -12,6 +12,7 @@ mod instance; mod macros; mod nested; mod post_return; +mod strings; #[test] fn components_importing_modules() -> Result<()> { diff --git a/tests/all/component_model/strings.rs b/tests/all/component_model/strings.rs new file mode 100644 index 000000000000..59997090deba --- /dev/null +++ b/tests/all/component_model/strings.rs @@ -0,0 +1,578 @@ +use super::REALLOC_AND_FREE; +use anyhow::Result; +use wasmtime::component::{Component, Linker}; +use wasmtime::{Engine, Store, StoreContextMut, Trap, TrapCode}; + +const UTF16_TAG: u32 = 1 << 31; + +// Special cases that this tries to test: +// +// * utf8 -> utf8 +// * various code point sizes +// +// * utf8 -> utf16 - the adapter here will make a pessimistic allocation that's +// twice the size of the utf8 encoding for the utf16 destination +// * utf16 byte size is twice the utf8 size +// * utf16 byte size is less than twice the utf8 size +// +// * utf8 -> latin1+utf16 - attempts to convert to latin1 then falls back to a +// pessimistic utf16 allocation that's downsized if necessary +// * utf8 fits exactly in latin1 +// * utf8 fits latin1 but is bigger byte-wise +// * utf8 is not latin1 and fits utf16 allocation precisely (NOT POSSIBLE) +// * utf8 is not latin1 and utf16 is smaller than allocation +// +// * utf16 -> utf8 - this starts with an optimistic size and then reallocates to +// a pessimistic size, interesting cases are: +// * utf8 size is 0.5x the utf16 byte size (perfect fit in initial alloc) +// * utf8 size is 1.5x the utf16 byte size (perfect fit in larger alloc) +// * utf8 size is 0.5x-1.5x the utf16 size (larger alloc is downsized) +// +// * utf16 -> utf16 +// * various code point sizes +// +// * utf16 -> latin1+utf16 - attempts to convert to latin1 then falls back to a +// pessimistic utf16 allocation that's downsized if necessary +// * utf16 fits exactly in latin1 +// * utf16 fits latin1 but is bigger byte-wise (NOT POSSIBLE) +// * utf16 is not latin1 and fits utf16 allocation precisely +// * utf16 is not latin1 and utf16 is smaller than allocation (NOT POSSIBLE) +// +// * compact-utf16 -> utf8 dynamically determines between one of +// * latin1 -> utf8 +// * latin1 size matches utf8 size +// * latin1 is smaller than utf8 size +// * utf16 -> utf8 +// * covered above +// +// * compact-utf16 -> utf16 dynamically determines between one of +// * latin1 -> utf16 - latin1 size always matches utf16 +// * test various code points +// * utf16 -> utf16 +// * covered above +// +// * compact-utf16 -> compact-utf16 dynamically determines between one of +// * latin1 -> latin1 +// * not much interesting here +// * utf16 -> compact-utf16-to-compact-probably-utf16 +// * utf16 actually fits within latin1 +// * otherwise not more interesting than utf16 -> utf16 +// +const STRINGS: &[&str] = &[ + "", + // 1 byte in utf8, 2 bytes in utf16 + "x", + "hello this is a particularly long string yes it is it keeps going", + // 35 bytes in utf8, 23 units in utf16, 23 bytes in latin1 + "à á â ã ä å æ ç è é ê ë", + // 47 bytes in utf8, 31 units in utf16 + "Ξ Ο Π Ρ Σ Τ Υ Φ Χ Ψ Ω Ϊ Ϋ ά έ ή", + // 24 bytes in utf8, 8 units in utf16 + "STUVWXYZ", + // 16 bytes in utf8, 8 units in utf16 + "ËÌÍÎÏÐÑÒ", + // 4 bytes in utf8, 1 unit in utf16 + "\u{10000}", + // latin1-compatible prefix followed by utf8/16-requiring suffix + // + // 24 bytes in utf8, 13 units in utf16, first 8 usvs are latin1-compatible + "à ascii VWXYZ", +]; + +static ENCODINGS: [&str; 3] = ["utf8", "utf16", "latin1+utf16"]; + +#[test] +fn roundtrip() -> Result<()> { + for debug in [true, false] { + let mut config = component_test_util::config(); + config.debug_adapter_modules(debug); + let engine = Engine::new(&config)?; + for src in ENCODINGS { + for dst in ENCODINGS { + test_roundtrip(&engine, src, dst)?; + } + } + } + Ok(()) +} + +fn test_roundtrip(engine: &Engine, src: &str, dst: &str) -> Result<()> { + println!("src={src} dst={dst}"); + + let mk_echo = |name: &str, encoding: &str| { + format!( + r#" +(component {name} + (import "echo" (func $echo (param string) (result string))) + (core instance $libc (instantiate $libc)) + (core func $echo (canon lower (func $echo) + (memory $libc "memory") + (realloc (func $libc "realloc")) + string-encoding={encoding} + )) + (core instance $echo (instantiate $echo + (with "libc" (instance $libc)) + (with "" (instance (export "echo" (func $echo)))) + )) + (func (export "echo") (param string) (result string) + (canon lift + (core func $echo "echo") + (memory $libc "memory") + (realloc (func $libc "realloc")) + string-encoding={encoding} + ) + ) +) + "# + ) + }; + + let src = mk_echo("$src", src); + let dst = mk_echo("$dst", dst); + let component = format!( + r#" +(component + (import "host" (func $host (param string) (result string))) + + (core module $libc + (memory (export "memory") 1) + {REALLOC_AND_FREE} + ) + (core module $echo + (import "" "echo" (func $echo (param i32 i32 i32))) + (import "libc" "memory" (memory 0)) + (import "libc" "realloc" (func $realloc (param i32 i32 i32 i32) (result i32))) + + (func (export "echo") (param i32 i32) (result i32) + (local $retptr i32) + (local.set $retptr + (call $realloc + (i32.const 0) + (i32.const 0) + (i32.const 4) + (i32.const 8))) + (call $echo + (local.get 0) + (local.get 1) + (local.get $retptr)) + local.get $retptr + ) + ) + + {src} + {dst} + + (instance $dst (instantiate $dst (with "echo" (func $host)))) + (instance $src (instantiate $src (with "echo" (func $dst "echo")))) + (export "echo" (func $src "echo")) +) +"# + ); + let component = Component::new(engine, &component)?; + let mut store = Store::new(engine, String::new()); + let mut linker = Linker::new(engine); + linker + .root() + .func_wrap("host", |store: StoreContextMut, arg: String| { + assert_eq!(*store.data(), arg); + Ok(arg) + })?; + let instance = linker.instantiate(&mut store, &component)?; + let func = instance.get_typed_func::<(String,), String, _>(&mut store, "echo")?; + + for string in STRINGS { + println!("testing string {string:?}"); + *store.data_mut() = string.to_string(); + let ret = func.call(&mut store, (string.to_string(),))?; + assert_eq!(ret, *string); + func.post_return(&mut store)?; + } + Ok(()) +} + +#[test] +fn ptr_out_of_bounds() -> Result<()> { + let engine = component_test_util::engine(); + for src in ENCODINGS { + for dst in ENCODINGS { + test_ptr_out_of_bounds(&engine, src, dst)?; + } + } + Ok(()) +} + +fn test_ptr_out_of_bounds(engine: &Engine, src: &str, dst: &str) -> Result<()> { + let test = |len: u32| -> Result<()> { + let component = format!( + r#" +(component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory") + string-encoding={dst}) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func $start (call $f (i32.const 0x8000_0000) (i32.const {len}))) + (start $start) + ) + (core instance (instantiate $m (with "" (instance (export "" (func $f)))))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) +) +"# + ); + let component = Component::new(engine, &component)?; + let mut store = Store::new(engine, ()); + let trap = Linker::new(engine) + .instantiate(&mut store, &component) + .err() + .unwrap() + .downcast::()?; + assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached)); + Ok(()) + }; + + test(0)?; + test(1)?; + + Ok(()) +} + +// Test that even if the ptr+len calculation overflows then a trap still +// happens. +#[test] +fn ptr_overflow() -> Result<()> { + let engine = component_test_util::engine(); + for src in ENCODINGS { + for dst in ENCODINGS { + test_ptr_overflow(&engine, src, dst)?; + } + } + Ok(()) +} + +fn test_ptr_overflow(engine: &Engine, src: &str, dst: &str) -> Result<()> { + let component = format!( + r#" +(component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory") + string-encoding={dst}) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func (export "f") (param i32) (call $f (i32.const 1000) (local.get 0))) + ) + (core instance $m (instantiate $m (with "" (instance (export "" (func $f)))))) + (func (export "f") (param u32) (canon lift (core func $m "f"))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + (export "f" (func $c2 "f")) +) +"# + ); + + let component = Component::new(engine, &component)?; + let mut store = Store::new(engine, ()); + + let mut test_overflow = |size: u32| -> Result<()> { + println!("src={src} dst={dst} size={size:#x}"); + let instance = Linker::new(engine).instantiate(&mut store, &component)?; + let func = instance.get_typed_func::<(u32,), (), _>(&mut store, "f")?; + let trap = func + .call(&mut store, (size,)) + .unwrap_err() + .downcast::()?; + assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached)); + Ok(()) + }; + + let max = 1 << 31; + + match src { + "utf8" => { + // This exceeds MAX_STRING_BYTE_LENGTH + test_overflow(max)?; + + if dst == "utf16" { + // exceeds MAX_STRING_BYTE_LENGTH when multiplied + test_overflow(max / 2)?; + + // Technically this fails on the first string, not the second. + // Ideally this would test the overflow check on the second + // string though. + test_overflow(max / 2 - 100)?; + } else { + // This will point into unmapped memory + test_overflow(max - 100)?; + } + } + + "utf16" => { + test_overflow(max / 2)?; + test_overflow(max / 2 - 100)?; + } + + "latin1+utf16" => { + test_overflow((max / 2) | UTF16_TAG)?; + // tag a utf16 string with the max length and it should overflow. + test_overflow((max / 2 - 100) | UTF16_TAG)?; + } + + _ => unreachable!(), + } + + Ok(()) +} + +// Test that that the pointer returned from `realloc` is bounds-checked. +#[test] +fn realloc_oob() -> Result<()> { + let engine = component_test_util::engine(); + for src in ENCODINGS { + for dst in ENCODINGS { + test_realloc_oob(&engine, src, dst)?; + } + } + Ok(()) +} + +fn test_realloc_oob(engine: &Engine, src: &str, dst: &str) -> Result<()> { + let component = format!( + r#" +(component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 100_000) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory") + string-encoding={dst}) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func (export "f") (call $f (i32.const 1000) (i32.const 10))) + ) + (core instance $m (instantiate $m (with "" (instance (export "" (func $f)))))) + (func (export "f") (canon lift (core func $m "f"))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + (export "f" (func $c2 "f")) +) +"# + ); + + let component = Component::new(engine, &component)?; + let mut store = Store::new(engine, ()); + + let instance = Linker::new(engine).instantiate(&mut store, &component)?; + let func = instance.get_typed_func::<(), (), _>(&mut store, "f")?; + let trap = func.call(&mut store, ()).unwrap_err().downcast::()?; + assert_eq!(trap.trap_code(), Some(TrapCode::UnreachableCodeReached)); + Ok(()) +} + +// Test that that the pointer returned from `realloc` is bounds-checked. +#[test] +fn raw_string_encodings() -> Result<()> { + let engine = component_test_util::engine(); + test_invalid_string_encoding(&engine, "utf8", "utf8", &[0xff], 1)?; + let array = b"valid string until \xffthen valid again"; + test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?; + test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?; + let array = b"symbol \xce\xa3 until \xffthen valid"; + test_invalid_string_encoding(&engine, "utf8", "utf8", array, array.len() as u32)?; + test_invalid_string_encoding(&engine, "utf8", "utf16", array, array.len() as u32)?; + test_invalid_string_encoding(&engine, "utf8", "latin1+utf16", array, array.len() as u32)?; + test_invalid_string_encoding(&engine, "utf16", "utf8", &[0x01, 0xd8], 1)?; + test_invalid_string_encoding(&engine, "utf16", "utf16", &[0x01, 0xd8], 1)?; + test_invalid_string_encoding( + &engine, + "utf16", + "latin1+utf16", + &[0xff, 0xff, 0x01, 0xd8], + 2, + )?; + test_invalid_string_encoding( + &engine, + "latin1+utf16", + "utf8", + &[0x01, 0xd8], + 1 | UTF16_TAG, + )?; + test_invalid_string_encoding( + &engine, + "latin1+utf16", + "utf16", + &[0x01, 0xd8], + 1 | UTF16_TAG, + )?; + test_invalid_string_encoding( + &engine, + "latin1+utf16", + "utf16", + &[0xff, 0xff, 0x01, 0xd8], + 2 | UTF16_TAG, + )?; + test_invalid_string_encoding( + &engine, + "latin1+utf16", + "latin1+utf16", + &[0xab, 0x00, 0xff, 0xff, 0x01, 0xd8], + 3 | UTF16_TAG, + )?; + + // This latin1+utf16 string should get compressed to latin1 across the + // boundary. + test_valid_string_encoding( + &engine, + "latin1+utf16", + "latin1+utf16", + &[0xab, 0x00, 0xff, 0x00], + 2 | UTF16_TAG, + )?; + Ok(()) +} + +fn test_invalid_string_encoding( + engine: &Engine, + src: &str, + dst: &str, + bytes: &[u8], + len: u32, +) -> Result<()> { + let trap = test_raw_when_encoded(engine, src, dst, bytes, len)?.unwrap(); + let src = src.replace("latin1+", ""); + assert!( + trap.to_string() + .contains(&format!("invalid {src} encoding")), + "bad error: {}", + trap, + ); + Ok(()) +} + +fn test_valid_string_encoding( + engine: &Engine, + src: &str, + dst: &str, + bytes: &[u8], + len: u32, +) -> Result<()> { + let err = test_raw_when_encoded(engine, src, dst, bytes, len)?; + assert!(err.is_none()); + Ok(()) +} + +fn test_raw_when_encoded( + engine: &Engine, + src: &str, + dst: &str, + bytes: &[u8], + len: u32, +) -> Result> { + let component = format!( + r#" +(component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory") + string-encoding={dst}) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding={src} (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func (export "f") (param i32 i32 i32) (call $f (local.get 0) (local.get 2))) + ) + (core instance $m (instantiate $m (with "" (instance (export "" (func $f)))))) + (func (export "f") (param (list u8)) (param u32) (canon lift (core func $m "f") + (memory $libc "memory") + (realloc (func $libc "realloc")))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + (export "f" (func $c2 "f")) +) +"# + ); + + let component = Component::new(engine, &component)?; + let mut store = Store::new(engine, ()); + + let instance = Linker::new(engine).instantiate(&mut store, &component)?; + let func = instance.get_typed_func::<(&[u8], u32), (), _>(&mut store, "f")?; + match func.call(&mut store, (bytes, len)) { + Ok(_) => Ok(None), + Err(e) => Ok(Some(e.downcast()?)), + } +} diff --git a/tests/misc_testsuite/component-model/strings.wast b/tests/misc_testsuite/component-model/strings.wast new file mode 100644 index 000000000000..398ca39563ae --- /dev/null +++ b/tests/misc_testsuite/component-model/strings.wast @@ -0,0 +1,108 @@ +;; unaligned utf16 string +(assert_trap + (component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding=utf16 (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func $start (call $f (i32.const 1) (i32.const 0))) + (start $start) + ) + (core instance (instantiate $m (with "" (instance (export "" (func $f)))))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + ) + "unreachable") + +;; unaligned latin1+utf16 string, even with the latin1 encoding +(assert_trap + (component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory")) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding=latin1+utf16 (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func $start (call $f (i32.const 1) (i32.const 0))) + (start $start) + ) + (core instance (instantiate $m (with "" (instance (export "" (func $f)))))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + ) + "unreachable") + +;; out of bounds utf8->utf8 string +(assert_trap + (component + (component $c + (core module $m + (func (export "") (param i32 i32)) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) i32.const 0) + (memory (export "memory") 1) + ) + (core instance $m (instantiate $m)) + (func (export "") (param string) + (canon lift (core func $m "") (realloc (func $m "realloc")) (memory $m "memory") + string-encoding=utf8) + ) + ) + + (component $c2 + (import "" (func $f (param string))) + (core module $libc + (memory (export "memory") 1) + ) + (core instance $libc (instantiate $libc)) + (core func $f (canon lower (func $f) string-encoding=utf8 (memory $libc "memory"))) + (core module $m + (import "" "" (func $f (param i32 i32))) + + (func $start (call $f (i32.const 0x8000_0000) (i32.const 1))) + (start $start) + ) + (core instance (instantiate $m (with "" (instance (export "" (func $f)))))) + ) + + (instance $c (instantiate $c)) + (instance $c2 (instantiate $c2 (with "" (func $c "")))) + ) + "unreachable")