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

Memory-Safe Proxy Code Without Allocation #67

Merged
merged 1 commit into from
Dec 14, 2023
Merged

Conversation

nlordell
Copy link
Collaborator

@nlordell nlordell commented Dec 1, 2023

This PR modifies the account fallback code to no longer use allocations, as they are not required for memory-safety. From the docs:

In particular, a memory-safe assembly block may only access the following memory ranges:

  • [...]
  • Temporary memory that is located after the value of the free memory pointer at the beginning of the assembly block, i.e. memory that is “allocated” at the free memory pointer without updating the free memory pointer.

[...]

On the other hand, the following code is memory safe, because memory beyond the location pointed to by the free memory pointer can safely be used as temporary scratch space:

assembly ("memory-safe") {
  let p := mload(0x40)
  returndatacopy(p, 0, returndatasize())
  revert(p, returndatasize())
}

I also did some investigation and found that when variables are moved into memory, the space gets reserved before user code starts, meaning that the free memory pointer already accounts for the reserved space (i.e. setting variables cannot write past the free memory pointer ever). With that in mind, we do not need to update the free memory pointer when we write past it for scratch space.

cc @mmv08

@nlordell nlordell requested a review from a team as a code owner December 1, 2023 12:19
@nlordell nlordell requested review from rmeissner, akshay-ap, mmv08 and remedcu and removed request for a team December 1, 2023 12:19
@mmv08
Copy link
Member

mmv08 commented Dec 11, 2023

I'm struggling to understand how it works. It says that the memory beyond the location can be safely used, but how? If we put some data to the memory location P, now the safe space is P + data size (in my understanding), but it didn't get updated. Or does the returndatacopy opcode update it under the hood?

@mmv08
Copy link
Member

mmv08 commented Dec 11, 2023

Is that related to the memoryguard(…) you mentioned in a comment in the original issue?

@nlordell
Copy link
Collaborator Author

Is that related to the memoryguard(…) you mentioned in a comment in the original issue?

Exactly!

Typically, the Solidity compiler sets the free memory pointer to 0x80 on startup (this is the end of the “reserved” memory regions where new allocations can be made). However, when it decides to move variables into memory, it reserves the extra needed space right on startup. Specifically, the initial value of the free memory pointer will instead be 0x80 + requiredReservedSpace. In the example linked in the issue, the very first thing that will be executed by the contract is:

                let _1 := memoryguard(0x0440)
                mstore(64, _1)

This translates to the following opcodes:

[...]
PUSH2 0x440
[DUP1] # compiler optimization as the 0x440 gets used for a CALLDATACOPY right after
PUSH1 0x40
MSTORE
[...]

So, basically the free memory pointer is initialized to something large enough so that the required space for the memory variables is already allocated.

@mmv08
Copy link
Member

mmv08 commented Dec 11, 2023

Is that related to the memoryguard(…) you mentioned in a comment in the original issue?

Exactly!

Typically, the Solidity compiler sets the free memory pointer to 0x80 on startup (this is the end of the “reserved” memory regions where new allocations can be made). However, when it decides to move variables into memory, it reserves the extra needed space right on startup. Specifically, the initial value of the free memory pointer will instead be 0x80 + requiredReservedSpace. In the example linked in the issue, the very first thing that will be executed by the contract is:

                let _1 := memoryguard(0x0440)
                mstore(64, _1)

This translates to the following opcodes:

[...]
PUSH2 0x440
[DUP1] # compiler optimization as the 0x440 gets used for a CALLDATACOPY right after
PUSH1 0x40
MSTORE
[...]

So, basically the free memory pointer is initialized to something large enough so that the required space for the memory variables is already allocated.

Understood now! Thanks so much for the detailed explanation

@nlordell
Copy link
Collaborator Author

@mmv08 - do you think we should merge as is or is further clarification needed?

@mmv08
Copy link
Member

mmv08 commented Dec 14, 2023

@mmv08 - do you think we should merge as is or is further clarification needed?

Ah, sorry, I forgot to approve. It can be merged for sure

@nlordell nlordell merged commit 2fe7273 into main Dec 14, 2023
1 check passed
@github-actions github-actions bot locked and limited conversation to collaborators Dec 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants