Skip to content

Commit ec5b8d8

Browse files
authored
Merge pull request #2677 from crytic/dev-improve-storage-pointer
Improve support for storage pointer analysis
2 parents 05db8c0 + bbedc22 commit ec5b8d8

File tree

8 files changed

+297
-57
lines changed

8 files changed

+297
-57
lines changed

Diff for: slither/core/cfg/node.py

+76-28
Original file line numberDiff line numberDiff line change
@@ -946,42 +946,90 @@ def _convert_ssa(v: Variable) -> Optional[Union[StateVariable, LocalVariable]]:
946946
non_ssa_var = function.get_local_variable_from_name(v.name)
947947
return non_ssa_var
948948

949-
def update_read_write_using_ssa(self) -> None:
950-
if not self.expression:
951-
return
952-
for ir in self.irs_ssa:
953-
if isinstance(ir, PhiCallback):
954-
continue
955-
if not isinstance(ir, (Phi, Index, Member)):
956-
self._ssa_vars_read += [
957-
v for v in ir.read if isinstance(v, (StateIRVariable, LocalIRVariable))
958-
]
959-
for var in ir.read:
960-
if isinstance(var, ReferenceVariable):
961-
origin = var.points_to_origin
962-
if isinstance(origin, (StateIRVariable, LocalIRVariable)):
963-
self._ssa_vars_read.append(origin)
964-
965-
elif isinstance(ir, (Member, Index)):
966-
variable_right: RVALUE = ir.variable_right
967-
if isinstance(variable_right, (StateIRVariable, LocalIRVariable)):
968-
self._ssa_vars_read.append(variable_right)
969-
if isinstance(variable_right, ReferenceVariable):
970-
origin = variable_right.points_to_origin
949+
def _update_read_using_ssa(self, ir: Operation) -> None:
950+
"""
951+
Update self._ssa_vars_read
952+
This look for all operations that read a IRvariable
953+
It uses the result of the storage pointer
954+
- For "normal" operation, the read are mostly everything in ir.read
955+
- For "index", the read is the left part (the right part being a reference variable)
956+
- For Phi, nothing is considered read
957+
958+
"""
959+
960+
# For variable read, phi and index have special treatments
961+
# Phi don't lead to values read
962+
# Index leads to read the variable right (the left variable is a ref variable, not the actual object)
963+
# Not that Member is a normal operation here, given we filter out constant by checking for the IRvaraible
964+
if not isinstance(ir, (Phi, Index)):
965+
self._ssa_vars_read += [
966+
v for v in ir.read if isinstance(v, (StateIRVariable, LocalIRVariable))
967+
]
968+
for var in ir.read:
969+
if isinstance(var, ReferenceVariable):
970+
origin = var.points_to_origin
971971
if isinstance(origin, (StateIRVariable, LocalIRVariable)):
972972
self._ssa_vars_read.append(origin)
973973

