Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libraries/chain/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ add_library( eosio_chain
chain_config.cpp
chain_id_type.cpp
genesis_state.cpp
code_object.cpp
${CMAKE_CURRENT_BINARY_DIR}/genesis_state_root_key.cpp

wast_to_wasm.cpp
Expand Down
11 changes: 11 additions & 0 deletions libraries/chain/code_object.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include <eosio/chain/code_object.hpp>
#include <eosio/chain/wasm_interface.hpp>

namespace eosio { namespace chain {

void code_object::reflector_init()
{
sync_call_supported = wasm_interface::is_sync_call_supported(code.data(), code.size());
}

}}
6 changes: 5 additions & 1 deletion libraries/chain/eosio_contract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <eosio/chain/global_property_object.hpp>
#include <eosio/chain/contract_types.hpp>

#include <eosio/chain/webassembly/eos-vm.hpp>
#include <eosio/chain/wasm_interface.hpp>
#include <eosio/chain/abi_serializer.hpp>

Expand Down Expand Up @@ -138,11 +139,13 @@ void apply_eosio_setcode(apply_context& context) {

fc::sha256 code_hash; /// default is the all zeros hash

bool sync_call_supported = false;
int64_t code_size = (int64_t)act.code.size();

if( code_size > 0 ) {
code_hash = fc::sha256::hash( act.code.data(), (uint32_t)act.code.size() );
wasm_interface::validate(context.control, act.code);
auto result = wasm_interface::validate(context.control, act.code);
sync_call_supported = result.sync_call_supported;
}

const auto& account = db.get<account_metadata_object,by_name>(act.account);
Expand Down Expand Up @@ -184,6 +187,7 @@ void apply_eosio_setcode(apply_context& context) {
o.first_block_used = context.control.head().block_num() + 1;
o.vm_type = act.vmtype;
o.vm_version = act.vmversion;
o.sync_call_supported = sync_call_supported;
});
}
}
Expand Down
5 changes: 5 additions & 0 deletions libraries/chain/host_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ int64_t host_context::execute_sync_call(name call_receiver, uint64_t flags, std:
return handle_call_failure();
}

const code_object* const codeobject = db.find<code_object, by_code_hash>(boost::make_tuple(receiver_account->code_hash, receiver_account->vm_type, receiver_account->vm_version));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you already look it up here. Seems like it would be cleaner to pass code_object into execute. Then code_hash, vm_type, and vm_version do not have to be passed in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_context uses the same execute() and it does not dip code_object. Even if we pass in code_object here, we still need to split it and pass in code_hash, vm_type and vm_version before calling wasm_interface_private::execute() to reuse the same code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think apply_context might as well look it up and pass it in. In either case it is needed inside wasm_interface_private::execute().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only looks code_object when the code is not cached. We will need two execute()s with different signatures to avoid unnecessary lookup for apply_context. Can we do this refactor in a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about code_object not always needing to be looked up. Maybe we should cache the sync_call_supported in wasm_cache_entry and code_descriptor. That would mean it would have to validated in execute().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have call_offset for oc we can check. Seems a bit fragile to rely on the JIT check, so probably should check it. So add it to the wasm_cache_entry?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have call_offset for oc we can check

Not really though, since it isn't checking for signature now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In current VM JIT implementation, it will crash if sync_call() is not exported. In OC, we have an assert if code.call_offset is 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I plan to do a separate PR for the sync call case to avoid second look up of code_object in wasm_interface_private::get_or_build_instantiated_module().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1461

Will maintain the current simplicity

if (!codeobject || !codeobject->sync_call_supported) {
return handle_call_failure();
}

// use a new sync_call_context for next sync call
sync_call_context call_ctx(control, trx_context, ordinal, get_current_action_trace(), get_sync_call_sender(), call_receiver, receiver_account->is_privileged(), depth, is_next_call_read_only, data);

