solidity icon indicating copy to clipboard operation
solidity copied to clipboard

[Yul Optimizer] Do not move loop invariants that consume a lot of gas

Open bshastry opened this issue 4 years ago • 14 comments

This is a weird one.

contract C0 {
  function f0() external returns (bytes memory o1)
  {
    assembly
    {
      for { let i := 0 } lt(i, 1) { i := add(i, 1) }
      {
        if o1 { break }
        o1 := keccak256(0, 12345678912)
      }
    }
  }
}
// ====
// compileViaYul: also
// ----
// f0() -> 0x20, 0

fails via Yul.

To repro

$ cp test.sol test/libsolidity/semanticTests
$ isoltest -t semanticTests/test --optimize --show-messages

bshastry avatar Oct 20 '21 21:10 bshastry

Asm diff that I tried to make sense of but gave me :-(

https://gist.github.com/bshastry/0cbd6bb471f3b9c22ebf4b859669ce08

--- obtained from solc test.sol --experimental-via-ir --asm +++ obtained from solc test.sol --ir-optimized --asm

bshastry avatar Oct 20 '21 21:10 bshastry

Another example

contract C0 {
  function f1() external returns (bytes memory o0)
  {
  assembly
    {
      {
          for { o0 := selfbalance() } o0
              {
                o0 := keccak256(0, 1155939646350332)
              } {}
      }
    }
  }
}
// ====
// compileViaYul: also
// ----
// f1() -> 0x20, 0

fails similarly via optimized IR.

bshastry avatar Oct 21 '21 12:10 bshastry

This seems to be due to loop-invariant code motion. cc @hrkrshnn Maybe we should not pull out things that are potentially very expensive? On the other hand, the assumption was that if a loop is present in the code, we expect it to be executed at least once.

chriseth avatar Oct 28 '21 13:10 chriseth

Yeah, the issue is with loop invariant code motion. The keccak is taken out of the loop, where it fails out of gas.

hrkrshnn avatar Oct 28 '21 16:10 hrkrshnn

To be fair, there is no way to fix this without preventing mload, keccak from getting taken out of the loop. Especially if the offsets are not static. It's somewhat implicit that the optimizer assumes infinite gas, but this is a grey area, since EVM implementations seems to explicitly use 2**32 as max memory size and revert in all other cases. So not sure how we want to proceed here.

hrkrshnn avatar Oct 28 '21 16:10 hrkrshnn

On the other hand, the assumption was that if a loop is present in the code, we expect it to be executed at least once.

Just to be precise, the assumption would be if loop present, all paths inside the loop body are executed at least once?

bshastry avatar Oct 28 '21 17:10 bshastry

It's somewhat implicit that the optimizer assumes infinite gas, but this is a grey area, since EVM implementations seems to explicitly use 2**32 as max memory size and revert in all other cases.

The new codegen does assume a Max memory size, right?

See: https://github.com/ethereum/solidity/pull/11714

bshastry avatar Oct 29 '21 12:10 bshastry

The new codegen does assume a Max memory size, right?

The optimizer doesn't assume this.

hrkrshnn avatar Oct 29 '21 12:10 hrkrshnn

I'm fine with closing this as won't fix since I can't think of a strong enough threat model.

I was thinking along the lines of

"Malicious smart contract steals ether (gas) by placing keccak inside a for loop that would only be evaluated if the Yul optimizer is enabled."

But I'm afraid that's not a strong enough threat model.

bshastry avatar Oct 30 '21 08:10 bshastry

This came up again while fuzzing the optimized EVM code transform

{
   function f()
   {
       for { } lt(0, 60) { }
       {
           {
               if xor(origin(), 0xff) { break }
               if mload(0x3fffffffff)
               {
                   revert(0, 0xf)
               }
           }
       }
   }
   f()
}

is optimised to

{
    let _1 := mload(0x3fffffffff)
    let _2 := iszero(eq(origin(), 0xff))
    for { } iszero(_2) { }
    { if _1 { revert(0, 0xf) } }
}

The unoptimised code does not run out of gas because the mload is not evaluated. The optimised code runs out of gas because mload is evaluated.

bshastry avatar Nov 28 '22 07:11 bshastry

JFYI: This is reported quite often by the fuzzer.

bshastry avatar Feb 19 '24 05:02 bshastry

In that case medium impact is probably warranted.

cameel avatar Feb 23 '24 10:02 cameel

djeva

GAMECHANGE avatar Feb 23 '24 11:02 GAMECHANGE

Does running the StructuralSimplifier (t) resolve the problem? Or is it too weak to detect that these conditions are false? It it can handle it, we could just consider it a recommended prerequisite of this step.

We should also mention this effect in LoopInvariantCodeMotion's docs.

cameel avatar May 06 '24 10:05 cameel