On July 1, 2021, a bug in the Solidity code generator was found by differential fuzzing.
The bug causes the legacy code generation pipeline to generate code that
may write dirty values to storage when copying bytes
arrays from calldata
or memory
.
Initially, it was assumed that the dirty values in storage are only observable using inline
assembly. However, resizing a bytes
array using an empty .push()
without actually
writing values to it, can expose the dirty bytes without any use of inline assembly.
The bug was already present in the initial implementation of bytes
arrays in very early versions of the compiler.
Despite this, it was never exploited or reported externally, and therefore we assigned it a severity of “low”.
Which Contracts are Affected?
Most instances of copying bytes
arrays from memory
or calldata
to storage
can be affected
in the sense that they may write dirty values. Those dirty values in storage only become visible,
if there are empty .push()
es to the bytes
array (in older version also assignments to its
.length
field) and the resulting elements are assumed to be zero without actually writing to them.
For example, the following used to result in the newly .push()
ed array element in h()
not being zero:
contract C {
event ev(uint[], uint);
bytes s;
constructor() {
// The following event emission involves writing to temporary memory at the current location
// of the free memory pointer. Several other operations (e.g. certain keccak256 calls) will
// use temporary memory in a similar manner.
// In this particular case, the length of the passed array will be written to temporary memory
// exactly such that the byte after the 63 bytes allocated below will be 0x02. This dirty byte
// will then be written to storage during the assignment and become visible with the push in ``h``.
emit ev(new uint[](2), 0);
bytes memory m = new bytes(63);
s = m;
}
function h() external returns (bytes memory) {
s.push();
return s;
}
}
Similarly, dirty values from calldata
may end up being copied to storage:
contract C {
bytes public s;
function f(bytes calldata c) external returns (bytes memory) {
s = c;
s.push();
return s;
}
}
The function f
here can be called with dirty bytes in calldata past the length of c
, which
will be copied to s
and become visible after s.push()
.
However, the fact that this bug remained undiscovered until now indicates that real projects do not seem to depend
on the fact that empty .push()
es to bytes
arrays are supposed to add a zeroed new element.