Expand Down
5 changes: 4 additions & 1 deletion libraries/chain/include/eosio/chain/code_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace eosio { namespace chain {

class code_object : public chainbase::object<code_object_type, code_object> {
class code_object : public chainbase::object<code_object_type, code_object>, fc::reflect_init {
OBJECT_CTOR(code_object, (code))

id_type id;
Expand All @@ -16,6 +16,9 @@ namespace eosio { namespace chain {
uint32_t first_block_used;
uint8_t vm_type = 0; //< vm_type should not be changed within a chainbase modifier lambda
uint8_t vm_version = 0; //< vm_version should not be changed within a chainbase modifier lambda
bool sync_call_supported = false; //< true if code contains a valid sync_call entry point

void reflector_init();
};

struct by_code_hash;
Expand Down
6 changes: 5 additions & 1 deletion libraries/chain/include/eosio/chain/wasm_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace eosio { namespace chain {
class wasm_runtime_interface;
class controller;
namespace eosvmoc { struct config; }
namespace webassembly { namespace eos_vm_runtime { struct validate_result; }}

struct wasm_exit {
int32_t code = 0;
Expand Down Expand Up @@ -69,7 +70,10 @@ namespace eosio { namespace chain {
void indicate_shutting_down();

//validates code -- does a WASM validation pass and checks the wasm against EOSIO specific constraints
static void validate(const controller& control, const bytes& code);
static webassembly::eos_vm_runtime::validate_result validate(const controller& control, const bytes& code);

//returns true if the code contains a valid sync_call entry point
static bool is_sync_call_supported(const char* code_bytes, size_t code_size);

//indicate that a particular code probably won't be used after given block_num
void code_block_num_last_used(const digest_type& code_hash, uint8_t vm_type, uint8_t vm_version,
Expand Down
8 changes: 6 additions & 2 deletions libraries/chain/include/eosio/chain/webassembly/eos-vm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ namespace webassembly { namespace eos_vm_runtime {
using namespace fc;
using namespace eosio::vm;

void validate(const bytes& code, const whitelisted_intrinsics_type& intrinsics );
struct validate_result {
bool sync_call_supported = false;
};

void validate(const controller& control, const bytes& code, const wasm_config& cfg, const whitelisted_intrinsics_type& intrinsics );
validate_result validate(const bytes& code, const whitelisted_intrinsics_type& intrinsics);
validate_result validate(const controller& control, const bytes& code, const wasm_config& cfg, const whitelisted_intrinsics_type& intrinsics);
bool is_sync_call_supported(const char* code_bytes, size_t code_size);

struct apply_options;

Expand Down
11 changes: 7 additions & 4 deletions libraries/chain/wasm_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,12 @@ namespace eosio { namespace chain {
}
#endif

void wasm_interface::validate(const controller& control, const bytes& code) {
webassembly::eos_vm_runtime::validate_result wasm_interface::validate(const controller& control, const bytes& code) {
const auto& pso = control.db().get<protocol_state_object>();

if (control.is_builtin_activated(builtin_protocol_feature_t::configurable_wasm_limits)) {
const auto& gpo = control.get_global_properties();
webassembly::eos_vm_runtime::validate( control, code, gpo.wasm_configuration, pso.whitelisted_intrinsics );
return;
return webassembly::eos_vm_runtime::validate( control, code, gpo.wasm_configuration, pso.whitelisted_intrinsics );
}
Module module;
try {
Expand All @@ -88,13 +87,17 @@ namespace eosio { namespace chain {
wasm_validations::wasm_binary_validation validator(control, module);
validator.validate();

webassembly::eos_vm_runtime::validate( code, pso.whitelisted_intrinsics );
return webassembly::eos_vm_runtime::validate( code, pso.whitelisted_intrinsics );

//there are a couple opportunties for improvement here--
//Easy: Cache the Module created here so it can be reused for instantiaion
//Hard: Kick off instantiation in a separate thread at this location
}

bool wasm_interface::is_sync_call_supported(const char* code_bytes, size_t code_size) {
return webassembly::eos_vm_runtime::is_sync_call_supported(code_bytes, code_size);
}

void wasm_interface::code_block_num_last_used(const digest_type& code_hash, uint8_t vm_type, uint8_t vm_version,
block_num_type first_used_block_num, block_num_type block_num_last_used)
{
Expand Down
48 changes: 46 additions & 2 deletions libraries/chain/webassembly/runtimes/eos-vm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,20 @@ struct setcode_options {
static constexpr bool allow_zero_blocktype = true;
};

void validate(const bytes& code, const whitelisted_intrinsics_type& intrinsics) {
static bool module_has_valid_sync_call(module& mod) {
bool supported = false;
const uint32_t i = mod.get_exported_function("sync_call");
if (i < std::numeric_limits<uint32_t>::max()) {
const vm::func_type& function_type = mod.get_function_type(i);
if (function_type == vm::host_function{{vm::i64, vm::i64, vm::i32}, {}}) {
supported = true;
}
}
return supported;
}

validate_result validate(const bytes& code, const whitelisted_intrinsics_type& intrinsics) {
bool sync_call_supported = false;
wasm_code_ptr code_ptr((uint8_t*)code.data(), code.size());
try {
eos_vm_null_backend_t<setcode_options> bkend(code_ptr, code.size(), nullptr);
Expand All @@ -80,13 +93,20 @@ void validate(const bytes& code, const whitelisted_intrinsics_type& intrinsics)
("module", std::string((char*)imports[i].module_str.raw(), imports[i].module_str.size()))
("fn", std::string((char*)imports[i].field_str.raw(), imports[i].field_str.size())));
}

sync_call_supported = module_has_valid_sync_call(bkend.get_module());
} catch(vm::exception& e) {
EOS_THROW(wasm_serialization_error, e.detail());
}

return validate_result {
.sync_call_supported = sync_call_supported
};
}

void validate( const controller& control, const bytes& code, const wasm_config& cfg, const whitelisted_intrinsics_type& intrinsics ) {
validate_result validate( const controller& control, const bytes& code, const wasm_config& cfg, const whitelisted_intrinsics_type& intrinsics ) {
EOS_ASSERT(code.size() <= cfg.max_module_bytes, wasm_serialization_error, "Code too large");
bool sync_call_supported = false;
wasm_code_ptr code_ptr((uint8_t*)code.data(), code.size());
try {
eos_vm_null_backend_t<wasm_config> bkend(code_ptr, code.size(), nullptr, cfg);
Expand All @@ -106,9 +126,33 @@ void validate( const controller& control, const bytes& code, const wasm_config&
EOS_ASSERT(apply_idx < std::numeric_limits<uint32_t>::max(), wasm_serialization_error, "apply not exported");
const vm::func_type& apply_type = bkend.get_module().get_function_type(apply_idx);
EOS_ASSERT((apply_type == vm::host_function{{vm::i64, vm::i64, vm::i64}, {}}), wasm_serialization_error, "apply has wrong type");

sync_call_supported = module_has_valid_sync_call(bkend.get_module());
} catch(vm::exception& e) {
EOS_THROW(wasm_serialization_error, e.detail());
}

return validate_result {
.sync_call_supported = sync_call_supported
};
}

bool is_sync_call_supported(const char* code_bytes, size_t code_size) {
wasm_code_ptr code_ptr((uint8_t*)code_bytes, code_size);
bool supported = false;
try {
// NOTE: if we ever relax WASM validation via some protocol feature in the future,
// we cannot simply create a backend using the default `setcode_options`.
// The caller of `is_sync_call_supported()` (`code_object::reflector_init()`)
// needs to pass in `controller` so we know what protocol features are enabled
// to decide what kind of backend to create. Or `code_object::sync_call_supported`
// needs to be populated explicitly when snapshot is read.
eos_vm_null_backend_t<setcode_options> bkend(code_ptr, code_size, nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaict this is good now, but it's something to be mindful of in the future -- if we ever relax WASM validation via some protocol feature in the future, this could cause problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What problems might be? Do you mean previously non-supported becomes supported?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When activating CONFIGURABLE_WASM_LIMITS2 validation becomes more strict on setcode to prevent some violations of wasm spec. (contracts that violate these rules would still work after activation but could not be setcode again)

So creating a backend with pre-CONFIGURABLE_WASM_LIMITS2 rules is okay since CONFIGURABLE_WASM_LIMITS2 is more strict.

However let's say we made a protocol feature that relaxed validation for the "read only host functions" we've considered; let's just call that READONLY_HF. This would be a problem because here we'd not allow that relaxation since we're still validating against pre-READONLY_HF (and pre-CONFIGURABLE_WASM_LIMITS2) rules. We'd need access to what protocol features are enabled to know what kind of backend to create, and we don't have access to that information readily in the reflector_init()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation. If that happens, we will have to save sync_call_supported in the snapshot, so that the caller of is_sync_call_supported() can pass in controller to have access to protocol feature set. I will add a comment.

supported = module_has_valid_sync_call(bkend.get_module());
} catch(vm::exception& e) {
EOS_THROW(wasm_serialization_error, e.detail());
}
return supported;
}

// Be permissive on apply.
Expand Down
25 changes: 19 additions & 6 deletions unittests/sync_call_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1097,9 +1097,6 @@ static const char no_sync_call_entry_point_wast[] = R"=====(
BOOST_AUTO_TEST_CASE(no_sync_call_entry_point_test) { try {
validating_tester t;

// disable temporarily
return;

create_accounts_and_set_code(entry_point_validation_caller_wast, no_sync_call_entry_point_wast, t);

BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "doit"_n, "caller"_n, {})); // entry_point_validation_caller_wast will throw if `call` does not return -1
Expand All @@ -1121,14 +1118,30 @@ static const char invalid_entry_point_wast[] = R"=====(
BOOST_AUTO_TEST_CASE(invalid_sync_call_entry_point_test) { try {
validating_tester t;

// disable temporarily
return;

create_accounts_and_set_code(entry_point_validation_caller_wast, invalid_entry_point_wast, t);

BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "doit"_n, "caller"_n, {})); // entry_point_validation_caller_wast will throw if `call` does not return -1
} FC_LOG_AND_RETHROW() }

// Number of parameters are mismatched
static const char entry_point_num_parms_mismatched_wast[] = R"=====(
(module
(export "sync_call" (func $sync_call))
(func $sync_call (param $sender i64) (param $receiver i64)) ;; data_size is missing

(export "apply" (func $apply))
(func $apply (param $receiver i64) (param $account i64) (param $action_name i64))
)
)=====";

// Verify sync call return -1 if sync call entry point signature is invalid
BOOST_AUTO_TEST_CASE(entry_point_num_parms_mismatched_test) { try {
call_tester t({ {"caller"_n, entry_point_validation_caller_wast},
{"callee"_n, entry_point_num_parms_mismatched_wast} });

BOOST_REQUIRE_NO_THROW(t.push_action("caller"_n, "doit"_n, "caller"_n, {})); // entry_point_validation_caller_wast will throw if `call` does not return -1
} FC_LOG_AND_RETHROW() }

// The last LSB is set
static const char valid_flags_wast[] = R"=====(
(module
Expand Down