You are on page 1of 23

Memory Ownership in

C# and Rust
Rainer Stropek | @rstropek | software architects
The Problem
Memory Safety is an Issue
~70% of CVEs/patch year are memory safety related
Source: Microsoft
It is not getting better 🤯
Memory Leak Note: This is not best-practice C++ code, just for demo
purposes. Use types like vector or string, RAII-techniques like
#include <iostream> shared pointers, etc. instead.
#include <stdlib.h>
#include <string.h>
using namespace std;

char *getData() {
char *buffer = (char*)malloc(1024);
strcpy(buffer, "Hi!\n"); // read data from somewhere
return buffer;
}

void getAndProcessData() {
char *buffer = getData();
cout << buffer;
}

int main() {
getAndProcessData();
}

See also CWE-401 Vulnerability


Try online: https://repl.it/@RainerStropek/C-Memory-Leak
Use After Free
#include <iostream>
#include <string.h>
#include <stdlib.h>
using namespace std;

bool execSql(const char *stmt) { … }


void logError(const char *category, const char *details) { … }

int main() {
char* stmt = (char*)malloc(100);
strcpy(stmt, "INSERT INTO ...");

bool errorHappened = false;


if (!execSql(stmt)) {
errorHappened = true;
free((void*)stmt);
}

// Do something else
if (errorHappened) {
logError("Error executing SQL", stmt);
}
} See also CWE-416 Vulnerability
Try online: https://repl.it/@RainerStropek/C-Use-After-Free
Use After Free
using System;
class SqlConnection : IDisposable { … }
class MainClass {
private SqlConnection Connection { get; set; }
public void OpenConnection() { … }
public void ExecSql(SqlConnection conn, string stmt) { … }
public void LogError(string category, SqlConnection conn) { … }
public static void Main (string[] args) { … }
public void Run() {
OpenConnection();
bool errorHappened = false;
try {
ExecSql(Connection, "INSERT INTO ...");
// Continue working with DB
}
catch {
errorHappened = true;
Connection.Dispose();
}
// Do something else
if (errorHappened) {
LogError("Error executing SQL", Connection);
}
}
}
Try online: https://repl.it/@RainerStropek/C-Use-after-Dispose
Double Free Problem
#include <iostream>
using namespace std;

void printValue(const int *p) {


cout << *p << '\n'; // Print value on screen
delete p; // Cleanup by freeing p
}

int main() {
int* p = new int; // Allocate and init
*p = 42;

printValue(p); // Call method to print value

delete p; // Cleanup by freeing p


cout << "done.\n";
}

See also Doubly freeing memory vulnerability, OWASP


Try online: https://repl.it/@RainerStropek/C-Double-Free
Double Free Problem
using System;
using System.Buffers;

class MainClass {
public static void Main(string[] args) {
var numbers = ArrayPool<int>.Shared.Rent(4);

try { DoSomethingWithNumbers(numbers); }
finally { ArrayPool<int>.Shared.Return(numbers); }
}

public static void DoSomethingWithNumbers(int[] numbers) {


Task.Run(async () => {
// Do something with Numbers
ArrayPool<int>.Shared.Return(numbers);
});
}
}
C# Solution
C# Solution
Managed Memory: Garbage Collector
Solves many problems, but not all
Not a zero-cost abstraction
Basic Memory abstractions
Span<T>, Memory<T>
Docs
Rules and helper classes
C# Solution
Ownership
Owner is responsible for lifetime management, including destroying the buffer.
All buffers have a single owner (creator, can be transferred).
After ownership transfer, transferrer may no longer use the buffer.
Consumption
Consumer (need not to be owner) is allowed to read/write from/to it.
Buffers can have one or more consumers (sync necessary?).
Lease
Length of time that a component is allowed to be the consumer.
C# Memory Abstractions
static void DoSomethingWithSpan(Span<byte> bytes)
{
bytes[^1] = (byte)(bytes[^2] + bytes[^3]);
foreach (var number in bytes) Console.WriteLine(number);
}

Memory<byte> bytes = new byte[] { 1, 2, 3, 0 };


DoSomethingWithSpan(bytes.Span);

IntPtr ptr = Marshal.AllocHGlobal(1024);


try
{
using var memMgr = new UnmanagedMemoryManager<byte>(ptr, bytes.Length + 1);
Memory<byte> unmanagedBytes = memMgr.Memory;
bytes.CopyTo(unmanagedBytes);
DoSomethingWithSpan(unmanagedBytes.Span);
}
finally { Marshal.FreeHGlobal(ptr); }
C# Memory Ownership
using System;
using System.Buffers;

