Lab 12 - Memory Errors
From now on, we’ll assume that you start lab by connecting to the CS portal and that you are familiar with the command line environment. If you have not been practicing with the terminal, we strongly encourage reviewing Lab 1.
We will also assume that you ran the setup script from that lab and have all modules (including clang and git) by default.
In this lab, we’ll be playing Trivia! But it turns out that our trivia game has some interesting memory errors that need to be identified and corrected before we’ll be able to play an entire round.
Lab Goals
After this lab, you should:
- Gain increasing familiariarity with C
- Be able to identify and understand different memory errors
- Be able to use the address sanitizer in your code to help debug
Getting Started
Reviewing Possible Memory Errors
Before continuing with the lab, please review the Memory Overview reading. Pay close attention to the Detecting and avoiding bugs section and the common kinds of memory bugs. You will need to know these to complete this lab.
Getting the Trivia Code
In order to get started with Trivia, you’ll need to copy our code into your home directory. Use cd
to enter your cso1-code
directory, then issue the following command to copy our buggy trivia code.
cp /p/cso1/labs/trivia/trivia.* .
Now you have two files, a trivia.c
file containing implemented functions and a trivia.h
header file. Take a few minutes to review these files in a text editor before continuing.
A First Bug
Let’s walk through the first memory bug together. Compile your code using clang
and adding debugging information:
clang -g trivia.c
Notice that it gives us a warning. What could that mean? We really should heed this warning, but let’s ignore it for now and see what happens.
Run ./a.out
. Our program gives you the following greeting and then calls a function, requestName()
to prompt the user for the name.
Hello and welcome to CSO1 Lab 12 Trivia!
We should have developed this better, but now it's up to you
to fix our memory errors!
Time to get started!
Review the main()
and requestName()
functions. Note that the requestName()
function instantiates a local array of 100 char
s named name
. The compiler will allocate this space on the stack since it is declared as a local variable to requestName
. This function then calls fgets
to read the user’s input into the name
array and then removes any new line characters (\n
). It then returns a pointer to the name
array.
Once you’ve entered your name, our main
function will then greet you by name (using the name
that requestName
returned) and let you know that it’ll be reading a total of 30 possible trivia questions from a category of your choice.
Please enter your name:
Ashley
Hello ! We'll now read 30 questions from a category of your choice!
Wait! What happened? The greeting says “Hello !” and not “Hello Ashley!”
It turns out that requestName
returned a pointer to memory in the stack and then returned. We know that when a function returns, we pop the return address off the stack and jump back to the code of the function that called us. That also means that we’ve “lost” our stack frame, which is the section of memory that our local function has access to (i.e., the part of memory below the return address at the top of the stack). When another function is called, which happens in main
right after the call to requestName
returns, that section of memory is possible overwritten by the new function call.
The pointer name
that is returned from requestName
is called an escaping pointer. We should take care to never have escaping pointers, so let’s fix it.
This warning told us that we’ve created a pointer to something on the stack, which is associated with local variables for our functions. (Remember when we compiled with the
-O0
flag and saw that the compiler modified%rsp
to store our variables?)trivia.c:21:12: warning: address of stack memory associated with local variable 'name' returned [-Wreturn-stack-address] return name; ^~~~ 1 warning generated.
Fixing the bug
There are multiple ways to fix each of the bugs in our program. In this instance, let’s move the name
buffer to the main()
function. Copy the declaration of name
into main
just before the function call:
char name[100];
Now, let’s update the function requestName
to take a pointer to the buffer as a parameter:
void requestName(char *name) {
Note: be sure to remove the declaration of name
from the body of the requestName
function, update the call to requestName
in main
, and update the trivia.h
function heading.
Re-compile your code, noticing that our warning has now gone. When you run the trivia game again, it should correctly show the user’s name:
Hello and welcome to CSO1 Lab 12 Trivia!
We should have developed this better, but now it's up to you
to fix our memory errors!
Time to get started!
Please enter your name:
Ashley
Hello Ashley! We'll now read 30 questions from a category of your choice!
Your Task
Now that we’ve covered one memory error in our trivia game, it’s your turn. For this lab, you need to find the other memory bugs in our trivia game. For each bug, you must:
- Locate the bug (note down the file, function, and line number)
- Determine what kind of bug it was (see the Memory reading for the full list)
- Fix the bug in a way that allows the trivia game to proceed (note down how you fixed it)
Once you’ve found and fixed all memory bugs, play trivia! Note: not all bugs will be fixed when trivia works! Double check that there are no memory leaks!
Hints
The address sanitizer will be incredibly helpful in finding the memory issues in our trivia game. You are encouraged to compile your code with the address sanitizer turned on, using the -fsanitize=address
compile flag.
Consider the following short program, which has a memory error.
int main () { char buffer[5]; for (int i = 0; i < 10; i++) buffer[i] = 'a'; }
If we compiled this program with the address sanitizer (
-fsanitize=address
), then when we run the program, the address sanitizer will find the error. It halts the program and print the following error message:================================================================= ==16485==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee81af385 at pc 0x0000004d8e7c bp 0x7ffee81af350 sp 0x7ffee81af348 WRITE of size 1 at 0x7ffee81af385 thread T0 #0 0x4d8e7b in main (/cso1-code/a.out+0x4d8e7b) #1 0x7f7a5bce9554 in __libc_start_main /usr/src/debug/glibc-2.17-c758a686/csu/../csu/libc-start.c:266 #2 0x41ba99 in _start (/cso1-code/a.out+0x41ba99) Address 0x7ffee81af385 is located in stack of thread T0 at offset 37 in frame #0 0x4d8d5f in main (/cso1-code/a.out+0x4d8d5f) This frame has 1 object(s): [32, 37) 'buffer' <== Memory access at offset 37 overflows this variable HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork (longjmp and C++ exceptions *are* supported) SUMMARY: AddressSanitizer: stack-buffer-overflow (/cso1-code/a.out+0x4d8e7b) in main Shadow bytes around the buggy address: 0x10005d02de20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de60: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1 =>0x10005d02de70:[05]f3 f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02de90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02dea0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02deb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x10005d02dec0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==16485==ABORTING
The first line of the message tells us what kind of error occurred: a stack buffer overflow.
==16485==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffee81af385 at pc 0x0000004d8e7c bp 0x7ffee81af350 sp 0x7ffee81af348
The next few lines show a “stacktrace” of where the bug occurred: in our main function.
WRITE of size 1 at 0x7ffee81af385 thread T0 #0 0x4d8e7b in main (/cso1-code/a.out+0x4d8e7b) #1 0x7f7a5bce9554 in __libc_start_main /usr/src/debug/glibc-2.17-c758a686/csu/../csu/libc-start.c:266 #2 0x41ba99 in _start (/cso1-code/a.out+0x41ba99)
Then, a few lines down, it tells us what happened with the stack buffer overflow: the variable
buffer
was overflowed. That is, we tried to write 10char
s to an array of size 5.This frame has 1 object(s): [32, 37) 'buffer' <== Memory access at offset 37 overflows this variable
AddressSanitizer also detects memory leaks, where memory is allocated but not freed. It reports memory leaks with output like:
==23580==ERROR: LeakSanitizer: detected memory leaks Direct leak of 24 byte(s) in 1 object(s) allocated from: #0 0x7f6499eb4602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602) #1 0x400e65 in ll_new /p/cso1/example.c:16 #2 0x4012de in ll_copy_list /p/cso1/example.c:75 #3 0x401b84 in replace_add_test /p/cso1/example-test.c:81 #4 0x402353 in main /p/cso1/example-test.c:138 #5 0x7f6499a7382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) ....
Each stack trace labelled a “leak of (some number) byte(s)” indicates a place where memory was allocated. To fix these memory leaks, you must ensure the memory is freed somewhere, probably not where it was allocated.
Check-off with a TA
To check-off this lab, show a TA
- Your working trivia game, playing a round with 5 questions.
- Five (5) memory errors you found, including:
- What kind of memory error it was (i.e., heap buffer overflow, use after free, escaping pointer, etc)
- Where the error was originally
- How you fixed it