Skip to content
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

SmartContract: use executing contract state to check permissions #3290

Merged
merged 5 commits into from
Jun 11, 2024

Conversation

AnnaShaleva
Copy link
Member

@AnnaShaleva AnnaShaleva commented Jun 3, 2024

Description

It's not correct to use an updated contract state got from native Management to check for the allowed method call. We need to use manifest from the currently executing context for that. It may be critical for cases when executing contract is being updated firstly, and after that it calls another contract. So we need an old (executing) contract manifest for this check.

This change likely does not affect the mainnet's state since it's hard to meet the trigger criteria, thus we suggest not to use a hardfok.

A port of nspcc-dev/neo-go#3473. This bug was discovered during the similar problem fix in nspcc-dev/neo-go#3471 and nspcc-dev/neo-go#3472. I've checked all other similar usages and the rest of them use proper contract state (executing one, not the Management's one).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • TestSystem_Contract_Call_Permissions unit test
  • In progress: check mainnet states for compatibility Moved under D hardfork.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@AnnaShaleva AnnaShaleva added the Bug Used to tag confirmed bugs label Jun 3, 2024
@AnnaShaleva AnnaShaleva added this to the v3.8.0 milestone Jun 3, 2024
@AnnaShaleva AnnaShaleva requested a review from a team June 3, 2024 16:04
cschuchardt88
cschuchardt88 previously approved these changes Jun 3, 2024
@Jim8y
Copy link
Contributor

Jim8y commented Jun 4, 2024

It's not correct to use an updated contract state got from native Management to check for the allowed method call. We need to use manifest from the currently executing context for that. It may be critical for cases when executing contract is being updated firstly, and after that it calls another contract. So we need an old (executing) contract manifest for this check.

Isn't this by design? I mean, it also makes sense to me to use the new state right after the its updated. Can you explain a little bit more about what problem can this cause that makes it a bug?

@Jim8y Jim8y self-requested a review June 4, 2024 02:53
@AnnaShaleva
Copy link
Member Author

Isn't this by design?

I doubt so. This is a quite severe bug that may lead to unwanted consequences. Besides, all other similar places use executing contract state, for example:

protected static void OnCallT(ExecutionEngine engine, Instruction instruction)

protected internal void RuntimeNotify(byte[] eventName, Array state)

Can you explain a little bit more about what problem can this cause that makes it a bug?

Sure, one of possible problems is described in nspcc-dev/neo-go#3471. The contract is being prohibited from updating (by buggy NeoGo node) because new manifest does not allow the notification that old contract code emits after update. Another unwanted consequence of this bug is: consider the contract being destroyed and calling another contract after that. Without this fix the destroy transaction will be FAULTed. In general, every contract call after update/destroy is affected by this bug.

