Dangling pointers problem in C vs Rust

·

5 min read

I encounter this original blog about Stack Memory and Dangling Pointer problem in Zig language, then I asked myself, how about other system programming languages, like C language and Rust, do they have the same problem? This blog is a very short article comparing how those languages deal with this problem.

TLDR

C language obviously has Dangling Pointer problem too, you have to solve this problem manually. Meanwhile, Rust doesn't allow you to even compile the code, because it has a guard rail - its ownership system.

The C language implementation

Here is my C implementation of the dangling pointer problem demonstrated in the original blog:

#include <stdio.h>
#include <stdlib.h>

// declare User struct
struct User
{
    int id;
};

// function that create an User struct in stack memory,
// then return it pointer, which cause the dangling pointer problem.
struct User *init(int id)
{
    struct User user;
    user.id = id;
    return &user;
}

// the main function
int main()
{
    struct User *user1 = init(100);
    struct User *user2 = init(1000);
    printf("user1.id = %d\n", user1->id);
    printf("user2.id = %d\n", user2->id);
}

The output on my machine one time (the output is different everytime you run it):

user1.id = 1000
user2.id = 10862404

which show that user1 is inheriting the value of user2 and user2 value are pointing to some invalid memory region, cause its id attribute to be nonsensical value.

C language solution

This is my C solution using malloc function:

#include <stdio.h>
#include <stdlib.h>

// declare User struct
struct User
{
    int id;
};

// function that create an User struct, but it ...
struct User *init(int id)
{
    // ... use heap memory instead, using the `malloc` function
    struct User * user;
    user = (struct User *) malloc(sizeof(struct User));
    user->id = id;
    return user;
}
int main()
{
    struct User *user1 = init(100);
    struct User *user2 = init(1000);
    printf("user1.id = %d\n", user1->id);
    printf("user2.id = %d\n", user2->id);
    // and remember to free the heap memory after used, each pointer,
    // one and only one time.
    free(user1);
    free(user2);
}

Now the output is consistent:

user1.id = 100
user2.id = 1000

The problems with this solution are:

  • you have to manually free the memory after use,

  • you will get memory leak application if you forget to free some pointers

  • you also get security issue if you free the same pointer more than once, because you may cause the modification of unexpected memory locations.

The Rust implementation

Rust prevent programmer from shooting themself in their foot, the equivalent code doesn't event compile:

fn main() {
    let user1 = User::init(100);
    let user2 = User::init(1000);
    println!("user1.id = {}", user1.id);
    println!("user2.id = {}", user2.id);

}
struct User {
    id: i32,
}

impl User {
    fn init(id: i32) -> &User {
        let u = User { id: id };
        &u
    }
}

The errors when you compile are:

   |
13 |     fn init(id: i32) -> &User {
   |                         ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
   |
13 |     fn init(id: i32) -> &'static User {
   |                          +++++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello-world` (bin "dangling_pointers") due to previous error

If you naively think that you can get away with this error just by following the compiler's suggestion of change (adding static lifetime), you are wrong, because now there is new error with this modified code:

fn main() {
    let user1 = User::init(100);
    let user2 = User::init(1000);
    println!("user1.id = {}", user1.id);
    println!("user2.id = {}", user2.id);

}
struct User {
    id: i32,
}

impl User {
    fn init(id: i32) -> &'static User {
        let u = User { id: id };
        &u
    }
}

The error now is:

   |
15 |         &u
   |         ^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.
error: could not compile `hello-world` (bin "dangling_pointers") due to previous errorp

Rust simply doesn't allow a function to return a reference to a data owned by that function itself. You have to move the ownership of this data back to the caller. For more details, please checkout this article about dangling references in "The Rust Programming Language" book.

Rust solution

So here is the solution in Rust:

fn main() {
    let user1 = User::init(100);
    let user2 = User::init(1000);
    println!("user1.id = {}", user1.id);
    println!("user2.id = {}", user2.id);

}
struct User {
    id: i32,
}

impl User {
    fn init(id: i32) -> User {
        let u = User { id: id };
        u
    }
}

Ending

Dangling pointers is just one in many problems that make C a unsafe language to work with.

For modern system programming, I prefer Rust, and If I need more low-level control, I might choose Zig. Rust is like the new C++, and Zig is like the new C. I think they both have their own strength, and I want to see more open-source software written in both of these languages in future.

This article is limited only to my personal experience, there are probably more ways to prevent dangling pointers in C that I don't know yet. Please let me know if I'm wrong.

Happy coding,