Rust Part 2: Memory Leaks Continued

Purpose

Rust is a multi-paradigm, general purpose programming language that can be used safely due to it's validity of memory checking before execution. It is similar to C but much safer to use as we will see in this project. This is a continuation of a previous project on Rust. In the previous project, most of the code written was in stacks for memory allocation purposes. In these examples you will see heaps of memory. See below for image comparsions.

Image credit to Geeks For Geeks. Some of the key differences between these two ways are as follows:

  • 1. In a stack, the allocation/de-allocation are automatically done by the compiler whereas in heap, it needs to be done by the programmer manually.
  • 2. Handling of heap frame is costlier than handling of the stack frame.
  • 3. Memory shortage problem is mostly a stack problem; heaps memory issue is fragmentation.
  • This will make more sense as we go through this project.

    You will need the following to complete this assignment:

  • Debian 11 VM
  • Expectations

    ****Need to add here.****

    Please be sure to create a Google Doc that contains screenshots with captions indicating what you are accomplishing. This will be used to help show that you completed the project and can be used as a reference later on for you. This will also be your submission for this project. The number of screenshots is to be determined but typically it should match the page count of the project.

    Directions

    As previously stated, this is a continuation of a previous project. You should already be loaded into your Debian VM with your terminal window available. Enter the following to create a heap file using C:
    cd ~
    nano heap.c
    

    Enter this code to pull the main functions and libraries:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
       char* name1 = (char *) malloc(20);
       char* name2 = (char *) malloc(20);
    
       printf("Enter name1: ");
       scanf("%s", name1);
       printf("Enter name2: ");
       scanf("%s", name2);
    
       printf("\n");
       printf("name1 address: %p ; name2 address: %p\n\n", name1, name2);
    
       printf("name1: \"%s\"\n\n", name1);
       printf("name2: \"%s\"\n\n", name2);
    }
    

    Compile it and run it using the code below:

    gcc -o heap heap.c
    ./heap
    

    There shouldn't be an errors and it should prompt you for a name1. Based on our code, we need to enter a name with 20 characters. Enter the following:

    DDDDDDDDDDDDDDDDDDD
    

    That should be 20 D's in a row. For name2, enter the letter E 20 times.

    EEEEEEEEEEEEEEEEEEE
    

    C should interpret the information correctly and display each name with 20 characters.

    Take a screenshot of your results.

    Now can we trick the buffer with more characters? Lets find out. Run the program again using ./heap and input 40 characters of D and E respectively.

    Take a screenshot of your results and indicate the differences.

    What you saw on your system can be a very cruel thing that novice developers in C may face if they don't understand how memory allocations work. Just like in the previous project, let's see how Rust can be beneficial.

    cd ~
    nano heapr.rs
    

    Enter this code:

    fn main() {
        let mut name1 = Box::new(String::new());
        let mut name2 = Box::new(String::new());
    
        println!("name1 address: {:p}. name2 address: {:p}.", &name1, &name2);
        println!("name1 contains {}.  name2 contains {}. ", name1, name2);
        
        println!("Enter name1: ");
        let _num = std::io::stdin().read_line(&mut name1).unwrap();
        println!("Enter name2: ");
        let _num = std::io::stdin().read_line(&mut name2).unwrap();
        
        println!("name1 address: {:p}. name2 address: {:p}.", &name1, &name2);
        println!("name1 contains {}.  name2 contains {}. ", name1, name2);
    }
    

    Compile it and run it using the code below:

    rustc -g heapr.rs
    ./heapr
    

    Just as before enter the Ds and Es.

    There shouldn't be an errors and it should prompt you for a name1. Based on our code, we need to enter a name with 20 characters. Enter the following:

    DDDDDDDDDDDDDDDDDDD
    

    That should be 20 D's in a row. For name2, enter the letter E 20 times.

    EEEEEEEEEEEEEEEEEEE
    

    Just as we've done before with the C program, let's try to overflow this heap buffer. Run the program again ./heapr and enter in 40 D's and E's.

    Take a screenshot of your result? Did the heap buffer overflow in this program? Support your screenshot with evidence.

    In a previous project, we have seen how a stacked string works. Let's dive deeper into debugging this heap string. Enter the following:

    gdb -q heapr
    list 1, 15
    

    The debugger ran and shows us lines 1 through 15 of our code. Let's set a breakpoint on line 14 and run the program.

    break 14
    run
    

    Just like before, issue the following name1:

    DDDDDDDDDDDDDDDDDDD
    

    For name2, enter the letter E 20 times.

    EEEEEEEEEEEEEEEEEEE
    

    Lets view the strings and then dive deeper into them with the second set of commands listed below:

    print name1
    print name2
    print *name1
    print *name2
    

    Rerun the commands above but instead of 20 characters (Ds and Es) use 40 characters. Take a couple of screenshots showing your progress. Did reallocation of space occur correctly? Why or why not?

    To quit the program type quit, Y and press Enter Now we are going to look at two special cases that often occur with memory. They are called dangling pointers and memory leaks. Lets start with dangling pointers. Issue the following:

    nano dangpt.c
    

    Enter the following:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main()
    {
       char* name1 = (char *) malloc(20);
       
       printf("Enter name1: ");
       scanf("%s", name1);
       printf("name1 address: %p ; value %s\n", name1, name1);
    
       free(name1);
       
       char* name2 = (char *) malloc(20);
    
       printf("Enter name2: ");
       scanf("%s", name2);
       printf("name2 address: %p ; value %s\n", name2, name2);
       printf("name1 address: %p ; value %s\n", name1, name1);
    }
    

    Now lets compile it and run it:

    gcc -o dangpt dangpt.c
    ./dangpt
    

    For name1 enter D and for name2 enterE

    If you noticed back in our program, we used the function of free() for the variable name1. This function deallocates the memory that was previously allocated by a call. In our case that was the malloc or memory allocation of name1. Because of this error, both name1 and name2 share the same memory address. This is an example of a dangling pointer record which can be dangerous if left unchecked. Take a screenshot showing your dangling pointer error.

    Now lets look to see what Rust does to help with this. Issue the following commands:

    nano dangptrs.rs
    

    fn main() {
      {
            let mut name1 = Box::new(String::new());
    
            println!("Enter name1: ");
            let _num = std::io::stdin().read_line(&mut name1).unwrap();
            println!("name1 address: {:p} ; contents {}.", &name1, name1);
      }
    
        let mut name2 = Box::new(String::new());
    
        println!("Enter name2: ");
        let _num = std::io::stdin().read_line(&mut name2).unwrap();
    
        println!("name2 address: {:p} ; contents {}.", &name2, name2);
        println!("name1 contents {}.", name1);
    }
    

    Now compile it with rustc dangptsrs.rs command. What happens?

    Take a screenshot of your results. Did you receive an error message? What does Rust do with heap objects? Refer to Overview of Memory Management in Rust for more information to answer the questions.

    Now lets take a closer look at the free function and how to use it the correct way. Issue the following commands:

    nano cnoleak.c
    
    Enter this code:
    #include <stdio.h>
    #include <stdlib.h>
    
    void noleak()
    {
       char * p = malloc(1000);
       free(p);
       return;
    }
    
    int main()
    {
       int i, j;
    
       for (i=0; i<10; i++) {
          for (j=0; j<1000; j++) noleak();
          printf("Press Enter to Cont.\n");
          getchar();
       }
    }
    

    Compile it with gcc -o cnoleak cnoleak.c and run it with ./cnoleak.

    Open a second terminal window and issue the following command:

    watch "pmap $(pgrep leak)"
    

    Continue to press enter as the directions describe. Notice that the memory count does not increase? This is the correct use for the free function. Go back to each window and use a Ctrl+C to kill both processes. Now let's see how an actual leak works. Enter the following:

    nano cleak.c
    
    Enter this code:
    #include <stdio.h>
    #include <stdlib.h>
    
    void leak()
    {
       char * p = malloc(1000);
       return;
    }
    
    int main()
    {
       int i, j;
    
       for (i=0; i<10; i++) {
          for (j=0; j<1000; j++) leak();
          printf("Press Enter to Cont.\n");
          getchar();
       }
    }
    

    Compile it with gcc -o cleak cleak.c and run it with ./cleak.

    Open a second terminal window and issue the following command:

    watch "pmap $(pgrep leak)"
    

    Now look at your memory counts in the watch window. What is occurring?

    Take a few screenshots using mine above a guide to showcase what is happening with the memory. Discuss it in a few sentences again using the resources provided.

    Now let's try to do the same thing in Rust. Issue the following:

    nano noleakrust.rs
    

    Rust does not have a "free" command. As you saw before, heap objects are freed when they are not contained within the scope of the program. In an earlier program, you used a function called box. Let's see it again in action with some differences:

    use std::io::{stdin, Read};
    
    fn noleak() {
      let _a = Box::new([0.0f64; 1000]);
    }
    
    fn main() {
       for _i in 1..11 {
           for _j in 1..1001 {
               noleak();
           }
           println!("Press Enter to Cont:");
           stdin().read(&mut [0]).unwrap();
       }
    }
    

    Now compile it and run it:

    rustc noleakrust.rs
    ./noleakrust
    

    Just like before, open two terminal sessions. In the new terminal session issue the following:

    watch "pmap $(pgrep leak)"
    

    Again, watch your memory counts. Is it increasing?

    Take a few screeshots showing what is occurring with your new program. Using this Boxes in Rust discuss how boxes allow you to store data. What are the pros to using boxes? Are there any negatives? Again, discuss these in a few sentences.

    Now that you have a better understanding of Rust, let's see it leak memory. You have done a handful of exercises at this point. I will provide the name of the file here leakyrust.rs and the code below. Create a file with the name leakyrust.rs compile it and run it below.

    use std::io::{stdin, Read};
    use std::mem;
    
    fn leak() {
      let a = Box::new([0.0f64; 1000]);
      mem::forget(a);
    }
    
    fn main() {
       for _i in 1..11 {
           for _j in 1..1001 {
               leak();
           }
           println!("Press Enter to Cont:");
           stdin().read(&mut [0]).unwrap();
       }
    }
    

    Take screenshots showing your progress here. This code also introduces a new function called "forget". What does this function do? See this website: Forget function in Rust Remember you will need to run two terminal sessions to test the memory allocations.

    The last parts of this project look at duplicate pointer records a little closer. Let's start with C. Enter the following:

    nano duppoint.c
    
    Enter this code in the text editor:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
       int * ptr1;
       ptr1 = (int*)malloc(sizeof(int));
       int * ptr2 = ptr1;
    
       *ptr1 = 1;
       printf("ptr1 points to address %p and contains %d\n", ptr1, *ptr1);
       printf("ptr2 points to address %p and contains %d\n\n", ptr2, *ptr2);
    
       printf("Changing *ptr1 to 2.\n");
       *ptr1 = 2;
       printf("ptr1 points to address %p and contains %d\n", ptr1, *ptr1);
       printf("ptr2 points to address %p and contains %d\n", ptr2, *ptr2);
    }
    

    Compile it and run it using gcc -o duppoint duppoint.c and ./duppoint. As you can see below, pointer 1 (ptr1) changes pointer 2(ptr2) which can be confusing when making memory allocations.

    So how do we fix this? Rust has two functions that can be of help. We can use Copy and Ownership to address these. Lets begin with copy.

    nano copy.rs
    
    Enter this code:
    fn main() {
        let a = 1;
        println!("a is stored at address {:p} and contains {}", &a, a);
        
        let b = a;
        println!("b is stored at address {:p} and contains {}", &b, b);
        println!("a is stored at address {:p} and contains {}\n", &a, a);
        println!("Changing b to 2\n");
    
        let b = 2;
        println!("b is stored at address {:p} and contains {}", &b, b);
        println!("a is stored at address {:p} and contains {}", &a, a);
    }
    

    Compile it and run it using rustc copy.rs and ./copy. Our code from above takes the newly created object 'b' with a new address in memory and copies the contents of 'a' into it.

    Read the following webpage: Rust Copy and provide a few details on how copy works. Use additional resources to discuss rust primitive types.

    Let's look at Ownership now. Ownership is used when more serious resources are needed.

    nano own.rs
    
    Enter this code:
    fn main() {
        let a = Box::from(1i8);
        println!("a is stored at address {:p} and contains {}", &a, a);
        
        let b = a;
        println!("b is stored at address {:p} and contains {}", &b, b);
        println!("a is stored at address {:p} and contains {}", &a, a);
    }
    

    Compile it and run it using rustc own.rs and ./own. What happens with our file?

    Take a screenshot of your program running. Does it work or does it produce errors? Ownership Rules read from that link about ownership. Explain in a few sentences the outcome of your program above. Be sure to include references from your materials above. What could be done differently to make this work?

    This concludes Rust.


    New Project created in April 2022

    References
    Stack vs Heap Memory Allocation
    Overview of Memory Management in Rust
    Rust Docs: Using Boxes
    References from above to added at a later date