Also, Jimmy, I'm kindly asking not to modify the PR (this or others). You may leave the comments, and I'm always here to give a response and fix something. But I think we need to come to an agreement before modifying the code (to avoid situations like #3209 (comment)).

Copy link
Member

@vncoelho vncoelho left a comment

Choose a reason for hiding this comment

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

I understand the change,
However, I see that it would need a HF, otherwise we could fall in the same problem and discussion we recently have had.

@AnnaShaleva
Copy link
Member Author

However, I see that it would need a HF, otherwise we could fall in the same problem and discussion we recently have had.

I understand, but: mainnet/testnet check is in progress (I'll write once it's finished) and this change likely did not affect the mainnet's state since it's hard to meet the trigger criteria. And if no one pushes such transaction to the chain in the nearest future, then it will be safe to update without hardfork. Thus, @roman-khimov's suggestion was to try to avoid introducing hardfork for this change.

@vncoelho
Copy link
Member

vncoelho commented Jun 4, 2024

I understand, but: mainnet/testnet check is in progress (I'll write once it's finished) and this change likely did not affect the mainnet's state since it's hard to meet the trigger criteria. And if no one pushes such transaction to the chain in the nearest future, then it will be safe to update without hardfork. Thus, @roman-khimov's suggestion was to try to avoid introducing hardfork for this change.

Unfortunately, this expectation can not guide our decision, @AnnaShaleva .
Some bad actor can watch github. It is dangerous because we never know when a TX can be pushed. The result can be catastrophic because there could be an attack and outflow from a CEX, for example.
If we had just DeFi in a SOLO-Chain attacks could be "reverted", but that is not the case of current blockchain ecosystem with bridges and etc...

@roman-khimov
Copy link
Contributor

We can have it HF-dependent, no problem with that.

@AnnaShaleva
Copy link
Member Author

Good, then I'll update the code.

@vncoelho
Copy link
Member

vncoelho commented Jun 4, 2024

Ok, that is a first point. Now it is time to discuss the issue itself...aheuaheua

It's not correct to use an updated contract state got from native
Management to check for the allowed method call. We need to use
manifest from the currently executing context for that. It may be
critical for cases when executing contract is being updated firstly,
and after that it calls another contract. So we need an old (executing)
contract manifest for this check.

This change is moved under D hardfork to avoid state diff issues on
nodes update. Although it should be noted that it's hard to meet the
trigger criteria.

A port of nspcc-dev/neo-go#3473. This bug was
discovered during the similar problem described in
nspcc-dev/neo-go#3471 and fixed in
nspcc-dev/neo-go#3472. I've checked all other
similar usages and the rest of them use proper contract state (executing
one, not the Management's one).

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
@AnnaShaleva
Copy link
Member Author

We can have it HF-dependent, no problem with that.

Done, ready for review.

@@ -15,6 +15,7 @@ public enum Hardfork : byte
{
HF_Aspidochelone,
HF_Basilisk,
HF_Cockatrice
HF_Cockatrice,
HF_Domovoi
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggestions on name are welcomed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not familier with any of them, LOL, LGTM

Copy link
Member

Choose a reason for hiding this comment

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

Same as Jimmy

@AnnaShaleva AnnaShaleva requested a review from Jim8y June 4, 2024 13:39
@vang1ong7ang
Copy link
Contributor

i think now it's okay to merge

@vang1ong7ang
Copy link
Contributor

vang1ong7ang commented Jun 6, 2024

but the necessity of this PR is worth discussing again

i don't think there's any FAULT in the original design. it's just which impl is better

@roman-khimov
Copy link
Contributor

i don't think there's any FAULT in the original design.

It's using wrong manifest for permission checks after the update. And can't work at all after destroy. Contracts can have any code after update/destroy (not supposed to, but can), so either it works correctly or fails in unpredictable manner.

@lingyido
Copy link
Contributor

lingyido commented Jun 6, 2024

It's using wrong manifest for permission checks after the update. And can't work at all after destroy. Contracts can have any code after update/destroy (not supposed to, but can), so either it works correctly or fails in unpredictable manner.

Maybe it's by design. And they're actually predictable if they can understand what will happen under current implementation. Running new _deploy code under new Environment and running old update-folllowed code under old Enviroment is somehow reasonable. As you said, one is not supposed to do such an operation. Do we indeed need this new HF for changing this behavior?

What is more curious is how many Environments are there apart from the external-call-permission-control and notification-control. Do they follow the same logic?

@AnnaShaleva
Copy link
Member Author

AnnaShaleva commented Jun 6, 2024

Do we indeed need this new HF for changing this behavior?

Mainnet is checked and confirmed not to be affected by this bug up to 5484582 height (ref. nspcc-dev/neo-go#3473 (comment)). However, it can be changed at any moment if a suitable transaction will be pushed to mainnet (see #3290 (comment)). And since we decide not to allow state changes anymore, we'd better move it under hardfork.

What is more curious is how many Environments are there apart from the permission-control and notification control.

There's one more explicit call to the executing contract state (see the #3290 (comment)) and a wide set of calls to other executing context-bound things like call flags.

Do they follow the same logic?

Yes, they do, that's why I've created this PR.

@lingyido
Copy link
Contributor

lingyido commented Jun 6, 2024

It's better to show an example here for developers to decide which is good.

Let's say. There is an old contract on chain whose code are as follows.

// Permission: allow call Contract_Other's "test"
// Events: Synced

public static void Update(ByteString nefFile, string manifest)
{
    some_code_here(); // this should follow old contract's restrict
    ContractManagement.Update(nefFile, manifest, null);
    some_other_code_here(); // containing notification and external call, what does these code should follow? new contract's or old contract's?
}
public static void destroy() {
    some_code_here(); // this should follow old contract's restrict
    ContractManagement.Destroy();
    some_other_code_here(); // containing notification and external call, what does these code should follow? act like a non-contract? act like the old contract still exists?
}
  1. what restriction should some_other_code_here follow for update and destroy?
    a. follow old contract's restriction
    b. follow new contract's restriction for udpate and act like a pure non-contract script for destroy (or fails because contract not exist anymore like what is done now)

  2. What if they decided to update the implementation to this way.
    a. follow old contract and succeed for Contract NEW_A, fail for Contract NEW_B
    b. follow new contract and fail for Contract NEW_A, succeed for Contract NEW_B

** Notice that _deploy is automatically called by ContractManagement at the end of ContractManagement.Update's internal implementation. **

Contract NEW_A

// Permission:  NO
// Events:  NO

public static void _deploy(object data, bool update) {
            Synced()
            Contract.Call(Contract_Other, "test", CallFlags.All, new object[] {})
}
Contract NEW_B

// Permission: Contract_Other's "newtest"
// Events:  NewSynced()

public static void _deploy(object data, bool update) {
            NewSynced()
            Contract.Call(Contract_Other, "newtest", CallFlags.All, new object[] {})
}

For problem 2, it seems that we should follow 2.b and current implementation for C# follows this way now.

From what I understand, what this PR is trying to change is for problem 1's external call control (the notification control already follows 1.a). The old implementation is 1.b and the new implementation is 1.a. Please correct me if I'm wrong here @AnnaShaleva @roman-khimov .

@roman-khimov
Copy link
Contributor

Execution context must be consistent. Contract is NEF+manifest. If we have a contract loaded into VM it must use appropriate manifest that is was deployed with. We're not changing contract's code (NEF, effectively) on the invocation stack after update, right? We're not aborting its execution after destroy. So, we must not change its manifest. Checking permissions against some new version (or failing if contract is destroyed) when we have old bytecode executing is just wrong.

@lingyido
Copy link
Contributor

lingyido commented Jun 6, 2024

After playing with those examples, I stand with @AnnaShaleva if my understanding on that example is right.
It's indeed a serious problem and have the necessity. What's your opinion @vang1ong7ang?

@vang1ong7ang
Copy link
Contributor

okay i'm convinced and let's merge

@lingyido
Copy link
Contributor

lingyido commented Jun 6, 2024

There is another external-call method called CallToken, it also reads tokens from old contract state. It mays this change more reasonable.

@AnnaShaleva AnnaShaleva added Ready to Merge and removed Blocked This issue can't be worked at the moment labels Jun 6, 2024
@AnnaShaleva AnnaShaleva mentioned this pull request Jun 7, 2024
14 tasks
@AnnaShaleva AnnaShaleva requested a review from NGDAdmin June 7, 2024 15:10
@AnnaShaleva AnnaShaleva removed this from the v3.8.0 milestone Jun 10, 2024
@vncoelho
Copy link
Member

Although simple I stand with @lingyido in his example, I just worry about the change in the logic as in comment #3290 (comment)

Devs should be aware of this change

@NGDAdmin NGDAdmin merged commit 5cf7446 into master Jun 11, 2024
7 checks passed
@NGDAdmin NGDAdmin deleted the fix-call-permissions branch June 11, 2024 03:42
AnnaShaleva added a commit to nspcc-dev/neo-go that referenced this pull request Jun 11, 2024
It's not correct to use an updated contract state got from Management to
check for the allowed method call. We need to use manifest from the
currently executing context for that. It may be critical for cases when
executing contract is being updated firstly, and after that calls
another contract. So we need an old (executing) contract manifest for
this check.

This change likely does not affect the mainnet's state since it's hard
to meet the trigger criteria, but I'd put it under the hardfork anyway.

Ref. neo-project/neo#3290.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
NGDAdmin added a commit that referenced this pull request Jun 12, 2024
* [Neo Core Bug]fix 3300 (#3301)

* fix 3300

* update format

* add state subitems to ref counter, with suggestion from DuSmart

* apply hardfork

* format

* my mistake

* fix hardfork

* remove negative check

* add unit test

* apply anna's suggestion

---------

Co-authored-by: Shargon <shargon@gmail.com>
Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com>

* SmartContract: use executing contract state to check permissions (#3290)

It's not correct to use an updated contract state got from native
Management to check for the allowed method call. We need to use
manifest from the currently executing context for that. It may be
critical for cases when executing contract is being updated firstly,
and after that it calls another contract. So we need an old (executing)
contract manifest for this check.

This change is moved under D hardfork to avoid state diff issues on
nodes update. Although it should be noted that it's hard to meet the
trigger criteria.

A port of nspcc-dev/neo-go#3473. This bug was
discovered during the similar problem described in
nspcc-dev/neo-go#3471 and fixed in
nspcc-dev/neo-go#3472. I've checked all other
similar usages and the rest of them use proper contract state (executing
one, not the Management's one).

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
Co-authored-by: Shargon <shargon@gmail.com>
Co-authored-by: Jimmy <jinghui@wayne.edu>
Co-authored-by: Vitor Nazário Coelho <vncoelho@gmail.com>

* v3.7.5

* Neo.CLI: enable hardforks for NeoFS mainnet (#3240)

Otherwise this configuration file is broken. Port changes from
nspcc-dev/neo-go#3446.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>

* fix workflow & FS config

* remove hardfork for fs testnet

---------

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
Co-authored-by: Jimmy <jinghui@wayne.edu>
Co-authored-by: Shargon <shargon@gmail.com>
Co-authored-by: NGD Admin <154295625+NGDAdmin@users.noreply.github.com>
Co-authored-by: Anna Shaleva <shaleva.ann@nspcc.ru>
Co-authored-by: Vitor Nazário Coelho <vncoelho@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug Used to tag confirmed bugs Hardfork Ready to Merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants