Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Memory Ownership in C# and Rust

Memory Ownership in C# and Rust

Recently, C# has learned a lot of new tricks when it comes to memory abstractions (e.g. Span, Memory) and memory management (e.g. IMemoryOwner). They can improve the efficiency of your C# apps significantly. As a C# developer, that made me look deeper into this topic, and I discovered Rust's extraordinary features regarding memory ownership. In this talk, I share my thoughts about Rust and compare its features with what C# currently is able to do.

Rainer Stropek

November 28, 2019
Tweet

More Decks by Rainer Stropek

Other Decks in Technology

Transcript

  1. Memory Safety is an Issue ~70% of CVEs/patch year are

    memory safety related Source: Microsoft It is not getting better
  2. 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 <iostream> #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; } 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.
  3. 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); } } 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); } }
  4. 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); } } }
  5. Double Free Problem #include <iostream> 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"; }
  6. 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) { } } 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); }); } } 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); }); } }
  7. 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
  8. 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.
  9. C# Memory Abstractions Memory<byte> bytes = new byte[] { 1,

    2, 3, 0 }; IntPtr ptr = Marshal.AllocHGlobal(1024); try { using var memMgr = new UnmanagedMemoryManager<byte>(ptr, bytes.Length + 1); Memory<byte> unmanagedBytes = memMgr.Memory; } finally { Marshal.FreeHGlobal(ptr); } 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); } 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); }
  10. 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); } } 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); // Pass memory to consumer, Main is still owner Memory<char> buffer = owner.Memory; WriteInt32ToBuffer(42, buffer); } static void WriteInt32ToBuffer(int value, Memory<char> buffer) { … } } 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); // 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); } } Owner Consumer Try online: https://repl.it/@RainerStropek/C-Memory-Ownership
  11. 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
  12. 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
  13. 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
  14. 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<i32>) { 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<i32>) { let sum: i32 = numbers.iter().sum(); println!("The sum is {}", sum); // numbers gets out of scope -> free memory } fn produce() -> Vec<i32> { let mut numbers: Vec<i32> = 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<i32>) { let sum: i32 = numbers.iter().sum(); println!("The sum is {}", sum); // numbers gets out of scope -> free memory } fn produce() -> Vec<i32> { let mut numbers: Vec<i32> = 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
  15. 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>) -> 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>) -> 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) }
  16. 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>) -> 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<i32>) { numbers.push(42); } async fn sum(numbers: &Vec<i32>) -> 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<int> {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<int> {1,2,3,5,8}; var sumTask = Sum(numbers); WriteLine($"The sum is {sumTask.Result}"); } static async Task<int> SumAsync( IEnumerable<int> 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<int> {1,2,3,5,8}; var sumTask = Sum(numbers); numbers.Add(13); // crashes AT RUNTIME WriteLine($"The sum is {sumTask.Result}"); } static async Task<int> SumAsync( IEnumerable<int> 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
  17. 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::<i32>(); } use std::time::Duration; use std::sync::mpsc; use std::thread; fn main() { let (sender, receiver) = mpsc::channel::<i32>(); 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::<i32>(); 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
  18. 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