diff --git a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/code_cache.hpp b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/code_cache.hpp index d753a9dcaa..f638dc8751 100644 --- a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/code_cache.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/code_cache.hpp @@ -69,6 +69,7 @@ class code_cache_base { code_cache_index _cache_index; const chainbase::database& _db; + eosvmoc::config _eosvmoc_config; std::filesystem::path _cache_file_path; int _cache_fd; diff --git a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/config.hpp b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/config.hpp index b3f2563831..43f9efede2 100644 --- a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/config.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/config.hpp @@ -7,11 +7,24 @@ #include +#include + namespace eosio { namespace chain { namespace eosvmoc { struct config { uint64_t cache_size = 1024u*1024u*1024u; uint64_t threads = 1u; + + // subjective limits for OC compilation. + // nodeos enforces the limits by the default values. + // libtester disables the limits in all tests, except enforces the limits + // in the tests in unittests/eosvmoc_limits_tests.cpp. + std::optional cpu_limit {20u}; + std::optional vm_limit {512u*1024u*1024u}; + std::optional stack_size_limit {16u*1024u}; + std::optional generated_code_size_limit {16u*1024u*1024u}; }; }}} + +FC_REFLECT(eosio::chain::eosvmoc::config, (cache_size)(threads)(cpu_limit)(vm_limit)(stack_size_limit)(generated_code_size_limit)) diff --git a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/ipc_protocol.hpp b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/ipc_protocol.hpp index 2674a2dbd4..59fb7f10fb 100644 --- a/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/ipc_protocol.hpp +++ b/libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/ipc_protocol.hpp @@ -22,6 +22,7 @@ struct code_tuple { struct compile_wasm_message { code_tuple code; + eosvmoc::config eosvmoc_config; //Two sent fd: 1) communication socket for result, 2) the wasm to compile }; @@ -62,7 +63,7 @@ using eosvmoc_message = std::variantcode); } - instantiated_code instantiateModule(const IR::Module& module) + instantiated_code instantiateModule(const IR::Module& module, uint64_t stack_size_limit, size_t generated_code_size_limit) { static bool inited; if(!inited) { @@ -315,13 +315,13 @@ namespace LLVMJIT WAVM_ASSERT_THROW(!!c); ++num_functions_stack_size_found; - if(stack_size > 16u*1024u) + if(stack_size > stack_size_limit) _exit(1); } } if(num_functions_stack_size_found != module.functions.defs.size()) _exit(1); - if(jitModule->final_pic_code.size() >= 16u*1024u*1024u) + if(jitModule->final_pic_code.size() >= generated_code_size_limit) _exit(1); instantiated_code ret; diff --git a/libraries/chain/webassembly/runtimes/eos-vm-oc/LLVMJIT.h b/libraries/chain/webassembly/runtimes/eos-vm-oc/LLVMJIT.h index 13e2510195..0cd022ce3b 100644 --- a/libraries/chain/webassembly/runtimes/eos-vm-oc/LLVMJIT.h +++ b/libraries/chain/webassembly/runtimes/eos-vm-oc/LLVMJIT.h @@ -15,6 +15,6 @@ struct instantiated_code { }; namespace LLVMJIT { - instantiated_code instantiateModule(const IR::Module& module); + instantiated_code instantiateModule(const IR::Module& module, uint64_t stack_size_limit, size_t generated_code_size_limit); } }}} diff --git a/libraries/chain/webassembly/runtimes/eos-vm-oc/code_cache.cpp b/libraries/chain/webassembly/runtimes/eos-vm-oc/code_cache.cpp index 7c48b5b079..e4a23686e8 100644 --- a/libraries/chain/webassembly/runtimes/eos-vm-oc/code_cache.cpp +++ b/libraries/chain/webassembly/runtimes/eos-vm-oc/code_cache.cpp @@ -126,7 +126,7 @@ const code_descriptor* const code_cache_async::get_descriptor_for_code(bool high _outstanding_compiles_and_poison.emplace(*nextup, false); std::vector fds_to_pass; fds_to_pass.emplace_back(memfd_for_bytearray(codeobject->code)); - FC_ASSERT(write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ *nextup }, fds_to_pass), "EOS VM failed to communicate to OOP manager"); + FC_ASSERT(write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ *nextup, _eosvmoc_config }, fds_to_pass), "EOS VM failed to communicate to OOP manager"); --count_processed; } _queued_compiles.erase(nextup); @@ -179,7 +179,7 @@ const code_descriptor* const code_cache_async::get_descriptor_for_code(bool high _outstanding_compiles_and_poison.emplace(ct, false); std::vector fds_to_pass; fds_to_pass.emplace_back(memfd_for_bytearray(codeobject->code)); - write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ ct }, fds_to_pass); + write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ ct, _eosvmoc_config }, fds_to_pass); failure = get_cd_failure::temporary; // Compile might not be done yet return nullptr; } @@ -211,7 +211,7 @@ const code_descriptor* const code_cache_sync::get_descriptor_for_code_sync(const std::vector fds_to_pass; fds_to_pass.emplace_back(memfd_for_bytearray(codeobject->code)); - write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ {code_id, vm_version} }, fds_to_pass); + write_message_with_fds(_compile_monitor_write_socket, compile_wasm_message{ {code_id, vm_version}, _eosvmoc_config }, fds_to_pass); auto [success, message, fds] = read_message_with_fds(_compile_monitor_read_socket); EOS_ASSERT(success, wasm_execution_error, "failed to read response from monitor process"); EOS_ASSERT(std::holds_alternative(message), wasm_execution_error, "unexpected response from monitor process"); @@ -226,6 +226,7 @@ const code_descriptor* const code_cache_sync::get_descriptor_for_code_sync(const code_cache_base::code_cache_base(const std::filesystem::path& data_dir, const eosvmoc::config& eosvmoc_config, const chainbase::database& db) : _db(db), + _eosvmoc_config(eosvmoc_config), _cache_file_path(data_dir/"code_cache.bin") { static_assert(sizeof(allocator_t) <= header_offset, "header offset intersects with allocator"); diff --git a/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_monitor.cpp b/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_monitor.cpp index 01ae7ecaba..92b55da717 100644 --- a/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_monitor.cpp +++ b/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_monitor.cpp @@ -71,7 +71,7 @@ struct compile_monitor_session { connection_dead_signal(); return; } - kick_compile_off(compile.code, std::move(fds[0])); + kick_compile_off(compile.code, compile.eosvmoc_config, std::move(fds[0])); }, [&](const evict_wasms_message& evict) { for(const code_descriptor& cd : evict.codes) { @@ -90,7 +90,7 @@ struct compile_monitor_session { }); } - void kick_compile_off(const code_tuple& code_id, wrapped_fd&& wasm_code) { + void kick_compile_off(const code_tuple& code_id, const eosvmoc::config& eosvmoc_config, wrapped_fd&& wasm_code) { //prepare a requst to go out to the trampoline int socks[2]; socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, socks); @@ -100,7 +100,7 @@ struct compile_monitor_session { fds_pass_to_trampoline.emplace_back(socks[1]); fds_pass_to_trampoline.emplace_back(std::move(wasm_code)); - eosvmoc_message trampoline_compile_request = compile_wasm_message{code_id}; + eosvmoc_message trampoline_compile_request = compile_wasm_message{code_id, eosvmoc_config}; if(write_message_with_fds(_trampoline_socket, trampoline_compile_request, fds_pass_to_trampoline) == false) { wasm_compilation_result_message reply{code_id, compilation_result_unknownfailure{}, _allocator->get_free_memory()}; write_message_with_fds(_nodeos_instance_socket, reply); diff --git a/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_trampoline.cpp b/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_trampoline.cpp index b2f16a6d87..11467afd27 100644 --- a/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_trampoline.cpp +++ b/libraries/chain/webassembly/runtimes/eos-vm-oc/compile_trampoline.cpp @@ -17,7 +17,7 @@ using namespace IR; namespace eosio { namespace chain { namespace eosvmoc { -void run_compile(wrapped_fd&& response_sock, wrapped_fd&& wasm_code) noexcept { //noexcept; we'll just blow up if anything tries to cross this boundry +void run_compile(wrapped_fd&& response_sock, wrapped_fd&& wasm_code, uint64_t stack_size_limit, size_t generated_code_size_limit) noexcept { //noexcept; we'll just blow up if anything tries to cross this boundry std::vector wasm = vector_for_memfd(wasm_code); //ideally we catch exceptions and sent them upstream as strings for easier reporting @@ -30,7 +30,7 @@ void run_compile(wrapped_fd&& response_sock, wrapped_fd&& wasm_code) noexcept { wasm_injections::wasm_binary_injection injector(module); injector.inject(); - instantiated_code code = LLVMJIT::instantiateModule(module); + instantiated_code code = LLVMJIT::instantiateModule(module, stack_size_limit, generated_code_size_limit); code_compilation_result_message result_message; @@ -165,16 +165,28 @@ void run_compile_trampoline(int fd) { prctl(PR_SET_NAME, "oc-compile"); prctl(PR_SET_PDEATHSIG, SIGKILL); - struct rlimit cpu_limits = {20u, 20u}; - setrlimit(RLIMIT_CPU, &cpu_limits); + const auto& conf = std::get(message).eosvmoc_config; - struct rlimit vm_limits = {512u*1024u*1024u, 512u*1024u*1024u}; - setrlimit(RLIMIT_AS, &vm_limits); + // enforce cpu limit only when it is set + // (libtester may disable it) + if(conf.cpu_limit) { + struct rlimit cpu_limit = {*conf.cpu_limit, *conf.cpu_limit}; + setrlimit(RLIMIT_CPU, &cpu_limit); + } + + // enforce vm limit only when it is set + // (libtester may disable it) + if(conf.vm_limit) { + struct rlimit vm_limit = {*conf.vm_limit, *conf.vm_limit}; + setrlimit(RLIMIT_AS, &vm_limit); + } struct rlimit core_limits = {0u, 0u}; setrlimit(RLIMIT_CORE, &core_limits); - run_compile(std::move(fds[0]), std::move(fds[1])); + uint64_t stack_size_limit = conf.stack_size_limit ? *conf.stack_size_limit : std::numeric_limits::max(); + size_t generated_code_size_limit = conf.generated_code_size_limit ? * conf.generated_code_size_limit : std::numeric_limits::max(); + run_compile(std::move(fds[0]), std::move(fds[1]), stack_size_limit, generated_code_size_limit); _exit(0); } else if(pid == -1) diff --git a/libraries/testing/include/eosio/testing/tester.hpp b/libraries/testing/include/eosio/testing/tester.hpp index 5389dc076c..ef559ad985 100644 --- a/libraries/testing/include/eosio/testing/tester.hpp +++ b/libraries/testing/include/eosio/testing/tester.hpp @@ -406,6 +406,13 @@ namespace eosio { namespace testing { cfg.contracts_console = true; cfg.eosvmoc_config.cache_size = 1024*1024*8; + // don't enforce OC compilation subject limits for tests, + // particularly EOS EVM tests may run over those limits + cfg.eosvmoc_config.cpu_limit.reset(); + cfg.eosvmoc_config.vm_limit.reset(); + cfg.eosvmoc_config.stack_size_limit.reset(); + cfg.eosvmoc_config.generated_code_size_limit.reset(); + // don't use auto tier up for tests, since the point is to test diff vms cfg.eosvmoc_tierup = chain::wasm_interface::vm_oc_enable::oc_none; diff --git a/unittests/eosvmoc_limits_tests.cpp b/unittests/eosvmoc_limits_tests.cpp new file mode 100644 index 0000000000..4bf3e0cff2 --- /dev/null +++ b/unittests/eosvmoc_limits_tests.cpp @@ -0,0 +1,135 @@ +#ifdef EOSIO_EOS_VM_OC_RUNTIME_ENABLED + +#include +#include +#include + +using namespace eosio; +using namespace eosio::chain; +using namespace eosio::testing; +using mvo = fc::mutable_variant_object; + +BOOST_AUTO_TEST_SUITE(eosvmoc_limits_tests) + +// common routine to verify wasm_execution_error is raised when a resource +// limit specified in eosvmoc_config is reached +void limit_violated_test(const eosvmoc::config& eosvmoc_config) { + fc::temp_directory tempdir; + + constexpr bool use_genesis = true; + validating_tester chain( + tempdir, + [&](controller::config& cfg) { + cfg.eosvmoc_config = eosvmoc_config; + }, + use_genesis + ); + + chain.create_accounts({"eosio.token"_n}); + chain.set_code("eosio.token"_n, test_contracts::eosio_token_wasm()); + chain.set_abi("eosio.token"_n, test_contracts::eosio_token_abi()); + + if (chain.control->is_eos_vm_oc_enabled()) { + BOOST_CHECK_EXCEPTION( + chain.push_action( "eosio.token"_n, "create"_n, "eosio.token"_n, mvo() + ( "issuer", "eosio.token" ) + ( "maximum_supply", "1000000.00 TOK" )), + eosio::chain::wasm_execution_error, + [](const eosio::chain::wasm_execution_error& e) { + return expect_assert_message(e, "failed to compile wasm"); + } + ); + } else { + chain.push_action( "eosio.token"_n, "create"_n, "eosio.token"_n, mvo() + ( "issuer", "eosio.token" ) + ( "maximum_supply", "1000000.00 TOK" ) + ); + } +} + +// common routine to verify no wasm_execution_error is raised +// because limits specified in eosvmoc_config are not reached +void limit_not_violated_test(const eosvmoc::config& eosvmoc_config) { + fc::temp_directory tempdir; + + constexpr bool use_genesis = true; + validating_tester chain( + tempdir, + [&](controller::config& cfg) { + cfg.eosvmoc_config = eosvmoc_config; + }, + use_genesis + ); + + chain.create_accounts({"eosio.token"_n}); + chain.set_code("eosio.token"_n, test_contracts::eosio_token_wasm()); + chain.set_abi("eosio.token"_n, test_contracts::eosio_token_abi()); + + chain.push_action( "eosio.token"_n, "create"_n, "eosio.token"_n, mvo() + ( "issuer", "eosio.token" ) + ( "maximum_supply", "1000000.00 TOK" ) + ); +} + +// test all limits are not set for tests +BOOST_AUTO_TEST_CASE( limits_not_set ) { try { + validating_tester chain; + auto& cfg = chain.get_config(); + + BOOST_REQUIRE(cfg.eosvmoc_config.cpu_limit == std::nullopt); + BOOST_REQUIRE(cfg.eosvmoc_config.vm_limit == std::nullopt); + BOOST_REQUIRE(cfg.eosvmoc_config.stack_size_limit == std::nullopt); + BOOST_REQUIRE(cfg.eosvmoc_config.generated_code_size_limit == std::nullopt); +} FC_LOG_AND_RETHROW() } + +// test limits are not enforced unless limits in eosvmoc_config +// are modified +BOOST_AUTO_TEST_CASE( limits_not_enforced ) { try { + eosvmoc::config eosvmoc_config; + limit_not_violated_test(eosvmoc_config); +} FC_LOG_AND_RETHROW() } + +// test VM limit are checked +BOOST_AUTO_TEST_CASE( vm_limit ) { try { + eosvmoc::config eosvmoc_config; + + // set vm_limit to a small value such that it is exceeded + eosvmoc_config.vm_limit = 64u*1024u*1024u; + limit_violated_test(eosvmoc_config); + + // set vm_limit to a large value such that it is not exceeded + eosvmoc_config.vm_limit = 128u*1024u*1024u; + limit_not_violated_test(eosvmoc_config); +} FC_LOG_AND_RETHROW() } + +// test stack size limit is checked +BOOST_AUTO_TEST_CASE( stack_limit ) { try { + eosvmoc::config eosvmoc_config; + + // The stack size of the compiled WASM in the test is 104. + // Set stack_size_limit one less than the actual needed stack size + eosvmoc_config.stack_size_limit = 103; + limit_violated_test(eosvmoc_config); + + // set stack_size_limit to the actual needed stack size + eosvmoc_config.stack_size_limit = 104; + limit_not_violated_test(eosvmoc_config); +} FC_LOG_AND_RETHROW() } + +// test generated code size limit is checked +BOOST_AUTO_TEST_CASE( generated_code_size_limit ) { try { + eosvmoc::config eosvmoc_config; + + // The generated code size of the compiled WASM in the test is 36856. + // Set generated_code_size_limit to the actual generated code size + eosvmoc_config.generated_code_size_limit = 36856; + limit_violated_test(eosvmoc_config); + + // Set generated_code_size_limit to one above the actual generated code size + eosvmoc_config.generated_code_size_limit = 36857; + limit_not_violated_test(eosvmoc_config); +} FC_LOG_AND_RETHROW() } + +BOOST_AUTO_TEST_SUITE_END() + +#endif