diff --git a/crates/llvm-context/src/lib.rs b/crates/llvm-context/src/lib.rs index 98d21eb..a51fd4e 100644 --- a/crates/llvm-context/src/lib.rs +++ b/crates/llvm-context/src/lib.rs @@ -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; diff --git a/crates/llvm-context/src/polkavm/context/function/runtime/call.rs b/crates/llvm-context/src/polkavm/context/function/runtime/call.rs new file mode 100644 index 0000000..3fd0f7a --- /dev/null +++ b/crates/llvm-context/src/polkavm/context/function/runtime/call.rs @@ -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 RuntimeFunction 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>> { + 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 WriteLLVM for CallReentrancyProtector +where + D: Dependency + Clone, +{ + fn declare(&mut self, context: &mut Context) -> anyhow::Result<()> { + >::declare(self, context) + } + + fn into_llvm(self, context: &mut Context) -> anyhow::Result<()> { + >::emit(&self, context) + } +} diff --git a/crates/llvm-context/src/polkavm/context/function/runtime/mod.rs b/crates/llvm-context/src/polkavm/context/function/runtime/mod.rs index 089de6e..5008d13 100644 --- a/crates/llvm-context/src/polkavm/context/function/runtime/mod.rs +++ b/crates/llvm-context/src/polkavm/context/function/runtime/mod.rs @@ -2,6 +2,7 @@ pub mod arithmetics; pub mod bitwise; +pub mod call; pub mod deploy_code; pub mod entry; pub mod revive; diff --git a/crates/llvm-context/src/polkavm/evm/call.rs b/crates/llvm-context/src/polkavm/evm/call.rs index e5d1c5f..dd0c3ea 100644 --- a/crates/llvm-context/src/polkavm/evm/call.rs +++ b/crates/llvm-context/src/polkavm/evm/call.rs @@ -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> where D: Dependency + Clone, { - // Branch-free SSA implementation: First derive the heuristic boolean (int1) value. - let input_length_or_output_length = + let name = >::NAME; + let declaration = >::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()) } diff --git a/crates/yul/src/parser/statement/object.rs b/crates/yul/src/parser/statement/object.rs index 452bb21..2b11171 100644 --- a/crates/yul/src/parser/statement/object.rs +++ b/crates/yul/src/parser/statement/object.rs @@ -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(())