-
Notifications
You must be signed in to change notification settings - Fork 33
SC: Validate sync call entry point #1266
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
Conversation
…ture is activated, enforce no_op_if_receiver_not_support_sync_call flag
…ode complex, hard to follow, and not needed for actions. Now check if sync_call entry point directly in do_sync_call which is simple and natural
| } | ||
|
|
||
| bool sync_call_context::no_op_if_receiver_not_support_sync_call()const { | ||
| return flags & 0b10; // second bit from LSB. bit index 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there some pattern we can use to get rid of the naked bit constants here and
spring/libraries/chain/apply_context.cpp
Line 262 in cdb1beb
| EOS_ASSERT((flags & ~0b11) == 0, sync_call_validate_exception, // all but last 2 bits are 0s |
Simplest being something like
enum sync_call_flags {
read_only = 1<<0;
noop.. = 1<<1;
last = noop..;
}then flags are valid if flags <= last.
Lots of other potential approaches too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are getting rid of the naked bit constants, I would recommend you do it like this:
spring/libraries/chain/include/eosio/chain/account_object.hpp
Lines 67 to 72 in e186a90
| bool is_privileged()const { return has_field( flags, flags_fields::privileged ); } | |
| void set_privileged( bool privileged ) { | |
| flags = set_field( flags, flags_fields::privileged, privileged ); | |
| } | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way is fine with me. @spoonincode Do you have any preferences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No preference from me; using has_field() seems, if I'm understanding right, to offer some minor additional paranoia that is good.
My main disappointment is that there is no automatic way to get the last item, thus requiring us to manually specify it. My hunch is it's possible to construct something fancy with constexpr to resolve that but not something I'm advocating we do now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change to use has_field() so it is more consistent with existing code style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked at this further. We need to verify bits other than the first bit can have a value. The has_field does not provide a way to do that. Looks like the existing implementation is a simple one.
| } | ||
| } | ||
| // If the contract contains sync_call entry point, its signature had already been | ||
| // validated when the contract was deployed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this comment is entirely correct -- its signature will be validated provided that the contract was deployed after the activation of SYNC_CALL. If someone deploys a contract that exports sync_call with an unexpected signature prior to activation of SYNC_CALL we will have never checked its signature ahead of time.
In this case here it seems okayish (not UB or such) because EOS VM does validation on each call() that the parameters and parameter count match up with the export. But it does mean if code is deployed with bad signature prior to SYNC_CALL and the call is made with no_op_if_receiver_not_support_sync_call you'll not get a noop but rather some other eosvm assert. In practice we would never expect this situation, but it is quirky behavior enshrined in the protocol we would need to remember and replicate in OC too. I feel like we should tighten this up so the rule is simpler to describe.
i.e. the rule should be something like "when a sync call is made to a contract that does not export an expected sync_call, it aborts with sync_call_not_supported_by_receiver_exception unless the sync call flag no_op_if_receiver_not_support_sync_call is set which makes it a noop"
I thought last time this topic was discussed we were going to cache away a bool somewhere that indicates if sync_call is properly exported. Ideally we do that in a common location so we are not repeating this check in each wasm runtime. Unfortunately I'm not sure exactly where that should go -- I initially thought wasm_cache_entry was the right place but seems not since OC can skip it.
Personally I don't think we should bother adding additional checks to setcode because even if we add a check there we still have have to be prepared for a mismatched sync_call anyways -- the check doesn't really help simplify anything in nodeos.1 Not sure how strongly I feel about it in this case.
Footnotes
-
There is an argument that the check in
setcodemakes it more upfront to a user they got something wrong. There is also an argument the other way that by making the validation more strict we potentially prevent valid code that is deployed today from being redeployed -- though of course that's very academic for this scenario. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like your simplified rule -- not checking the signature.
But we will miss one feature "someone wants a no-op if the signature is bad". But I think it is very rare and we don't need to support it, at least for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"expected sync_call" was meant to imply proper signature.
So the rule should be something like
When a sync call is made to a contract that does not export
sync_callwith a proper signature, it aborts withsync_call_not_supported_by_receiver_exceptionunless the sync call flagno_op_if_receiver_not_support_sync_callis set which makes it a noop
The code does not follow this simple rule right now. The behavior is more complex -- behavior is dependent on when the code was deployed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per team discussion, if the receiver does not have valid entry point, return -1 and let the caller decide what to do about the failure.
…contract deployment, cache a flag indicating whethere a contract is a valid entry point, at execution check the flag and return -1 if the receiver does not have the valid sync call entry point
libraries/chain/apply_context.cpp
Outdated
| EOS_ASSERT((flags & 0xFFFFFFFFFFFFFFFE) == 0, sync_call_validate_exception, | ||
| "sync call's flags ${f} can only set bit 0", ("f", flags)); | ||
| EOS_ASSERT(flags <= static_cast<uint64_t>(sync_call_flags::last), sync_call_validate_exception, // all but last `sync_call_flags::last` bits must be 0s | ||
| "only ${last} least significant bits of sync call's flags (${flags}) can be set", ("last", static_cast<uint64_t>(sync_call_flags::last))("flags", flags)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using sync_call_flags::last here doesn't seem right. If last is 1<<3 then this message will say "only 8 least significant bits of sync call's flags (${flags}) can be set". That value should be 4 instead of 8. I think you want the std::bit_width(sync_call_flags::last)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for noticing this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the check flags <= static_cast<uint64_t>(sync_call_flags::last) is wrong. For example if two bits out of two were set, flags would be 3 and static_cast<uint64_t>(sync_call_flags::last) would be 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I renamed last and added comments to prevent future confusions. Also added flags validation tests.
| * | ||
| */ | ||
| uint32_t call(name receiver, uint64_t flags, span<const char> data); | ||
| int32_t call(name receiver, uint64_t flags, span<const char> data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is academic but it's the cost of CONFIGURABLE_WASM_LIMITS2's infinite knobs: int32_t isn't enough for greater than 2GB unless the contract treats this as unsigned. Unless there is a compelling reason to keep it 32-bit I'd suggest moving to int64_t so that the semantics are clearer for these academic edge cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value returned by call() (the number of sync call return value) is used as the size component for constructing memory argument in get_call_return_value(span<char> memory). In WASM, the memory span is represented as (param i32 i32). Is it safe to cast down from i64 to i32 when the user uses it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another point: the return value size is limited by max_sync_call_data_size, which is likely less than int32_t size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@arhag Any comment about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really have a position on this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| public: | ||
| virtual std::unique_ptr<wasm_instantiated_module_interface> instantiate_module(const char* code_bytes, size_t code_size, | ||
| const digest_type& code_hash, const uint8_t& vm_type, const uint8_t& vm_version) = 0; | ||
| const digest_type& code_hash, const uint8_t& vm_type, const uint8_t& vm_version, bool& sync_call_supporte) = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spelling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no_op_if_receiver_not_support_sync_call flag…d comments about how to add new flags, add tests for flags validation
spoonincode
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are going to need to do some non trivial refactoring when wiring up OC but we can keep this as is for now
…w return type i64_t for call() host function
I copied the link to this comment to the OC issue #1043 |
Implement this feature earlier to help prevent errors in WASM test contracts.
wasm_instantiation_cache, check ifsync_callentry point is present in the contract; if present, validate the signature. Setreceiver_supports_sync_callflag of the cache entry accordingly.receiver_supports_sync_callof the receiver. Iffalse, return-1and let the caller decide what to do about the failure.Resolves #1219