Peter Girnus

View Original

Rust vs. C/C++: Ensuring Memory Safety & Security

C and other C-like languages, such as C++, have been the go-to systems programming languages for many years due to their high control over hardware, making them ideal for performance-intensive tasks. However, manual memory management in these languages can lead to memory corruption vulnerabilities, which can be challenging to prevent. Enter Rust, a modern systems programming language that offers the same level of control as C/C++ while ensuring memory safety. This blog post will explore the key differences between Rust and C/C++ from a memory safety and security standpoint. In this post, we'll explore some of the key differences between Rust and C-like languages and provide both C/C++ code and Rust code to give examples allowing developers to write code that is memory safe using rust programming. 

Type Safety & Compilers

Developers use Rust, C, and C++ as compiled programming languages. They write source code, which is then translated into machine code for execution. The principal compiler for Rust is called rustc, which is responsible for converting Rust source code into machine code.

The widely used compilers for C and C++ are gcc (GNU Compiler Collection) for both C and C++, clang (a compiler frontend for C, C++, and other languages), and g++ (the GNU C++ compiler) specifically for C++.

One key feature of the Rust compiler is its enforcement of strict rules during compilation that prevent programs from compiling if they violate these safety rules and ensure type safety.

Memory Management & Memory Safety: C/C++ Manual vs. Rust Borrow Checker

C/C++ - Manual Memory Management ≠ Memory Safety 

Both C and C++ rely primarily on manual memory management, which means the developer is responsible for ensuring memory is allocated and freed correctly using functions like malloc and free in C or new and delete in C++. Improper memory management can introduce unsafe conditions while allowing security vulnerabilities such as buffer overflows or use-after-free errors. In another example, a programmer might be setting memory with malloc, calloc, or new and forget to release it with free (in C) or delete (in C++) could lead developers into a memory leak condition. Additionally, Attackers can exploit these vulnerabilities to compromise the integrity and security of applications built with these languages.

See this content in the original post

Rust - Borrow Checker = Memory Safe

Rust introduces a unique system called the borrow checker, which enforces strict rules to prevent memory errors. One of the core features of rust is the borrow checker. The barrow checker ensures memory safety by ensuring that references to data obey two key rules: either there can be multiple immutable references (read-only) to a piece of data or a single mutable reference, but never both simultaneously. This ensures Rust is inherently a memory-safe language. Since Rust is a safe language by default, we can feel confident allowing Rust to manage memory.

See this content in the original post

Memory Safety & Security: Common Unsafe Pitfalls & Safe Rust Solutions

1. Dangling Pointers

C/C++ - Pointers

Dangling pointers are a common issue due to manual memory management. Attackers can exploit dangling pointers to control a program's execution, inject malicious code, or compromise the system's security. Security vulnerabilities stemming from dangling pointers are a common entry point for various attacks, including remote code execution and privilege escalation.

See this content in the original post

Rust - Scoped Ownership

The ownership model deallocates values in Rust when the owner variable goes out of scope, preventing common memory safety issues that plague C and C-like languages such as C++. This prevents dangling pointers from occurring—strict rules regarding how references and ownership work together are in place to enforce this. Once a value's owner is no longer in scope, Rust guarantees that any references or pointers to that value will become invalid. This ensures that data that no longer exists cannot be used, effectively eliminating many potential memory-related bugs. Rust's strict control over memory and references helps to enhance the safety and security of Rust programs.

See this content in the original post

2. Buffer Overflows

C/C++ - A Consequence of Manual Memory Management

In C/C++ programs, buffer overflows are a major cause of security vulnerabilities. A buffer overflow occurs when a program writes more data into a memory buffer, such as an array than the buffer can hold. As a result, the excess data spills over into neighboring memory locations, causing unintended and possibly hazardous consequences. This makes buffer overflows a frequent culprit of security vulnerabilities.

See this content in the original post

Rust - Boundary Checking & Scope Enforcement

In Rust, the standard library offers handy types such as Vec, which are dynamic arrays with built-in boundary checking and scope enforcement. This feature prevents buffer overflows by automatically verifying boundaries whenever arrays or collections are accessed. By providing this essential aspect of memory safety guarantees, Rust allows developers to create better-protected and more reliable code while still providing the option to use lower-level, unsafe operations when necessary.

See this content in the original post

3. Null Pointers

C/C++ - Pointer Dereferences

In C/C++, null pointer dereferences are well-known issues. Whenever a pointer or reference is invalid, it is called a null pointer or null reference. Often, these null pointers or null reference can lead to undefined behavior. Attackers can exploit null pointer dereferences to overwrite function pointers or jump to specific memory locations, which can result in code execution exploits.

See this content in the original post

Rust - Option Type

The Option type in Rust is used to indicate null values explicitly. This is achieved by enforcing strict rules through the ownership system and pattern matching. By doing so, Rust eliminates the risk of null pointer dereference errors that are frequently encountered in languages with nullable references. This approach enhances the safety and reliability of the code, making Rust an excellent choice for applications that require memory safety and reliability, such as systems programming.

See this content in the original post

Concurrency & Thread Safety

C/C++ - Threads & Synchronization Primitives

Concurrency in C and C++ is possible through threads and synchronization primitives, but issues like data races or deadlocks can occur.

See this content in the original post

Rust - Concurrency Protection

Rust's borrow checker extends its protection to concurrency to ensure data races cannot occur. Additionally, the language offers abstractions like Mutex and RwLock for safely sharing data between threads.

See this content in the original post

Conclusion

While C and C++ have been foundational in systems programming, the Rust programming language offers a compelling alternative to prioritize memory, thread safety, and security. By enforcing strict memory safety guarantees at compile time, Rust significantly reduces potential errors and vulnerabilities that can arise at runtime. Using Rust represents a promising direction for organizations and developers who value secure and reliable code. While there may be a learning curve, the potential benefits of safety and security are significant.