In Tutorial 2, we used printf and GDB to find bugs.
This time, we’ll get the program itself to tell us where something went wrong :)
By the end of this tutorial, you should be able to:
Compile and run a program with AddressSanitizer and UndefinedBehaviorSanitizer
Read their error messages and find the relevant line in your code
Fix relevant bugs and check that the program still does what it should
Volumes and PEs Now Run With Sanitisers
Note that from now on, we’re enabling sanitisers in Volumes and PEs,
so you should learn how to use them!
The supplied Makefiles add the sanitiser flags automatically.
make compiles with both ASan and UBSan, including debug information,
and stops the program when UBSan detects an error.
You do not need a separate clang command or extra flags.
After editing, run make again to rebuild.
Introduction
Your program compiles without a warning.
You run it, and it prints a number.
So far, so good.
Unfortunately, it got that number by reading past the end of an array :(
C doesn’t normally check array indices for you,
so you might get a crash, a strange answer, or an answer that looks perfectly reasonable.
The last one is particularly unhelpful.
You could add printfs to see how the index changes,
or step through the loop in GDB.
Both work, but first you have to know which loop to look at.
What if the program is a few hundred lines long?
That’s where sanitisers come in.
They add checks to your program when you compile it,
then report problems they detect while it runs.
The report gives you a place to start: a file, a line, and what went wrong.
We will practise reading that report before opening the source.
Getting Started
Go to https://code.cs1010.org and start T5: Sanitisers.
The practice program is already in ~/tutorial.
Open a terminal and change into the practice folder:
cd ~/tutorial
The file is workshop.c. Leave it closed for now;
we’ll use the sanitiser report to decide where to look
The program for this tutorial manages a campus workshop.
It prints visitor numbers, bookings, stock levels, and a few other things.
It compiles without a warning,
but it has three bugs.
The program is over two hundred lines long.
You could read all of it and try to spot the mistakes,
but let’s see how much of that work the tools can do for us.
The program asks you to choose a report: a, b, or c.
It uses getchar() to read a single character,
so a letter followed by Enter is all it needs.
We will try each report in turn.
Switching the Tools On
We will use two sanitisers built into the Clang toolchain.
Both add checks at compile time, so we need to rebuild the program to use them.
AddressSanitizer
AddressSanitizer, or ASan, checks for invalid memory accesses,
including reading or writing outside an array.
It’s built into the compiler toolchain;
we just have to switch it on with -fsanitize=address.
Remember -g from GDB?
It’s useful here for the same reason: we want to see our source lines.
Reading an ASan Report
That’s quite a lot of output for one mistake.
Here’s a shortened example of an ASan report.
The file and line numbers are just examples;
yours will refer to workshop.c.
ERROR: AddressSanitizer: stack-buffer-overflow on address ...READ of size 4 at ... #0 ... in add_scores example.c:83:18 #1 ... in print_report example.c:117:5 #2 ... in main example.c:149:5...SUMMARY: AddressSanitizer: stack-buffer-overflow example.c:83:18 in add_scores
Read it in this order.
stack-buffer-overflow: an access went outside a region of stack storage, such as a local array.
Despite the name, the operation can be a read or a write
READ of size 4: the program tried to read four bytes.
A bad write would say WRITE instead
example.c:83:18: file example.c, line 83, column 18.
This is where the invalid access happened
#0, #1, #2: the call stack.
main called print_report, which called add_scores, where the problem was detected
Some reports start inside a library function.
The useful starting point is the first stack frame that names your source file.
The stack frames below it tell you how you got there.
Further down, ASan can tell you which local array was involved.
There’s also a table of “shadow bytes” and plenty of hexadecimal addresses.
We can leave those alone for now.
Note
A stack-buffer-overflow is different from running out of stack space through too much recursion.
Here, we’re interested in the bounds of an array stored on the stack.
Finding the Line
Now we have a place to look.
ASan reports where the invalid access happened,
but the cause of the error can be somewhere else,
such as the code that decides which elements get accessed.
In Vim, :83 followed by Enter takes you straight to line 83.
In nano, you can use Ctrl-/.
Finding the Bug
Take note of the following things before going to your source file:
The type of error, and whether it was a read or a write
The first line number in workshop.c
The function containing that line, and the function that called it
The name of the array that ASan complains about
Now open the source file, go to the line,
and find the portion of code that causes the invalid access.
(Note: sometimes the cause of the error is not the place where the access went wrong.)
Squashing the Bug
In this case, the loop decides which elements get accessed.
The line doing the addition is just following instructions.
Fix the loop so it adds every valid element exactly once.
Then compile again with the same ASan command and run choice a
The program should print Kits remaining: 267, without an ASan error
Tip
Don’t forget to recompile after editing!
Otherwise, you’re still running the old code.
ASan normally stops at the first error it detects.
Once you’ve fixed that one, another might appear, but that’s progress :)
You now have a new place to look.
UndefinedBehaviorSanitizer
An array access isn’t the only way to get into trouble.
You can also add two perfectly valid int values
and get a result too large for an int.
That is signed integer overflow, which is undefined behaviour in C.
Integer division by zero is another example.
UndefinedBehaviorSanitizer, or UBSan, can detect these operations.
It catches several kinds of undefined behaviour, but not all of them.
We switch it on with -fsanitize=undefined.
The extra -fno-sanitize-recover=undefined tells it to stop when it finds an error.
Without that option, many UBSan checks print a complaint and let the program carry on.
We’d rather fix the problem first.
The workshop’s credit counter is being tested close to INT_MAX,
the largest value an int can hold on this system.
It’s defined in <limits.h>.
A UBSan report for an overflowing addition looks something like this:
example.c:61:20: runtime error: signed integer overflow: 2147483640 + 30 cannot be represented in type 'int'
This one’s much shorter.
The file and line come first,
then the operation and the values that caused the problem.
It even tells you which type couldn’t hold the result.
The exact limits depend on the implementation.
Finding the Bug
Take note of the following things before going to your source file:
The line number of the failing expression
The two values the program is adding
Their sum on paper
Why both values fit in an int, but their sum cannot
Now open the source file, go to the line,
and find the addition that overflows
Squashing the Bug
An incoming amount which exceeds the credit limit should be rejected.
The program should print Credit limit exceeded without doing the addition.
For this program, both the balance and the incoming amount are nonnegative.
Change print_credits to check whether the incoming amount will fit
before calling projected_credits.
How could you check this without calculating the sum that caused the error?
Hint
Work out how much room is left: INT_MAX - balance.
If incoming is greater than that, the addition would overflow.
Checking whether the sum became negative afterwards is too late;
the undefined behaviour has already happened.
Then compile again with the same UBSan command and run choice b:
With incoming still set to 30, it should print Credit limit exceeded
Change incoming to 12, then to 0, recompiling and testing each time.
Both should give a valid balance without a UBSan error
Restore incoming to 30
Changing the counter to unsigned would allow it to wrap around,
but a very large balance turning into a small one is still a fairly bad credit counter.
A quiet sanitiser is only useful if the balance is still correct.
One More
Finding the Bug
Run the UBSan executable again and choose c.
Find the operation and line in the report,
then find the source of the bug
Why does the error happen?
Squashing the Bug (Complete the Previous Exercise First)
Fix print_waiting_times so that with zero completed sessions,
it prints No completed sessions without calling average_wait.
To test your fix:
Recompile and check that choice c prints No completed sessions
Temporarily set completed = 3 and total_minutes = 18.
Recompile and check that the average is 6
Restore both values to zero and recompile
Using Both Together
You don’t have to choose between ASan and UBSan.
You can enable both with -fsanitize=address,undefined.
Compile your repaired program with both sanitisers:
Run it three times, choosing a, b, and c.
Check that each one finishes without an error report,
and still prints the answer or message you expect
Some of their checks overlap.
For example, UBSan can also catch certain invalid array indices,
so enabling both might change which tool complains first.
Either way, you have a source line to investigate.
You run choice a, and neither sanitiser complains.
Does that tell you anything about the credit counter?
Suppose you “fix” the stock calculation by making the loop skip its last valid element.
Would you expect ASan to complain?
Sanitisers give you another way to find bugs.
You still need tests, expected answers, and sometimes GDB.
They also don’t generally detect reads of uninitialised values.
For now, the useful habit is simple:
read the error, jump to the line, work out why it happened, then fix and rerun.
Which Tool Catches What?
Problem
Tool to Try
Access outside a local array
ASan; UBSan can also catch some array-index errors
Signed integer overflow
UBSan
Integer division by zero
UBSan
Read of an uninitialised value
Neither of these reliably detects it
Wrong answer from valid operations
Check the expected output
Why don’t we compile every program with sanitisers enabled all the time?
Graded Task(s)
Bob wrote a program for his upcoming games night
to add up the scores and print the average.
Then he added a results table, a score distribution,
and a banner that takes up most of the terminal.
The program is now about two hundred lines long.
It compiles, so Bob declares it ready,
but sometimes (read: most of the time) the numbers come out wrong.
The banner works perfectly though, because Bob has his priorities straight.
He decides to debug it by staring at the screen.
When that fails, he calls a friend, who calls another,
and soon five friends are staring with him.
Will you be the seventh (five friends + Bob + you),
or will you let the sanitisers do the looking?
Enter the Graded Task Folder
Go to ~/tutorial-graded before starting.
It contains student.c, a Makefile, and public test inputs with expected output.
The program reads a count, followed by that many scores.
There are between 0 and 1,000 scores
Each score is a nonnegative int
The report should include every score exactly once.
The TOTALS section prints the total and the integer average:
Case
Total
Average
Example
No scores
0
0
empty.in
Total would exceed INT_MAX
INT_MAX
total / count
overflow.in
Otherwise
scores[0] + ... + scores[count - 1]
total / count
ordinary.in
Everything else, including the other sections, separators, and banner, stays unchanged.
To see an example in full, open its .in and .out files in test/test_cases/.
Help Bob fix student.c
Run make test, and use the reports to find the relevant source lines
Fix the causes, keeping the results table, score distribution, and Bob’s precious banner
Run make test again until every case passes
Run verify . in ~/tutorial-graded to complete the task:
abc@tut5:~/tutorial-graded$ verify .
Before You Leave: Verify Your Progress
Run this command inside your Tutorial environment:
progress
It shows which tasks have been recorded as complete.
This command is available inside Tutorial environments, not the general CS1010 WebTop.