call reentrancy heuristic function

Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
This commit is contained in:
Cyrill Leutwiler
2025-07-04 12:01:10 +02:00
parent 4028f7a985
commit 3204b61ea0
5 changed files with 169 additions and 88 deletions
+1
View File
@@ -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;
+36 -88
View File
@@ -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(())