Skip to content

SC: Sync call entry point validation for eosvm, eosvm-jit, and eosvmoc #1455

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 8, 2025

Conversation

linh2931
Copy link
Member

@linh2931 linh2931 commented Apr 28, 2025

This PR implements a new method to validate sync call entry point. It is applicable to eosvm, eosvm-jit, and eosvmoc. When an account code is deployed, check whether sync_call entry point exists. If it does, its signature is checked. A flag sync_call_supported is added to code_object. Before a sync call is executed, sync_call_supported is used to determine whether the call be proceeded.

sync_call_supported is not written into snapshots. It is populated from the code when reading from snapshots.

Resolves #1219 and #1392

@linh2931 linh2931 requested review from spoonincode and heifner April 28, 2025 14:13
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);
wasm_interface::validate(context.control, act.code, sync_call_supported);
Copy link
Member Author

Choose a reason for hiding this comment

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

I was debating whether I should do the sync_call check here inside validate(), or call is_sync_call_supported() separately so not changing validate()'s signature. I opted to do it in validate() to save reconstruction of vm::backend from the code and an extra call to is_sync_call_supported().

Copy link
Member

Choose a reason for hiding this comment

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

Returning a struct of bools (only sync_call_supported for now) would be better I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

Initially I thought about returning a bool, but that does not sound right as the method name is validate. A struct of bools is a better choice. Thanks.

uint8_t vm_type = 0;
uint8_t vm_version = 0;
};

Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused on this new snapshot_code_object_v1 because it's the same as the code_object previously was serialized. Also, when we're converting from different chainbase<->snapshot types usually (always?) we use snapshot_row_traits which I don't see anywhere in here.

I'm suspicious we don't need any of these snapshot changes and can instead add a code_object::reflector_init() which just populates the new sync_call_supported field. (this would mean that we'd not include sync_call_supported as part of the fc_reflect which is a little weird but not unheard of)

I'm not 100% confident on the above but I'd suggest trying it

Copy link
Member Author

Choose a reason for hiding this comment

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

The new snapshot_code_object_v1 is for snapshot versions prior to sync call protocol feature. We have similar structs for block_state and chain_config.

Will try using code_object::reflector_init(). Thanks.

Copy link
Member

Choose a reason for hiding this comment

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

ah okay yeah I saw maximum_version = 8; but I forgot we were on 9 on this branch

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks!

code_object::reflector_init() works. We don't need to place sync_call_supported into snapshots.

Copy link
Member

@spoonincode spoonincode left a comment

Choose a reason for hiding this comment

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

Looks really good

@@ -174,6 +175,7 @@ void apply_eosio_setcode(apply_context& context) {
if( new_code_entry ) {
db.modify(*new_code_entry, [&](code_object& o) {
++o.code_ref_count;
o.sync_call_supported = sync_call_supported;
Copy link
Member

Choose a reason for hiding this comment

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

This line is unnecessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

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 );
webassembly::eos_vm_runtime::validate( control, code, gpo.wasm_configuration, pso.whitelisted_intrinsics, sync_call_supported );
Copy link
Member

Choose a reason for hiding this comment

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

I know it is academic for Vaulta, but in the case where CONFIGURABLE_WASM_LIMITS2 has not be activated we fall down below and don't populate sync_call_supported. We really should populate it both places.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about it. Because sync_call depends on all previous protocol features, CONFIGURABLE_WASM_LIMITS2 should have been activated. That's why I did not add to the other validate(). But will add for completeness and for not missing some corner cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

@linh2931
Copy link
Member Author

Looks really good

Thanks for all your suggestions

@@ -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
Member

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
Member 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
Member

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
Member 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
Member

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
Member

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
Member

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
Member 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
Member 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
Member 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

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);
wasm_interface::validate(context.control, act.code, sync_call_supported);
Copy link
Member

Choose a reason for hiding this comment

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

Returning a struct of bools (only sync_call_supported for now) would be better I think.

wasm_code_ptr code_ptr((uint8_t*)code_bytes, code_size);
bool supported = false;
try {
eos_vm_null_backend_t<setcode_options> bkend(code_ptr, code_size, nullptr);
Copy link
Member

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
Member 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
Member

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
Member 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.

Base automatically changed from revert_entry_point_validation to sync_call May 7, 2025 22:02
@linh2931 linh2931 merged commit 2e4f2c2 into sync_call May 8, 2025
28 checks passed
@linh2931 linh2931 deleted the new_entry_point_validation branch May 8, 2025 13:00
@linh2931 linh2931 restored the new_entry_point_validation branch July 5, 2025 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants