Skip to content

Commit c9d657e

Browse files
authored
Merge pull request #1016 from AntelopeIO/oc_opt_small_const_memcpy
workaround cdt's small const memcpy host function calls in EOS VM OC
2 parents d34c099 + b1579ed commit c9d657e

File tree

10 files changed

+223
-21
lines changed

10 files changed

+223
-21
lines changed

libraries/chain/include/eosio/chain/unapplied_transaction_queue.hpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
#include <boost/multi_index/hashed_index.hpp>
99
#include <boost/multi_index/member.hpp>
1010

11-
namespace fc {
12-
inline std::size_t hash_value( const fc::sha256& v ) {
13-
return v._hash[3];
14-
}
15-
}
16-
1711
namespace eosio { namespace chain {
1812

1913
using namespace boost::multi_index;

libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/code_cache.hpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@
1717

1818
#include <thread>
1919

20-
namespace fc {
21-
inline size_t hash_value(const fc::sha256& code_id) {
22-
return boost::hash<fc::sha256>()(code_id);
23-
}
24-
}
25-
2620
namespace eosio { namespace chain { namespace eosvmoc {
2721

2822
using namespace boost::multi_index;

libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/eos-vm-oc.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ enum eosvmoc_exitcode : int {
4444
EOSVMOC_EXIT_EXCEPTION
4545
};
4646

47-
static constexpr uint8_t current_codegen_version = 1;
47+
static constexpr uint8_t current_codegen_version = 2;
4848

4949
}
5050

libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ using intrinsic_map_t = std::map<std::string, intrinsic_entry>;
2525

2626
const intrinsic_map_t& get_intrinsic_map();
2727

28+
static constexpr unsigned minimum_const_memcpy_intrinsic_to_optimize = 1;
29+
static constexpr unsigned maximum_const_memcpy_intrinsic_to_optimize = 128;
30+
2831
}}}

libraries/chain/include/eosio/chain/webassembly/eos-vm-oc/intrinsic_mapping.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,8 @@ inline constexpr auto get_intrinsic_table() {
278278
"env.bls_fp_mod",
279279
"env.bls_fp_mul",
280280
"env.bls_fp_exp",
281-
"env.set_finalizers"
281+
"env.set_finalizers",
282+
"eosvmoc_internal.check_memcpy_params"
282283
);
283284
}
284285
inline constexpr std::size_t find_intrinsic_index(std::string_view hf) {

libraries/chain/webassembly/runtimes/eos-vm-oc/LLVMEmitIR.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,12 +697,14 @@ namespace LLVMJIT
697697
llvm::Value* callee;
698698
const FunctionType* calleeType;
699699
bool isExit = false;
700+
bool isMemcpy = false;
700701
if(imm.functionIndex < moduleContext.importedFunctionOffsets.size())
701702
{
702703
calleeType = module.types[module.functions.imports[imm.functionIndex].type.index];
703704
llvm::Value* ic = irBuilder.CreateLoad( emitLiteralPointer((void*)(OFFSET_OF_FIRST_INTRINSIC-moduleContext.importedFunctionOffsets[imm.functionIndex]*8), llvmI64Type->getPointerTo(256)) );
704705
callee = irBuilder.CreateIntToPtr(ic, asLLVMType(calleeType)->getPointerTo());
705706
isExit = module.functions.imports[imm.functionIndex].moduleName == "env" && module.functions.imports[imm.functionIndex].exportName == "eosio_exit";
707+
isMemcpy = module.functions.imports[imm.functionIndex].moduleName == "env" && module.functions.imports[imm.functionIndex].exportName == "memcpy";
706708
}
707709
else
708710
{
@@ -715,6 +717,28 @@ namespace LLVMJIT
715717
auto llvmArgs = (llvm::Value**)alloca(sizeof(llvm::Value*) * calleeType->parameters.size());
716718
popMultiple(llvmArgs,calleeType->parameters.size());
717719

720+
//convert small constant sized memcpy host function calls to a load+store (plus small call to validate non-overlap rule)
721+
if(isMemcpy) {
722+
assert(calleeType->parameters.size() == 3);
723+
if(llvm::ConstantInt* const_memcpy_sz = llvm::dyn_cast<llvm::ConstantInt>(llvmArgs[2]);
724+
const_memcpy_sz &&
725+
const_memcpy_sz->getZExtValue() >= minimum_const_memcpy_intrinsic_to_optimize &&
726+
const_memcpy_sz->getZExtValue() <= maximum_const_memcpy_intrinsic_to_optimize) {
727+
const unsigned sz_value = const_memcpy_sz->getZExtValue();
728+
llvm::IntegerType* type_of_memcpy_width = llvm::Type::getIntNTy(context, sz_value*8);
729+
730+
llvm::Value* load_pointer = coerceByteIndexToPointer(llvmArgs[1],0,type_of_memcpy_width);
731+
llvm::Value* store_pointer = coerceByteIndexToPointer(llvmArgs[0],0,type_of_memcpy_width);
732+
irBuilder.CreateStore(irBuilder.CreateLoad(load_pointer), store_pointer, true);
733+
734+
emitRuntimeIntrinsic("eosvmoc_internal.check_memcpy_params",
735+
FunctionType::get(ResultType::none,{ValueType::i32,ValueType::i32,ValueType::i32}),
736+
{llvmArgs[0],llvmArgs[1],llvmArgs[2]});
737+
push(llvmArgs[0]);
738+
return;
739+
}
740+
}
741+
718742
// Call the function.
719743
auto result = createCall(callee,llvm::ArrayRef<llvm::Value*>(llvmArgs,calleeType->parameters.size()));
720744
if(isExit) {

libraries/chain/webassembly/runtimes/eos-vm-oc/executor.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ static intrinsic eosio_exit_intrinsic("env.eosio_exit", IR::FunctionType::get(IR
8787
std::integral_constant<std::size_t, find_intrinsic_index("env.eosio_exit")>::value
8888
);
8989

90-
static void throw_internal_exception(const char* const s) {
91-
*reinterpret_cast<std::exception_ptr*>(eos_vm_oc_get_exception_ptr()) = std::make_exception_ptr(wasm_execution_error(FC_LOG_MESSAGE(error, s)));
90+
template <typename E>
91+
static void throw_internal_exception(const E& e) {
92+
*reinterpret_cast<std::exception_ptr*>(eos_vm_oc_get_exception_ptr()) = std::make_exception_ptr(e);
9293
siglongjmp(*eos_vm_oc_get_jmp_buf(), EOSVMOC_EXIT_EXCEPTION);
9394
__builtin_unreachable();
9495
}
@@ -101,25 +102,42 @@ static void throw_internal_exception(const char* const s) {
101102
void name()
102103

103104
DEFINE_EOSVMOC_TRAP_INTRINSIC(eosvmoc_internal,depth_assert) {
104-
throw_internal_exception("Exceeded call depth maximum");
105+
throw_internal_exception(wasm_execution_error(FC_LOG_MESSAGE(error, "Exceeded call depth maximum")));
105106
}
106107

107108
DEFINE_EOSVMOC_TRAP_INTRINSIC(eosvmoc_internal,div0_or_overflow) {
108-
throw_internal_exception("Division by 0 or integer overflow trapped");
109+
throw_internal_exception(wasm_execution_error(FC_LOG_MESSAGE(error, "Division by 0 or integer overflow trapped")));
109110
}
110111

111112
DEFINE_EOSVMOC_TRAP_INTRINSIC(eosvmoc_internal,indirect_call_mismatch) {
112-
throw_internal_exception("Indirect call function type mismatch");
113+
throw_internal_exception(wasm_execution_error(FC_LOG_MESSAGE(error, "Indirect call function type mismatch")));
113114
}
114115

115116
DEFINE_EOSVMOC_TRAP_INTRINSIC(eosvmoc_internal,indirect_call_oob) {
116-
throw_internal_exception("Indirect call index out of bounds");
117+
throw_internal_exception(wasm_execution_error(FC_LOG_MESSAGE(error, "Indirect call index out of bounds")));
117118
}
118119

119120
DEFINE_EOSVMOC_TRAP_INTRINSIC(eosvmoc_internal,unreachable) {
120-
throw_internal_exception("Unreachable reached");
121+
throw_internal_exception(wasm_execution_error(FC_LOG_MESSAGE(error, "Unreachable reached")));
121122
}
122123

124+
static void eos_vm_oc_check_memcpy_params(int32_t dest, int32_t src, int32_t length) {
125+
//make sure dest & src are zexted when converted from signed 32-bit to signed ptrdiff_t; length should always be small but do it too
126+
const unsigned udest = dest;
127+
const unsigned usrc = src;
128+
const unsigned ulength = length;
129+
130+
//this must remain the same behavior as the memcpy host function
131+
if((size_t)(std::abs((ptrdiff_t)udest - (ptrdiff_t)usrc)) >= ulength)
132+
return;
133+
throw_internal_exception(overlapping_memory_error(FC_LOG_MESSAGE(error, "memcpy can only accept non-aliasing pointers")));
134+
}
135+
136+
static intrinsic check_memcpy_params_intrinsic("eosvmoc_internal.check_memcpy_params", IR::FunctionType::get(IR::ResultType::none,{IR::ValueType::i32,IR::ValueType::i32,IR::ValueType::i32}),
137+
(void*)&eos_vm_oc_check_memcpy_params,
138+
std::integral_constant<std::size_t, find_intrinsic_index("eosvmoc_internal.check_memcpy_params")>::value
139+
);
140+
123141
struct executor_signal_init {
124142
executor_signal_init() {
125143
struct sigaction sig_action, old_sig_action;

libraries/libfc/include/fc/crypto/sha256.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,12 @@ namespace boost
147147
}
148148
};
149149
}
150+
151+
namespace fc {
152+
inline size_t hash_value(const fc::sha256& s) {
153+
return boost::hash<fc::sha256>()(s);
154+
}
155+
}
156+
150157
#include <fc/reflect/reflect.hpp>
151158
FC_REFLECT_TYPENAME( fc::sha256 )

unittests/api_tests.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <eosio/chain/wasm_interface.hpp>
1717
#include <eosio/chain/resource_limits.hpp>
1818
#include <eosio/chain/finalizer_authority.hpp>
19+
#include <eosio/chain/webassembly/eos-vm-oc.hpp>
1920

2021
#include <fc/crypto/digest.hpp>
2122
#include <fc/crypto/sha256.hpp>
@@ -3352,6 +3353,86 @@ BOOST_AUTO_TEST_CASE_TEMPLATE( get_code_hash_tests, T, validating_testers ) { tr
33523353
check("test"_n, 3);
33533354
} FC_LOG_AND_RETHROW() }
33543355

3356+
BOOST_AUTO_TEST_CASE_TEMPLATE( small_const_memcpy_tests, T, validating_testers ) { try {
3357+
T t;
3358+
t.create_account("smallmemcpy"_n);
3359+
t.produce_block();
3360+
3361+
for(unsigned i = eosvmoc::minimum_const_memcpy_intrinsic_to_optimize; i <= eosvmoc::maximum_const_memcpy_intrinsic_to_optimize; ++i) {
3362+
t.set_code("smallmemcpy"_n, fc::format_string(small_memcpy_const_dstsrc_wastfmt, fc::mutable_variant_object("COPY_SIZE", i)).c_str());
3363+
3364+
signed_transaction trx;
3365+
action act;
3366+
act.account = "smallmemcpy"_n;
3367+
act.name = ""_n;
3368+
act.authorization = vector<permission_level>{{"smallmemcpy"_n,config::active_name}};
3369+
act.data.push_back(i);
3370+
trx.actions.push_back(act);
3371+
t.set_transaction_headers(trx);
3372+
trx.sign(t.get_private_key( "smallmemcpy"_n, "active" ), t.get_chain_id());
3373+
t.push_transaction(trx);
3374+
3375+
if(i%10 == 0)
3376+
t.produce_block();
3377+
}
3378+
3379+
} FC_LOG_AND_RETHROW() }
3380+
3381+
//similar to above, but the source and destination values passed to memcpy are not consts
3382+
BOOST_AUTO_TEST_CASE_TEMPLATE( small_var_memcpy_tests, T, validating_testers ) { try {
3383+
T t;
3384+
t.create_account("smallmemcpy"_n);
3385+
t.produce_block();
3386+
3387+
for(unsigned i = eosvmoc::minimum_const_memcpy_intrinsic_to_optimize; i <= eosvmoc::maximum_const_memcpy_intrinsic_to_optimize; ++i) {
3388+
t.set_code("smallmemcpy"_n, fc::format_string(small_memcpy_var_dstsrc_wastfmt, fc::mutable_variant_object("COPY_SIZE", i)).c_str());
3389+
3390+
signed_transaction trx;
3391+
action act;
3392+
act.account = "smallmemcpy"_n;
3393+
act.name = ""_n;
3394+
act.authorization = vector<permission_level>{{"smallmemcpy"_n,config::active_name}};
3395+
act.data.push_back(i);
3396+
trx.actions.push_back(act);
3397+
t.set_transaction_headers(trx);
3398+
trx.sign(t.get_private_key( "smallmemcpy"_n, "active" ), t.get_chain_id());
3399+
t.push_transaction(trx);
3400+
3401+
if(i%10 == 0)
3402+
t.produce_block();
3403+
}
3404+
3405+
} FC_LOG_AND_RETHROW() }
3406+
3407+
//check that small constant sized memcpys (that OC will optimize "away") correctly fail on edge or high side of invalid memory
3408+
BOOST_AUTO_TEST_CASE_TEMPLATE( small_const_memcpy_oob_tests, T, validating_testers ) { try {
3409+
T t;
3410+
t.create_account("smallmemcpy"_n);
3411+
t.produce_block();
3412+
3413+
auto sendit = [&]() {
3414+
signed_transaction trx;
3415+
action act;
3416+
act.account = "smallmemcpy"_n;
3417+
act.name = ""_n;
3418+
act.authorization = vector<permission_level>{{"smallmemcpy"_n,config::active_name}};
3419+
trx.actions.push_back(act);
3420+
t.set_transaction_headers(trx);
3421+
trx.sign(t.get_private_key( "smallmemcpy"_n, "active" ), t.get_chain_id());
3422+
t.push_transaction(trx);
3423+
};
3424+
3425+
t.set_code("smallmemcpy"_n, small_memcpy_overlapenddst_wast);
3426+
BOOST_REQUIRE_EXCEPTION(sendit(), eosio::chain::wasm_execution_error, [](const eosio::chain::wasm_execution_error& e) {return expect_assert_message(e, "access violation");});
3427+
3428+
t.set_code("smallmemcpy"_n, small_memcpy_overlapendsrc_wast);
3429+
BOOST_REQUIRE_EXCEPTION(sendit(), eosio::chain::wasm_execution_error, [](const eosio::chain::wasm_execution_error& e) {return expect_assert_message(e, "access violation");});
3430+
3431+
t.set_code("smallmemcpy"_n, small_memcpy_high_wast);
3432+
BOOST_REQUIRE_EXCEPTION(sendit(), eosio::chain::wasm_execution_error, [](const eosio::chain::wasm_execution_error& e) {return expect_assert_message(e, "access violation");});
3433+
3434+
} FC_LOG_AND_RETHROW() }
3435+
33553436
//test that find_secondary_key behaves like lowerbound
33563437
BOOST_AUTO_TEST_CASE_TEMPLATE( find_seconary_key, T, validating_testers ) {
33573438
try {

unittests/contracts/test_wasts.hpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,4 +1037,84 @@ static const char divmod_host_function_overflow_wast[] = R"=====(
10371037
))
10381038
)
10391039
)
1040+
)=====";
1041+
1042+
static const char small_memcpy_const_dstsrc_wastfmt[] = R"=====(
1043+
(module
1044+
(import "env" "memcpy" (func $$memcpy (param i32 i32 i32) (result i32)))
1045+
(import "env" "memcmp" (func $$memcmp (param i32 i32 i32) (result i32)))
1046+
(export "apply" (func $$apply))
1047+
(memory 1)
1048+
(func $$apply (param i64) (param i64) (param i64)
1049+
;; do copy and check that return value is dst
1050+
(if (i32.ne (call $$memcpy (i32.const 256) (i32.const 64) (i32.const ${COPY_SIZE})) (i32.const 256)) (then unreachable))
1051+
1052+
;; validate copied region
1053+
(if (i32.ne (call $$memcmp (i32.const 256) (i32.const 64) (i32.const ${COPY_SIZE})) (i32.const 0)) (then unreachable))
1054+
1055+
;; check the 4 bytes before and and after the copied region and expect them to still be 0x0
1056+
(if (i32.ne (i32.load (i32.const 252)) (i32.const 0)) (then unreachable))
1057+
(if (i32.ne (i32.load (i32.add (i32.const 256) (i32.const ${COPY_SIZE}))) (i32.const 0)) (then unreachable))
1058+
)
1059+
(data (i32.const 64) "1234567890-abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1060+
)
1061+
)=====";
1062+
1063+
static const char small_memcpy_var_dstsrc_wastfmt[] = R"=====(
1064+
(module
1065+
(import "env" "memcpy" (func $$memcpy (param i32 i32 i32) (result i32)))
1066+
(import "env" "memcmp" (func $$memcmp (param i32 i32 i32) (result i32)))
1067+
(export "apply" (func $$apply))
1068+
(memory 1)
1069+
(func $$doit (param $$dst i32) (param $$src i32)
1070+
;; do copy and check that return value is dst
1071+
(if (i32.ne (call $$memcpy (get_local $$dst) (get_local $$src) (i32.const ${COPY_SIZE})) (get_local $$dst)) (then unreachable))
1072+
1073+
;; validate copied region
1074+
(if (i32.ne (call $$memcmp (get_local $$dst) (get_local $$src) (i32.const ${COPY_SIZE})) (i32.const 0)) (then unreachable))
1075+
1076+
;; check the 4 bytes before and and after the copied region and expect them to still be 0x0
1077+
(if (i32.ne (i32.load (i32.sub (get_local $$dst) (i32.const 4))) (i32.const 0)) (then unreachable))
1078+
(if (i32.ne (i32.load (i32.add (get_local $$dst) (i32.const ${COPY_SIZE}))) (i32.const 0)) (then unreachable))
1079+
)
1080+
(func $$apply (param i64) (param i64) (param i64)
1081+
(call $$doit (i32.const 256) (i32.const 64))
1082+
)
1083+
(data (i32.const 64) "1234567890-abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1084+
)
1085+
)=====";
1086+
1087+
static const char small_memcpy_overlapenddst_wast[] = R"=====(
1088+
(module
1089+
(import "env" "memcpy" (func $memcpy (param i32 i32 i32) (result i32)))
1090+
(export "apply" (func $apply))
1091+
(memory 1)
1092+
(func $apply (param i64) (param i64) (param i64)
1093+
(drop (call $memcpy (i32.const 65532) (i32.const 64) (i32.const 8)))
1094+
)
1095+
1096+
(data (i32.const 64) "1234567890-abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ")
1097+
)
1098+
)=====";
1099+
1100+
static const char small_memcpy_overlapendsrc_wast[] = R"=====(
1101+
(module
1102+
(import "env" "memcpy" (func $memcpy (param i32 i32 i32) (result i32)))
1103+
(export "apply" (func $apply))
1104+
(memory 1)
1105+
(func $apply (param i64) (param i64) (param i64)
1106+
(drop (call $memcpy (i32.const 4) (i32.const 65532) (i32.const 8)))
1107+
)
1108+
)
1109+
)=====";
1110+
1111+
static const char small_memcpy_high_wast[] = R"=====(
1112+
(module
1113+
(import "env" "memcpy" (func $memcpy (param i32 i32 i32) (result i32)))
1114+
(export "apply" (func $apply))
1115+
(memory 1)
1116+
(func $apply (param i64) (param i64) (param i64)
1117+
(drop (call $memcpy (i32.const 4294967295) (i32.const 4294967200) (i32.const 8)))
1118+
)
1119+
)
10401120
)=====";

0 commit comments

Comments
 (0)