diff --git a/CODEOWNERS b/CODEOWNERS index 7c3617ef6292..59b85b44b615 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,3 +55,5 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/listener/http_inspector @crazyxy @PiotrSikora @lizan # attribute context /*/extensions/filters/common/expr @kyessenov @yangminzhu +# webassembly common extension +/*/extensions/common/wasm @jplevyak @PiotrSikora diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 42360c572b39..a443ce11ab5e 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -60,7 +60,8 @@ namespace Logger { FUNCTION(thrift) \ FUNCTION(tracing) \ FUNCTION(upstream) \ - FUNCTION(udp) + FUNCTION(udp) \ + FUNCTION(wasm) enum class Id { ALL_LOGGER_IDS(GENERATE_ENUM) diff --git a/source/extensions/common/wasm/BUILD b/source/extensions/common/wasm/BUILD new file mode 100644 index 000000000000..7eda2486185d --- /dev/null +++ b/source/extensions/common/wasm/BUILD @@ -0,0 +1,36 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "well_known_names", + hdrs = ["well_known_names.h"], + deps = [ + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "wasm_vm_interface", + hdrs = ["wasm_vm.h"], + deps = [ + ":well_known_names", + "//source/common/common:minimal_logger_lib", + ], +) + +envoy_cc_library( + name = "wasm_vm_lib", + srcs = ["wasm_vm.cc"], + deps = [ + ":wasm_vm_interface", + "//source/common/common:assert_lib", + "//source/extensions/common/wasm/null:null_lib", + ], +) diff --git a/source/extensions/common/wasm/null/BUILD b/source/extensions/common/wasm/null/BUILD new file mode 100644 index 000000000000..eed8e62d2e49 --- /dev/null +++ b/source/extensions/common/wasm/null/BUILD @@ -0,0 +1,47 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "null_vm_plugin_interface", + hdrs = ["null_vm_plugin.h"], + deps = [ + "//source/extensions/common/wasm:wasm_vm_interface", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_cc_library( + name = "null_vm_lib", + srcs = ["null_vm.cc"], + hdrs = ["null_vm.h"], + deps = [ + ":null_vm_plugin_interface", + "//external:abseil_node_hash_map", + "//include/envoy/registry", + "//source/common/common:assert_lib", + "//source/extensions/common/wasm:wasm_vm_interface", + "//source/extensions/common/wasm:well_known_names", + ], +) + +envoy_cc_library( + name = "null_lib", + srcs = ["null.cc"], + hdrs = ["null.h"], + deps = [ + ":null_vm_lib", + ":null_vm_plugin_interface", + "//external:abseil_node_hash_map", + "//include/envoy/registry", + "//source/common/common:assert_lib", + "//source/extensions/common/wasm:wasm_vm_interface", + "//source/extensions/common/wasm:well_known_names", + ], +) diff --git a/source/extensions/common/wasm/null/null.cc b/source/extensions/common/wasm/null/null.cc new file mode 100644 index 000000000000..185dde60780a --- /dev/null +++ b/source/extensions/common/wasm/null/null.cc @@ -0,0 +1,27 @@ +#include "extensions/common/wasm/null/null.h" + +#include +#include +#include + +#include "envoy/registry/registry.h" + +#include "common/common/assert.h" + +#include "extensions/common/wasm/null/null_vm.h" +#include "extensions/common/wasm/null/null_vm_plugin.h" +#include "extensions/common/wasm/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace Null { + +WasmVmPtr createVm() { return std::make_unique(); } + +} // namespace Null +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null.h b/source/extensions/common/wasm/null/null.h new file mode 100644 index 000000000000..7d88fb356923 --- /dev/null +++ b/source/extensions/common/wasm/null/null.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "extensions/common/wasm/null/null_vm_plugin.h" +#include "extensions/common/wasm/wasm_vm.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace Null { + +WasmVmPtr createVm(); + +} // namespace Null +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm.cc b/source/extensions/common/wasm/null/null_vm.cc new file mode 100644 index 000000000000..b9957c6a67f2 --- /dev/null +++ b/source/extensions/common/wasm/null/null_vm.cc @@ -0,0 +1,104 @@ +#include "extensions/common/wasm/null/null_vm.h" + +#include +#include +#include + +#include "envoy/registry/registry.h" + +#include "common/common/assert.h" + +#include "extensions/common/wasm/null/null_vm_plugin.h" +#include "extensions/common/wasm/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace Null { + +WasmVmPtr NullVm::clone() { + auto cloned_null_vm = std::make_unique(*this); + cloned_null_vm->load(plugin_name_, false /* unused */); + return cloned_null_vm; +} + +// "Load" the plugin by obtaining a pointer to it from the factory. +bool NullVm::load(const std::string& name, bool /* allow_precompiled */) { + auto factory = Registry::FactoryRegistry::getFactory(name); + if (!factory) { + return false; + } + plugin_name_ = name; + plugin_ = factory->create(); + return true; +} + +void NullVm::link(absl::string_view /* name */, bool /* needs_emscripten */) {} + +void NullVm::makeModule(absl::string_view /* name */) { + // NullVm does not advertise code as emscripten so this will not get called. + NOT_REACHED_GCOVR_EXCL_LINE; +} + +void NullVm::start(Common::Wasm::Context* context) { + SaveRestoreContext saved_context(context); + plugin_->start(); +} + +uint64_t NullVm::getMemorySize() { return std::numeric_limits::max(); } + +// NulVm pointers are just native pointers. +absl::optional NullVm::getMemory(uint64_t pointer, uint64_t size) { + if (pointer == 0 && size != 0) { + return absl::nullopt; + } + return absl::string_view(reinterpret_cast(pointer), static_cast(size)); +} + +bool NullVm::getMemoryOffset(void* host_pointer, uint64_t* vm_pointer) { + *vm_pointer = reinterpret_cast(host_pointer); + return true; +} + +bool NullVm::setMemory(uint64_t pointer, uint64_t size, const void* data) { + if ((pointer == 0 || data == nullptr)) { + if (size != 0) { + return false; + } else { + return true; + } + } + auto p = reinterpret_cast(pointer); + memcpy(p, data, size); + return true; +} + +bool NullVm::setWord(uint64_t pointer, Word data) { + if (pointer == 0) { + return false; + } + auto p = reinterpret_cast(pointer); + memcpy(p, &data.u64_, sizeof(data.u64_)); + return true; +} + +bool NullVm::getWord(uint64_t pointer, Word* data) { + if (pointer == 0) { + return false; + } + auto p = reinterpret_cast(pointer); + memcpy(&data->u64_, p, sizeof(data->u64_)); + return true; +} + +absl::string_view NullVm::getUserSection(absl::string_view /* name */) { + // Return nothing: there is no WASM file. + return {}; +} + +} // namespace Null +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm.h b/source/extensions/common/wasm/null/null_vm.h new file mode 100644 index 000000000000..f3b90fabf1c8 --- /dev/null +++ b/source/extensions/common/wasm/null/null_vm.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include + +#include "envoy/registry/registry.h" + +#include "common/common/assert.h" + +#include "extensions/common/wasm/null/null_vm_plugin.h" +#include "extensions/common/wasm/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace Null { + +// The NullVm wraps a C++ WASM plugin which has been compiled with the WASM API +// and linked directly into the Envoy process. This is useful for development +// in that it permits the debugger to set breakpoints in both Envoy and the plugin. +struct NullVm : public WasmVm { + NullVm() = default; + NullVm(const NullVm& other) : plugin_name_(other.plugin_name_) {} + + // WasmVm + absl::string_view vm() override { return WasmVmNames::get().Null; } + bool cloneable() override { return true; }; + WasmVmPtr clone() override; + bool load(const std::string& code, bool allow_precompiled) override; + void link(absl::string_view debug_name, bool needs_emscripten) override; + void setMemoryLayout(uint64_t, uint64_t, uint64_t) override {} + void start(Common::Wasm::Context* context) override; + uint64_t getMemorySize() override; + absl::optional getMemory(uint64_t pointer, uint64_t size) override; + bool getMemoryOffset(void* host_pointer, uint64_t* vm_pointer) override; + bool setMemory(uint64_t pointer, uint64_t size, const void* data) override; + bool setWord(uint64_t pointer, Word data) override; + bool getWord(uint64_t pointer, Word* data) override; + void makeModule(absl::string_view name) override; + absl::string_view getUserSection(absl::string_view name) override; + +#define _FORWARD_GET_FUNCTION(_T) \ + void getFunction(absl::string_view function_name, _T* f) override { \ + plugin_->getFunction(function_name, f); \ + } + FOR_ALL_WASM_VM_EXPORTS(_FORWARD_GET_FUNCTION) +#undef _FORWARD_GET_FUNCTION + + // These are not needed for NullVm which invokes the handlers directly. +#define _REGISTER_CALLBACK(_T) \ + void registerCallback(absl::string_view, absl::string_view, _T, \ + typename ConvertFunctionTypeWordToUint32<_T>::type) override{}; + FOR_ALL_WASM_VM_IMPORTS(_REGISTER_CALLBACK) +#undef _REGISTER_CALLBACK + + // NullVm does not advertise code as emscripten so this will not get called. + std::unique_ptr> makeGlobal(absl::string_view, absl::string_view, + double) override { + NOT_REACHED_GCOVR_EXCL_LINE; + }; + std::unique_ptr> makeGlobal(absl::string_view, absl::string_view, Word) override { + NOT_REACHED_GCOVR_EXCL_LINE; + }; + + std::string plugin_name_; + std::unique_ptr plugin_; +}; + +} // namespace Null +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/null/null_vm_plugin.h b/source/extensions/common/wasm/null/null_vm_plugin.h new file mode 100644 index 000000000000..4dce2c617238 --- /dev/null +++ b/source/extensions/common/wasm/null/null_vm_plugin.h @@ -0,0 +1,50 @@ +#pragma once + +#include "extensions/common/wasm/wasm_vm.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace Null { + +// A wrapper for the natively compiled NullVm plugin which implements the WASM ABI. +class NullVmPlugin { +public: + NullVmPlugin() = default; + virtual ~NullVmPlugin() = default; + + // NB: These are defined rather than declared PURE because gmock uses __LINE__ internally for + // uniqueness, making it impossible to use FOR_ALL_WASM_VM_EXPORTS with MOCK_METHOD2. +#define _DEFINE_GET_FUNCTION(_T) \ + virtual void getFunction(absl::string_view, _T* f) { *f = nullptr; } + FOR_ALL_WASM_VM_EXPORTS(_DEFINE_GET_FUNCTION) +#undef _DEFIN_GET_FUNCTIONE + + virtual void start() PURE; +}; + +/** + * Pseudo-WASM plugins using the NullVM should implement this factory and register via + * Registry::registerFactory or the convenience class RegisterFactory. + */ +class NullVmPluginFactory { +public: + virtual ~NullVmPluginFactory() = default; + + /** + * Name of the plugin. + */ + virtual const std::string name() const PURE; + + /** + * Create an instance of the plugin. + */ + virtual std::unique_ptr create() const PURE; +}; + +} // namespace Null +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_vm.cc b/source/extensions/common/wasm/wasm_vm.cc new file mode 100644 index 000000000000..9a8dc2f98778 --- /dev/null +++ b/source/extensions/common/wasm/wasm_vm.cc @@ -0,0 +1,31 @@ +#include "extensions/common/wasm/wasm_vm.h" + +#include + +#include "extensions/common/wasm/null/null.h" +#include "extensions/common/wasm/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +thread_local Envoy::Extensions::Common::Wasm::Context* current_context_ = nullptr; +thread_local uint32_t effective_context_id_ = 0; + +WasmVmPtr createWasmVm(absl::string_view wasm_vm) { + if (wasm_vm.empty()) { + throw WasmException("Failed to create WASM VM with unspecified runtime."); + } else if (wasm_vm == WasmVmNames::get().Null) { + return Null::createVm(); + } else { + throw WasmException(fmt::format( + "Failed to create WASM VM using {} runtime. Envoy was compiled without support for it.", + wasm_vm)); + } +} + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/wasm_vm.h b/source/extensions/common/wasm/wasm_vm.h new file mode 100644 index 000000000000..42e5a08b0d33 --- /dev/null +++ b/source/extensions/common/wasm/wasm_vm.h @@ -0,0 +1,326 @@ +#pragma once + +#include + +#include "envoy/common/exception.h" + +#include "common/common/logger.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +class Context; + +// Represents a WASM-native word-sized datum. On 32-bit VMs, the high bits are always zero. +// The WASM/VM API treats all bits as significant. +struct Word { + Word(uint64_t w) : u64_(w) {} // Implicit conversion into Word. + uint32_t u32() const { return static_cast(u64_); } + uint64_t u64_; +}; + +// Convert Word type for use by 32-bit VMs. +template struct ConvertWordTypeToUint32 { using type = T; }; +template <> struct ConvertWordTypeToUint32 { using type = uint32_t; }; + +// Convert Word-based function types for 32-bit VMs. +template struct ConvertFunctionTypeWordToUint32 {}; +template struct ConvertFunctionTypeWordToUint32 { + using type = typename ConvertWordTypeToUint32::type (*)( + typename ConvertWordTypeToUint32::type...); +}; + +// A wrapper for a global variable within the VM. +template struct Global { + virtual ~Global() = default; + virtual T get() PURE; + virtual void set(const T& t) PURE; +}; + +// These are templates and its helper for constructing signatures of functions calling into and out +// of WASM VMs. +// - WasmFuncTypeHelper is a helper for WasmFuncType and shouldn't be used anywhere else than +// WasmFuncType definition. +// - WasmFuncType takes 4 template parameter which are number of argument, return type, context type +// and param type respectively, resolve to a function type. +// For example `WasmFuncType<3, void, Context*, Word>` resolves to `void(Context*, Word, Word, +// Word)` +template +struct WasmFuncTypeHelper {}; + +template +struct WasmFuncTypeHelper { + using type = typename WasmFuncTypeHelper::type; +}; + +template +struct WasmFuncTypeHelper<0, ReturnType, ContextType, ParamType, ReturnType(ContextType, Args...)> { + using type = ReturnType(ContextType, Args...); +}; + +template +using WasmFuncType = typename WasmFuncTypeHelper::type; + +// Calls into the WASM VM. +// 1st arg is always a pointer to Context (Context*). +template using WasmCallVoid = std::function>; +template using WasmCallWord = std::function>; + +#define FOR_ALL_WASM_VM_EXPORTS(_f) \ + _f(WasmCallVoid<0>) _f(WasmCallVoid<1>) _f(WasmCallVoid<2>) _f(WasmCallVoid<3>) \ + _f(WasmCallVoid<4>) _f(WasmCallVoid<5>) _f(WasmCallVoid<8>) _f(WasmCallWord<0>) \ + _f(WasmCallWord<1>) _f(WasmCallWord<3>) + +// Calls out of the WASM VM. +// 1st arg is always a pointer to raw_context (void*). +template using WasmCallbackVoid = WasmFuncType*; +template using WasmCallbackWord = WasmFuncType*; + +// Using the standard g++/clang mangling algorithm: +// https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling-builtin +// Extended with W = Word +// Z = void, j = uint32_t, l = int64_t, m = uint64_t +using WasmCallback_WWl = Word (*)(void*, Word, int64_t); +using WasmCallback_WWm = Word (*)(void*, Word, uint64_t); + +#define FOR_ALL_WASM_VM_IMPORTS(_f) \ + _f(WasmCallbackVoid<0>) _f(WasmCallbackVoid<1>) _f(WasmCallbackVoid<2>) _f(WasmCallbackVoid<3>) \ + _f(WasmCallbackVoid<4>) _f(WasmCallbackWord<0>) _f(WasmCallbackWord<1>) \ + _f(WasmCallbackWord<2>) _f(WasmCallbackWord<3>) _f(WasmCallbackWord<4>) \ + _f(WasmCallbackWord<5>) _f(WasmCallbackWord<6>) _f(WasmCallbackWord<7>) \ + _f(WasmCallbackWord<8>) _f(WasmCallbackWord<9>) _f(WasmCallback_WWl) \ + _f(WasmCallback_WWm) + +// Wasm VM instance. Provides the low level WASM interface. +class WasmVm : public Logger::Loggable { +public: + using WasmVmPtr = std::unique_ptr; + + virtual ~WasmVm() = default; + /** + * Return the VM identifier. + * @return one of WasmVmValues from well_known_names.h e.g. "envoy.wasm.vm.null". + */ + virtual absl::string_view vm() PURE; + + /** + * Whether or not the VM implementation supports cloning. Cloning is VM system dependent. + * When a VM is configured a single VM is instantiated to check that the .wasm file is valid and + * to do VM system specific initialization. In the case of WAVM this is potentially ahead-of-time + * compilation. Then, if cloning is supported, we clone that VM for each worker, potentially + * copying and sharing the initialized data structures for efficiency. Otherwise we create an new + * VM from scratch for each worker. + * @return true if the VM is cloneable. + */ + virtual bool cloneable() PURE; + + /** + * Make a worker/thread-specific copy if supported by the underlying VM system (see cloneable() + * above). If not supported, the caller will need to create a new VM from scratch. If supported, + * the clone may share compiled code and other read-only data with the source VM. + * @return a clone of 'this' (e.g. for a different worker/thread). + */ + virtual WasmVmPtr clone() PURE; + + /** + * Load the WASM code from a file. Return true on success. Once the module is loaded it can be + * queried, e.g. to see which version of emscripten support is required. After loading, the + * appropriate ABI callbacks can be registered and then the module can be link()ed (see below). + * @param code the WASM binary code (or registered NullVm plugin name). + * @param allow_precompiled if true, allows supporting VMs (e.g. WAVM) to load the binary + * machine code from a user-defined section of the WASM file. Because that code is not verified by + * the envoy process it is up to the user to ensure that the code is both safe and is built for + * the linked in version of WAVM. + * @return whether or not the load was successful. + */ + virtual bool load(const std::string& code, bool allow_precompiled) PURE; + + /** + * Link the WASM code to the host-provided functions and globals, e.g. the ABI. Prior to linking, + * the module should be loaded and the ABI callbacks registered (see above). Linking should be + * done once between load() and start(). + * @param debug_name user-provided name for use in log and error messages. + * @param needs_emscripten whether emscripten support should be provided (e.g. + * _emscripten_memcpy_bigHandler). Emscripten (http://https://emscripten.org/) is + * a C++ WebAssembly tool chain. + */ + virtual void link(absl::string_view debug_name, bool needs_emscripten) PURE; + + /** + * Set memory layout (start of dynamic heap base, etc.) in the VM. + * @param stack_base the location in VM memory of the stack. + * @param heap_base the location in VM memory of the heap. + * @param heap_base_ptr the location in VM memory of a location to store the heap pointer. + */ + virtual void setMemoryLayout(uint64_t stack_base, uint64_t heap_base, + uint64_t heap_base_pointer) PURE; + + /** + * Initialize globals (including calling global constructors) and call the 'start' function. + * Prior to calling start() the module should be load()ed, ABI callbacks should be registered + * (registerCallback), the module link()ed, and any exported functions should be gotten + * (getFunction). + * @param vm_context a context which represents the caller: in this case Envoy itself. + */ + virtual void start(Context* vm_context) PURE; + + /** + * Get size of the currently allocated memory in the VM. + * @return the size of memory in bytes. + */ + virtual uint64_t getMemorySize() PURE; + + /** + * Convert a block of memory in the VM to a string_view. + * @param pointer the offset into VM memory of the requested VM memory block. + * @param size the size of the requested VM memory block. + * @return if std::nullopt then the pointer/size pair were invalid, otherwise returns + * a host string_view pointing to the pointer/size pair in VM memory. + */ + virtual absl::optional getMemory(uint64_t pointer, uint64_t size) PURE; + + /** + * Convert a host pointer to memory in the VM into a VM "pointer" (an offset into the Memory). + * @param host_pointer a pointer to host memory to be converted into a VM offset (pointer). + * @param vm_pointer a pointer to an uint64_t to be filled with the offset in VM memory + * corresponding to 'host_pointer'. + * @return whether or not the host_pointer was a valid VM memory offset. + */ + virtual bool getMemoryOffset(void* host_pointer, uint64_t* vm_pointer) PURE; + + /** + * Set a block of memory in the VM, returns true on success, false if the pointer/size is invalid. + * @param pointer the offset into VM memory describing the start of a region of VM memory. + * @param size the size of the region of VM memory. + * @return whether or not the pointer/size pair was a valid VM memory block. + */ + virtual bool setMemory(uint64_t pointer, uint64_t size, const void* data) PURE; + + /** + * Get a VM native Word (e.g. sizeof(void*) or sizeof(size_t)) from VM memory, returns true on + * success, false if the pointer is invalid. WASM-32 VMs have 32-bit native words and WASM-64 VMs + * (not yet supported) will have 64-bit words as does the Null VM (compiled into 64-bit Envoy). + * This function can be used to chase pointers in VM memory. + * @param pointer the offset into VM memory describing the start of VM native word size block. + * @param data a pointer to a Word whose contents will be filled from the VM native word at + * 'pointer'. + * @return whether or not the pointer was to a valid VM memory block of VM native word size. + */ + virtual bool getWord(uint64_t pointer, Word* data) PURE; + + /** + * Set a Word in the VM, returns true on success, false if the pointer is invalid. + * See getWord above for details. This function can be used (for example) to set indirect pointer + * return values (e.g. proxy_getHeaderHapValue(... const char** value_ptr, size_t* value_size). + * @param pointer the offset into VM memory describing the start of VM native word size block. + * @param data a Word whose contents will be written in VM native word size at 'pointer'. + * @return whether or not the pointer was to a valid VM memory block of VM native word size. + */ + virtual bool setWord(uint64_t pointer, Word data) PURE; + + /** + * Make a new intrinsic module (e.g. for Emscripten support). + * @param name the name of the module to make. + */ + virtual void makeModule(absl::string_view name) PURE; + + /** + * Get the contents of the user section with the given name or "" if it does not exist. + * @param name the name of the user section to get. + * @return the contents of the user section (if any). The result will be empty() if there + * is no such section. + */ + virtual absl::string_view getUserSection(absl::string_view name) PURE; + + /** + * Get typed function exported by the WASM module. + */ +#define _GET_FUNCTION(_T) virtual void getFunction(absl::string_view function_name, _T* f) PURE; + FOR_ALL_WASM_VM_EXPORTS(_GET_FUNCTION) +#undef _GET_FUNCTION + + /** + * Register typed callbacks exported by the host environment. + */ +#define _REGISTER_CALLBACK(_T) \ + virtual void registerCallback(absl::string_view moduleName, absl::string_view function_name, \ + _T f, typename ConvertFunctionTypeWordToUint32<_T>::type) PURE; + FOR_ALL_WASM_VM_IMPORTS(_REGISTER_CALLBACK) +#undef _REGISTER_CALLBACK + + /** + * Register typed value exported by the host environment. + * @param module_name the name of the module to which to export the global. + * @param name the name of the global variable to export. + * @param initial_value the initial value of the global. + * @return a Global object which can be used to access the exported global. + */ + virtual std::unique_ptr> makeGlobal(absl::string_view module_name, + absl::string_view name, Word initial_value) PURE; + + /** + * Register typed value exported by the host environment. + * @param module_name the name of the module to which to export the global. + * @param name the name of the global variable to export. + * @param initial_value the initial value of the global. + * @return a Global object which can be used to access the exported global. + */ + virtual std::unique_ptr> + makeGlobal(absl::string_view module_name, absl::string_view name, double initial_value) PURE; +}; +using WasmVmPtr = std::unique_ptr; + +// Exceptions for issues with the WasmVm. +class WasmVmException : public EnvoyException { +public: + using EnvoyException::EnvoyException; +}; + +// Exceptions for issues with the WebAssembly code. +class WasmException : public EnvoyException { +public: + using EnvoyException::EnvoyException; +}; + +// Thread local state set during a call into a WASM VM so that calls coming out of the +// VM can be attributed correctly to calling Filter. We use thread_local instead of ThreadLocal +// because this state is live only during the calls and does not need to be initialized consistently +// over all workers as with ThreadLocal data. +extern thread_local Envoy::Extensions::Common::Wasm::Context* current_context_; + +// Requested effective context set by code within the VM to request that the calls coming out of the +// VM be attributed to another filter, for example if a control plane gRPC comes back to the +// RootContext which effects some set of waiting filters. +extern thread_local uint32_t effective_context_id_; + +// Helper to save and restore thread local VM call context information to support reentrant calls. +// NB: this happens for example when a call from the VM invokes a handler which needs to _malloc +// memory in the VM. +struct SaveRestoreContext { + explicit SaveRestoreContext(Context* context) { + saved_context = current_context_; + saved_effective_context_id_ = effective_context_id_; + current_context_ = context; + effective_context_id_ = 0; // No effective context id. + } + ~SaveRestoreContext() { + current_context_ = saved_context; + effective_context_id_ = saved_effective_context_id_; + } + Context* saved_context; + uint32_t saved_effective_context_id_; +}; + +// Create a new low-level WASM VM of the give type (e.g. "envoy.wasm.vm.wavm"). +WasmVmPtr createWasmVm(absl::string_view vm); + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/wasm/well_known_names.h b/source/extensions/common/wasm/well_known_names.h new file mode 100644 index 000000000000..3674ed3ee8b5 --- /dev/null +++ b/source/extensions/common/wasm/well_known_names.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "common/singleton/const_singleton.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { + +/** + * Well-known wasm VM names. + * NOTE: New wasm VMs should use the well known name: envoy.wasm.vm.name. + */ +class WasmVmValues { +public: + // Null sandbox: modules must be compiled into envoy and registered name is given in the + // DataSource.inline_string. + const std::string Null = "envoy.wasm.vm.null"; +}; + +using WasmVmNames = ConstSingleton; + +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/wasm/BUILD b/test/extensions/common/wasm/BUILD new file mode 100644 index 000000000000..490402dc1784 --- /dev/null +++ b/test/extensions/common/wasm/BUILD @@ -0,0 +1,19 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_cc_test_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_test( + name = "wasm_vm_test", + srcs = ["wasm_vm_test.cc"], + deps = [ + "//source/extensions/common/wasm:wasm_vm_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/common/wasm/wasm_vm_test.cc b/test/extensions/common/wasm/wasm_vm_test.cc new file mode 100644 index 000000000000..e1ecd2590f6f --- /dev/null +++ b/test/extensions/common/wasm/wasm_vm_test.cc @@ -0,0 +1,103 @@ +#include "envoy/registry/registry.h" + +#include "extensions/common/wasm/null/null_vm_plugin.h" +#include "extensions/common/wasm/wasm_vm.h" + +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Wasm { +namespace { + +class TestNullVmPlugin : public Null::NullVmPlugin { +public: + TestNullVmPlugin() = default; + ~TestNullVmPlugin() override = default; + + MOCK_METHOD0(start, void()); +}; + +class PluginFactory : public Null::NullVmPluginFactory { +public: + PluginFactory() = default; + + const std::string name() const override { return "test_null_vm_plugin"; } + std::unique_ptr create() const override; +}; + +TestNullVmPlugin* test_null_vm_plugin_ = nullptr; +Envoy::Registry::RegisterFactory register_; + +std::unique_ptr PluginFactory::create() const { + auto result = std::make_unique(); + test_null_vm_plugin_ = result.get(); + return result; +} + +TEST(WasmVmTest, BadVmType) { EXPECT_THROW(createWasmVm("bad.vm"), WasmException); } + +TEST(WasmVmTest, NullVmStartup) { + auto wasm_vm = createWasmVm("envoy.wasm.vm.null"); + EXPECT_TRUE(wasm_vm != nullptr); + EXPECT_TRUE(wasm_vm->cloneable()); + auto wasm_vm_clone = wasm_vm->clone(); + EXPECT_TRUE(wasm_vm_clone != nullptr); + EXPECT_TRUE(wasm_vm->getUserSection("user").empty()); +} + +TEST(WasmVmTest, NullVmMemory) { + auto wasm_vm = createWasmVm("envoy.wasm.vm.null"); + EXPECT_EQ(wasm_vm->getMemorySize(), std::numeric_limits::max()); + std::string d = "data"; + auto m = wasm_vm->getMemory(reinterpret_cast(d.data()), d.size()).value(); + EXPECT_EQ(m.data(), d.data()); + EXPECT_EQ(m.size(), d.size()); + uint64_t offset; + char l; + EXPECT_TRUE(wasm_vm->getMemoryOffset(&l, &offset)); + EXPECT_EQ(offset, reinterpret_cast(&l)); + char c; + char z = 'z'; + EXPECT_TRUE(wasm_vm->setMemory(reinterpret_cast(&c), 1, &z)); + EXPECT_EQ(c, z); + + Word w(13); + EXPECT_TRUE( + wasm_vm->setWord(reinterpret_cast(&w), std::numeric_limits::max())); + EXPECT_EQ(w.u64_, std::numeric_limits::max()); + + Word w2(0); + w.u64_ = 7; + EXPECT_TRUE(wasm_vm->getWord(reinterpret_cast(&w), &w2)); + EXPECT_EQ(w2.u64_, 7); +} + +TEST(WasmVmTest, NullVmStart) { + auto wasm_vm = createWasmVm("envoy.wasm.vm.null"); + EXPECT_TRUE(wasm_vm->load("test_null_vm_plugin", true)); + wasm_vm->link("test", false); + // Test that context argument to start is pushed and that the effective_context_id_ is reset. + // Test that the original values are restored. + Context* context1 = reinterpret_cast(1); + Context* context2 = reinterpret_cast(2); + current_context_ = context1; + effective_context_id_ = 1; + EXPECT_CALL(*test_null_vm_plugin_, start()).WillOnce(Invoke([context2]() { + EXPECT_EQ(current_context_, context2); + EXPECT_EQ(effective_context_id_, 0); + })); + wasm_vm->start(context2); + EXPECT_EQ(current_context_, context1); + EXPECT_EQ(effective_context_id_, 1); +} + +} // namespace +} // namespace Wasm +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index e0ccece9cfc5..a6bbeb6ec043 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -9,6 +9,7 @@ ALPN ALS AMZ API +ABI ASAN ASCII ASSERTs @@ -294,6 +295,9 @@ VC VH VHDS VLOG +VM +WASM +WAVM WKT WRR WS @@ -451,6 +455,7 @@ dynamodb eg emplace emplaced +emscripten enablement encodings endian