class MainClass {
public static void Main (string[] args) {
// Get memory pool, Main is now owner
IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent(100); Owner Consumer
// Pass memory to consumer, Main is still owner
Memory<char> buffer = owner.Memory;
WriteInt32ToBuffer(42, buffer);

// Pass ownership to other function


DisplayAndFreeBuffer(owner);
// Here we must not access owner anymore!
}

static void WriteInt32ToBuffer(int value, Memory<char> buffer) { … }

static void DisplayAndFreeBuffer(IMemoryOwner<char> ownership) {


using (ownership)
Console.WriteLine(ownership.Memory);
}
}
Try online: https://repl.it/@RainerStropek/C-Memory-Ownership
C# Solution
Framework, not language
Basic abstractions
Interfaces, abstract base classes
Follow set of rules to make memory management robust
Not enforced by compiler
Bug → runtime error
Rust Solution
Rust Solution
Ownership rules enforced by compiler
Each value in Rust has a variable that is called its owner
There can only be one owner at a time
When the owner goes out of scope, the value will be dropped
Rust Move Ownership
fn main() {
// Allocate array on heap
let numbers = vec![1, 2, 3, 5, 8]; Owner
println!("{:?}", numbers);

// Move ownership to other_numbers


let other_numbers = numbers;
println!("{:?}", other_numbers);

// Now we cannot access numbers anymore


// because value was moved.
// println!("{:?}", numbers); // -> does NOT COMPILE

// Make a (deep) copy -> no move of ownership


let cloned_numbers = other_numbers.clone();
println!("clone = {:?}, source = {:?}", cloned_numbers, other_numbers);
}

Try online: https://repl.it/@RainerStropek/Rust-Move-Ownership


Rust Ownership and Functions
fn main() {
let numbers = vec![1, 2, 3, 5, 8]; Owner
consume(numbers); // Gives ownership to `consume`

let produced_numbers = produce(); // Takes ownership


println!("{:?}", produced_numbers);
// produced_numbers gets of of scope -> free memory
}

fn consume(numbers: Vec<i32>) {
let sum: i32 = numbers.iter().sum();
consume does not need
println!("The sum is {}", sum);
// numbers gets out of scope -> free memory ownership → borrowing
}

fn produce() -> Vec<i32> {


let mut numbers: Vec<i32> = Vec::new(); Owner
for i in 0..4 { numbers.push(i); }
numbers // Gives ownership to caller
}
Try online: https://repl.it/@RainerStropek/Rust-Ownership-and-Functions
Rust References and Borrowing
fn main() {
let mut numbers = vec![1, 2, 3, 5, 8];

println!("The sum is {}",


consume(&numbers)); // Passes reference, keeps ownership
println!("The sum is {}",
add_and_consume(&mut numbers)); // Mutable reference, keeps ownership

println!("{:?}", numbers);
}

fn consume(numbers: &Vec<i32>) -> i32 {


// numbers is read-only, cannot be mutated
//numbers.push(42); // -> does NOT COMPILE
let sum: i32 = numbers.iter().sum();
sum
}

fn add_and_consume(numbers: &mut Vec<i32>) -> i32 {


numbers.push(42);
consume(numbers)
}

For lifetime annotations see https://repl.it/@RainerStropek/Rust-Lifetime


Rust vs. C# Broken Iterator
use futures::executor::block_on; using System;
use futures_timer::Delay; using static System.Console;
use std::time::Duration; using System.Collections.Generic;
using System.Threading.Tasks;
fn main() {
block_on(async_main()); class MainClass {
} public static void Main (string[] args) {
var numbers = new List<int> {1,2,3,5,8};
async fn async_main() { var sumTask = Sum(numbers);
let mut numbers = vec![1, 2, 3, 5, 8]; numbers.Add(13); // crashes AT RUNTIME
let sum_future = sum(&numbers); WriteLine($"The sum is {sumTask.Result}");
add(&mut numbers); → does NOT COMPILE }
println!("The sum is {}", sum_future.await);
} static async Task<int> SumAsync(
IEnumerable<int> numbers) {
fn _add(numbers: &mut Vec<i32>) { var sum = 0;
numbers.push(42); foreach (var n in numbers) {
} await Task.Delay(10);
sum += n;
async fn sum(numbers: &Vec<i32>) -> i32 { }
let iter = numbers.iter();
Delay::new(Duration::from_secs(2)).await; return sum;
iter.sum() }
} }
Try online: https://repl.it/@RainerStropek/C-Broken-Iterator
Rust References and Borrowing
use std::time::Duration;
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel::<i32>();
thread::spawn(move || {
for i in 0..5 { Owner of Sender
sender.send(i).unwrap();
thread::sleep(Duration::from_millis(500));
}
});
loop {
match receiver.recv() { Owner of Receiver
Ok(result) => println!("Received: {}", result),
Err(_) => {
println!("Done!");
break;
}
};
}
}

Try online: https://repl.it/@RainerStropek/Rust-Channels


So What?
Conclusion
C# has learned new tricks
Managed memory solves many problems, but not all
GC is not a zery-cost abstraction
New classes for better memory efficiency in latest .NET versions
You have to follow rules, otherwise runtime errors
Rust is an interesting, new language
Gaining popularity rapidly
E.g. No. 1 loved technology in Stack Overflow’s Dev Survey 2019
Combines efficiency and safety
More compile-time errors, less runtime errors

You might also like