As software becomes increasingly integrated into our daily lives, the importance of secure coding and programming safety cannot be overstated. From online banking to healthcare records, sensitive information is being transmitted and stored in software systems, making them prime targets for cyber attacks. As a result, it's crucial for developers to understand and implement best practices for secure coding to ensure the safety and privacy of user data. In this article, we explore the various techniques and strategies that can be used to make software more secure and resilient to attacks.
Programming safety
Programming safety is multidimensional and subjective. We consider four types of programming safety: memory safety, null safety, type safety, and thread safety.
Memory safety
A service provided by the standard library and the runtime where memory access violations are impeded is known as memory safety. Memory safety is a property of a programming language or system that ensures programs only access memory locations that they are allowed to access. Essentially, memory safety prevents certain types of errors and vulnerabilities related to memory access, such as buffer overflows, dangling pointers, and use-after-free errors. Some languages like C and C++ are not secure when best coding practices are not followed, leaving them insecure by nature. Double-free errors in C or C++ occur when you accidentally free a pointer twice. A use-after-free (UAF) error or dangling pointer error occurs when software continues to utilize a pointer after it has been released. These actions are examples of undefined behavior since they are unexpected and create security holes rather than solely crashing the application. A software crash is advantageous in some situations since it won't lead to a security risk.
Problems with memory safety cause most of the Common Vulnerabilities and Exposures (CVEs) that we come across. A hacker might take advantage of a memory vulnerability to take over a system, execute malicious code, or expose sensitive information. A memory-safe language will crash, panic, or fail if you attempt to access an out-of-bounds array element; this is expected behavior. This is why CVEs and urgent fixes are frequently issued in response to memory-related problems in C and C++ systems. Other memory-unsafe activities in C and C++ include using memory that has been deallocated, invalidating iterators, and more. Even languages that are memory-safe with weaker security features guard against these security flaws.
See the use-after-free example below, where the pointer in C is instantly released when an error has occurred. The LogError
method later uses this reference improperly. The code is checking if the err
is true; if it is true, it sets the variable Automatic Bug Reporting Tool (ABRT) to 1 and frees the memory allocated to the pointer ptr
.
// Allocate memory of size SIZE and assign the pointer to "ptr"
char* ptr = (char*)malloc (SIZE);
// If an error occurs
if (err) {
// Set the "abrt" variable to 1
abrt = 1;
// Free the memory allocated to "ptr"
free(ptr);
}
...
// If "abrt" is set to 1
if (abrt) {
// Log an error message with the given message and the value of "ptr"
logError("Operation Aborted Before Commit", ptr);
}
Null safety
Memory safety and null safety are connected. Nothing is required in garbage collecting languages for pointers to be released when they are not in use, which causes problems such as null pointer exceptions. Most memory-safe programming languages permit the use of null as a value, which also results in null pointer problems.
See the null pointer dereference example below using Java, where the programmer takes for granted that the system's cmd
attribute is always defined. When attempting to use the trim()
function, the application raises a null pointer exception if an attacker is able to manipulate the environment of the program such that cmd
is not specified.
// Get the value of the "cmd" system property and assign it to a variable named "cmd"
String cmd = System.getProperty("cmd");
// Remove any leading and trailing whitespaces from the "cmd" variable
cmd = cmd.trim();
Type safety
We access a variable in a type-safe language exactly as the data is stored. As a result, we are confident in dealing with this data without explicitly verifying the data type at runtime. Lacking type safety can cause security problems because it is crucial for assuring memory safety.
Languages that are not type-safe are susceptible to low-level attacks because an attacker can modify the data type and manipulate the data structure to acquire private information. Although this kind of attack is uncommon, it does happen occasionally.
In C++, the code below will compile without error but the result of the addition will be incorrect because x and y have different types, and the addition operation will be done between them but the result will be wrong.
int x = 5;
double y = x; // implicit type conversion
int sum = x + y; // this will compile, but the result will be incorrect
Thread safety
When using a language that is thread-safe, you may have many threads access the same memory at the same time without having to worry about data races. Mutual exclusion locks (mutexes), thread synchronization, and message forwarding mechanisms are typically used to accomplish this. Thread safety may also result in security problems for developers.
Two types of vulnerabilities may result from thread safety problems. The first is when one thread overwrites data from another, and second is the interlacing of data from many threads. The most common example of thread safety is a time-of-check time-of-use (TOCTOU) attack.
In the code below we see a TOCTOU attack, where we consider that many threads will call this code block. Depending on when RedHat.txt
was last edited, the switch statement will run a different piece of code. If RedHat.txt
changed while this code block was running on different threads, it would be possible for this code block to crash, and our switch case may produce unexpected outcomes.
#include <sys/types.h>
#include <sys/stat.h>
...
// Declare a struct stat variable named sb
struct stat sb;
// Get information about the file "RedHat.txt" and store it in the sb variable
stat("RedHat.txt",&sb);
// Print the change time of the file
printf("file change time: %d\n",sb.st_ctime);
// Use the modulo operator to get the remainder of dividing the file change time by 2
switch(sb.st_ctime % 2){
// If the remainder is 0
case 0:
printf("Option 1\n"); // Print "Option 1"
break;
// If the remainder is 1
case 1:
printf("Option 2\n"); // Print "Option 2"
break;
// If none of the above cases are true, (which should not happen)
default:
printf("This should be unreachable?\n");
break;
}
Why Rust?
Let's discuss Rust and why it could be a better alternative when it comes to secure coding. Memory safety or safety in general is one of the important features of the Rust language. By default, the compiler will fail to compile insecure code. Rust provides an unsafe
keyword that allows us to bypass the security guarantees provided by the Rust compiler, however, its usage won’t be required in most cases. Rust uses an inventive ownership structure and a borrow checker integrated into the compiler to guarantee memory safety at build time. Through the use of extra runtime checks and static compile-time analysis, Rust ensures memory safety while removing many types of memory issues. At the language level, null does not exist. Instead, the Option enum
in Rust is available and may be used to indicate whether a value is present or not. This means that null pointer exceptions will not occur with Rust since the resulting code is much simpler and null-safe.
Rust is one of the most memory-efficient languages because of the ownership and borrowing systems, which helps programmers avoid problems with manual memory management and garbage collection. It is as fast and efficient with memory as C and C++, and it also has higher memory safety than garbage-collected programming languages such as Java and Go.
// Create a new string "Hello" and assign it to a variable named "s1"
let s1 = String::from("Hello");
// Assign the value of "s1" to a new variable named "s2"
let s2 = s1;
// Try to print the value of "s1"
println!("{}", s1); // Does not compile
The code above creates a new string "Hello"
using the String::from
function and assigns it to a variable s1
. Then, it creates a new variable s2
and assigns s1
to it. The println!("{}", s1);
statement is trying to print the value of s1
, but it does not compile because after the assignment s2 = s1
, the value of s1
has been moved to s2
.
In Rust, variables have a property called ownership, and when a value is assigned to another variable, the original variable loses its ownership of the value. In this case, after the assignment s2 = s1
, s1
no longer owns the string "Hello"
, so it cannot be used or accessed.
It is important to note that this is a feature in Rust that helps prevent data races and other undefined behavior by ensuring that there is only one owner of a value at any given time.
// Create a mutable variable named "num" with the value of 5
let mut num = 5;
// Create a constant pointer "r1" to the value of "num"
let r1 = &num as *const i32;
// Create a mutable pointer "r2" to the value of "num"
let r2 = &mut num as *mut i32;
// Enter an unsafe block
unsafe {
// Dereference pointers "r1" and "r2" and print their values
println!("{}{}", *r1, *r2);
// Dereferencing a raw pointer is only allowed within unsafe block
}
The code above creates a mutable variable num
with the value of 5 and then creates two pointers: r1
and r2
. r1
is a const
pointer to the value of num
and is created using the &
operator, which creates a reference to the value of num
, and the as *const i32
syntax, which casts the reference to a raw pointer. r2
is a mutable pointer to the value of num
and is created using the &mut
operator, which creates a mutable reference to the value of num
, and the as *mut i32
syntax, which casts the reference to a raw pointer. The unsafe
keyword is used to indicate that the code inside the block may be dangerous and could cause undefined behavior if it’s not used correctly. The println!("{}{}", *r1, *r2);
statement is trying to dereference the pointers r1
and r2
, and print the values they point to. Dereferencing a raw pointer means accessing the value it points to. This can only be done inside an unsafe
block because it could cause undefined behavior if the pointer is not valid or if it is a null pointer.
It's important to note that using raw pointers in Rust is generally discouraged, and it is recommended to use references or smart pointers instead because they provide safer and more robust ways of managing memory.
Type safety in Rust
Because Rust is statically typed, type safety is ensured by rigorous compile-time type verification. Using the dyn keyword, Rust permits a degree of dynamic typing. Although this is the case, type safety is guaranteed by the compiler and sophisticated type inference.
let x: i32 = 5;
let y: f64 = x as f64; // explicit type conversion
let sum = x + y; // this will not compile because x and y have different types
In the example shown above, variable x
is of type i32
, and variable y
is of type f64
. When we try to add these two variables together, the Rust compiler will raise an error because these two variables have different types. The addition operation between them is not allowed and the compiler will not allow the code to be compiled. This is an example of how Rust's type safety helps prevent logical errors and makes the code more robust.
Thread safety in Rust
Rust offers common library functionality including channels, mutex, Automatic Reference Counted (ARC) pointers, as well as a guarantee for thread safety using notions similar to those used for memory safety. Secure Rust allows for limitless read-only references to a resource as well as one mutable reference at any given moment. Due to the ownership mechanism, a shared state cannot accidentally start a data race. This gives us the peace of mind to concentrate on the code and let the compiler handle shared data across threads.
Outside programming safety, Rust also has features that enhance system security. A few features include:
- Awesome concurrency support
- Flexible pattern matching
- Immutable by default
- Easy to write functional code
- Easy to write system-level programming
- Growing community
- Helpful compiler messages
- Amazing tooling support (Cargo, crates.io, rust-analyzer, etc.)
Rust at Red Hat
Rust has a growing community and is actively being used by large corporations, including Red Hat projects like the stratis storage project utilizes Rust heavily also Podman recently released a new network stack written in Rust called Netavark and Aardvark, in addition to the existing CNI stack. Other notable projects written in Rust are rpm-ostree, coreos-installer, and aya eBPF library for Rust. And some well known upstream Projects like Linux Kernel, Firefox, Thunderbird, python-cryptography are actively using Rust.
Conclusion
Programming safety and secure coding are critical aspects of software development, particularly in the field of systems programming. With a focus on memory safety and ownership, Rust provides a number of features that help developers write more secure code. The ownership model, lifetime annotations, and borrow checker work together to prevent common programming errors such as null or dangling pointer references. Additionally, their type system and strong type inference make it more difficult for developers to introduce type-related bugs into their code. Rust’s community also provides various tooling to help developers detect and prevent potential vulnerabilities like memory leaks and data races. Overall, Rust's focus on safety and security makes it an excellent choice for systems programming and other critical applications.
References
- Learn Rust - Rust Programming Language
- Introduction - Secure Rust Guidelines
- RustSec
- GitHub - rust-secure-code/projects: Contains a list of security related Rust projects.
- Black Hat Rust
- Fearless Concurrency with Rust
- Concurrency - The Rust Programming Language
- Understanding Rust Thread Safety
- RustZone: Writing Trusted Applications in Rust
- The art and science of secure open source software development
- Secure Coding Tutorials | Red Hat Developer