974-
if isinstance(ir, OperationWithLValue):
975-
if isinstance(ir, (Index, Member, Length)):
976-
continue # Don't consider Member and Index operations -> ReferenceVariable
977-
var = ir.lvalue
978-
if isinstance(var, ReferenceVariable):
979-
var = var.points_to_origin
974+
# If we read from a storage variable (outside of phi operator)
975+
if isinstance(var, LocalIRVariable) and var.is_storage:
976+
for refer_to in var.refers_to:
977+
# the following should always be true
978+
if isinstance(refer_to, (StateIRVariable, LocalIRVariable)):
979+
self._ssa_vars_read.append(refer_to)
980+
981+
elif isinstance(ir, Index):
982+
variable_right: RVALUE = ir.variable_right
983+
if isinstance(variable_right, (StateIRVariable, LocalIRVariable)):
984+
self._ssa_vars_read.append(variable_right)
985+
986+
if isinstance(variable_right, ReferenceVariable):
987+
origin = variable_right.points_to_origin
988+
if isinstance(origin, (StateIRVariable, LocalIRVariable)):
989+
self._ssa_vars_read.append(origin)
990+
991+
def _update_write_using_ssa(self, ir: Operation) -> None:
992+
"""
993+
Update self._ssa_vars_written
994+
This look for all operations that write a IRvariable
995+
It uses the result of the storage pointer
996+
997+
Index/member/Length are not considering writing to anything
998+
For index/member it is implictely handled when their associated RefernceVarible are written
999+
1000+
"""
1001+
1002+
if isinstance(ir, OperationWithLValue) and not isinstance(ir, Phi):
1003+
if isinstance(ir, (Index, Member, Length)):
1004+
return # Don't consider Member and Index operations -> ReferenceVariable
1005+
1006+
var = ir.lvalue
1007+
1008+
if isinstance(var, ReferenceVariable):
1009+
var = var.points_to_origin
1010+
1011+
candidates = [var]
1012+
1013+
# If we write to a storage pointer, add everything it points to as target
1014+
if isinstance(var, LocalIRVariable) and var.is_storage:
1015+
candidates += var.refers_to
1016+
1017+
for var in candidates:
9801018
# Only store non-slithIR variables
9811019
if var and isinstance(var, (StateIRVariable, LocalIRVariable)):
9821020
if isinstance(ir, PhiCallback):
9831021
continue
9841022
self._ssa_vars_written.append(var)
1023+
1024+
def update_read_write_using_ssa(self) -> None:
1025+
1026+
for ir in self.irs_ssa:
1027+
if isinstance(ir, PhiCallback):
1028+
continue
1029+
1030+
self._update_read_using_ssa(ir)
1031+
self._update_write_using_ssa(ir)
1032+
9851033
self._ssa_vars_read = list(set(self._ssa_vars_read))
9861034
self._ssa_state_vars_read = [v for v in self._ssa_vars_read if isinstance(v, StateVariable)]
9871035
self._ssa_local_vars_read = [v for v in self._ssa_vars_read if isinstance(v, LocalVariable)]

Diff for: slither/core/declarations/function.py

+57-16
Original file line numberDiff line numberDiff line change
@@ -1793,30 +1793,52 @@ def _unchange_phi(ir: "Operation") -> bool:
17931793
return True
17941794
return ir.rvalues[0] == ir.lvalue
17951795

1796+
def _fix_phi_entry(
1797+
self,
1798+
node: "Node",
1799+
last_state_variables_instances: Dict[str, List["StateVariable"]],
1800+
initial_state_variables_instances: Dict[str, "StateVariable"],
1801+
) -> None:
1802+
from slither.slithir.variables import Constant, StateIRVariable, LocalIRVariable
1803+
1804+
for ir in node.irs_ssa:
1805+
if isinstance(ir.lvalue, StateIRVariable):
1806+
additional = [initial_state_variables_instances[ir.lvalue.canonical_name]]
1807+
additional += last_state_variables_instances[ir.lvalue.canonical_name]
1808+
ir.rvalues = list(set(additional + ir.rvalues))
1809+
# function parameter that are storage pointer
1810+
else:
1811+
# find index of the parameter
1812+
idx = self.parameters.index(ir.lvalue.non_ssa_version)
1813+
# find non ssa version of that index
1814+
additional = [n.ir.arguments[idx] for n in self.reachable_from_nodes]
1815+
additional = unroll(additional)
1816+
additional = [a for a in additional if not isinstance(a, Constant)]
1817+
ir.rvalues = list(set(additional + ir.rvalues))
1818+
1819+
if isinstance(ir.lvalue, LocalIRVariable) and ir.lvalue.is_storage:
1820+
# Update the refers_to to point to the phi rvalues
1821+
# This basically means that the local variable is a storage that point to any
1822+
# state variable that the storage pointer alias analysis found
1823+
ir.lvalue.refers_to = [
1824+
rvalue for rvalue in ir.rvalues if isinstance(rvalue, StateIRVariable)
1825+
]
1826+
17961827
def fix_phi(
17971828
self,
17981829
last_state_variables_instances: Dict[str, List["StateVariable"]],
17991830
initial_state_variables_instances: Dict[str, "StateVariable"],
18001831
) -> None:
1801-
from slither.slithir.operations import InternalCall, PhiCallback
1802-
from slither.slithir.variables import Constant, StateIRVariable
1832+
from slither.slithir.operations import InternalCall, PhiCallback, Phi
1833+
from slither.slithir.variables import StateIRVariable, LocalIRVariable
18031834

