Closed r0qs closed 10 months ago
Just to clarify what I originally said in https://github.com/ethereum/solidity/pull/14086/files#discussion_r1157464962:
Actually, a small correction. @ekpyron rightly pointed out that this is happening with full optimizer enabled, so https://github.com/ethereum/solidity/issues/13972 is not really a solution. It's not something that will be fixed on compiler side because the compiler already has a mechanism to deal with it, it's simply that memory-unsafe assembly prevents that mechanism from kicking in. This will require a change on the Safe side.
Your code worked on earlier 0.8.x releases without marking assembly blocks as memory safe because it was just slightly below the complexity that triggers "Stack too deep" here. Later changes in the compiler seem to have pushed it over the edge. The compiler has a mechanism to keep it compiling (i.e. the memory mover) but that mechanism won't work in presence of memory-unsafe assembly. Your blocks may very well be memory-safe but the compiler only assumes that when they a block does not touch memory. It it does, you have to verify yourself if it's really memory safe and mark it as such manually. The memory-safe annotation was not available in 0.7.x but there's an alternative mechanism via a magic /// @solidity
comment that lets you enable it in a portable way.
@cameel, to clarify if I'm getting it right: all assembly blocks must be memory safe for the compiler to use that mechanism?
Yes. All blocks that are included in the code a contract is built from. So e.g. if you have two different contracts in a file and they don't inherit from each other, blocks in one do not influence the other. But if both use a library function imported from a different file and that function has an unsafe block, both are affected.
Also, like I said, some blocks are implicitly safe and do not need to be explicitly annotated:
Inline assembly that neither involves any operations that access memory nor assigns to any Solidity variables in memory is automatically considered memory-safe and does not need to be annotated.
but an explicit annotation won't hurt either.
One way to check if all blocks are memory safe and there's nothing preventing the memory mover from kicking in is to check the IR output (solc --ir --optimized
on the CLI, not sure if you can easily get that from Hardhat). If you see a memoryguard()
instruction there, it's all safe. But if you get "Stack too deep" you don't even have to check that - the error message will tell you if it's missing (No memoryguard was present
).
Hey @cameel I'm looking into the issue now, and I found an assembly block that's not memory safe: https://github.com/safe-global/safe-contracts/blob/e870f514ad34cd9654c72174d6d4a839e3c6639f/contracts/base/FallbackManager.sol#L64-L80
Here we write data to the memory that may go out of the scratch space bounds, but in our case, it is OK since we never access the memory outside the assembly block. It does compile when marked memory safe. Is there a potential drawback from a security standpoint in doing so? The solidity docs about memory safety tell that memory shall be allocated via the free memory pointer in all cases. I would like to confirm if that's the only option
Yes, this is a security issue. It's not overly likely to become an actual issue, but by telling the compiler that this snippet is memory safe, you allow it to move variables like success
and handler
to memory, which would cause invalid results, since the memory used for this is will potentially be overwritten by the calldatacopy
.
The issue is really on our side, since we should do a better job of localizing stack issues - it's unlikely that there is actually a stack issue in this assembly snippet, but the compiler will currently refuse to move variables to memory anywhere if it sees any memory-unsafe assembly, rather than merely refusing to move variables that can possibly overlap with such memory-unsafe assembly. But short of that being fixed on our end, I'd recommend not to declare this assembly block as memory-safe as is without changing the way it uses memory (by actually reading the value of the free memory pointer and only using memory beyond that offset, rather than overwriting arbitrarily large memory starting at offset zero).
For context, we are re-opening this issue as there are still some non-memory-safe
sections in contracts that should be changed to be memory-safe
. Some examples that have been found are:
memory-safe
, but lacks actual allocation by adding the length to the free memory pointer).There may be other instances as well.
I'm reopening this because the error is back again. After a quick investigation, there seem to be two problematic spots:
CompatibilityFallbackHandler
- https://github.com/safe-global/safe-contracts/blob/ba5324eb9609ce4290428f023843807fe46a83e2/contracts/handler/CompatibilityFallbackHandler.sol#L88
The function is mismarked memory-safe since it never updates the free memory pointer.Race condition!
Hey @ekpyron and @cameel - sorry to necro this thread a bit, but we are changing all assembly to be memory safe right now, and wanted to double check something with you that isn’t clear to us.
In particular the Solidity docs state that the following memory use is safe:
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
One thing I wanted to confirm from @ekpyron’s comment regarding:
It's not overly likely to become an actual issue, but by telling the compiler that this snippet is memory safe, you allow it to move variables like
success
andhandler
to memory
Does the compiler guarantee this to happen before the assembly block? Otherwise, couldn’t you potentially overwrite data written to after the free memory pointer if the allocation happens within the assembly block? I.e. imagine an assembly block that does the following:
t
(so let t := mload(0x40)
)calldatacopy(t, 0, n)
for some n > 0
let variable := someValue()
that the compiler decides to move into memoryNow to clarify, is the memory location of variable
guaranteed to come before t
(in other words, the compiler allocates the variable memory in some “preface” code guaranteed to run before the assembly block), or would the compiler mstore(0x40, add(mload(0x40), 32))
within the assembly block around where the variable is used? If it is the latter, then that means that you need to always “allocate” to the free memory pointer otherwise, from the example above, the variable assignment from step 3 could overwrite the data copied from step 2.
Edit: I did some investigation, and it looks like the compiler is emitting memoryguard(…)
s with the space for the memory variables before any user code gets run. Notably, this means that the free memory pointer is already placed past the space for variables that were moved to memory 🎉. This means that you don’t need to allocate
past the free memory pointer, and can use the memory there within an assembly
block and still be memory-safe (i.e. the docs are right).
Description
Hi, we recently find an issue when compiling
safe-contracts
in our test suite using the latest version of the Solidity compiler with theyul
optimizer enabled. You can read more details and discussions about the issue here: https://github.com/ethereum/solidity/issues/14082In summary, it throws
stack too deep
error (see below) when using the following compiler settings:{"viaIR":true,"optimizer":{"enabled":true, "details": {"yul": true}}}
The problem was introduced by this commit: https://github.com/safe-global/safe-contracts/commit/b9fdbde766eb0fee958cb9b12a3b3e5193fd96ae
Environment
0.8.19
evmVersion: 'paris', viaIR: true, optimizer: {enabled: true, details: {yul: true}}}
Steps to Reproduce
Set the
.env
to:And run:
yarn build
. This will produce the following error:Possible solution
Add the "memory-safe" annotation to assembly code blocks when needed.