Incorrect Calldata Validation in Inter-Contract Communication — Certora Bug Disclosure

John Toman
Certora
Published in
4 min readApr 12, 2022

--

John Toman, VP of R&D at Certora, discovered a previously unknown code generation bug in the Solidity compiler (version 8.13 and lower). This bug allows maliciously crafted calldata buffers to cause victim contracts to incorrectly introduce extra information into their own external calls.

Inter-Contract Communication

As discussed in our previous bug disclosure, information is exchanged between contracts via a serialization format known as the “ABI Specification”. This specification describes how the many different types supported by Solidity are encoded into the untyped and unstructured byte buffers used for inter-contract messages.

Like our previous disclosure, this code generation bug affects the validation of these ABI serialized messages.

Calldata To Calldata

A relatively new feature of Solidity allows programmers to mark external function parameters of reference type as having calldata storage. For these calldata parameters, the Solidity compiler does not eagerly deserialize the contents into the contract's memory; instead, the information is incrementally extracted from the contract's calldata buffer according to how the calldata parameter is used during function execution. Further, aside from basic validation, the Solidity compiler will omit full validation of the encoding of parameters with calldata storage; such validation would require traversing the entire serialized representation, which could be a waste of gas. Instead, for each use of the calldata parameter, the Solidity compiler adds the appropriate validation logic at that use site.

One such use is to pass a parameter with calldata location to another external call. In this case, the Solidity compiler cannot directly reuse the serialized representation in the contract’s calldata buffer. Call buffers (i.e., the buffers sent as part of an inter-contract message) are constructed in memory, which is a totally separate data location within the EVM. Thus, when a parameter with calldata storage is “passed through” to another external call, the Solidity compiler generates code that copies the information out of calldata into the buffer for the external call. As with all calldata parameters, as part of the copying routine, the Solidity compiler includes code that should validate the calldata parameters.

The Bug

Similar to our previous disclosure, the bug affects the validation of nested “dynamic types”, specifically, arrays of arrays. As one would expect, part of the validation of calldata array encoding is that the calldata buffer contains enough data to support the declared length of the arrays encoded in the buffer. In other words, a caller is not allowed to claim that the buffer holds an array of length n and provide fewer than n elements of data in the calldata buffer.

The bug manifests in how this “available elements” check is implemented. The (conceptual) validation process for an array a at offset p in the calldata buffer is as follows:

  1. Compute the offset of the beginning of a's elements (i.e., p + 32)
  2. Subtract a.length * elemSize(a) from the length of the calldata buffer
  3. Check if the value computed in step 2 is greater than or equal to the value in step 1

If implemented correctly, the above process guarantees that, at the point where a's elements are stored, there are at least a.length * elemSize(a) bytes remaining in the calldata buffer.

Unfortunately, the above process is implemented incorrectly when validating the encoding of nested arrays. Consider some array c of type uint[] that is itself an element of an array a (which is of type uint[][]). In step three, instead of comparing to the start location of c's elements, the value computed in step 2 is compared to the start location of a's elements. However, the location of a's elements has no relation to the location of c's elements; in other words, the success (or failure) of this (incorrect) comparison provides no information about whether there is enough data in the calldata buffer for c's declared length.

A proof of concept of this bug is available here.

Impact

The above validation error means that code generated by Solidity will blindly copy non-existent elements past the end of calldata into the buffer for an external call. Note that reading past the end of calldata on the EVM is not an error: any reads past the “end” of the calldata are assumed to return 0. Thus, when copying such malformed calldata arrays into a buffer for an external call, the code generated by Solidity will effectively zero-pad the source array to make up for missing elements. Thus, a contract C can incorrectly change a caller's requested input; if C is a trusted proxy contract, this can lead to difficult-to-revert, erroneous changes being committed to Ethereum's immutable ledger. In addition, combined with other bugs we have detailed elsewhere, malicious attackers could corrupt in-memory bytes buffers that form the calldata input for an external call.

Conclusion

The bug was fixed at Solidity version 0.8.14 at May 18, 2022 https://blog.soliditylang.org/2022/05/17/calldata-reencode-size-check-bug/

--

--