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:

  1. Assignment = instead of equality comparison == (line 28)
  2. Excess argument to printf (line 23)
  3. File descriptor leak (after line 26)
  4. Forgotten braces in multi-line if (line 22)
  5. Forgotten break in a switch statement (line 32)
  6. Forgotten NUL-termination of the buf string, leading to a buffer overflow (line 29)
  7. Memory leak by not freeing the malloc-allocated buffer (line 21)
  8. Out-of-bounds access (line 17)
  9. Unchecked cases in the switch statement (line 11)
  10. Unchecked return values of stat and fopen (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:

How is Rust any better here?
Safe Rust makes all of these bugs impossible:

  1. Assignments inside an if clause are not supported.
  2. Format strings are checked at compile-time.
  3. Resources are freed at the end of scope via the Drop trait.
  4. All if clauses require braces.
  5. match (as the Rust equivalent to switch) does not fall-through, hence you can’t accidentally forget a break.
  6. Buffer slices carry their size and don’t rely on a NUL terminator.
  7. Heap-allocated memory is freed via the Drop trait when the corresponding Box leaves the scope.
  8. Out-of-bounds accesses cause a panic or can be checked via the get method of a slice.
  9. match mandates that all cases are handled.
  10. 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].