Introduction
This website contains teaching material for the laboratory sessions of the course Safe System Programming (in Rust) (SSP-RS), at Télécom Paris / Polytechnique Institute of Paris.
ⓒ 2022-2026 Samuel Tardieu and Stefano Zacchiroli
Lab 01 — Unsafe System Programming
In the first lab session of the course, you will mainly work on:
- Setting up your development environment for the rest of the course, and
- Experimenting with unsafe system programming, leading to buffer overflows and other nasty stuff.
Let’s get to it.
Development setup
Throughout the lab sessions of this course, you are going to use a number of tools, mostly related to development in the C and Rust programming languages. As the first task of this lab session, you are hence going to install the relevant toolchains and a comfortable (for you) development environment.
Operating system requirements: we are going to assume that your work machine runs Linux as its operating system (OS). If that is not the case, you should fix this before proceeding. Any Linux-based distribution will do, but in the following, we are going to rely on package names from Debian and Debian-based distributions (e.g., Ubuntu); you should translate them to your distro if need be.
Work environment
You have various options in terms of which “machine” to work on:
- work on your real (hardware) machine: laptop or desktop computer;
- work in a Docker container;
- work in a virtual machine.
Option 1: Work on your machine
We recommend this option as it is the most flexible, but it requires more manual setup on your side—setup which we are not going to detail here as it heavily depends on your OS choice.
We only list below the tools (and corresponding Debian package names) that you should install on your machine to be able to complete the exercises of this course.
Option 2: Work in a Docker container
If you would like to work in a Docker container, we provide a Dockerfile that you can use to locally create a Docker image that contains all the tools used in the lab sessions. Download the Dockerfile locally and build a matching image like this:
$ docker build -t ssprs - < Dockerfile
(It will take a few minutes to complete, depending mostly on download and I/O speeds.)
Then, create a shell script named run.sh to easily create and run a fresh container on that image every time you need it.
It can be something like the following:
#!/bin/bash
host_labdir="${HOME}/ssprs-lab"
cont_labdir="/home/ssprs/lab"
docker run -ti \
--net host \
-e DISPLAY="unix${DISPLAY}" \
-v /dev/shm:/dev/shm \
-v /etc/hosts:/etc/hosts \
-v /etc/localtime:/etc/localtime:ro \
-v /tmp/.X11-unix:/tmp/.X11-unix \
-v /var/run/dbus/system_bus_socket:/var/run/dbus/system_bus_socket \
-v $HOME/.Xauthority:/home/ssprs/.Xauthority \
-v "${host_labdir}:${cont_labdir}" \
--rm \
--name ssprs \
ssprs
Note that host_labdir should point to a directory on your real machine, which will be mounted within the container so that you can share files between the two worlds.
All your lab session material will need to be in this directory if you want to access it from within the container.
You can then enter and use a fresh container every time you need it like this:
$ ./run.sh
ssprs@machine:~$ rustc --version
rustc 1.98.1 (48a229cea 2026-09-01)
Also, if you create a file in your home directory under ~/ssprs-lab, you can access it from within the container, e.g.:
# outside the container
$ echo "// hello.c" > ~/ssprs-lab/hello.c
$ ./run.sh
ssprs@machine:~$ cat ~/lab/hello.c
// hello.c
ssprs@machine:~$
Option 3: Work in a virtual machine
If you prefer to fully control your work environment (as with option 1), but without impacting your day-to-day work machine, you have the option to install a virtual machine and configure it manually. Just install in it the same tools that you would have installed on your real machine, following the instructions below. We are not going to detail this step further, as it heavily depends on your choice of virtualization technology.
C development toolchain
If you have chosen to work on your (virtual) machine, you should install the following tools:
| Tool | Debian package name |
|---|---|
| bats | bats |
| check | check |
| clang-tidy | clang-tidy |
| clang | clang |
| gcc | gcc |
| gdb | gdb |
| ltrace | ltrace |
| make | make |
| strace | strace |
| valgrind | valgrind |
If you have chosen the Docker option, they are all already installed in the container.
Exercise 0.a: Install all the above dependencies in the work environment of your choice.
When ready, make sure you can compile and run the following Hello World program with both gcc and clang:
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
} // end of hello.c
like this:
$ gcc -Wall hello.c
$ ./a.out
Hello, World!
$ clang -Wall hello.c
$ ./a.out
Hello, World!
Now make sure that all of the following commands execute properly without failing on your setup (the exact output might vary depending on version, configuration, etc.; that’s OK):
$ bats -v
Bats 1.8.2
$ clang-tidy --version
Debian LLVM version 19.1.7
Optimized build.
$ gdb --version
GNU gdb (Debian 16.3-1) 16.3
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ ltrace --version
ltrace 0.7.91
Copyright (C) 2010-2013 Petr Machata, Red Hat Inc.
Copyright (C) 1997-2009 Juan Cespedes <cespedes@debian.org>.
License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ make --version
GNU Make 4.4.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ strace --version
strace -- version 6.13
Copyright (c) 1991-2025 The strace developers <https://strace.io>.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Optional features enabled: stack-trace=libunwind stack-demangle m32-mpers mx32-mpers
$ valgrind --version
valgrind-3.24.0
Rust development toolchain
If you have chosen to work on your (virtual) machine, you should install the following tools:
| Tool | Debian package |
|---|---|
| rustup | none (install by hand) |
If you have chosen the Docker option, they are all already installed in the container.
Exercise 0.b: Install all the above dependencies in the work environment of your choice.
Once installed, verify that you can successfully run the following basic tools of the Rust toolchain, like this (your output might vary depending on the installed versions):
$ rustup --version
rustup 1.29.1 (d95a37b6a 2026-08-13)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: the currently active `rustc` version is `rustc 1.98.1 (48a229cea 2026-09-01)`
$ cargo --version
cargo 1.98.1 (797e8a9bc 2026-08-05)
$ rustc --version
rustc 1.98.1 (48a229cea 2026-09-01)
Editor / IDE
You are also going to need a development editor or an IDE (Integrated Development Environment). As developers tend to be opinionated about this stuff, we are not going to force you to use any specific one: pick your own!
We recommend either a flexible editor with IDE-like features (e.g., Emacs or Vim with LSP support for Rust) or a full-featured IDE like Visual Studio Code or NetBeans (do make sure it has support for Rust, though, as that will be needed later in the semester).
Exercise 0.c: Install the editor or IDE you want in the work environment of your choice.
When done, open the hello.c file you used previously and make sure you can comfortably edit/compile/run it, ideally from within the editor/IDE itself.
Done?
Good! You are now all set with the developer setup!
Let’s now move to the other matters of the day.
Memory unsafety
Out-of-bounds write
Exercise 1.a: write C programs (new, different from those seen in lecture 01) that exhibit weakness CWE 787: Out-of-bounds write. Your goal is to make your program segfault due to this CWE! Try out-of-bounds writes in various memory segments, writing one program for each of the following cases:
- out-of-bounds write in dynamically allocated memory on the heap (e.g., with
malloc), - out-of-bounds write in statically allocated memory (i.e., global variables, initialized or not),
- out-of-bounds write in automatic memory (i.e., variables allocated on the stack).
Bonus point: make sure your programs do not emit any warnings when compiled with the -Wall flag (which enables most compile-time warnings, and which you should always use anyway!).
Try to determine experimentally what each program was doing just before the segfault (tip: try with gdb, nm, ltrace, strace).
What helped you determine this?
Exercise 1.b: consider the following local variable declarations at the beginning of a function (possibly main):
char s1[16];
char s2[16];
Write a program that does not segfault but that, with a single memory (or string) copy operation writing to only one of these variables (s1 or s2), modifies the content of both variables, with data of your choice.
Verify the result experimentally (e.g., with printf or strcmp).
Warning: do not forget about string \0 terminators!
Out-of-bounds read
Exercise 1.c: building on the idea of the previous exercise, write a program that exhibits at least one variant of CWE 125: Out-of-bounds read. The program should not segfault, but by performing a memory read operation on a given variable, it should be able to read the content of another variable, correctly and in full.
Bonus point: make your program read the content of an out-of-scope local variable, e.g., one declared in a calling function.
Smashing the stack
Segmentation fault
Consider the following program (example1.c):
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}
int main() {
function(1, 2, 3);
return 0;
}
Exercise 2.a: compile the program to assembly language like this:
$ gcc -S -o example1.s example1.c
and identify where function is called in the generated assembly code.
How are arguments passed to the function?
(The answer might vary depending on the compiler and architecture, but the assembly code will tell you!)
Exercise 2.b: compile, execute, and debug the following program:
#include <string.h>
void function(char *str) {
char buffer[16];
strcpy(buffer, str);
}
int main() {
char large_string[256];
int i;
for(i = 0; i < 255; i++) {
large_string[i] = 'A';
}
function(large_string);
return 0;
}
Explain what’s happening and why.
Subverting the control flow
Historically (on 32-bit architectures and with fewer built-in defenses in C compilers), subverting the control flow via stack overflows was pretty easy. You can get an idea of how easy it was by reading the famous tutorial article Smashing The Stack For Fun And Profit by Aleph One (1996).
Nowadays it has become a little more complicated, but it is still very doable (and regularly done, as we have seen in the course examples!).
Exercise 2.c: go through the modern incarnation of Aleph One’s tutorial, namely: Smashing the Stack in the 21st Century by Jon Gjengset (archived copy), reproducing the reported experiments on your machine. (Next week, we will go through some of the defenses that you will have to manually disable to exploit a stack overflow, and discuss what they are and are not good for.)
Lab 02 — Hardening System Programming
In this lab session, we will take a practical tour of existing techniques to secure system programs implemented in traditional system programming languages. We will experiment with testing, fuzzing, code instrumentation (of both binary and source code), and static analysis.
By the end of the session, you will have a better understanding of the state of the art of hardening system programming, including its limitations.
Credits
The exercises of this lab session contain material and ideas reused with permission from the week 1 assignments of Stanford’s course CS 110L (2022) by Ryan Eberhardt, Armin Namavari, Will Crichton, Julio Ballista, and Thea Rossman.
Unit testing
Setup
Consider the initial C implementation of a simple currency type (a pair of a currency name and a currency amount) given in money.h and money.c.
We want to reassure ourselves that the code thus far is correct, using unit testing.
In check_money_tiny.c you can find the skeleton of a C program that uses money.c as a library and unit tests it with a single test case.
It uses the Check unit testing framework for C, which you should have already installed as part of the first lab session.
It can be built and run like this:
$ gcc -Wall -c -o money.o money.c
$ gcc -Wall -c -o check_money_tiny.o check_money_tiny.c
$ gcc -Wall -o check_money_tiny check_money_tiny.o money.o `pkg-config --libs check`
$ ./check_money_tiny
Running suite(s): Money
100%: Checks: 1, Failures: 0, Errors: 0
check_money_tiny.c:17:P:Core:test_money_create_amount:0: Passed
Exercise 1.a: read through the implementation of money.c and check_money_tiny.c to make sure you understand what the code does.
Help yourself with the Check documentation as needed.
Code coverage
Code coverage is a measure of how much of your implementation code is exercised during the execution of your test suite. It can be measured in various ways; we will focus on the simplest metric: how many lines of code are (not) executed while running the test suite (i.e., line coverage). You can verify the line coverage of your current test suite using gcov, which is part of gcc, as follows:
- compile/link your project passing the
-fprofile-arcs -ftest-coverageoptions to the compiler and linker, - execute your test suite (
./check_money_tinyin this case), - inspect code coverage of a source code file you care about, e.g.:
$ gcov money.c
File 'money.c'
Lines executed:75.00% of 16
Creating 'money.c.gcov'
Lines executed:75.00% of 16
The summary tells you the percentage of lines of code exercised during execution.
The mentioned file money.c.gcov is an annotated version of money.c that shows precisely which lines have been executed (or not).
Exercise 1.b: measure the code coverage of the current test suite for money.c.
Then, add all the test cases you need to maximize code coverage.
Can you reach 100% line coverage?
Why or why not?
Exercise 1.c: (optional and tricky, feel free to postpone this one to the end of the session) add a memory unsafety issue to the money.c implementation, and specifically one that crashes the execution of your test case.
For example, you can implement a new money_add function that performs an out-of-bounds read or write.
Then add a test that runs the buggy code, crashing the execution of the test case.
Does the test case crash cause the crash of the entire test suite? (i.e., do other test cases supposed to run after it keep running or not?)
Why or why not?
Stack overflow detection
Setup
Retrieve the program uppercase.c, compile it (recommended compiler options for this session are -g -O0 -Wall -Wextra), and run it, e.g., like this:
$ gcc -g -O0 -Wall -Wextra -o uppercase uppercase.c
$ ./uppercase 'Hello, World!'
HELLO, WORLD!
Now take some time to study the code of the program.
Exercise 2.a: the program is affected by a memory-related bug (EEEK!). Explain where the bug is, what its consequences might be, and how to fix it.
Static analysis with Clang-Tidy
Clang-Tidy (which you have installed already, as part of last week’s lab session) performs some simple static analysis checks, ranging from basic linting to more advanced techniques. From the tool documentation:
clang-tidy is a clang-based C++ “linter” tool. Its purpose is to provide an extensible framework for diagnosing and fixing typical programming errors, like style violations, interface misuse, or bugs that can be deduced via static analysis.
Exercise 2.b: go (briefly) through the documentation of clang-tidy and learn how to use it to analyze C source code files.
Then use clang-tidy on uppercase.c to check if it detects the bug you have identified in the previous exercise.
Does clang-tidy catch the problem?
Based on what you have learned in the last lecture, explain why clang-tidy is able to identify the problem, or why it isn’t.
Dynamic analysis with Valgrind
Clang-Tidy performs static analysis; Valgrind, on the other hand, performs dynamic analysis based on dynamic binary re-compilation, as we have seen in the last lecture. Let’s see how Valgrind fares in detecting the bug in this program. After compiling your program, you can give it a try like this:
$ valgrind [--tool=memcheck] ./uppercase 'Hello, World!'
(Note: --tool=memcheck is optional because Memcheck is the default tool that Valgrind uses unless otherwise requested.)
Exercise 2.c: does Valgrind detect the issue? Based on what you have learned in the last lecture, explain why or why not.
Exercise 2.d: benchmark the time required to execute the program with and without Valgrind.
(Tip: as a simple way to do that, you can use the standard UNIX time command.
If you want to be fancier, this is your chance to learn about the amazing benchmarking tool Hyperfine and use it for a more scientifically accurate comparison.)
Dynamic analysis with LLVM sanitizers
As we have seen in the last lecture, dynamic analysis can also be performed via source code instrumentation, which happens at compile time to add runtime checks. LLVM sanitizers (look for the various entries with “Sanitizer” in their name in the Clang documentation) are a set of plugins for the Clang compiler that do just that.
Exercise 2.e: switch the compiler you are using from gcc to clang and rebuild the program with it.
(You can pass the same options suggested above, as they are compatible between the two compilers.)
Then, add all of the following sanitizers (check the documentation for how to do that) and rebuild the program: AddressSanitizer, LeakSanitizer, UndefinedBehaviorSanitizer.
Compare the size of the executables built with and without sanitizers.
How different are they and why?
Exercise 2.f: run the instrumented program. Do sanitizers detect the issue? Based on what you have learned in the last lecture, explain why or why not.
Let’s streamline our build process, as it is becoming a bit of a mess.
Retrieve the generic Makefile we make available for building the programs associated with this lab session.
If you are not familiar with Makefiles, go (briefly) through the GNU make documentation to make sure you know the basics.
In particular, read through the introductory material and make sure you understand the parts of the Makefile syntax used in the given Makefile, notably: rules, variables, and static pattern rules.
(If you know about Makefiles already, feel free to skip this documentation study part.)
Exercise 2.g: change the retrieved Makefile so that:
- it only builds the
uppercase.cprogram (other programs mentioned in there will arrive later in this lab session!), and - it passes the sanitizer flags to the compiler when compiling.
Recheck the result of the previous exercise about sanitizers to make sure they are there when building with make.
Memory leak detection
Linked lists
Now retrieve the program linkedlist.c, add it to the Makefile, and compile it as before.
Take some time to study the program and understand what it does.
Exercise 3.a: this program is affected by (at least) two memory-related bugs this time, but of a different nature than before. Identify both of them, explain what their consequences might be, and how to fix them.
Exercise 3.b: try linting/static analysis with Clang-Tidy on this program, as you did before.
Does it detect the (right) issues? (Tip: beware of false positives!)
Exercise 3.c: try source code instrumentation with LLVM sanitizers on this program, as you did before.
Do they detect the issues?
In both cases, feel free to speculate about why the approaches used did or did not identify the memory issues affecting this program.
String parsing
You know the drill!
The next program is bracket-parser.c, which extracts a "substring [within brackets]" from a larger string.
Exercise 4.a: read the program, find the memory problem with it, try Clang-Tidy and LLVM sanitizers on it, and explain what you see.
Exercise 4.b: on most inputs the LLVM sanitizers will not detect a memory issue.
Based on your understanding of the bug and your reading of the code (= white-box inspection), can you craft a specific input for bracket-parser that will make the LLVM sanitizer spot the bug at runtime?
(And while we are at it: what does this tell you about the reliability of dynamic analysis for detecting memory issues?)
Fuzzing with LibFuzzer
Let’s now see if fuzzing can help us find automatically both the memory issue affecting bracket-parser and a test input that proves the issue is there.
Read the introductory documentation of LibFuzzer.
Make sure to understand how to add fuzz targets to your code, what the required API is, and the fact that you should not have a main function when using LibFuzzer (because it adds its own main).
Exercise 4.c: add a fuzz target to your code that allows you to fuzz the input of the parse function of bracket-parser.c.
Tip: you can take inspiration from the example seen in the lecture, but remember that in this case you should pass well-formed C strings (trailing \0!) to avoid triggering bugs other than the one you are looking for.
Exercise 4.d: build the fuzzer (adding fuzzer to the list of -fsanitize=... sanitizers) and run it.
Does it find the issue?
Does it produce a test case with a sample input that proves the issue is there?
Explain why.
(Pretty cool, huh?)