18041835
for node in self.nodes:
1836+
if node == self.entry_point:
1837+
self._fix_phi_entry(
1838+
node, last_state_variables_instances, initial_state_variables_instances
1839+
)
18051840
for ir in node.irs_ssa:
1806-
if node == self.entry_point:
1807-
if isinstance(ir.lvalue, StateIRVariable):
1808-
additional = [initial_state_variables_instances[ir.lvalue.canonical_name]]
1809-
additional += last_state_variables_instances[ir.lvalue.canonical_name]
1810-
ir.rvalues = list(set(additional + ir.rvalues))
1811-
# function parameter
1812-
else:
1813-
# find index of the parameter
1814-
idx = self.parameters.index(ir.lvalue.non_ssa_version)
1815-
# find non ssa version of that index
1816-
additional = [n.ir.arguments[idx] for n in self.reachable_from_nodes]
1817-
additional = unroll(additional)
1818-
additional = [a for a in additional if not isinstance(a, Constant)]
1819-
ir.rvalues = list(set(additional + ir.rvalues))
1841+
18201842
if isinstance(ir, PhiCallback):
18211843
callee_ir = ir.callee_ir
18221844
if isinstance(callee_ir, InternalCall):
@@ -1829,6 +1851,25 @@ def fix_phi(
18291851
additional = last_state_variables_instances[ir.lvalue.canonical_name]
18301852
ir.rvalues = list(set(additional + ir.rvalues))
18311853

1854+
# Propage storage ref information if it does not exist
1855+
# This can happen if the refers_to variable was discovered through the phi operator on function parameter
1856+
# aka you have storage pointer as function parameter
1857+
# instead of having a storage pointer for which the aliases belong to the function body
1858+
if (
1859+
isinstance(ir, Phi)
1860+
and isinstance(ir.lvalue, LocalIRVariable)
1861+
and ir.lvalue.is_storage
1862+
and not ir.lvalue.refers_to
1863+
):
1864+
refers_to = []
1865+
for candidate in ir.rvalues:
1866+
if isinstance(candidate, StateIRVariable):
1867+
refers_to.append(candidate)
1868+
if isinstance(candidate, LocalIRVariable) and candidate.is_storage:
1869+
refers_to += candidate.refers_to
1870+
1871+
ir.lvalue.refers_to = refers_to
1872+
18321873
node.irs_ssa = [ir for ir in node.irs_ssa if not self._unchange_phi(ir)]
18331874

18341875
def generate_slithir_and_analyze(self) -> None:

Diff for: slither/slithir/variables/local_variable.py

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def index(self, idx: int) -> None:
5050

5151
@property
5252
def refers_to(self):
53+
"""
54+
Return the alias for local variable that are storage pointers
55+
"""
5356
if self.is_storage:
5457
return self._refers_to
5558
return set()

Diff for: tests/e2e/detectors/snapshots/detectors__detector_ReentrancyEth_0_4_25_DAO_sol__0.txt

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,3 @@
1-
Reentrancy in TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332):
2-
External calls:
3-
- extraBalance.balance >= extraBalance.accumulatedInput() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#321)
4-
- extraBalance.payOut(address(this),extraBalance.accumulatedInput()) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#322)
5-
- msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325)
6-
External calls sending eth:
7-
- msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325)
8-
State variables written after the call(s):
9-
- weiGiven[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#329)
10-
TokenCreationInterface.weiGiven (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#251) can be used in cross function reentrancies:
11-
- TokenCreation.createTokenProxy(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#299-316)
12-
- TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332)
13-
141
Reentrancy in DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#853-937):
152
External calls:
163
- ! isRecipientAllowed(p.recipient) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#881)
@@ -34,6 +21,7 @@ Reentrancy in DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/
3421
- DAO.splitDAO(uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#947-1020)
3522
- DAO.vote(uint256,bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#820-850)
3623
- closeProposal(_proposalID) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#933)
24+
- p = proposals[_proposalID] (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#941)
3725
- p.open = false (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#944)
3826
DAOInterface.proposals (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#394) can be used in cross function reentrancies:
3927
- DAO.DAO(address,DAO_Creator,uint256,uint256,uint256,address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#702-726)
@@ -70,3 +58,16 @@ Reentrancy in DAO.executeProposal(uint256,bytes) (tests/e2e/detectors/test_data/
7058
- DAO.retrieveDAOReward(bool) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#1037-1057)
7159
- DAOInterface.totalRewardToken (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#412)
7260

61+
Reentrancy in TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332):
62+
External calls:
63+
- extraBalance.balance >= extraBalance.accumulatedInput() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#321)
64+
- extraBalance.payOut(address(this),extraBalance.accumulatedInput()) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#322)
65+
- msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325)
66+
External calls sending eth:
67+
- msg.sender.call.value(weiGiven[msg.sender])() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#325)
68+
State variables written after the call(s):
69+
- weiGiven[msg.sender] = 0 (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#329)
70+
TokenCreationInterface.weiGiven (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#251) can be used in cross function reentrancies:
71+
- TokenCreation.createTokenProxy(address) (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#299-316)
72+
- TokenCreation.refund() (tests/e2e/detectors/test_data/reentrancy-eth/0.4.25/DAO.sol#318-332)
73+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
contract Test{
2+
3+
struct S{
4+
uint a;
5+
}
6+
7+
S s0;
8+
S s1;
9+
10+
function test() public{
11+
S storage s_local = s0;
12+
13+
if(true){
14+
s_local = s1;
15+
}
16+
17+
s_local.a = 10;
18+
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Simplified from https://github.com/crytic/slither/issues/2598
2+
3+
pragma solidity ^0.8.0;
4+
5+
library Roles {
6+
struct Role {
7+
mapping (address => bool) bearer;
8+
}
9+
10+
/**
11+
* @dev Give an account access to this role.
12+
*/
13+
function add(Role storage role, address account) internal {
14+
require(!has(role, account), "Roles: account already has role");
15+
role.bearer[account] = true;
16+
}
17+
18+
function has(Role storage role, address account) internal view returns (bool) {
19+
require(account != address(0), "Roles: account is the zero address");
20+
return role.bearer[account];
21+
}
22+
23+
}
24+
25+
contract Context {
26+
27+
function _msgSender() internal view returns (address payable) {
28+
return payable(msg.sender);
29+
}
30+
31+
}
32+
33+
34+
contract MinterRole is Context {
35+
using Roles for Roles.Role;
36+
37+
Roles.Role private _minters;
38+
39+
function addMinter(address account) public {
40+
_addMinter(account);
41+
}
42+
43+
function _addMinter(address account) internal {
44+
_minters.add(account);
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Simplified from https://github.com/crytic/slither/issues/2598
2+
3+
pragma solidity ^0.8.0;
4+
5+
contract MinterRole {
6+
7+
struct Role {
8+
mapping (address => bool) bearer;
9+
}
10+
11+
12+
/**
13+
* @dev Give an account access to this role.
14+
*/
15+
function add(Role storage role, address account) internal {
16+
require(!has(role, account), "Roles: account already has role");
17+
role.bearer[account] = true;
18+
}
19+
20+
function has(Role storage role, address account) internal view returns (bool) {
21+
require(account != address(0), "Roles: account is the zero address");
22+
return role.bearer[account];
23+
}
24+
25+
Role private _minters;
26+
27+
function addMinter(address account) public {
28+
_addMinter(account);
29+
}
30+
31+
function _addMinter(address account) internal {
32+
add(_minters, account);
33+
}
34+
35+
}

0 commit comments

Comments
 (0)