mirror of
https://github.com/pezkuwichain/revive.git
synced 2026-04-29 21:47:56 +00:00
call reentrancy heuristic function
Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
This commit is contained in:
@@ -33,6 +33,7 @@ pub use self::polkavm::context::function::runtime::bitwise::Sar as PolkaVMSarFun
|
||||
pub use self::polkavm::context::function::runtime::bitwise::Shl as PolkaVMShlFunction;
|
||||
pub use self::polkavm::context::function::runtime::bitwise::Shr as PolkaVMShrFunction;
|
||||
pub use self::polkavm::context::function::runtime::bitwise::Xor as PolkaVMXorFunction;
|
||||
pub use self::polkavm::context::function::runtime::call::CallReentrancyProtector as PolkaVMCallReentrancyProtector;
|
||||
pub use self::polkavm::context::function::runtime::deploy_code::DeployCode as PolkaVMDeployCodeFunction;
|
||||
pub use self::polkavm::context::function::runtime::entry::Entry as PolkaVMEntryFunction;
|
||||
pub use self::polkavm::context::function::runtime::revive::Exit as PolkaVMExitFunction;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
//! Translates the arithmetic operations.
|
||||
|
||||
use inkwell::values::BasicValue;
|
||||
|
||||
use crate::polkavm::context::runtime::RuntimeFunction;
|
||||
use crate::polkavm::context::Context;
|
||||
use crate::polkavm::Dependency;
|
||||
use crate::polkavm::WriteLLVM;
|
||||
|
||||
const SOLIDITY_TRANSFER_GAS_STIPEND_THRESHOLD: u64 = 2300;
|
||||
|
||||
/// The Solidity `address.transfer` and `address.send` call detection heuristic.
|
||||
///
|
||||
/// # Why
|
||||
/// This heuristic is an additional security feature to guard against re-entrancy attacks
|
||||
/// in case contract authors violate Solidity best practices and use `address.transfer` or
|
||||
/// `address.send`.
|
||||
/// While contract authors are supposed to never use `address.transfer` or `address.send`,
|
||||
/// for a small cost we can be extra defensive about it.
|
||||
///
|
||||
/// # How
|
||||
/// The gas stipend emitted by solc for `transfer` and `send` is not static, thus:
|
||||
/// - Dynamically allow re-entrancy only for calls considered not transfer or send.
|
||||
/// - Detected balance transfers will supply 0 deposit limit instead of `u256::MAX`.
|
||||
///
|
||||
/// Calls are considered transfer or send if:
|
||||
/// - (Input length | Output lenght) == 0;
|
||||
/// - Gas <= 2300;
|
||||
///
|
||||
/// # Arguments:
|
||||
/// - The deposit value pointer.
|
||||
/// - The gas value.
|
||||
/// - `input_length | output_length`.
|
||||
///
|
||||
///
|
||||
/// # Returns:
|
||||
/// The call flags xlen `IntValue`
|
||||
pub struct CallReentrancyProtector;
|
||||
|
||||
impl<D> RuntimeFunction<D> for CallReentrancyProtector
|
||||
where
|
||||
D: Dependency + Clone,
|
||||
{
|
||||
const NAME: &'static str = "__revive_call_reentrancy_protector";
|
||||
|
||||
fn r#type<'ctx>(context: &Context<'ctx, D>) -> inkwell::types::FunctionType<'ctx> {
|
||||
context.xlen_type().fn_type(
|
||||
&[
|
||||
context.llvm().ptr_type(Default::default()).into(),
|
||||
context.word_type().into(),
|
||||
context.xlen_type().into(),
|
||||
],
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
fn emit_body<'ctx>(
|
||||
&self,
|
||||
context: &mut Context<'ctx, D>,
|
||||
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>> {
|
||||
let deposit_pointer = Self::paramater(context, 0).into_pointer_value();
|
||||
let gas = Self::paramater(context, 1).into_int_value();
|
||||
let input_length_or_output_length = Self::paramater(context, 2).into_int_value();
|
||||
|
||||
// Branch-free SSA implementation: First derive the heuristic boolean (int1) value.
|
||||
let is_no_input_no_output = context.builder().build_int_compare(
|
||||
inkwell::IntPredicate::EQ,
|
||||
context.xlen_type().const_zero(),
|
||||
input_length_or_output_length,
|
||||
"is_no_input_no_output",
|
||||
)?;
|
||||
let gas_stipend = context
|
||||
.word_type()
|
||||
.const_int(SOLIDITY_TRANSFER_GAS_STIPEND_THRESHOLD, false);
|
||||
let is_gas_stipend_for_transfer_or_send = context.builder().build_int_compare(
|
||||
inkwell::IntPredicate::ULE,
|
||||
gas,
|
||||
gas_stipend,
|
||||
"is_gas_stipend_for_transfer_or_send",
|
||||
)?;
|
||||
let is_balance_transfer = context.builder().build_and(
|
||||
is_no_input_no_output,
|
||||
is_gas_stipend_for_transfer_or_send,
|
||||
"is_balance_transfer",
|
||||
)?;
|
||||
let is_regular_call = context
|
||||
.builder()
|
||||
.build_not(is_balance_transfer, "is_balance_transfer_inverted")?;
|
||||
|
||||
// Call flag: Left shift the heuristic boolean value.
|
||||
let is_regular_call_xlen = context.builder().build_int_z_extend(
|
||||
is_regular_call,
|
||||
context.xlen_type(),
|
||||
"is_balance_transfer_xlen",
|
||||
)?;
|
||||
let call_flags = context.builder().build_left_shift(
|
||||
is_regular_call_xlen,
|
||||
context.xlen_type().const_int(3, false),
|
||||
"flags",
|
||||
)?;
|
||||
|
||||
// Deposit limit value: Sign-extended the heuristic boolean value.
|
||||
let deposit_limit_value = context.builder().build_int_s_extend(
|
||||
is_regular_call,
|
||||
context.word_type(),
|
||||
"deposit_limit_value",
|
||||
)?;
|
||||
context
|
||||
.builder()
|
||||
.build_store(deposit_pointer, deposit_limit_value)?;
|
||||
|
||||
Ok(Some(call_flags.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> WriteLLVM<D> for CallReentrancyProtector
|
||||
where
|
||||
D: Dependency + Clone,
|
||||
{
|
||||
fn declare(&mut self, context: &mut Context<D>) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction<_>>::declare(self, context)
|
||||
}
|
||||
|
||||
fn into_llvm(self, context: &mut Context<D>) -> anyhow::Result<()> {
|
||||
<Self as RuntimeFunction<_>>::emit(&self, context)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod arithmetics;
|
||||
pub mod bitwise;
|
||||
pub mod call;
|
||||
pub mod deploy_code;
|
||||
pub mod entry;
|
||||
pub mod revive;
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
use inkwell::values::BasicValue;
|
||||
|
||||
use crate::polkavm::context::argument::Argument;
|
||||
use crate::polkavm::context::runtime::RuntimeFunction;
|
||||
use crate::polkavm::context::Context;
|
||||
use crate::polkavm::Dependency;
|
||||
use crate::PolkaVMCallReentrancyProtector;
|
||||
|
||||
const STATIC_CALL_FLAG: u32 = 0b0001_0000;
|
||||
const REENTRANT_CALL_FLAG: u32 = 0b0000_1000;
|
||||
const SOLIDITY_TRANSFER_GAS_STIPEND_THRESHOLD: u64 = 2300;
|
||||
|
||||
/// Translates a contract call.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -27,6 +28,25 @@ pub fn call<'ctx, D>(
|
||||
where
|
||||
D: Dependency + Clone,
|
||||
{
|
||||
let input_length = context.safe_truncate_int_to_xlen(input_length)?;
|
||||
let output_length = context.safe_truncate_int_to_xlen(output_length)?;
|
||||
|
||||
let deposit_limit_pointer =
|
||||
context.build_alloca_at_entry(context.word_type(), "deposit_pointer");
|
||||
let flags = if static_call {
|
||||
let flags = REENTRANT_CALL_FLAG | STATIC_CALL_FLAG;
|
||||
context.build_store(deposit_limit_pointer, context.word_type().const_zero())?;
|
||||
context.xlen_type().const_int(flags as u64, false)
|
||||
} else {
|
||||
call_reentrancy_heuristic(
|
||||
context,
|
||||
deposit_limit_pointer.value,
|
||||
gas,
|
||||
input_length,
|
||||
output_length,
|
||||
)?
|
||||
};
|
||||
|
||||
let address_pointer = context.build_address_argument_store(address)?;
|
||||
|
||||
let value = value.unwrap_or_else(|| context.word_const(0));
|
||||
@@ -34,9 +54,7 @@ where
|
||||
context.build_store(value_pointer, value)?;
|
||||
|
||||
let input_offset = context.safe_truncate_int_to_xlen(input_offset)?;
|
||||
let input_length = context.safe_truncate_int_to_xlen(input_length)?;
|
||||
let output_offset = context.safe_truncate_int_to_xlen(output_offset)?;
|
||||
let output_length = context.safe_truncate_int_to_xlen(output_length)?;
|
||||
|
||||
let input_pointer = context.build_heap_gep(input_offset, input_length)?;
|
||||
let output_pointer = context.build_heap_gep(output_offset, output_length)?;
|
||||
@@ -44,19 +62,6 @@ where
|
||||
let output_length_pointer = context.build_alloca_at_entry(context.xlen_type(), "output_length");
|
||||
context.build_store(output_length_pointer, output_length)?;
|
||||
|
||||
let (flags, deposit_limit_value) = if static_call {
|
||||
let flags = REENTRANT_CALL_FLAG | STATIC_CALL_FLAG;
|
||||
(
|
||||
context.xlen_type().const_int(flags as u64, false),
|
||||
context.word_type().const_zero(),
|
||||
)
|
||||
} else {
|
||||
call_reentrancy_heuristic(context, gas, input_length, output_length)?
|
||||
};
|
||||
|
||||
let deposit_pointer = context.build_alloca_at_entry(context.word_type(), "deposit_pointer");
|
||||
context.build_store(deposit_pointer, deposit_limit_value)?;
|
||||
|
||||
let flags_and_callee = revive_runtime_api::calling_convention::pack_hi_lo_reg(
|
||||
context.builder(),
|
||||
context.llvm(),
|
||||
@@ -67,7 +72,7 @@ where
|
||||
let deposit_and_value = revive_runtime_api::calling_convention::pack_hi_lo_reg(
|
||||
context.builder(),
|
||||
context.llvm(),
|
||||
deposit_pointer.to_int(context),
|
||||
deposit_limit_pointer.to_int(context),
|
||||
value_pointer.to_int(context),
|
||||
"deposit_and_value",
|
||||
)?;
|
||||
@@ -216,85 +221,28 @@ where
|
||||
.as_basic_value_enum())
|
||||
}
|
||||
|
||||
/// The Solidity `address.transfer` and `address.send` call detection heuristic.
|
||||
///
|
||||
/// # Why
|
||||
/// This heuristic is an additional security feature to guard against re-entrancy attacks
|
||||
/// in case contract authors violate Solidity best practices and use `address.transfer` or
|
||||
/// `address.send`.
|
||||
/// While contract authors are supposed to never use `address.transfer` or `address.send`,
|
||||
/// for a small cost we can be extra defensive about it.
|
||||
///
|
||||
/// # How
|
||||
/// The gas stipend emitted by solc for `transfer` and `send` is not static, thus:
|
||||
/// - Dynamically allow re-entrancy only for calls considered not transfer or send.
|
||||
/// - Detected balance transfers will supply 0 deposit limit instead of `u256::MAX`.
|
||||
///
|
||||
/// Calls are considered transfer or send if:
|
||||
/// - (Input length | Output lenght) == 0;
|
||||
/// - Gas <= 2300;
|
||||
///
|
||||
/// # Returns
|
||||
/// The call flags xlen `IntValue` and the deposit limit word `IntValue`.
|
||||
fn call_reentrancy_heuristic<'ctx, D>(
|
||||
context: &mut Context<'ctx, D>,
|
||||
deposit_limit_pointer: inkwell::values::PointerValue<'ctx>,
|
||||
gas: inkwell::values::IntValue<'ctx>,
|
||||
input_length: inkwell::values::IntValue<'ctx>,
|
||||
output_length: inkwell::values::IntValue<'ctx>,
|
||||
) -> anyhow::Result<(
|
||||
inkwell::values::IntValue<'ctx>,
|
||||
inkwell::values::IntValue<'ctx>,
|
||||
)>
|
||||
) -> anyhow::Result<inkwell::values::IntValue<'ctx>>
|
||||
where
|
||||
D: Dependency + Clone,
|
||||
{
|
||||
// Branch-free SSA implementation: First derive the heuristic boolean (int1) value.
|
||||
let input_length_or_output_length =
|
||||
let name = <PolkaVMCallReentrancyProtector as RuntimeFunction<D>>::NAME;
|
||||
let declaration = <PolkaVMCallReentrancyProtector as RuntimeFunction<D>>::declaration(context);
|
||||
let arguments = &[
|
||||
deposit_limit_pointer.into(),
|
||||
gas.into(),
|
||||
context
|
||||
.builder()
|
||||
.build_or(input_length, output_length, "input_length_or_output_length")?;
|
||||
let is_no_input_no_output = context.builder().build_int_compare(
|
||||
inkwell::IntPredicate::EQ,
|
||||
context.xlen_type().const_zero(),
|
||||
input_length_or_output_length,
|
||||
"is_no_input_no_output",
|
||||
)?;
|
||||
let gas_stipend = context
|
||||
.word_type()
|
||||
.const_int(SOLIDITY_TRANSFER_GAS_STIPEND_THRESHOLD, false);
|
||||
let is_gas_stipend_for_transfer_or_send = context.builder().build_int_compare(
|
||||
inkwell::IntPredicate::ULE,
|
||||
gas,
|
||||
gas_stipend,
|
||||
"is_gas_stipend_for_transfer_or_send",
|
||||
)?;
|
||||
let is_balance_transfer = context.builder().build_and(
|
||||
is_no_input_no_output,
|
||||
is_gas_stipend_for_transfer_or_send,
|
||||
"is_balance_transfer",
|
||||
)?;
|
||||
let is_regular_call = context
|
||||
.builder()
|
||||
.build_not(is_balance_transfer, "is_balance_transfer_inverted")?;
|
||||
|
||||
// Call flag: Left shift the heuristic boolean value.
|
||||
let is_regular_call_xlen = context.builder().build_int_z_extend(
|
||||
is_regular_call,
|
||||
context.xlen_type(),
|
||||
"is_balance_transfer_xlen",
|
||||
)?;
|
||||
let call_flags = context.builder().build_left_shift(
|
||||
is_regular_call_xlen,
|
||||
context.xlen_type().const_int(3, false),
|
||||
"flags",
|
||||
)?;
|
||||
|
||||
// Deposit limit value: Sign-extended the heuristic boolean value.
|
||||
let deposit_limit_value = context.builder().build_int_s_extend(
|
||||
is_regular_call,
|
||||
context.word_type(),
|
||||
"deposit_limit_value",
|
||||
)?;
|
||||
|
||||
Ok((call_flags, deposit_limit_value))
|
||||
.build_or(input_length, output_length, "input_length_or_output_length")?
|
||||
.into(),
|
||||
];
|
||||
Ok(context
|
||||
.build_call(declaration, arguments, "call_flags")
|
||||
.unwrap_or_else(|| panic!("revive runtime function {name} should return a value"))
|
||||
.into_int_value())
|
||||
}
|
||||
|
||||
@@ -220,6 +220,8 @@ where
|
||||
revive_llvm_context::PolkaVMSarFunction.declare(context)?;
|
||||
revive_llvm_context::PolkaVMByteFunction.declare(context)?;
|
||||
|
||||
revive_llvm_context::PolkaVMCallReentrancyProtector.declare(context)?;
|
||||
|
||||
revive_llvm_context::PolkaVMSbrkFunction.declare(context)?;
|
||||
|
||||
let mut entry = revive_llvm_context::PolkaVMEntryFunction::default();
|
||||
@@ -285,6 +287,8 @@ where
|
||||
revive_llvm_context::PolkaVMSarFunction.into_llvm(context)?;
|
||||
revive_llvm_context::PolkaVMByteFunction.into_llvm(context)?;
|
||||
|
||||
revive_llvm_context::PolkaVMCallReentrancyProtector.into_llvm(context)?;
|
||||
|
||||
revive_llvm_context::PolkaVMSbrkFunction.into_llvm(context)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user