An Example in C
Let’s consider the following “minimum wrong example” program in C:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char* argv[]) {
char *buf, *filename;
FILE *fp;
size_t bytes, len;
struct stat st;
switch (argc) {
case 1:
printf("Too few arguments!\n");
return 1;
case 2:
filename = argv[argc];
stat(filename, &st);
len = st.st_size;
buf = (char*)malloc(len);
if (!buf)
printf("malloc failed!\n", len);
return 1;
fp = fopen(filename, "rb");
bytes = fread(buf, 1, len, fp);
if (bytes = st.st_size)
printf("%s", buf);
else
printf("fread failed!\n");
case 3:
printf("Too many arguments!\n");
return 1;
}
return 0;
}
How many bugs do you spot?
Despite just 29 lines of code, this C example contains serious bugs in at least 11:
- Assignment
=
instead of equality comparison==
(line 28) - Excess argument to
printf
(line 23) - File descriptor leak (after line 26)
- Forgotten braces in multi-line
if
(line 22) - Forgotten
break
in aswitch
statement (line 32) - Forgotten NUL-termination of the
buf
string, leading to a buffer overflow (line 29) - Memory leak by not freeing the
malloc
-allocated buffer (line 21) - Out-of-bounds access (line 17)
- Unchecked cases in the
switch
statement (line 11) - Unchecked return values of
stat
andfopen
(lines 18 and 26)
Shouldn’t these bugs be obvious even for a C compiler?
No, surprisingly this code compiles warning-free at the default warning level, even in the latest GCC version (13.2 as of writing).
Isn’t this a highly unrealistic example?
Absolutely not, these kind of bugs have lead to serious security vulnerabilities in the past. Some examples:
- Assignment
=
instead of equality comparison==
: The Linux Backdoor Attempt of 2003 - Forgotten braces in multi-line
if
: The Apple goto fail vulnerability - Forgotten
break
in aswitch
statement: The break that broke sudo
How is Rust any better here?
Safe Rust makes all of these bugs impossible:
- Assignments inside an
if
clause are not supported. - Format strings are checked at compile-time.
- Resources are freed at the end of scope via the
Drop
trait. - All
if
clauses require braces. match
(as the Rust equivalent toswitch
) does not fall-through, hence you can’t accidentally forget abreak
.- Buffer slices carry their size and don’t rely on a NUL terminator.
- Heap-allocated memory is freed via the
Drop
trait when the correspondingBox
leaves the scope. - Out-of-bounds accesses cause a panic or can be checked via the
get
method of a slice. match
mandates that all cases are handled.- Fallible Rust functions return
Result
values that need to be unwrapped and thereby checked for success. Additionally, the compiler emits a warning if you miss to check the return value of a function marked with#[must_use]
.