Rust, developed by Mozilla in 2010, is a safe and productive programming language that is gaining popularity in system programming. It boasts impressive speed and strict memory safety through ownership and borrowing mechanisms, which prevent errors like data races and memory misuse. The cargo package manager aids developers in creating crates, running tests, and generating documentation. Although it is not entirely memory leak-proof, it significantly reduces such risks. It provides lifetime checking, frees memory automatically, and offers an Option type for handling missing values. The active developer community contributes to its growth with new libraries and tools, making it attractive to developers of various levels. It offers a systematic approach to understanding Web3 security, thanks to the Rust Security builders and community. Therefore, what follows is not a typical article, but a systematization of knowledge (SoK)! Let's get started!
Contributors: hyperstructured.greg, officercia, yehor0111, Mikhail, kalloc).
The vulnerability found by Guido Vranken that could cause catastrophic consequences in Geth(go-ethereum) and the whole rhombus ecosystem is a great representation of how Rust can prevent such leaks in the system and why it’s more robust and secure than other programming languages.
So definitely the problem was the result of Go’s ability to have non-mutable and mutable referencing in the same slot of memory that led to faulty computation in the client and emerged in a network split.
The bug is demonstrated in a simple function here:
-GO-
func returnAndCopy(mem []byte, n int, copyTo int) []byte {
// boundary checks omitted
ret := mem[0:n]
copy(mem[copyTo:copyTo+n], mem[0:n]) // copy(dst, src)
return ret
}
Before function returns n
bytes, it copies those n
to a different location in a memory chain. Therefore interval between return and copy should have shared spots in the array and point to the exact same subarray, but if not func
fails to generate a correct outcome. This happens because returned and original chunks point to a corresponding backing memory.
Looking at Rust’s approach/memory safety principles which don’t permit having a mutable and non-mutable reference pointing to the same memory simultaneously, we can be sure that Rust could prevent a bug. Conservative and strict rules of cRustecean language would have saved the network from a disaster.
Hopefully, the submitter received approval from Ethereum Foundation and received his bounty, while also becoming top-1 of the leaderboard after that. You can check the official post-mortem to have all details.
Additional links to his works and media:
OpenSSL Source Toolkit for the Transport Layer Security (TLS)
$4 billion dollar cryptocurrency startup gave a hacker $120,000
Go version:
func returnAndCopy(mem []byte, n int, copyTo int) []byte {
ret := mem[0:n]
copy(mem[copyTo:copyTo+n], mem[0:n]) // copy(dst, src)
return ret
}
func main() {
slice := []byte{0,1,2,3,4,5,6,7,8,9}
fmt.Println(returnAndCopy(slice, 3, 1)) // prints: [0 0 1]
}
returnAndCopy func
returns a piece of n
bytes of mem
, but before it copies the same n to a distinct location in mem
. The problem is similar to a top code, where returned and original chunks are pointed to the same memory.
Familiar function is written in Rust:
fn return_and_copy(mem: &mut [u8], n: usize, copy_to: usize) -> &[u8] {
let ret = &mem[0..n];
mem[copy_to..copy_to+n].copy_from_slice(&mem[0..n]);
ret
}
fn main() {
let mut slice = vec![0,1,2,3,4,5,6,7,8,9];
println!("{:?}", return_and_copy(&mut slice, 3, 1)); // Will not compile :<
}
The key is that the code will not compile. The compiler will give an error message saying that you cannot borrow mem
as mutable more than once at a time. By virtue of internal rules, you do not allow a mutable and immutable reference to the equal memory pocket place.
CVE-2022-37450 (manipulation attack of time-difference values to increase rewards);
CVE-2022-29177 (high verbosity logging);
CVE-2022-23328 (DDoS attack using pending transactions);
To uncover an advance of Rust, perhaps it would be great to see more exemplars from Solidity as it is the most common language for blockchain development.
The gap mostly occurs in Solidity when a contract initiates a call to an external contract and prior to the completion of the initial call, an external one makes a callback to the genesis. On the other side Rust’s ownership mandatory laws embargo fn
to be reentered, while it still has an active mutable reference.
pragma solidity ^0.8.0;
contract ReentrVul {
mapping(address => uint) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw() public {
uint amount = balances[msg.sender];
require(msg.sender.call.value(amount)(""));
balances[msg.sender] = 0;
}
}
Based on EVM implementation, here is an example of how we could write similar contract in Rust using the EVM crate.
use evm::{Context, ExitReason, ExitSucceed, Handler};
use primitive_types::{H160, U256};
use std::collections::HashMap;
struct Account { balance: U256, }
struct MyContract { accounts: HashMap<H160, Account>, }
impl MyContract { fn new() -> Self { Self { accounts: HashMap::new(), } }
fn deposit(&mut self, sender: H160, value: U256) {
let account = self.accounts.entry(sender).or_insert(Account {
balance: U256::zero(),
});
account.balance += value;
}
fn withdraw(&mut self, sender: H160) -> Result<(), &'static str> {
let account = self.accounts.get_mut(&sender).ok_or("Account not found")?;
let amount = account.balance;
// Here, we use a Rust feature called "Result" for error handling.
// If the call fails, the function will return early, and the balance will not be set to zero.
self.call(sender, amount)?;
account.balance = U256::zero();
Ok(())
}
fn call(&self, to: H160, value: U256) -> Result<(), &'static str> {
// Here, we simulate a call to another contract.
// In a real-world scenario, you would use the EVM's `call` function.
// If the call is successful, it returns `Ok(())`. If it fails, it returns `Err`.
// This way, we can ensure that the balance is only set to zero if the call is successful.
Ok(())
}
}
The Result
type is used by the withdraw function to handle errors. The function will return early and the account balance won't be reset if the call fails. Because the contract's state isn't changed until the external call has properly concluded, this stops reentrancy attacks.
To avoid disadvantages in coding, you should remember about minuses, as Rust is not an absolute language. By paying attention to common anti-patterns you can increase your chance to do better software. Important to realize that the blockchain environment is a high-stake field, here any mistake will cost you a lot, so be sure to protect your future users appropriately.
INCONSPICUOUS MEMORY LEAKS
Even though ownership features ward off many memory leaks, it's still possible to miss them by creating reference cycles with Rc
and Arc
. In long-duration blockchain node processing, it can lead to out-of-memory errors.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// create a reference cycle
(*leaf.children.borrow_mut()).push(Rc::clone(&branch));
}
In this situation, two elements called 'branch' and 'leaf' are linked in a loop that locks up their memory. The problem can be fixed by reorganizing your data or using a special type of pointer, called 'Weak', where these memory loops might occur. For complex systems, finding and avoiding these issues can be hard and might need special tools.
ABSTRACTION
Trait systems are admitted for powerful abstractions and polymorphism
known from OOP concept. Do not ignore such characteristics and stay away from writing a monolithic code. It will be quite hard to test and extend, if so.
Aliasing + traits:
Using such structures allows you exponentially decrease quantity of outbreaks in code architecture.
MODULES AND CRATES(Tap)
The module system helps in sorting out the code and controlling visibility. You can group related lines and manage their parameters, like pub
status, etc. Moreover, crate(library) has a root module that contains all the other modules, by using cargo new
you can create new projects and easily interact with them. Don’t pass over that, save time by cleaning up after yourself :>
You must test your code in any case, even if someone assures you that it is already quite secure. Check out the best links with practices that we have collected for you here.
Obi-money smart-contracts debugging
Rektoff
- Decentralized Security Collective that focuses on protecting blockchain applications and infrastructure by doing deep research and educating the whole web3 market and beyond. If you want to know about us more or how we plan to achieve new horizons, go to our official webpage:
###Socials###