diff --git a/contracts/eoslib/memory.h b/contracts/eoslib/memory.h new file mode 100644 index 00000000000..b0b38cf1b28 --- /dev/null +++ b/contracts/eoslib/memory.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +extern "C" { + /** + * @defgroup memorycapi Memory C API + * @brief Defines common memory functions + * @ingroup TBD + * + * @{ + */ + + /** + * Copy a block of memory from source to destination. + * @brief Copy a block of memory from source to destination. + * @param destination Pointer to the destination to copy to. + * @param source Pointer to the source for copy from. + * @param num Number of bytes to copy. + * + * @return the destination pointer + * + * Example: + * @code + * char dest[6] = { 0 }; + * char source[6] = { 'H', 'e', 'l', 'l', 'o', '\0' }; + * memcpy(dest, source, 6 * sizeof(char)); + * prints(dest); // Output: Hello + * @endcode + */ + void* memcpy( void* destination, const void* source, uint32_t num ); + + /** + * Fill block of memory. + * @brief Fill a block of memory with the provided value. + * @param ptr Pointer to memory to fill. + * @param value Value to set (it is passed as an int but converted to unsigned char). + * @param num Number of bytes to be set to the value. + * + * @return the destination pointer + * + * Example: + * @code + * char ptr[6] = { 'H', 'e', 'l', 'l', 'o', '\0' }; + * memset(ptr, 'y', 6 * sizeof(char)); + * prints(ptr); // Output: yyyyyy + * @endcode + */ + void* memset( void* ptr, uint32_t value, uint32_t num ); + /// @} +} // extern "C" diff --git a/contracts/eoslib/memory.hpp b/contracts/eoslib/memory.hpp new file mode 100644 index 00000000000..9f512f3a8d9 --- /dev/null +++ b/contracts/eoslib/memory.hpp @@ -0,0 +1,215 @@ +#pragma once + +#include +#include + +namespace eos { + + using ::memset; + using ::memcpy; + + /** + * @defgroup memorycppapi Memory C++ API + * @brief Defines common memory functions + * @ingroup TBD + * + * @{ + */ + + class memory + { + friend void* malloc(uint32_t size); + friend void* realloc(void* ptr, uint32_t size); + friend void free(void* ptr); + + public: + memory() + : _offset(0) + { + memset(_initial_heap, 0, sizeof(_initial_heap)); + } + + private: + void* malloc(uint32_t size) + { + if (_offset + size + SIZE_MARKER > INITIAL_HEAP_SIZE || size == 0) + return nullptr; + + buffer_ptr new_buff(&_initial_heap[_offset + SIZE_MARKER], size); + _offset += size + SIZE_MARKER; + return new_buff.ptr(); + } + + void* realloc(void* ptr, uint32_t size) + { + uint32_t orig_ptr_size = 0; + const char* const END_OF_BUFFER = _initial_heap + INITIAL_HEAP_SIZE; + char* const char_ptr = static_cast(ptr); + if (ptr != nullptr) + { + buffer_ptr orig_buffer(ptr); + if (orig_buffer.size_ptr() >= _initial_heap && ptr < END_OF_BUFFER) + { + orig_ptr_size = orig_buffer.size(); + // is the passed in pointer valid + char* const orig_buffer_end = orig_buffer.end(); + if (orig_buffer_end < END_OF_BUFFER) + { + // is there enough memory to allocate new buffer + if (ptr >= END_OF_BUFFER - size) + { + // not handling in current implementation + return nullptr; + } + + const int32_t diff = size - orig_ptr_size; + if (diff < 0) + { + memset(orig_buffer_end + diff, 0, -diff); + // if ptr was the last allocated buffer, we can contract + if (orig_buffer_end == &_initial_heap[_offset]) + { + _offset += diff; + } + // else current implementation doesn't worry about freeing excess memory + + return ptr; + } + // if ptr was the last allocated buffer, we can expand + else if (orig_buffer_end == &_initial_heap[_offset]) + { + orig_buffer.size(size); + _offset += diff; + + return ptr; + } + else if (diff == 0) + return ptr; + } + else + { + orig_ptr_size = 0; + } + } + } + + char* new_alloc = static_cast(malloc(size)); + + const uint32_t copy_size = (size < orig_ptr_size) ? size : orig_ptr_size; + if (copy_size > 0) + { + memcpy(new_alloc, ptr, copy_size); + free (ptr); + } + + return new_alloc; + } + + void free(void* ) + { + // currently no-op + } + + class buffer_ptr + { + public: + buffer_ptr(void* ptr) + : _ptr(static_cast(ptr)) + , _size(*(uint32_t*)(static_cast(ptr) - SIZE_MARKER)) + { + } + + buffer_ptr(void* ptr, uint32_t buff_size) + : _ptr(static_cast(ptr)) + { + size(buff_size); + } + + const void* size_ptr() + { + return _ptr - SIZE_MARKER; + } + + uint32_t size() + { + return _size; + } + + void size(uint32_t val) + { + *reinterpret_cast(_ptr - SIZE_MARKER) = val; + _size = val; + } + + char* end() + { + return _ptr + _size; + } + + char* ptr() + { + return _ptr; + } + private: + + char* const _ptr; + uint32_t _size; + }; + + static const uint32_t SIZE_MARKER = sizeof(uint32_t); + static const uint32_t INITIAL_HEAP_SIZE = 8192;//32768; + char _initial_heap[INITIAL_HEAP_SIZE]; + uint32_t _offset; + } memory_heap; + + /** + * Allocate a block of memory. + * @brief Allocate a block of memory. + * @param size Size of memory block + * + * Example: + * @code + * uint64_t* int_buffer = malloc(500 * sizeof(uint64_t)); + * @endcode + */ + inline void* malloc(uint32_t size) + { + return memory_heap.malloc(size); + } + + /** + * Allocate a block of memory. + * @brief Allocate a block of memory. + * @param size Size of memory block + * + * Example: + * @code + * uint64_t* int_buffer = malloc(500 * sizeof(uint64_t)); + * ... + * uint64_t* bigger_int_buffer = realloc(int_buffer, 600 * sizeof(uint64_t)); + * @endcode + */ + + inline void* realloc(void* ptr, uint32_t size) + { + return memory_heap.realloc(ptr, size); + } + + /** + * Free a block of memory. + * @brief Free a block of memory. + * @param ptr Pointer to memory block to free. + * + * Example: + * @code + * uint64_t* int_buffer = malloc(500 * sizeof(uint64_t)); + * ... + * free(int_buffer); + * @endcode + */ + inline void free(void* ptr) + { + return memory_heap.free(ptr); + } + /// @} /// mathcppapi +} diff --git a/libraries/chain/wasm_interface.cpp b/libraries/chain/wasm_interface.cpp index 012480026ba..e1bc7f703c1 100644 --- a/libraries/chain/wasm_interface.cpp +++ b/libraries/chain/wasm_interface.cpp @@ -240,6 +240,16 @@ DEFINE_INTRINSIC_FUNCTION3(env,memcpy,memcpy,i32,i32,dstp,i32,srcp,i32,len) { return dstp; } +DEFINE_INTRINSIC_FUNCTION3(env,memset,memset,i32,i32,rel_ptr,i32,value,i32,len) { + auto& wasm = wasm_interface::get(); + auto mem = wasm.current_memory; + char* ptr = memoryArrayPtr( mem, rel_ptr, len); + FC_ASSERT( len > 0 ); + + memset( ptr, value, len ); + return rel_ptr; +} + /** * Transaction C API implementation diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bd6bc145bdd..8029557d535 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,11 +17,12 @@ if(WASM_TOOLCHAIN) target_include_directories( slow_test PUBLIC ${CMAKE_BINARY_DIR}/contracts ) add_dependencies(slow_test currency exchange) + add_subdirectory(api_tests/memory_test) file(GLOB API_TESTS "api_tests/*.cpp") add_executable( api_test ${API_TESTS} ${COMMON_SOURCES} ) target_link_libraries( api_test eos_native_contract eos_chain chainbase eos_utilities eos_egenesis_none fc ${PLATFORM_SPECIFIC_LIBS} ) - target_include_directories( api_test PUBLIC ${CMAKE_BINARY_DIR}/contracts ${CMAKE_SOURCE_DIR}/contracts ) - add_dependencies(api_test test_api) + target_include_directories( api_test PUBLIC ${CMAKE_BINARY_DIR}/contracts ${CMAKE_SOURCE_DIR}/contracts ${CMAKE_CURRENT_BINARY_DIR}/api_tests ) + add_dependencies(api_test test_api memory_test) endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/eosd_run_test.sh ${CMAKE_CURRENT_BINARY_DIR}/eosd_run_test.sh COPYONLY) diff --git a/tests/api_tests/api_tests.cpp b/tests/api_tests/api_tests.cpp index bbdbe088b1c..0b351789962 100644 --- a/tests/api_tests/api_tests.cpp +++ b/tests/api_tests/api_tests.cpp @@ -33,6 +33,9 @@ #include #include + +#include "memory_test/memory_test.wast.hpp" + FC_REFLECT( dummy_message, (a)(b)(c) ); FC_REFLECT( u128_msg, (values) ); @@ -408,5 +411,85 @@ BOOST_FIXTURE_TEST_CASE(test_all, testing_fixture) } FC_LOG_AND_RETHROW() } +#define MEMORY_TEST_RUN(account_name) \ + Make_Blockchain(chain); \ + chain.produce_blocks(1); \ + Make_Account(chain, account_name); \ + chain.produce_blocks(1); \ + \ + \ + types::setcode handler; \ + handler.account = #account_name; \ + \ + auto wasm = assemble_wast( memory_test_wast ); \ + handler.code.resize(wasm.size()); \ + memcpy( handler.code.data(), wasm.data(), wasm.size() ); \ + \ + { \ + eos::chain::SignedTransaction trx; \ + trx.scope = {#account_name}; \ + trx.messages.resize(1); \ + trx.messages[0].code = config::EosContractName; \ + trx.messages[0].authorization.emplace_back(types::AccountPermission{#account_name,"active"}); \ + transaction_set_message(trx, 0, "setcode", handler); \ + trx.expiration = chain.head_block_time() + 100; \ + transaction_set_reference_block(trx, chain.head_block_id()); \ + chain.push_transaction(trx); \ + chain.produce_blocks(1); \ + } \ + \ + \ + { \ + eos::chain::SignedTransaction trx; \ + trx.scope = sort_names({#account_name,"inita"}); \ + transaction_emplace_message(trx, #account_name, \ + vector{}, \ + "transfer", types::transfer{#account_name, "inita", 1,""}); \ + trx.expiration = chain.head_block_time() + 100; \ + transaction_set_reference_block(trx, chain.head_block_id()); \ + chain.push_transaction(trx); \ + chain.produce_blocks(1); \ + } + +#define MEMORY_TEST_CASE(test_case_name, account_name) \ +BOOST_FIXTURE_TEST_CASE(test_case_name, testing_fixture) \ +{ try{ \ + MEMORY_TEST_RUN(account_name); \ +} FC_LOG_AND_RETHROW() } + +//Test wasm memory allocation +MEMORY_TEST_CASE(test_memory, testmemory) + +//Test wasm memory allocation at boundaries +MEMORY_TEST_CASE(test_memory_bounds, testbounds) + +//Test intrinsic provided memset and memcpy +MEMORY_TEST_CASE(test_memset_memcpy, testmemset) + +//Test memcpy overlap at start of destination +BOOST_FIXTURE_TEST_CASE(test_memcpy_overlap_start, testing_fixture) +{ + try { + MEMORY_TEST_RUN(testolstart); + BOOST_FAIL("memcpy should have thrown assert acception"); + } + catch(fc::assert_exception& ex) + { + BOOST_REQUIRE(ex.to_detail_string().find("overlap of memory range is undefined") != std::string::npos); + } +} + +//Test memcpy overlap at end of destination +BOOST_FIXTURE_TEST_CASE(test_memcpy_overlap_end, testing_fixture) +{ + try { + MEMORY_TEST_RUN(testolend); + BOOST_FAIL("memcpy should have thrown assert acception"); + } + catch(fc::assert_exception& ex) + { + BOOST_REQUIRE(ex.to_detail_string().find("overlap of memory range is undefined") != std::string::npos); + } +} BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/api_tests/memory_test/CMakeLists.txt b/tests/api_tests/memory_test/CMakeLists.txt new file mode 100644 index 00000000000..a641b341bd9 --- /dev/null +++ b/tests/api_tests/memory_test/CMakeLists.txt @@ -0,0 +1,2 @@ +file(GLOB SOURCE_FILES "*.cpp") +add_wast_target(memory_test "${SOURCE_FILES}" "${CMAKE_SOURCE_DIR}/contracts" ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/api_tests/memory_test/memory_test.cpp b/tests/api_tests/memory_test/memory_test.cpp new file mode 100644 index 00000000000..a993f189b21 --- /dev/null +++ b/tests/api_tests/memory_test/memory_test.cpp @@ -0,0 +1,205 @@ +#include "memory_test.hpp" /// defines transfer struct (abi) +#include +//#include +#include + +extern "C" { + void init() + { + } + + void verify(const void* const ptr, const uint32_t val, const uint32_t size) + { + const char* char_ptr = (const char*)ptr; + for (uint32_t i = 0; i < size; ++i) + { + assert(static_cast(static_cast(char_ptr[i])) == val, "buffer slot doesn't match"); + } + } + + void test_memory() + { + char* ptr1 = (char*)eos::malloc(20); + assert(ptr1 != nullptr, "should have allocated a buffer"); + verify(ptr1, 0, 20); + char* ptr1_realloc = (char*)eos::realloc(ptr1, 30); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc == ptr1, "should have just enlarged the buffer"); + char* ptr2 = (char*)eos::malloc(20); + assert(ptr2 != nullptr, "should have allocated a buffer"); + assert(ptr1 + 30 < ptr2, "should have been created after ptr1"); // test specific to implementation (can remove for refactor) + verify(ptr1, 0, 30); + assert(ptr1[30] != 0, "should not have empty bytes following since block allocated"); // test specific to implementation (can remove for refactor) + + //shrink the buffer + ptr1[14] = 0x7e; + ptr1[29] = 0x7f; + ptr1_realloc = (char*)eos::realloc(ptr1, 15); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc == ptr1, "should have just shrunk the buffer"); + assert(ptr1[14] == 0x7e, "remaining portion of buffer should be untouched"); + verify(ptr1 + 15, 0, 15); // test specific to implementation (can remove for refactor) + + //same size the buffer (verify corner case + ptr1_realloc = (char*)eos::realloc(ptr1, 15); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc == ptr1, "should have same buffer"); + assert(ptr1[14] == 0x7e, "remaining portion of buffer should be untouched"); + + //same size as max allocated buffer -- test specific to implementation (can remove for refactor) + ptr1_realloc = (char*)eos::realloc(ptr1, 30); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc == ptr1, "should have just increased the buffer back to original max"); //test specific to implementation (can remove for refactor) + assert(ptr1[14] == 0x7e, "remaining portion of buffer should be untouched"); + + //increase buffer beyond allocated space + ptr1[29] = 0x7f; + ptr1_realloc = (char*)eos::realloc(ptr1, 31); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc != ptr1, "should have had to reallocate the buffer"); + assert(ptr2 < ptr1_realloc, "should have been created after ptr2"); // test specific to implementation (can remove for refactor) + assert(ptr1_realloc[14] == 0x7e, "original buffers content should be copied"); + assert(ptr1_realloc[29] == 0x7f, "original buffers content should be copied"); + + //realloc with nullptr + char* nullptr_realloc = (char*)eos::realloc(nullptr, 50); + assert(nullptr_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc < nullptr_realloc, "should have been created after ptr1_realloc"); // test specific to implementation (can remove for refactor) + + //realloc with invalid ptr + char* invalid_ptr_realloc = (char*)eos::realloc(nullptr_realloc + 4, 10); + assert(invalid_ptr_realloc != nullptr, "should have returned a buffer"); + assert(nullptr_realloc < invalid_ptr_realloc, "should have been created after ptr2"); // test specific to implementation (can remove for refactor) + + // try to re-allocate past max + ptr1 = ptr1_realloc; + ptr1_realloc = (char*)eos::realloc(ptr1, 8028); + assert(ptr1_realloc == nullptr, "should have returned a nullptr"); + + // re-allocate to max + ptr1_realloc = (char*)eos::realloc(invalid_ptr_realloc, 8027); + assert(ptr1_realloc != nullptr, "should have returned a buffer"); + assert(ptr1_realloc == invalid_ptr_realloc, "should have just extended the original buffer"); + } + + void test_memory_bounds() + { + // full buffer is 8188 (8192 - ptr size header) + + // try to malloc past buffer + char* ptr1 = (char*)eos::malloc(8189); + assert(ptr1 == nullptr, "should not have allocated a buffer"); + + + // takes up 5 (1 + ptr size header) + ptr1 = (char*)eos::malloc(1); + assert(ptr1 != nullptr, "should have allocated a buffer"); + + // takes up 5 (1 + ptr size header) + char* ptr2 = (char*)eos::malloc(1); + assert(ptr2 != nullptr, "should have allocated a buffer"); + + // realloc to buffer (tests verifying relloc boundary logic and malloc logic + char* ptr1_realloc = (char*)eos::realloc(ptr1, 8178); + assert(ptr1_realloc != nullptr, "should have allocated a buffer"); + assert(ptr1_realloc != ptr1, "should have had to reallocate the buffer"); + verify(ptr1_realloc, 0, 8178); + } + + void test_memset_memcpy() + { + char buf1[40] = {}; + char buf2[40] = {}; + + verify(buf1, 0, 40); + verify(buf2, 0, 40); + + memset(buf1, 0x22, 20); + verify(buf1, 0x22, 20); + verify(&buf1[20], 0, 20); + + memset(&buf2[20], 0xff, 20); + verify(buf2, 0, 20); + verify(&buf2[20], 0xff, 20); + + memcpy(&buf1[10], &buf2[10], 20); + verify(buf1, 0x22, 10); + verify(&buf1[10], 0, 10); + verify(&buf1[20], 0xff, 10); + verify(&buf1[30], 0, 10); + + memset(&buf1[1], 1, 1); + verify(buf1, 0x22, 1); + verify(&buf1[1], 1, 1); + verify(&buf1[2], 0x22, 8); + + // verify adjacent non-overlapping buffers + char buf3[50] = {}; + memset(&buf3[25], 0xee, 25); + verify(buf3, 0, 25); + memcpy(buf3, &buf3[25], 25); + verify(buf3, 0xee, 50); + + memset(buf3, 0, 25); + verify(&buf3[25], 0xee, 25); + memcpy(&buf3[25], buf3, 25); + verify(buf3, 0, 50); + } + + void test_memcpy_overlap_start() + { + char buf3[99] = {}; + memset(buf3, 0xee, 50); + memset(&buf3[50], 0xff, 49); + memcpy(&buf3[49], buf3, 50); + } + + + void test_memcpy_overlap_end() + { + char buf3[99] = {}; + memset(buf3, 0xee, 50); + memset(&buf3[50], 0xff, 49); + memcpy(buf3, &buf3[49], 50); + } + + /// The apply method implements the dispatch of events to this contract + void apply( uint64_t code, uint64_t action ) + { + if( code == N(testmemory) ) + { + if( action == N(transfer) ) + { + test_memory(); + } + } + else if( code == N(testbounds) ) + { + if( action == N(transfer) ) + { + test_memory_bounds(); + } + } + else if( code == N(testmemset) ) + { + if( action == N(transfer) ) + { + test_memset_memcpy(); + } + } + else if( code == N(testolstart) ) + { + if( action == N(transfer) ) + { + test_memcpy_overlap_start(); + } + } + else if( code == N(testolend) ) + { + if( action == N(transfer) ) + { + test_memcpy_overlap_end(); + } + } + } +} diff --git a/tests/api_tests/memory_test/memory_test.hpp b/tests/api_tests/memory_test/memory_test.hpp new file mode 100644 index 00000000000..3c38763c4f1 --- /dev/null +++ b/tests/api_tests/memory_test/memory_test.hpp @@ -0,0 +1,5 @@ + +namespace memory_test { + +} /// namespace memory_test +