12

From a blogpost on security from Christian Reitwiessner:

Because of the maximal stack depth of 1024 the new bidder can always increase the stack size to 1023 and then call bid() which will cause the send(highestBid) call to silently fail.

So it seems that you can prepare the stack that a send() will fail but the contract will still continue its execution and terminate.

What is the rational behind this behaviour and is it possible to "attack" more than one send() or would a second send() cause an exception if the first already failed "silently".

eth
  • 85,679
  • 53
  • 285
  • 406
mKoeppelmann
  • 7,616
  • 8
  • 36
  • 48

1 Answers1

10

Rationale for depth limit:

Having a 1024 call depth limit - many programming languages break at high stack depths much more quickly than they break at high levels of memory usage or computational load, so the implied limit from the block gas limit may not be sufficient.

The EVM needs to be deterministic. If an Ethereum client is implemented in a language that breaks at high stack depths, then such a client would not be able to implement the Ethereum protocol: it would not be able to produce the results according to consensus.

A recent proposal by Vitalik Buterin is to simply have gas as the limiting factor (though the depth could reach 2091).


function foo() {
  send()
  send()
}

Both send() can be attacked. Invoking the first send() increases the depth by 1, but reduces it again by 1 when it returns. send() doesn't throw, so no exceptions are generated.

To attack foo(), an attacker recursively invokes itself 1023 times, then invokes foo(). (In Solidity, the recursive function should be external so that the call depth is increased [unlike internal functions which are jumps within the code and don't increase the depth].)

(Invokes itself 1023 times because the first invocation before the recursion increases the depth by 1. By the time foo is called, the depth will be at the 1024 limit.)

EDIT: EIP 150 makes it impractical to reach a stack depth of 1024, effectively eliminating call depth attacks. See How does EIP 150 change the call depth attack?

eth
  • 85,679
  • 53
  • 285
  • 406
  • What do you mean by "an attacker recursively invokes itself" ? @eth – alper Dec 16 '17 at 20:41
  • 1
    @Alper Something like an attack() function that does if (i==1023) foo() else { i++; attack(); } – eth Dec 30 '17 at 13:20
  • What's the logic behind attacking yourself, i.e., writing a contract with a function that calls itself recursively 1023 times and then calls something else, which never gets executed? There doesn't seem to be any financial gain to this exploit also. – Garen Vartanian Aug 21 '18 at 02:26
  • @Gviz You're not attacking yourself. The blogpost in the question has an example. Another example is suppose you're buying a non-fungible token from someone, if a call depth attack was possible, it could be used so that you would get the token but end up not paying the other person. – eth Sep 11 '18 at 11:18
  • @eth thanks I see now. One point of clarification: to increase stack depth, an external call is required. Is it sufficient for the attacker contract to have defined a function attack() that is invoked with this.attack() to behave as external? – Garen Vartanian Sep 18 '18 at 18:14
  • @GViz To increase CALL stack depth, the CALL opcode must be used. You're correct that an external call is required, because Solidity implements an internal call (like this.attack) using JUMP opcode instead of CALL. – eth Sep 23 '18 at 03:24
  • @eth in that case, the attack() function should be invoked externally and code should be if (i==1023) someExternalContract.foo() else { i++; this.attack(); } correct? Supposing a bid contract, the previous highest bidder would fail to receive value and the balance on the contract would grow. – Garen Vartanian Oct 03 '18 at 02:25
  • @GViz Looks right. – eth Oct 06 '18 at 05:29