diff --git a/source/extensions/common/wasm/exports.cc b/source/extensions/common/wasm/exports.cc index 0ad208b066e5..81d48833b37d 100644 --- a/source/extensions/common/wasm/exports.cc +++ b/source/extensions/common/wasm/exports.cc @@ -705,6 +705,13 @@ Word wasi_unstable_fd_write(void* raw_context, Word fd, Word iovs, Word iovs_len return 0; // __WASI_ESUCCESS } +// __wasi_errno_t __wasi_fd_read(_wasi_fd_t fd, const __wasi_iovec_t *iovs, +// size_t iovs_len, __wasi_size_t *nread); +Word wasi_unstable_fd_read(void*, Word, Word, Word, Word) { + // Don't support reading of any files. + return 52; // __WASI_ERRNO_ENOSYS +} + // __wasi_errno_t __wasi_fd_seek(__wasi_fd_t fd, __wasi_filedelta_t offset, __wasi_whence_t // whence,__wasi_filesize_t *newoffset); Word wasi_unstable_fd_seek(void*, Word, int64_t, Word, Word) { @@ -714,6 +721,26 @@ Word wasi_unstable_fd_seek(void*, Word, int64_t, Word, Word) { // __wasi_errno_t __wasi_fd_close(__wasi_fd_t fd); Word wasi_unstable_fd_close(void*, Word) { throw WasmException("wasi_unstable fd_close"); } +// __wasi_errno_t __wasi_fd_fdstat_get(__wasi_fd_t fd, __wasi_fdstat_t *stat) +Word wasi_unstable_fd_fdstat_get(void* raw_context, Word fd, Word statOut) { + // We will only support this interface on stdout and stderr + if (fd.u64_ != 1 && fd.u64_ != 2) { + return 8; // __WASI_EBADF; + } + + // The last word points to a 24-byte structure, which we + // are mostly going to zero out. + uint64_t wasi_fdstat[3]; + wasi_fdstat[0] = 0; + wasi_fdstat[1] = 64; // This sets "fs_rights_base" to __WASI_RIGHTS_FD_WRITE + wasi_fdstat[2] = 0; + + auto context = WASM_CONTEXT(raw_context); + context->wasmVm()->setMemory(statOut.u64_, 3 * sizeof(uint64_t), &wasi_fdstat); + + return 0; // __WASI_ESUCCESS +} + // __wasi_errno_t __wasi_environ_get(char **environ, char *environ_buf); Word wasi_unstable_environ_get(void*, Word, Word) { return 0; // __WASI_ESUCCESS diff --git a/source/extensions/common/wasm/exports.h b/source/extensions/common/wasm/exports.h index 02d3bdd405ae..3d16c6660422 100644 --- a/source/extensions/common/wasm/exports.h +++ b/source/extensions/common/wasm/exports.h @@ -82,8 +82,10 @@ Word call_foreign_function(void* raw_context, Word function_name, Word function_ Word wasi_unstable_fd_write(void* raw_context, Word fd, Word iovs, Word iovs_len, Word nwritten_ptr); +Word wasi_unstable_fd_read(void*, Word, Word, Word, Word); Word wasi_unstable_fd_seek(void*, Word, int64_t, Word, Word); Word wasi_unstable_fd_close(void*, Word); +Word wasi_unstable_fd_fdstat_get(void*, Word fd, Word statOut); Word wasi_unstable_environ_get(void*, Word, Word); Word wasi_unstable_environ_sizes_get(void* raw_context, Word count_ptr, Word buf_size_ptr); Word wasi_unstable_args_get(void* raw_context, Word argc_ptr, Word argv_buf_size_ptr); diff --git a/source/extensions/common/wasm/wasm.cc b/source/extensions/common/wasm/wasm.cc index bdf789d822d4..f7f899f1f164 100644 --- a/source/extensions/common/wasm/wasm.cc +++ b/source/extensions/common/wasm/wasm.cc @@ -140,8 +140,10 @@ void Wasm::registerCallbacks() { &ConvertFunctionWordToUint32::convertFunctionWordToUint32) _REGISTER_WASI(fd_write); + _REGISTER_WASI(fd_read); _REGISTER_WASI(fd_seek); _REGISTER_WASI(fd_close); + _REGISTER_WASI(fd_fdstat_get); _REGISTER_WASI(environ_get); _REGISTER_WASI(environ_sizes_get); _REGISTER_WASI(args_get); diff --git a/test/extensions/common/wasm/test_data/test_cpp.cc b/test/extensions/common/wasm/test_data/test_cpp.cc index 07275b0d9f0e..48dc428f69b2 100644 --- a/test/extensions/common/wasm/test_data/test_cpp.cc +++ b/test/extensions/common/wasm/test_data/test_cpp.cc @@ -1,5 +1,10 @@ // NOLINT(namespace-envoy) +#include + +#include #include +#include +#include #include #include @@ -27,6 +32,13 @@ static float gInfinity = INFINITY; } while (0) #endif +#define FAIL_NOW(_msg) \ + do { \ + const std::string __message = _msg; \ + proxy_log(LogLevel::critical, __message.c_str(), __message.size()); \ + abort(); \ + } while (0) + WASM_EXPORT(uint32_t, proxy_on_vm_start, (uint32_t, uint32_t context_id)) { const char* configuration_ptr = nullptr; size_t size; @@ -123,6 +135,41 @@ WASM_EXPORT(uint32_t, proxy_on_vm_start, (uint32_t, uint32_t context_id)) { } ::free(compressed); ::free(result); + } else if (configuration == "WASI") { + // These checks depend on Emscripten's support for WASI and will only + // work if invoked on a "real" WASM VM. + int err = fprintf(stdout, "WASI write to stdout\n"); + if (err < 0) { + FAIL_NOW("stdout write should succeed"); + } + err = fprintf(stderr, "WASI write to stderr\n"); + if (err < 0) { + FAIL_NOW("stderr write should succeed"); + } + // We explicitly don't support reading from stdin + char tmp[16]; + size_t rc = fread(static_cast(tmp), 1, 16, stdin); + if (rc != 0 || errno != ENOSYS) { + FAIL_NOW("stdin read should fail. errno = " + std::to_string(errno)); + } + // No environment variables should be available + char* pathenv = getenv("PATH"); + if (pathenv != nullptr) { + FAIL_NOW("PATH environment variable should not be available"); + } + // Exercise the WASI "fd_fdstat_get" a little bit + int tty = isatty(1); + if (errno != ENOTTY || tty != 0) { + FAIL_NOW("stdout is not a tty"); + } + tty = isatty(2); + if (errno != ENOTTY || tty != 0) { + FAIL_NOW("stderr is not a tty"); + } + tty = isatty(99); + if (errno != EBADF || tty != 0) { + FAIL_NOW("isatty errors on bad fds. errno = " + std::to_string(errno)); + } } else { std::string message = "on_vm_start " + configuration; proxy_log(LogLevel::info, message.c_str(), message.size()); diff --git a/test/extensions/common/wasm/test_data/test_cpp.wasm b/test/extensions/common/wasm/test_data/test_cpp.wasm index 6eadeb381626..6f1553886615 100644 Binary files a/test/extensions/common/wasm/test_data/test_cpp.wasm and b/test/extensions/common/wasm/test_data/test_cpp.wasm differ diff --git a/test/extensions/common/wasm/wasm_test.cc b/test/extensions/common/wasm/wasm_test.cc index d8d32ea48b1f..cd3f50ad1883 100644 --- a/test/extensions/common/wasm/wasm_test.cc +++ b/test/extensions/common/wasm/wasm_test.cc @@ -356,6 +356,47 @@ TEST_P(WasmCommonTest, Foreign) { wasm->startForTesting(std::move(context), plugin); } +TEST_P(WasmCommonTest, WASI) { + if (GetParam() == "null") { + // This test has no meaning unless it is invoked by actual WASM code + return; + } + Stats::IsolatedStoreImpl stats_store; + Api::ApiPtr api = Api::createApiForTest(stats_store); + Upstream::MockClusterManager cluster_manager; + Event::DispatcherPtr dispatcher(api->allocateDispatcher()); + auto scope = Stats::ScopeSharedPtr(stats_store.createScope("wasm.")); + NiceMock local_info; + auto name = ""; + auto root_id = ""; + auto vm_id = ""; + auto vm_configuration = "WASI"; + auto vm_key = ""; + auto plugin = std::make_shared( + name, root_id, vm_id, envoy::config::core::v3::TrafficDirection::UNSPECIFIED, local_info, + nullptr); + auto wasm = std::make_unique( + absl::StrCat("envoy.wasm.runtime.", GetParam()), vm_id, vm_configuration, vm_key, scope, + cluster_manager, *dispatcher); + EXPECT_NE(wasm, nullptr); + std::string code; + if (GetParam() != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir }}/test/extensions/common/wasm/test_data/test_cpp.wasm"))); + } else { + // The name of the Null VM plugin. + code = "CommonWasmTestCpp"; + } + EXPECT_FALSE(code.empty()); + auto context = std::make_unique(wasm.get()); + + EXPECT_CALL(*context, scriptLog_(spdlog::level::info, Eq("WASI write to stdout"))).Times(1); + EXPECT_CALL(*context, scriptLog_(spdlog::level::err, Eq("WASI write to stderr"))).Times(1); + + EXPECT_TRUE(wasm->initialize(code, false)); + wasm->startForTesting(std::move(context), plugin); +} + TEST_P(WasmCommonTest, VmCache) { Stats::IsolatedStoreImpl stats_store; Api::ApiPtr api = Api::createApiForTest(stats_store);