blob: abad6d5149afd0c889f194a9bda5717d33ef5ed6 [file] [log] [blame]
// Copyright 2016-2019 Envoy Project Authors
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "include/proxy-wasm/v8.h"
#include <cassert>
#include <iomanip>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "include/proxy-wasm/limits.h"
#include "include/v8-version.h"
#include "include/v8.h"
#include "src/wasm/c-api.h"
#include "wasm-api/wasm.hh"
namespace v8::internal {
extern unsigned int FLAG_wasm_max_mem_pages;
} // namespace v8::internal
namespace proxy_wasm {
namespace v8 {
wasm::Engine *engine() {
static std::once_flag init;
static wasm::own<wasm::Engine> engine;
std::call_once(init, []() {
::v8::internal::FLAG_wasm_max_mem_pages =
PROXY_WASM_HOST_MAX_WASM_MEMORY_SIZE_BYTES / PROXY_WASM_HOST_WASM_MEMORY_PAGE_SIZE_BYTES;
::v8::V8::EnableWebAssemblyTrapHandler(true);
engine = wasm::Engine::make();
});
return engine.get();
}
struct FuncData {
FuncData(std::string name) : name_(std::move(name)) {}
std::string name_;
wasm::own<wasm::Func> callback_;
void *raw_func_{};
WasmVm *vm_{};
};
using FuncDataPtr = std::unique_ptr<FuncData>;
class V8 : public WasmVm {
public:
V8() = default;
// WasmVm
std::string_view getEngineName() override { return "v8"; }
bool load(std::string_view bytecode, std::string_view precompiled,
const std::unordered_map<uint32_t, std::string> &function_names) override;
std::string_view getPrecompiledSectionName() override;
bool link(std::string_view debug_name) override;
Cloneable cloneable() override { return Cloneable::CompiledBytecode; }
std::unique_ptr<WasmVm> clone() override;
uint64_t getMemorySize() override;
std::optional<std::string_view> getMemory(uint64_t pointer, uint64_t size) override;
bool setMemory(uint64_t pointer, uint64_t size, const void *data) override;
bool getWord(uint64_t pointer, Word *word) override;
bool setWord(uint64_t pointer, Word word) override;
size_t getWordSize() override { return sizeof(uint32_t); };
#define _REGISTER_HOST_FUNCTION(T) \
void registerCallback(std::string_view module_name, std::string_view function_name, T, \
typename ConvertFunctionTypeWordToUint32<T>::type f) override { \
registerHostFunctionImpl(module_name, function_name, f); \
};
FOR_ALL_WASM_VM_IMPORTS(_REGISTER_HOST_FUNCTION)
#undef _REGISTER_HOST_FUNCTION
#define _GET_MODULE_FUNCTION(T) \
void getFunction(std::string_view function_name, T *f) override { \
getModuleFunctionImpl(function_name, f); \
};
FOR_ALL_WASM_VM_EXPORTS(_GET_MODULE_FUNCTION)
#undef _GET_MODULE_FUNCTION
void terminate() override;
private:
std::string getFailMessage(std::string_view function_name, wasm::own<wasm::Trap> trap);
template <typename... Args>
void registerHostFunctionImpl(std::string_view module_name, std::string_view function_name,
void (*function)(Args...));
template <typename R, typename... Args>
void registerHostFunctionImpl(std::string_view module_name, std::string_view function_name,
R (*function)(Args...));
template <typename... Args>
void getModuleFunctionImpl(std::string_view function_name,
std::function<void(ContextBase *, Args...)> *function);
template <typename R, typename... Args>
void getModuleFunctionImpl(std::string_view function_name,
std::function<R(ContextBase *, Args...)> *function);
wasm::own<wasm::Store> store_;
wasm::own<wasm::Module> module_;
wasm::own<wasm::Shared<wasm::Module>> shared_module_;
wasm::own<wasm::Instance> instance_;
wasm::own<wasm::Memory> memory_;
wasm::own<wasm::Table> table_;
std::unordered_map<std::string, FuncDataPtr> host_functions_;
std::unordered_map<std::string, wasm::own<wasm::Func>> module_functions_;
std::unordered_map<uint32_t, std::string> function_names_index_;
};
// Helper functions.
static std::string printValue(const wasm::Val &value) {
switch (value.kind()) {
case wasm::I32:
return std::to_string(value.get<uint32_t>());
case wasm::I64:
return std::to_string(value.get<uint64_t>());
case wasm::F32:
return std::to_string(value.get<float>());
case wasm::F64:
return std::to_string(value.get<double>());
default:
return "unknown";
}
}
static std::string printValues(const wasm::Val values[], size_t size) {
if (size == 0) {
return "";
}
std::string s;
for (size_t i = 0; i < size; i++) {
if (i != 0U) {
s.append(", ");
}
s.append(printValue(values[i]));
}
return s;
}
static const char *printValKind(wasm::ValKind kind) {
switch (kind) {
case wasm::I32:
return "i32";
case wasm::I64:
return "i64";
case wasm::F32:
return "f32";
case wasm::F64:
return "f64";
case wasm::ANYREF:
return "anyref";
case wasm::FUNCREF:
return "funcref";
default:
return "unknown";
}
}
static std::string printValTypes(const wasm::ownvec<wasm::ValType> &types) {
if (types.size() == 0) {
return "void";
}
std::string s;
s.reserve(types.size() * 8 /* max size + " " */ - 1);
for (size_t i = 0; i < types.size(); i++) {
if (i != 0U) {
s.append(" ");
}
s.append(printValKind(types[i]->kind()));
}
return s;
}
static bool equalValTypes(const wasm::ownvec<wasm::ValType> &left,
const wasm::ownvec<wasm::ValType> &right) {
if (left.size() != right.size()) {
return false;
}
for (size_t i = 0; i < left.size(); i++) {
if (left[i]->kind() != right[i]->kind()) {
return false;
}
}
return true;
}
// Template magic.
template <typename T> struct ConvertWordType {
using type = T; // NOLINT(readability-identifier-naming)
};
template <> struct ConvertWordType<Word> {
using type = uint32_t; // NOLINT(readability-identifier-naming)
};
template <typename T> wasm::Val makeVal(T t) { return wasm::Val::make(t); }
template <> wasm::Val makeVal(Word t) { return wasm::Val::make(static_cast<uint32_t>(t.u64_)); }
template <typename T> constexpr auto convertArgToValKind();
template <> constexpr auto convertArgToValKind<Word>() { return wasm::I32; };
template <> constexpr auto convertArgToValKind<uint32_t>() { return wasm::I32; };
template <> constexpr auto convertArgToValKind<int64_t>() { return wasm::I64; };
template <> constexpr auto convertArgToValKind<uint64_t>() { return wasm::I64; };
template <> constexpr auto convertArgToValKind<double>() { return wasm::F64; };
template <typename T, std::size_t... I>
constexpr auto convertArgsTupleToValTypesImpl(std::index_sequence<I...> /*comptime*/) {
return wasm::ownvec<wasm::ValType>::make(
wasm::ValType::make(convertArgToValKind<typename std::tuple_element<I, T>::type>())...);
}
template <typename T> constexpr auto convertArgsTupleToValTypes() {
return convertArgsTupleToValTypesImpl<T>(std::make_index_sequence<std::tuple_size<T>::value>());
}
template <typename T, typename U, std::size_t... I>
constexpr T convertValTypesToArgsTupleImpl(const U &arr, std::index_sequence<I...> /*comptime*/) {
return std::make_tuple(
(arr[I]
.template get<
typename ConvertWordType<typename std::tuple_element<I, T>::type>::type>())...);
}
template <typename T, typename U> constexpr T convertValTypesToArgsTuple(const U &arr) {
return convertValTypesToArgsTupleImpl<T>(arr,
std::make_index_sequence<std::tuple_size<T>::value>());
}
// V8 implementation.
bool V8::load(std::string_view bytecode, std::string_view precompiled,
const std::unordered_map<uint32_t, std::string> &function_names) {
store_ = wasm::Store::make(engine());
if (store_ == nullptr) {
return false;
}
if (!precompiled.empty()) {
auto vec = wasm::vec<byte_t>::make_uninitialized(precompiled.size());
::memcpy(vec.get(), precompiled.data(), precompiled.size());
module_ = wasm::Module::deserialize(store_.get(), vec);
if (module_ == nullptr) {
return false;
}
} else {
auto vec = wasm::vec<byte_t>::make_uninitialized(bytecode.size());
::memcpy(vec.get(), bytecode.data(), bytecode.size());
module_ = wasm::Module::make(store_.get(), vec);
if (module_ == nullptr) {
return false;
}
}
shared_module_ = module_->share();
if (shared_module_ == nullptr) {
return false;
}
function_names_index_ = function_names;
return true;
}
std::unique_ptr<WasmVm> V8::clone() {
assert(shared_module_ != nullptr);
auto clone = std::make_unique<V8>();
if (clone == nullptr) {
return nullptr;
}
clone->store_ = wasm::Store::make(engine());
if (clone->store_ == nullptr) {
return nullptr;
}
clone->module_ = wasm::Module::obtain(clone->store_.get(), shared_module_.get());
if (clone->module_ == nullptr) {
return nullptr;
}
auto *integration_clone = integration()->clone();
if (integration_clone == nullptr) {
return nullptr;
}
clone->integration().reset(integration_clone);
clone->function_names_index_ = function_names_index_;
return clone;
}
#if defined(__linux__) && defined(__x86_64__)
#define WEE8_PLATFORM "linux_x86_64"
#else
#define WEE8_PLATFORM ""
#endif
std::string_view V8::getPrecompiledSectionName() {
static const auto name =
sizeof(WEE8_PLATFORM) - 1 > 0
? ("precompiled_wee8_v" + std::to_string(V8_MAJOR_VERSION) + "." +
std::to_string(V8_MINOR_VERSION) + "." + std::to_string(V8_BUILD_NUMBER) + "." +
std::to_string(V8_PATCH_LEVEL) + "_" + WEE8_PLATFORM)
: "";
return name;
}
bool V8::link(std::string_view /*debug_name*/) {
assert(module_ != nullptr);
const auto import_types = module_.get()->imports();
std::vector<const wasm::Extern *> imports;
for (size_t i = 0; i < import_types.size(); i++) {
std::string_view module(import_types[i]->module().get(), import_types[i]->module().size());
std::string_view name(import_types[i]->name().get(), import_types[i]->name().size());
const auto *import_type = import_types[i]->type();
switch (import_type->kind()) {
case wasm::EXTERN_FUNC: {
auto it = host_functions_.find(std::string(module) + "." + std::string(name));
if (it == host_functions_.end()) {
fail(FailState::UnableToInitializeCode,
std::string("Failed to load Wasm module due to a missing import: ") +
std::string(module) + "." + std::string(name));
return false;
}
auto *func = it->second->callback_.get();
if (!equalValTypes(import_type->func()->params(), func->type()->params()) ||
!equalValTypes(import_type->func()->results(), func->type()->results())) {
fail(FailState::UnableToInitializeCode,
std::string("Failed to load Wasm module due to an import type mismatch: ") +
std::string(module) + "." + std::string(name) +
", want: " + printValTypes(import_type->func()->params()) + " -> " +
printValTypes(import_type->func()->results()) +
", but host exports: " + printValTypes(func->type()->params()) + " -> " +
printValTypes(func->type()->results()));
return false;
}
imports.push_back(func);
} break;
case wasm::EXTERN_GLOBAL: {
// TODO(PiotrSikora): add support when/if needed.
fail(FailState::UnableToInitializeCode,
"Failed to load Wasm module due to a missing import: " + std::string(module) + "." +
std::string(name));
return false;
} break;
case wasm::EXTERN_MEMORY: {
assert(memory_ == nullptr);
auto type = wasm::MemoryType::make(import_type->memory()->limits());
if (type == nullptr) {
return false;
}
memory_ = wasm::Memory::make(store_.get(), type.get());
if (memory_ == nullptr) {
return false;
}
imports.push_back(memory_.get());
} break;
case wasm::EXTERN_TABLE: {
assert(table_ == nullptr);
auto type =
wasm::TableType::make(wasm::ValType::make(import_type->table()->element()->kind()),
import_type->table()->limits());
if (type == nullptr) {
return false;
}
table_ = wasm::Table::make(store_.get(), type.get());
if (table_ == nullptr) {
return false;
}
imports.push_back(table_.get());
} break;
}
}
if (import_types.size() != imports.size()) {
return false;
}
instance_ = wasm::Instance::make(store_.get(), module_.get(), imports.data());
if (instance_ == nullptr) {
fail(FailState::UnableToInitializeCode, "Failed to create new Wasm instance");
return false;
}
const auto export_types = module_.get()->exports();
const auto exports = instance_.get()->exports();
assert(export_types.size() == exports.size());
for (size_t i = 0; i < export_types.size(); i++) {
std::string_view name(export_types[i]->name().get(), export_types[i]->name().size());
const auto *export_type = export_types[i]->type();
auto *export_item = exports[i].get();
assert(export_type->kind() == export_item->kind());
switch (export_type->kind()) {
case wasm::EXTERN_FUNC: {
assert(export_item->func() != nullptr);
module_functions_.insert_or_assign(std::string(name), export_item->func()->copy());
} break;
case wasm::EXTERN_GLOBAL: {
// TODO(PiotrSikora): add support when/if needed.
} break;
case wasm::EXTERN_MEMORY: {
assert(export_item->memory() != nullptr);
assert(memory_ == nullptr);
memory_ = exports[i]->memory()->copy();
if (memory_ == nullptr) {
return false;
}
} break;
case wasm::EXTERN_TABLE: {
// TODO(PiotrSikora): add support when/if needed.
} break;
}
}
return true;
}
uint64_t V8::getMemorySize() { return memory_->data_size(); }
std::optional<std::string_view> V8::getMemory(uint64_t pointer, uint64_t size) {
assert(memory_ != nullptr);
if (pointer + size > memory_->data_size()) {
return std::nullopt;
}
return std::string_view(memory_->data() + pointer, size);
}
bool V8::setMemory(uint64_t pointer, uint64_t size, const void *data) {
assert(memory_ != nullptr);
if (pointer + size > memory_->data_size()) {
return false;
}
::memcpy(memory_->data() + pointer, data, size);
return true;
}
bool V8::getWord(uint64_t pointer, Word *word) {
constexpr auto size = sizeof(uint32_t);
if (pointer + size > memory_->data_size()) {
return false;
}
uint32_t word32;
::memcpy(&word32, memory_->data() + pointer, size);
word->u64_ = wasmtoh(word32);
return true;
}
bool V8::setWord(uint64_t pointer, Word word) {
constexpr auto size = sizeof(uint32_t);
if (pointer + size > memory_->data_size()) {
return false;
}
uint32_t word32 = htowasm(word.u32());
::memcpy(memory_->data() + pointer, &word32, size);
return true;
}
template <typename... Args>
void V8::registerHostFunctionImpl(std::string_view module_name, std::string_view function_name,
void (*function)(Args...)) {
auto data =
std::make_unique<FuncData>(std::string(module_name) + "." + std::string(function_name));
auto type = wasm::FuncType::make(convertArgsTupleToValTypes<std::tuple<Args...>>(),
convertArgsTupleToValTypes<std::tuple<>>());
auto func = wasm::Func::make(
store_.get(), type.get(),
[](void *data, const wasm::Val params[], wasm::Val /*results*/[]) -> wasm::own<wasm::Trap> {
auto *func_data = reinterpret_cast<FuncData *>(data);
const bool log = func_data->vm_->cmpLogLevel(LogLevel::trace);
if (log) {
func_data->vm_->integration()->trace("[vm->host] " + func_data->name_ + "(" +
printValues(params, sizeof...(Args)) + ")");
}
auto args = convertValTypesToArgsTuple<std::tuple<Args...>>(params);
auto function = reinterpret_cast<void (*)(Args...)>(func_data->raw_func_);
std::apply(function, args);
if (log) {
func_data->vm_->integration()->trace("[vm<-host] " + func_data->name_ + " return: void");
}
return nullptr;
},
data.get());
data->vm_ = this;
data->callback_ = std::move(func);
data->raw_func_ = reinterpret_cast<void *>(function);
host_functions_.insert_or_assign(std::string(module_name) + "." + std::string(function_name),
std::move(data));
}
template <typename R, typename... Args>
void V8::registerHostFunctionImpl(std::string_view module_name, std::string_view function_name,
R (*function)(Args...)) {
auto data =
std::make_unique<FuncData>(std::string(module_name) + "." + std::string(function_name));
auto type = wasm::FuncType::make(convertArgsTupleToValTypes<std::tuple<Args...>>(),
convertArgsTupleToValTypes<std::tuple<R>>());
auto func = wasm::Func::make(
store_.get(), type.get(),
[](void *data, const wasm::Val params[], wasm::Val results[]) -> wasm::own<wasm::Trap> {
auto *func_data = reinterpret_cast<FuncData *>(data);
const bool log = func_data->vm_->cmpLogLevel(LogLevel::trace);
if (log) {
func_data->vm_->integration()->trace("[vm->host] " + func_data->name_ + "(" +
printValues(params, sizeof...(Args)) + ")");
}
auto args = convertValTypesToArgsTuple<std::tuple<Args...>>(params);
auto function = reinterpret_cast<R (*)(Args...)>(func_data->raw_func_);
R rvalue = std::apply(function, args);
results[0] = makeVal(rvalue);
if (log) {
func_data->vm_->integration()->trace("[vm<-host] " + func_data->name_ +
" return: " + std::to_string(rvalue));
}
return nullptr;
},
data.get());
data->vm_ = this;
data->callback_ = std::move(func);
data->raw_func_ = reinterpret_cast<void *>(function);
host_functions_.insert_or_assign(std::string(module_name) + "." + std::string(function_name),
std::move(data));
}
template <typename... Args>
void V8::getModuleFunctionImpl(std::string_view function_name,
std::function<void(ContextBase *, Args...)> *function) {
auto it = module_functions_.find(std::string(function_name));
if (it == module_functions_.end()) {
*function = nullptr;
return;
}
const wasm::Func *func = it->second.get();
auto arg_valtypes = convertArgsTupleToValTypes<std::tuple<Args...>>();
auto result_valtypes = convertArgsTupleToValTypes<std::tuple<>>();
if (!equalValTypes(func->type()->params(), arg_valtypes) ||
!equalValTypes(func->type()->results(), result_valtypes)) {
fail(FailState::UnableToInitializeCode,
"Bad function signature for: " + std::string(function_name) +
", want: " + printValTypes(arg_valtypes) + " -> " + printValTypes(result_valtypes) +
", but the module exports: " + printValTypes(func->type()->params()) + " -> " +
printValTypes(func->type()->results()));
*function = nullptr;
return;
}
*function = [func, function_name, this](ContextBase *context, Args... args) -> void {
const bool log = cmpLogLevel(LogLevel::trace);
SaveRestoreContext saved_context(context);
wasm::own<wasm::Trap> trap = nullptr;
// Workaround for MSVC++ not supporting zero-sized arrays.
if constexpr (sizeof...(args) > 0) {
wasm::Val params[] = {makeVal(args)...};
if (log) {
integration()->trace("[host->vm] " + std::string(function_name) + "(" +
printValues(params, sizeof...(Args)) + ")");
}
trap = func->call(params, nullptr);
} else {
if (log) {
integration()->trace("[host->vm] " + std::string(function_name) + "()");
}
trap = func->call(nullptr, nullptr);
}
if (trap) {
fail(FailState::RuntimeError, getFailMessage(std::string(function_name), std::move(trap)));
return;
}
if (log) {
integration()->trace("[host<-vm] " + std::string(function_name) + " return: void");
}
};
}
template <typename R, typename... Args>
void V8::getModuleFunctionImpl(std::string_view function_name,
std::function<R(ContextBase *, Args...)> *function) {
auto it = module_functions_.find(std::string(function_name));
if (it == module_functions_.end()) {
*function = nullptr;
return;
}
const wasm::Func *func = it->second.get();
auto arg_valtypes = convertArgsTupleToValTypes<std::tuple<Args...>>();
auto result_valtypes = convertArgsTupleToValTypes<std::tuple<R>>();
if (!equalValTypes(func->type()->params(), arg_valtypes) ||
!equalValTypes(func->type()->results(), result_valtypes)) {
fail(FailState::UnableToInitializeCode,
"Bad function signature for: " + std::string(function_name) +
", want: " + printValTypes(arg_valtypes) + " -> " + printValTypes(result_valtypes) +
", but the module exports: " + printValTypes(func->type()->params()) + " -> " +
printValTypes(func->type()->results()));
*function = nullptr;
return;
}
*function = [func, function_name, this](ContextBase *context, Args... args) -> R {
const bool log = cmpLogLevel(LogLevel::trace);
SaveRestoreContext saved_context(context);
wasm::Val results[1];
wasm::own<wasm::Trap> trap = nullptr;
// Workaround for MSVC++ not supporting zero-sized arrays.
if constexpr (sizeof...(args) > 0) {
wasm::Val params[] = {makeVal(args)...};
if (log) {
integration()->trace("[host->vm] " + std::string(function_name) + "(" +
printValues(params, sizeof...(Args)) + ")");
}
trap = func->call(params, results);
} else {
if (log) {
integration()->trace("[host->vm] " + std::string(function_name) + "()");
}
trap = func->call(nullptr, results);
}
if (trap) {
fail(FailState::RuntimeError, getFailMessage(std::string(function_name), std::move(trap)));
return R{};
}
R rvalue = results[0].get<typename ConvertWordTypeToUint32<R>::type>();
if (log) {
integration()->trace("[host<-vm] " + std::string(function_name) +
" return: " + std::to_string(rvalue));
}
return rvalue;
};
}
void V8::terminate() {
auto *store_impl = reinterpret_cast<wasm::StoreImpl *>(store_.get());
auto *isolate = store_impl->isolate();
isolate->TerminateExecution();
while (isolate->IsExecutionTerminating()) {
std::this_thread::yield();
}
}
std::string V8::getFailMessage(std::string_view function_name, wasm::own<wasm::Trap> trap) {
auto message = "Function: " + std::string(function_name) + " failed: ";
message += std::string(trap->message().get(), trap->message().size());
if (function_names_index_.empty()) {
return message;
}
auto trace = trap->trace();
message += "\nProxy-Wasm plugin in-VM backtrace:";
for (size_t i = 0; i < trace.size(); ++i) {
auto *frame = trace[i].get();
std::ostringstream oss;
oss << std::setw(3) << std::setfill(' ') << std::to_string(i);
message += "\n" + oss.str() + ": ";
std::stringstream address;
address << std::hex << frame->module_offset();
message += " 0x" + address.str() + " - ";
auto func_index = frame->func_index();
auto it = function_names_index_.find(func_index);
if (it != function_names_index_.end()) {
message += it->second;
} else {
message += "unknown(function index:" + std::to_string(func_index) + ")";
}
}
return message;
}
} // namespace v8
std::unique_ptr<WasmVm> createV8Vm() { return std::make_unique<v8::V8>(); }
} // namespace proxy_wasm