Skip to content
This repository was archived by the owner on May 22, 2023. It is now read-only.

Commit 802d0f1

Browse files
atheiMichael Müller
authored andcommitted
contracts: Add new seal_call that offers new features (paritytech#8909)
* Add new `seal_call` that offers new features * Fix doc typo Co-authored-by: Michael Müller <michi@parity.io> * Fix doc typos Co-authored-by: Michael Müller <michi@parity.io> * Fix comment on assert * Update CHANGELOG.md Co-authored-by: Michael Müller <michi@parity.io>
1 parent 7266b62 commit 802d0f1

File tree

7 files changed

+503
-71
lines changed

7 files changed

+503
-71
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frame/contracts/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ In other words: Upgrading this pallet will not break pre-existing contracts.
2020

2121
### Added
2222

23+
- New **unstable** version of `seal_call` that offers more features.
24+
[#8909](https://github.com/paritytech/substrate/pull/8909)
25+
2326
- New **unstable** `seal_rent_params` and `seal_rent_status` contract callable function.
2427
[#8231](https://github.com/paritytech/substrate/pull/8231)
2528
[#8780](https://github.com/paritytech/substrate/pull/8780)

frame/contracts/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ readme = "README.md"
1313
targets = ["x86_64-unknown-linux-gnu"]
1414

1515
[dependencies]
16+
bitflags = "1.0"
1617
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
1718
log = { version = "0.4", default-features = false }
1819
pwasm-utils = { version = "0.18", default-features = false }

frame/contracts/src/exec.rs

Lines changed: 136 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ pub trait Ext: sealing::Sealed {
167167
to: AccountIdOf<Self::T>,
168168
value: BalanceOf<Self::T>,
169169
input_data: Vec<u8>,
170+
allows_reentry: bool,
170171
) -> Result<(ExecReturnValue, u32), (ExecError, u32)>;
171172

172173
/// Instantiate a contract from the given code.
@@ -457,6 +458,8 @@ pub struct Frame<T: Config> {
457458
entry_point: ExportedFunction,
458459
/// The gas meter capped to the supplied gas limit.
459460
nested_meter: GasMeter<T>,
461+
/// If `false` the contract enabled its defense against reentrance attacks.
462+
allows_reentry: bool,
460463
}
461464

462465
/// Parameter passed in when creating a new `Frame`.
@@ -731,6 +734,7 @@ where
731734
entry_point,
732735
nested_meter: gas_meter.nested(gas_limit)
733736
.map_err(|e| (e.into(), executable.code_len()))?,
737+
allows_reentry: true,
734738
};
735739

736740
Ok((frame, executable))
@@ -1014,6 +1018,11 @@ where
10141018
self.frames().skip(1).any(|f| &f.account_id == account_id)
10151019
}
10161020

1021+
/// Returns whether the specified contract allows to be reentered right now.
1022+
fn allows_reentry(&self, id: &AccountIdOf<T>) -> bool {
1023+
!self.frames().any(|f| &f.account_id == id && !f.allows_reentry)
1024+
}
1025+
10171026
/// Increments the cached account id and returns the value to be used for the trie_id.
10181027
fn next_trie_seed(&mut self) -> u64 {
10191028
let next = if let Some(current) = self.account_counter {
@@ -1045,25 +1054,44 @@ where
10451054
to: T::AccountId,
10461055
value: BalanceOf<T>,
10471056
input_data: Vec<u8>,
1057+
allows_reentry: bool,
10481058
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
1049-
// We ignore instantiate frames in our search for a cached contract.
1050-
// Otherwise it would be possible to recursively call a contract from its own
1051-
// constructor: We disallow calling not fully constructed contracts.
1052-
let cached_info = self
1053-
.frames()
1054-
.find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to)
1055-
.and_then(|f| {
1056-
match &f.contract_info {
1057-
CachedContract::Cached(contract) => Some(contract.clone()),
1058-
_ => None,
1059-
}
1060-
});
1061-
let executable = self.push_frame(
1062-
FrameArgs::Call{dest: to, cached_info},
1063-
value,
1064-
gas_limit
1065-
)?;
1066-
self.run(executable, input_data)
1059+
// Before pushing the new frame: Protect the caller contract against reentrancy attacks.
1060+
// It is important to do this before calling `allows_reentry` so that a direct recursion
1061+
// is caught by it.
1062+
self.top_frame_mut().allows_reentry = allows_reentry;
1063+
1064+
let try_call = || {
1065+
if !self.allows_reentry(&to) {
1066+
return Err((<Error<T>>::ReentranceDenied.into(), 0));
1067+
}
1068+
// We ignore instantiate frames in our search for a cached contract.
1069+
// Otherwise it would be possible to recursively call a contract from its own
1070+
// constructor: We disallow calling not fully constructed contracts.
1071+
let cached_info = self
1072+
.frames()
1073+
.find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to)
1074+
.and_then(|f| {
1075+
match &f.contract_info {
1076+
CachedContract::Cached(contract) => Some(contract.clone()),
1077+
_ => None,
1078+
}
1079+
});
1080+
let executable = self.push_frame(
1081+
FrameArgs::Call{dest: to, cached_info},
1082+
value,
1083+
gas_limit
1084+
)?;
1085+
self.run(executable, input_data)
1086+
};
1087+
1088+
// We need to make sure to reset `allows_reentry` even on failure.
1089+
let result = try_call();
1090+
1091+
// Protection is on a per call basis.
1092+
self.top_frame_mut().allows_reentry = true;
1093+
1094+
result
10671095
}
10681096

10691097
fn instantiate(
@@ -1097,7 +1125,7 @@ where
10971125
beneficiary: &AccountIdOf<Self::T>,
10981126
) -> Result<u32, (DispatchError, u32)> {
10991127
if self.is_recursive() {
1100-
return Err((Error::<T>::ReentranceDenied.into(), 0));
1128+
return Err((Error::<T>::TerminatedWhileReentrant.into(), 0));
11011129
}
11021130
let frame = self.top_frame_mut();
11031131
let info = frame.terminate();
@@ -1125,7 +1153,7 @@ where
11251153
delta: Vec<StorageKey>,
11261154
) -> Result<(u32, u32), (DispatchError, u32, u32)> {
11271155
if self.is_recursive() {
1128-
return Err((Error::<T>::ReentranceDenied.into(), 0, 0));
1156+
return Err((Error::<T>::TerminatedWhileReentrant.into(), 0, 0));
11291157
}
11301158
let origin_contract = self.top_frame_mut().contract_info().clone();
11311159
let result = Rent::<T, E>::restore_to(
@@ -1308,12 +1336,14 @@ mod tests {
13081336
exec::ExportedFunction::*,
13091337
Error, Weight,
13101338
};
1339+
use codec::{Encode, Decode};
13111340
use sp_core::Bytes;
13121341
use sp_runtime::DispatchError;
13131342
use assert_matches::assert_matches;
13141343
use std::{cell::RefCell, collections::HashMap, rc::Rc};
13151344
use pretty_assertions::{assert_eq, assert_ne};
13161345
use pallet_contracts_primitives::ReturnFlags;
1346+
use frame_support::{assert_ok, assert_err};
13171347

13181348
type MockStack<'a> = Stack<'a, Test, MockExecutable>;
13191349

@@ -1731,7 +1761,7 @@ mod tests {
17311761
let value = Default::default();
17321762
let recurse_ch = MockLoader::insert(Call, |ctx, _| {
17331763
// Try to call into yourself.
1734-
let r = ctx.ext.call(0, BOB, 0, vec![]);
1764+
let r = ctx.ext.call(0, BOB, 0, vec![], true);
17351765

17361766
REACHED_BOTTOM.with(|reached_bottom| {
17371767
let mut reached_bottom = reached_bottom.borrow_mut();
@@ -1789,7 +1819,7 @@ mod tests {
17891819

17901820
// Call into CHARLIE contract.
17911821
assert_matches!(
1792-
ctx.ext.call(0, CHARLIE, 0, vec![]),
1822+
ctx.ext.call(0, CHARLIE, 0, vec![], true),
17931823
Ok(_)
17941824
);
17951825
exec_success()
@@ -1832,7 +1862,7 @@ mod tests {
18321862

18331863
// Call into charlie contract.
18341864
assert_matches!(
1835-
ctx.ext.call(0, CHARLIE, 0, vec![]),
1865+
ctx.ext.call(0, CHARLIE, 0, vec![], true),
18361866
Ok(_)
18371867
);
18381868
exec_success()
@@ -2263,7 +2293,7 @@ mod tests {
22632293
assert_ne!(original_allowance, changed_allowance);
22642294
ctx.ext.set_rent_allowance(changed_allowance);
22652295
assert_eq!(
2266-
ctx.ext.call(0, CHARLIE, 0, vec![]).map(|v| v.0).map_err(|e| e.0),
2296+
ctx.ext.call(0, CHARLIE, 0, vec![], true).map(|v| v.0).map_err(|e| e.0),
22672297
exec_trapped()
22682298
);
22692299
assert_eq!(ctx.ext.rent_allowance(), changed_allowance);
@@ -2272,7 +2302,7 @@ mod tests {
22722302
exec_success()
22732303
});
22742304
let code_charlie = MockLoader::insert(Call, |ctx, _| {
2275-
assert!(ctx.ext.call(0, BOB, 0, vec![99]).is_ok());
2305+
assert!(ctx.ext.call(0, BOB, 0, vec![99], true).is_ok());
22762306
exec_trapped()
22772307
});
22782308

@@ -2299,7 +2329,7 @@ mod tests {
22992329
fn recursive_call_during_constructor_fails() {
23002330
let code = MockLoader::insert(Constructor, |ctx, _| {
23012331
assert_matches!(
2302-
ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![]),
2332+
ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![], true),
23032333
Err((ExecError{error, ..}, _)) if error == <Error<Test>>::ContractNotFound.into()
23042334
);
23052335
exec_success()
@@ -2390,4 +2420,84 @@ mod tests {
23902420

23912421
assert_eq!(&String::from_utf8(debug_buffer).unwrap(), "This is a testMore text");
23922422
}
2423+
2424+
#[test]
2425+
fn call_reentry_direct_recursion() {
2426+
// call the contract passed as input with disabled reentry
2427+
let code_bob = MockLoader::insert(Call, |ctx, _| {
2428+
let dest = Decode::decode(&mut ctx.input_data.as_ref()).unwrap();
2429+
ctx.ext.call(0, dest, 0, vec![], false).map(|v| v.0).map_err(|e| e.0)
2430+
});
2431+
2432+
let code_charlie = MockLoader::insert(Call, |_, _| {
2433+
exec_success()
2434+
});
2435+
2436+
ExtBuilder::default().build().execute_with(|| {
2437+
let schedule = <Test as Config>::Schedule::get();
2438+
place_contract(&BOB, code_bob);
2439+
place_contract(&CHARLIE, code_charlie);
2440+
2441+
// Calling another contract should succeed
2442+
assert_ok!(MockStack::run_call(
2443+
ALICE,
2444+
BOB,
2445+
&mut GasMeter::<Test>::new(GAS_LIMIT),
2446+
&schedule,
2447+
0,
2448+
CHARLIE.encode(),
2449+
None,
2450+
));
2451+
2452+
// Calling into oneself fails
2453+
assert_err!(
2454+
MockStack::run_call(
2455+
ALICE,
2456+
BOB,
2457+
&mut GasMeter::<Test>::new(GAS_LIMIT),
2458+
&schedule,
2459+
0,
2460+
BOB.encode(),
2461+
None,
2462+
).map_err(|e| e.0.error),
2463+
<Error<Test>>::ReentranceDenied,
2464+
);
2465+
});
2466+
}
2467+
2468+
#[test]
2469+
fn call_deny_reentry() {
2470+
let code_bob = MockLoader::insert(Call, |ctx, _| {
2471+
if ctx.input_data[0] == 0 {
2472+
ctx.ext.call(0, CHARLIE, 0, vec![], false).map(|v| v.0).map_err(|e| e.0)
2473+
} else {
2474+
exec_success()
2475+
}
2476+
});
2477+
2478+
// call BOB with input set to '1'
2479+
let code_charlie = MockLoader::insert(Call, |ctx, _| {
2480+
ctx.ext.call(0, BOB, 0, vec![1], true).map(|v| v.0).map_err(|e| e.0)
2481+
});
2482+
2483+
ExtBuilder::default().build().execute_with(|| {
2484+
let schedule = <Test as Config>::Schedule::get();
2485+
place_contract(&BOB, code_bob);
2486+
place_contract(&CHARLIE, code_charlie);
2487+
2488+
// BOB -> CHARLIE -> BOB fails as BOB denies reentry.
2489+
assert_err!(
2490+
MockStack::run_call(
2491+
ALICE,
2492+
BOB,
2493+
&mut GasMeter::<Test>::new(GAS_LIMIT),
2494+
&schedule,
2495+
0,
2496+
vec![0],
2497+
None,
2498+
).map_err(|e| e.0.error),
2499+
<Error<Test>>::ReentranceDenied,
2500+
);
2501+
});
2502+
}
23932503
}

frame/contracts/src/lib.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -562,12 +562,11 @@ pub mod pallet {
562562
ContractTrapped,
563563
/// The size defined in `T::MaxValueSize` was exceeded.
564564
ValueTooLarge,
565-
/// The action performed is not allowed while the contract performing it is already
566-
/// on the call stack. Those actions are contract self destruction and restoration
567-
/// of a tombstone.
568-
ReentranceDenied,
569-
/// `seal_input` was called twice from the same contract execution context.
570-
InputAlreadyRead,
565+
/// Termination of a contract is not allowed while the contract is already
566+
/// on the call stack. Can be triggered by `seal_terminate` or `seal_restore_to.
567+
TerminatedWhileReentrant,
568+
/// `seal_call` forwarded this contracts input. It therefore is no longer available.
569+
InputForwarded,
571570
/// The subject passed to `seal_random` exceeds the limit.
572571
RandomSubjectTooLong,
573572
/// The amount of topics passed to `seal_deposit_events` exceeds the limit.
@@ -602,6 +601,8 @@ pub mod pallet {
602601
TerminatedInConstructor,
603602
/// The debug message specified to `seal_debug_message` does contain invalid UTF-8.
604603
DebugMessageInvalidUTF8,
604+
/// A call tried to invoke a contract that is flagged as non-reentrant.
605+
ReentranceDenied,
605606
}
606607

607608
/// A mapping from an original code hash to the original code, untouched by instrumentation.

0 commit comments

Comments
 (0)