Tutorial Goals

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:

  1. Compile and run a program with AddressSanitizer and UndefinedBehaviorSanitizer
  2. Read their error messages and find the relevant line in your code
  3. 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.

Compile workshop.c with ASan enabled:

clang -g -fsanitize=address workshop.c -o workshop-asan
./workshop-asan

Enter a, then read what appears in the terminal

Tip

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.

  1. 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
  2. READ of size 4: the program tried to read four bytes. A bad write would say WRITE instead
  3. example.c:83:18: file example.c, line 83, column 18. This is where the invalid access happened
  4. #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:

  1. The type of error, and whether it was a read or a write
  2. The first line number in workshop.c
  3. The function containing that line, and the function that called it
  4. 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.

Compile workshop.c with UBSan enabled:

clang -g -fsanitize=undefined -fno-sanitize-recover=undefined workshop.c -o workshop-ubsan
./workshop-ubsan

Enter b, then read what appears in the terminal

Reading a UBSan Report

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:

  1. The line number of the failing expression
  2. The two values the program is adding
  3. Their sum on paper
  4. 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?

Then compile again with the same UBSan command and run choice b:

  1. With incoming still set to 30, it should print Credit limit exceeded
  2. Change incoming to 12, then to 0, recompiling and testing each time. Both should give a valid balance without a UBSan error
  3. 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?

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:

clang -g -fsanitize=address,undefined -fno-sanitize-recover=undefined workshop.c -o workshop-check
./workshop-check

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?

ProblemTool to Try
Access outside a local arrayASan; UBSan can also catch some array-index errors
Signed integer overflowUBSan
Integer division by zeroUBSan
Read of an uninitialised valueNeither of these reliably detects it
Wrong answer from valid operationsCheck 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:

CaseTotalAverageExample
No scores00empty.in
Total would exceed INT_MAXINT_MAXtotal / countoverflow.in
Otherwisescores[0] + ... + scores[count - 1]total / countordinary.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

  1. Run make test, and use the reports to find the relevant source lines

  2. Fix the causes, keeping the results table, score distribution, and Bob’s precious banner

  3. Run make test again until every case passes

  4. 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.

Further Reading

The Clang documentation explains the options and supported checks: AddressSanitizer and UndefinedBehaviorSanitizer.