From 1cb8e17d949610ec2f9edbc81f2a783e1a461511 Mon Sep 17 00:00:00 2001 From: Daniel Kirchner Date: Tue, 14 Jun 2022 16:34:20 +0200 Subject: [PATCH] Add documentation section about dangling references. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kamil ƚliwak --- docs/types/reference-types.rst | 113 +++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/docs/types/reference-types.rst b/docs/types/reference-types.rst index 5903ac3be..3502df1ab 100644 --- a/docs/types/reference-types.rst +++ b/docs/types/reference-types.rst @@ -468,6 +468,119 @@ Array Members } } +.. index:: ! array;dangling storage references + +Dangling References to Storage Array Elements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When working with storage arrays, you need to take care to avoid dangling references. +A dangling reference is a reference that points to something that no longer exists or has been +moved without updating the reference. A dangling reference can for example occur, if you store a +reference to an array element in a local variable and then ``.pop()`` from the containing array: + +.. code-block:: solidity + + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.8.0 <0.9.0; + + contract C { + uint[][] s; + + function f() public { + // Stores a pointer to the last array element of s. + uint[] storage ptr = s[s.length - 1]; + // Removes the last array element of s. + s.pop(); + // Writes to the array element that is no longer within the array. + ptr.push(0x42); + // Adding a new element to ``s`` now will not add an empty array, but + // will result in an array of length 1 with ``0x42`` as element. + s.push(); + assert(s[s.length - 1][0] == 0x42); + } + } + +The write in ``ptr.push(0x42)`` will **not** revert, despite the fact that ``ptr`` no +longer refers to a valid element of ``s``. Since the compiler assumes that unused storage +is always zeroed, a subsequent ``s.push()`` will not explicitly write zeroes to storage, +so the last element of ``s`` after that ``push()`` will have length ``1`` and contain +``0x42`` as its first element. + +Note that Solidity does not allow to declare references to value types in storage. These kinds +of explicit dangling references are restricted to nested reference types. However, dangling references +can also occur temporarily when using complex expressions in tuple assignments: + +.. code-block:: solidity + + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.8.0 <0.9.0; + + contract C { + uint[] s; + uint[] t; + constructor() { + // Push some initial values to the storage arrays. + s.push(0x07); + t.push(0x03); + } + + function g() internal returns (uint[] storage) { + s.pop(); + return t; + } + + function f() public returns (uint[] memory) { + // The following will first evaluate ``s.push()`` to a reference to a new element + // at index 1. Afterwards, the call to ``g`` pops this new element, resulting in + // the left-most tuple element to become a dangling reference. The assignment still + // takes place and will write outside the data area of ``s``. + (s.push(), g()[0]) = (0x42, 0x17); + // A subsequent push to ``s`` will reveal the value written by the previous + // statement, i.e. the last element of ``s`` at the end of this function will have + // the value ``0x42``. + s.push(); + return s; + } + } + +It is always safer to only assign to storage once per statement and to avoid +complex expressions on the left-hand-side of an assignment. + +You need to take particular care when dealing with references to elements of +``bytes`` arrays, since a ``.push()`` on a bytes array may switch :ref:`from short +to long layout in storage`. + +.. code-block:: solidity + + // SPDX-License-Identifier: GPL-3.0 + pragma solidity >=0.8.0 <0.9.0; + + contract C { + bytes x = "012345678901234567890123456789"; + + function test() external returns(uint) { + (x.push(), x.push()) = (0x01, 0x02); + return x.length; + } + } + +Here, when the first ``x.push()`` is evaluated, ``x`` is still stored in short +layout, thereby ``x.push()`` returns a reference to an element in the first storage slot of +``x``. However, the second ``x.push()`` switches the bytes array to large layout. +Now the element that ``x.push()`` referred to is in the data area of the array while +the reference still points at its original location, which is now a part of the length field +and the assignment will effectively garble the length of ``x``. +To be safe, only enlarge bytes arrays by at most one element during a single +assignment and do not simultaneously index-access the array in the same statement. + +While the above describes the behaviour of dangling storage references in the +current version of the compiler, any code with dangling references should be +considered to have *undefined behaviour*. In particular, this means that +any future version of the compiler may change the behaviour of code that +involves dangling references. + +Be sure to avoid dangling references in your code! + .. index:: ! array;slice .. _array-slices: