Slide 1

Slide 1 text

Memory Ownership in C# and Rust Rainer Stropek | @rstropek | software architects

Slide 2

Slide 2 text

The Problem

Slide 3

Slide 3 text

Memory Safety is an Issue ~70% of CVEs/patch year are memory safety related Source: Microsoft It is not getting better

Slide 4

Slide 4 text

Memory Leak See also CWE-401 Vulnerability Try online: https://repl.it/@RainerStropek/C-Memory-Leak 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(); } #include #include #include using namespace std; char *getData() { char *buffer = (char*)malloc(1024); strcpy(buffer, "Hi!\n"); // read data from somewhere return buffer; } char *buffer = (char*)malloc(1024); Note: This is not best-practice C++ code, just for demo purposes. Use types like vector or string, RAII-techniques like shared pointers, etc. instead.

Slide 5

Slide 5 text

Use After Free #include #include #include 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); } } See also CWE-416 Vulnerability Try online: https://repl.it/@RainerStropek/C-Use-After-Free 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); } }

Slide 6

Slide 6 text

Use After Free Try online: https://repl.it/@RainerStropek/C-Use-after-Dispose using System; class SqlConnection : IDisposable { … } class MainClass { private SqlConnection Connection { get; set; } public void OpenConnection() { … } public static void Main (string[] args) { … } public void Run() { OpenConnection(); } } using System; class SqlConnection : IDisposable { … } class MainClass { private SqlConnection Connection { get; set; } public void OpenConnection() { … } public void ExecSql(SqlConnection conn, string stmt) { … } 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(); } } } 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); } } } 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); } } }

Slide 7

Slide 7 text

Double Free Problem #include using namespace std; int main() { int* p = new int; // Allocate and init *p = 42; } See also Doubly freeing memory vulnerability, OWASP Try online: https://repl.it/@RainerStropek/C-Double-Free int* p = new int; // Allocate and init *p = 42; delete p; // Cleanup by freeing p cout << "done.\n"; } void printValue(const int *p) { cout << *p << '\n'; // Print value on screen delete p; // Cleanup by freeing p } printValue(p); // Call method to print value delete p; // Cleanup by freeing p cout << "done.\n"; } void printValue(const int *p) { cout << *p << '\n'; // Print value on screen delete p; // Cleanup by freeing p } printValue(p); // Call method to print value delete p; // Cleanup by freeing p cout << "done.\n"; }

Slide 8

Slide 8 text

Double Free Problem using System; using System.Buffers; class MainClass { public static void Main(string[] args) { var numbers = ArrayPool.Shared.Rent(4); try { DoSomethingWithNumbers(numbers); } finally { ArrayPool.Shared.Return(numbers); } } public static void DoSomethingWithNumbers(int[] numbers) { } } using System; using System.Buffers; class MainClass { public static void Main(string[] args) { var numbers = ArrayPool.Shared.Rent(4); try { DoSomethingWithNumbers(numbers); } finally { ArrayPool.Shared.Return(numbers); } } public static void DoSomethingWithNumbers(int[] numbers) { Task.Run(async () => { // Do something with Numbers ArrayPool.Shared.Return(numbers); }); } } using System; using System.Buffers; class MainClass { public static void Main(string[] args) { var numbers = ArrayPool.Shared.Rent(4); try { DoSomethingWithNumbers(numbers); } finally { ArrayPool.Shared.Return(numbers); } } public static void DoSomethingWithNumbers(int[] numbers) { Task.Run(async () => { // Do something with Numbers ArrayPool.Shared.Return(numbers); }); } }

Slide 9

Slide 9 text

C# Solution

Slide 10

Slide 10 text

C# Solution Managed Memory: Garbage Collector Solves many problems, but not all Not a zero-cost abstraction Basic Memory abstractions Span, Memory Docs Rules and helper classes

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

C# Memory Abstractions Memory bytes = new byte[] { 1, 2, 3, 0 }; IntPtr ptr = Marshal.AllocHGlobal(1024); try { using var memMgr = new UnmanagedMemoryManager(ptr, bytes.Length + 1); Memory unmanagedBytes = memMgr.Memory; } finally { Marshal.FreeHGlobal(ptr); } static void DoSomethingWithSpan(Span bytes) { bytes[^1] = (byte)(bytes[^2] + bytes[^3]); foreach (var number in bytes) Console.WriteLine(number); } Memory bytes = new byte[] { 1, 2, 3, 0 }; DoSomethingWithSpan(bytes.Span); IntPtr ptr = Marshal.AllocHGlobal(1024); try { using var memMgr = new UnmanagedMemoryManager(ptr, bytes.Length + 1); Memory unmanagedBytes = memMgr.Memory; bytes.CopyTo(unmanagedBytes); DoSomethingWithSpan(unmanagedBytes.Span); } finally { Marshal.FreeHGlobal(ptr); } static void DoSomethingWithSpan(Span bytes) { bytes[^1] = (byte)(bytes[^2] + bytes[^3]); foreach (var number in bytes) Console.WriteLine(number); } Memory bytes = new byte[] { 1, 2, 3, 0 }; DoSomethingWithSpan(bytes.Span); IntPtr ptr = Marshal.AllocHGlobal(1024); try { using var memMgr = new UnmanagedMemoryManager(ptr, bytes.Length + 1); Memory unmanagedBytes = memMgr.Memory; bytes.CopyTo(unmanagedBytes); DoSomethingWithSpan(unmanagedBytes.Span); } finally { Marshal.FreeHGlobal(ptr); }

Slide 13

Slide 13 text

C# Memory Ownership using System; using System.Buffers; class MainClass { public static void Main (string[] args) { // Get memory pool, Main is now owner IMemoryOwner owner = MemoryPool.Shared.Rent(100); } } using System; using System.Buffers; class MainClass { public static void Main (string[] args) { // Get memory pool, Main is now owner IMemoryOwner owner = MemoryPool.Shared.Rent(100); // Pass memory to consumer, Main is still owner Memory buffer = owner.Memory; WriteInt32ToBuffer(42, buffer); } static void WriteInt32ToBuffer(int value, Memory buffer) { … } } using System; using System.Buffers; class MainClass { public static void Main (string[] args) { // Get memory pool, Main is now owner IMemoryOwner owner = MemoryPool.Shared.Rent(100); // Pass memory to consumer, Main is still owner Memory 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 buffer) { … } static void DisplayAndFreeBuffer(IMemoryOwner ownership) { using (ownership) Console.WriteLine(ownership.Memory); } } Owner Consumer Try online: https://repl.it/@RainerStropek/C-Memory-Ownership

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Rust Solution

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Rust Move Ownership fn main() { // Allocate array on heap let numbers = vec![1, 2, 3, 5, 8]; println!("{:?}", numbers); } Owner fn main() { // Allocate array on heap let numbers = vec![1, 2, 3, 5, 8]; println!("{:?}", numbers); // Move ownership to other_numbers let other_numbers = numbers; println!("{:?}", other_numbers); } fn main() { // Allocate array on heap let numbers = vec![1, 2, 3, 5, 8]; 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 } fn main() { // Allocate array on heap let numbers = vec![1, 2, 3, 5, 8]; 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

Slide 18

Slide 18 text

Rust Ownership and Functions fn main() { let numbers = vec![1, 2, 3, 5, 8]; } Owner fn main() { let numbers = vec![1, 2, 3, 5, 8]; consume(numbers); // Gives ownership to `consume` } fn consume(numbers: Vec) { let sum: i32 = numbers.iter().sum(); println!("The sum is {}", sum); // numbers gets out of scope -> free memory } fn main() { let numbers = vec![1, 2, 3, 5, 8]; consume(numbers); // Gives ownership to `consume` } fn consume(numbers: Vec) { let sum: i32 = numbers.iter().sum(); println!("The sum is {}", sum); // numbers gets out of scope -> free memory } fn produce() -> Vec { let mut numbers: Vec = Vec::new(); for i in 0..4 { numbers.push(i); } numbers // Gives ownership to caller } fn main() { let numbers = vec![1, 2, 3, 5, 8]; 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) { let sum: i32 = numbers.iter().sum(); println!("The sum is {}", sum); // numbers gets out of scope -> free memory } fn produce() -> Vec { let mut numbers: Vec = Vec::new(); for i in 0..4 { numbers.push(i); } numbers // Gives ownership to caller } Owner consume does not need ownership → borrowing Try online: https://repl.it/@RainerStropek/Rust-Ownership-and-Functions

Slide 19

Slide 19 text

Rust References and Borrowing fn main() { let mut numbers = vec![1, 2, 3, 5, 8]; println!("{:?}", numbers); } For lifetime annotations see https://repl.it/@RainerStropek/Rust-Lifetime fn main() { let mut numbers = vec![1, 2, 3, 5, 8]; println!("The sum is {}", consume(&numbers)); // Passes reference, keeps ownership println!("{:?}", numbers); } fn consume(numbers: &Vec) -> i32 { // numbers is read-only, cannot be mutated //numbers.push(42); // -> does NOT COMPILE let sum: i32 = numbers.iter().sum(); sum } 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 { // 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 { numbers.push(42); consume(numbers) }

Slide 20

Slide 20 text

Rust vs. C# Broken Iterator use futures::executor::block_on; use futures_timer::Delay; use std::time::Duration; fn main() { block_on(async_main()); } async fn async_main() { let numbers = vec![1, 2, 3, 5, 8]; } use futures::executor::block_on; use futures_timer::Delay; use std::time::Duration; fn main() { block_on(async_main()); } async fn async_main() { let numbers = vec![1, 2, 3, 5, 8]; let sum_future = sum(&numbers); println!("The sum is {}", sum_future.await); } async fn sum(numbers: &Vec) -> i32 { let iter = numbers.iter(); Delay::new(Duration::from_secs(2)).await; iter.sum() } use futures::executor::block_on; use futures_timer::Delay; use std::time::Duration; fn main() { block_on(async_main()); } async fn async_main() { let mut numbers = vec![1, 2, 3, 5, 8]; let sum_future = sum(&numbers); add(&mut numbers); → does NOT COMPILE println!("The sum is {}", sum_future.await); } fn _add(numbers: &mut Vec) { numbers.push(42); } async fn sum(numbers: &Vec) -> i32 { let iter = numbers.iter(); Delay::new(Duration::from_secs(2)).await; iter.sum() } using System; using static System.Console; using System.Collections.Generic; using System.Threading.Tasks; class MainClass { public static void Main (string[] args) { var numbers = new List {1,2,3,5,8}; } } using System; using static System.Console; using System.Collections.Generic; using System.Threading.Tasks; class MainClass { public static void Main (string[] args) { var numbers = new List {1,2,3,5,8}; var sumTask = Sum(numbers); WriteLine($"The sum is {sumTask.Result}"); } static async Task SumAsync( IEnumerable numbers) { var sum = 0; foreach (var n in numbers) { await Task.Delay(10); sum += n; } return sum; } } using System; using static System.Console; using System.Collections.Generic; using System.Threading.Tasks; class MainClass { public static void Main (string[] args) { var numbers = new List {1,2,3,5,8}; var sumTask = Sum(numbers); numbers.Add(13); // crashes AT RUNTIME WriteLine($"The sum is {sumTask.Result}"); } static async Task SumAsync( IEnumerable numbers) { var sum = 0; foreach (var n in numbers) { await Task.Delay(10); sum += n; } return sum; } } Try online: https://repl.it/@RainerStropek/C-Broken-Iterator

Slide 21

Slide 21 text

Rust References and Borrowing Try online: https://repl.it/@RainerStropek/Rust-Channels use std::time::Duration; use std::sync::mpsc; use std::thread; fn main() { let (sender, receiver) = mpsc::channel::(); } use std::time::Duration; use std::sync::mpsc; use std::thread; fn main() { let (sender, receiver) = mpsc::channel::(); loop { match receiver.recv() { Ok(result) => println!("Received: {}", result), Err(_) => { println!("Done!"); break; } }; } } use std::time::Duration; use std::sync::mpsc; use std::thread; fn main() { let (sender, receiver) = mpsc::channel::(); thread::spawn(move || { for i in 0..5 { sender.send(i).unwrap(); thread::sleep(Duration::from_millis(500)); } }); loop { match receiver.recv() { Ok(result) => println!("Received: {}", result), Err(_) => { println!("Done!"); break; } }; } } Owner of Sender Owner of Receiver

Slide 22

Slide 22 text

So What?

Slide 23

Slide 23 text

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