On April 7, 2022, a bug in the Solidity code generator was reported by John Toman of the Certora development team. Certora’s bug disclosure post can be found here.

The bug is fixed with Solidity version 0.8.14 released on May 17, 2022. The bug was first introduced in Solidity version 0.5.8.

We assigned the bug a severity of “very low”.

Which Contracts are Affected?

You might be affected if you pass a nested array directly to another external function call or use abi.encode on it.

If calldata is malformed in a certain way, the call might not revert as it should. Instead, data with extra zeros at the end is passed on to the called contract or to the abi.encode function.

Technical Details

Calldata validation for nested dynamic types is deferred until the first access to the nested values. Such an access may for example be a copy to memory or an index or member access to the outer type. While in most such accesses calldata validation correctly checks that the data area of the nested array (see the ABI encoding specification) is completely contained in the passed calldata (i.e. in the range [0, calldatasize()]), this check may not be performed, when ABI encoding such nested types again directly from calldata.

For instance, this can happen, if a value in calldata with a nested dynamic array is passed to an external call, used in abi.encode or emitted as event. In such cases, if the data area of the nested array extends beyond calldatasize(), ABI encoding it did not revert, but continued reading values from beyond calldatasize() (i.e. zero values).

For example, in this contract:

contract C {
	event e(uint[][]);
	function g(uint[][] calldata) external { ... }
	function f(uint[][] calldata a) external pure {
		bytes memory data = abi.encode(a);
		this.g(a);
		emit e(a);
	}
}

A call to f with corrupted calldata, in which an element of a has a data area that extends beyond calldatasize, will not revert.

However, each of the following cases will properly validate against calldatasize and will revert with similarly corrupted calldata. This is because the calldata array is only being decoded rather than decoded and encoded back in a single operation. This is handled by a different code path in the compiler that is unaffected by this bug:

contract C {
	function f1(uint[][] calldata a) external pure {
		a[0]; // Where a[0] is the element that extends beyond ``calldatasize``.
	}
	uint[][] s;
	function f2(uint[][] calldata a) external pure {
		s = a;
	}
	function f2(uint[][] calldata a) external pure {
		uint[][] memory x = a;
	}
}