Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow delete on types containing nested mappings. #11843

Merged
merged 1 commit into from
Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
### 0.9.0 (unreleased)

Breaking changes:
* Disallow ``.pop()`` on arrays containing nested mappings.
* Disallow ``delete`` on types that contain nested mappings.
Comment on lines +4 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this in alphabetic order :-D? I'm honestly not sure :-D. [no need to do anything as far as I'm concerned]

Copy link
Contributor Author

@Marenz Marenz Sep 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. . before d, also originally done by vim using :sort (that's why we had that rebase artefact, it reordered another change, too)

* Inline Assembly: Consider functions, function parameters and return variables for shadowing checks.


Expand Down
44 changes: 4 additions & 40 deletions docs/security-considerations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -317,48 +317,12 @@ key-value data structure that does not keep track of the keys that were
assigned a non-zero value. Because of that, cleaning a mapping without extra
information about the written keys is not possible.
If a ``mapping`` is used as the base type of a dynamic storage array, deleting
or popping the array will have no effect over the ``mapping`` elements. The
same happens, for example, if a ``mapping`` is used as the type of a member
field of a ``struct`` that is the base type of a dynamic storage array. The
``mapping`` is also ignored in assignments of structs or arrays containing a
or popping the array is not possible.
The same is true, for example, if a ``mapping`` is used as the type of a member
field of a ``struct`` that is the base type of a dynamic storage array.
It is also not possible to assign new values to structs or arrays containing a
``mapping``.

.. code-block:: solidity

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Map {
mapping (uint => uint)[] array;

function allocate(uint _newMaps) public {
for (uint i = 0; i < _newMaps; i++)
array.push();
}

function writeMap(uint _map, uint _key, uint _value) public {
array[_map][_key] = _value;
}

function readMap(uint _map, uint _key) public view returns (uint) {
return array[_map][_key];
}

function eraseMaps() public {
delete array;
}
}

Consider the example above and the following sequence of calls: ``allocate(10)``,
``writeMap(4, 128, 256)``.
At this point, calling ``readMap(4, 128)`` returns 256.
If we call ``eraseMaps``, the length of state variable ``array`` is zeroed, but
since its ``mapping`` elements cannot be zeroed, their information stays alive
in the contract's storage.
After deleting ``array``, calling ``allocate(5)`` allows us to access
``array[4]`` again, and calling ``readMap(4, 128)`` returns 256 even without
another call to ``writeMap``.

If your ``mapping`` information must be deleted, consider using a library similar to
`iterable mapping <https://github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol>`_,
allowing you to traverse the keys and delete their values in the appropriate ``mapping``.
Expand Down
22 changes: 19 additions & 3 deletions libsolidity/analysis/TypeChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1594,11 +1594,18 @@ bool TypeChecker::visit(UnaryOperation const& _operation)
requireLValue(_operation.subExpression(), false);
else
_operation.subExpression().accept(*this);

Type const* subExprType = type(_operation.subExpression());
Type const* t = type(_operation.subExpression())->unaryOperatorResult(op);
if (!t)
TypeResult result = subExprType->unaryOperatorResult(op);
Type const* t = result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just remove this and replace every t below with result? If not: also fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember I actually tried that and that there was a problem with that. Something with TypeResult vs Type..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes. We're assigning to t in line 1615 and that's why. Assigning to a TypeResult is at the very least confusing here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind assigning to TypeResult is not that big of a deal (although not sure if it just works and jumping through hoops for being able to would definitely be too much). I'm fine with keeping the t.

if (!result)
{
string description = "Unary operator " + string(TokenTraits::toString(op)) + " cannot be applied to type " + subExprType->toString();
string description = "Unary operator " +
string(TokenTraits::toString(op)) +
" cannot be applied to type " +
subExprType->toString() +
(result.message().empty() ? "" : (": " + result.message()));

if (modifying)
// Cannot just report the error, ignore the unary operator, and continue,
// because the sub-expression was already processed with requireLValue()
Expand Down Expand Up @@ -2838,6 +2845,15 @@ bool TypeChecker::visit(MemberAccess const& _memberAccess)
);

if (
funType->kind() == FunctionType::Kind::ArrayPop &&
exprType->containsNestedMapping()
)
m_errorReporter.typeError(
6298_error,
_memberAccess.location(),
"Storage arrays with nested mappings do not support .pop()."
);
else if (
funType->kind() == FunctionType::Kind::ArrayPush &&
arguments.value().numArguments() != 0 &&
exprType->containsNestedMapping()
Expand Down
4 changes: 4 additions & 0 deletions libsolidity/ast/Types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1449,6 +1449,10 @@ TypeResult ReferenceType::unaryOperatorResult(Token _operator) const
{
if (_operator != Token::Delete)
return nullptr;

if (containsNestedMapping())
return TypeResult::err("Contains a (possibly nested) mapping");

// delete can be used on everything except calldata references or storage pointers
// (storage references are ok)
switch (location())
Expand Down

This file was deleted.

This file was deleted.

16 changes: 0 additions & 16 deletions test/libsolidity/semanticTests/structs/delete_struct.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@ contract test {
struct topStruct {
nestedStruct nstr;
uint topValue;
mapping (uint => uint) topMapping;
}
uint toDelete;
topStruct str;
struct nestedStruct {
uint nestedValue;
mapping (uint => bool) nestedMapping;
}
constructor() {
toDelete = 5;
str.topValue = 1;
str.topMapping[0] = 1;
str.topMapping[1] = 2;

str.nstr.nestedValue = 2;
str.nstr.nestedMapping[0] = true;
str.nstr.nestedMapping[1] = false;
delete str;
delete toDelete;
}
Expand All @@ -31,20 +25,10 @@ contract test {
function getNestedValue() public returns(uint nestedValue){
nestedValue = str.nstr.nestedValue;
}
function getTopMapping(uint index) public returns(uint ret) {
ret = str.topMapping[index];
}
function getNestedMapping(uint index) public returns(bool ret) {
return str.nstr.nestedMapping[index];
}
}
// ====
// compileViaYul: also
// ----
// getToDelete() -> 0
// getTopValue() -> 0
// getNestedValue() -> 0 #mapping values should be the same#
// getTopMapping(uint256): 0 -> 1
// getTopMapping(uint256): 1 -> 2
// getNestedMapping(uint256): 0 -> true
// getNestedMapping(uint256): 1 -> false
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ contract C {
}
}
// ----
// TypeError 6298: (109-118): Storage arrays with nested mappings do not support .pop().
2 changes: 1 addition & 1 deletion test/libsolidity/syntaxTests/negation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ contract test {
}
}
// ----
// TypeError 4907: (97-99): Unary operator - cannot be applied to type uint256
// TypeError 4907: (97-99): Unary operator - cannot be applied to type uint256: Unary negation is only allowed for signed integers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
contract D {
struct Test {
mapping(address => uint) balances;
}

Test test;

constructor()
{
test = Test();
}
}
// ----
// TypeError 9214: (102-106): Types in storage containing (nested) mappings cannot be assigned to.
// TypeError 9515: (109-115): Struct containing a (nested) mapping cannot be constructed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
contract D {
struct Test {
mapping(address => uint) balances;
}

Test test;

constructor()
{
delete test;
}
}
// ----
// TypeError 9767: (102-113): Unary operator delete cannot be applied to type struct D.Test storage ref: Contains a (possibly nested) mapping