Common Pitfalls and Secure Coding
C pitfalls are like potholes on the road — they look harmless, but step in one and your program crashes. Learn to recognize these potholes so you can walk around them.
Operator Precedence Pitfalls
C has 15 levels of operator precedence, and some combinations are error-prone:
The Most Common Precedence Mistake
int *p = malloc(10 * sizeof(int));
if (p == NULL)
That's not an error, but this is:
int a = 1, b = 2, c = 3;
int result = a & b == 0;
== has higher precedence than &, so this is actually a & (b == 0), which is 1 & 0 = 0, not (a & b) == 0.
Precedence Quick Reference (Error-Prone Cases)
| Trap Expression | Actual Meaning | Intended Meaning | Fix |
|---|---|---|---|
a & b == 0 |
a & (b == 0) |
(a & b) == 0 |
Add parentheses |
a << 2 + 1 |
a << (2+1) |
(a<<2)+1 |
Add parentheses |
*p++ |
*(p++) |
(*p)++ |
Add parentheses |
a | b + c |
a | (b+c) |
(a|b)+c |
Add parentheses |
Array Out-of-Bounds
C doesn't check array indices — out-of-bounds access is undefined behavior. You might read garbage values, crash, or it might "happen to work" (which is the most dangerous outcome).
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100;
arr[-1] = 99;
Both lines write out of bounds. The compiler won't complain, but this could corrupt other variables or cause a crash.
Classic Out-of-Bounds: Off-by-One in Loops
int arr[5];
for (int i = 0; i <= 5; i++) {
arr[i] = 0;
}
i <= 5 should be i < 5. This off-by-one error causes arr[5] to be written.
String Out-of-Bounds
char buf[5];
strcpy(buf, "Hello, World!");
buf has only 5 bytes of space, but the string needs 14 bytes (including '\0') — a classic buffer overflow.
Dangling Pointers
A pointer that still references memory that has already been freed:
int *create_value(void) {
int x = 42;
return &x;
}
x is a local variable. After the function returns, the stack frame is released, making &x a dangling pointer. Accessing it may read garbage values.
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);
After free(p), the memory pointed to by p has been returned. Accessing it through p is undefined behavior.
free(p); p = NULL;. This way, if you accidentally use it later, you'll at least get a segfault rather than a hard-to-trace random error.
Memory Leaks
Allocated memory that is never freed:
void leak_example(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) return;
free(p);
}
When some_error is true, the function returns early, free(p) is skipped, and 100 ints' worth of memory leaks.
Correct Version
void no_leak(void) {
int *p = malloc(100 * sizeof(int));
if (p == NULL) return;
if (some_error) {
free(p);
return;
}
free(p);
}
Undefined Behavior
Undefined behavior (UB) is the most dangerous concept in C — the standard says "behavior is undefined," which means anything can happen.
Common Undefined Behaviors
| Behavior | Description |
|---|---|
| Array out-of-bounds | Index exceeds array bounds |
| Dereferencing a null pointer | *NULL |
| Using a dangling pointer | Accessing freed memory |
| Signed integer overflow | INT_MAX + 1 |
| Shifting beyond bit width | 1 << 100 |
| Modifying the same variable twice in one expression | i = i++ + 1 |
| Division by zero | int x = 1 / 0 |
| Reading an uninitialized variable | int x; printf("%d", x); |
Example
The trap of i = i++ + 1:
#include <stdio.h>
int main(void) {
int i = 3;
i = i++ + 1;
printf("%d\n", i);
return 0;
}
Different compilers may output 4, 5, or other values. Because i++ modifies i, and the assignment also modifies i, modifying the same variable twice in a single statement is undefined behavior.
Buffer Overflow Prevention
Buffer overflow is C's most famous security vulnerability. The 1988 Morris Worm exploited buffer overflow attacks.
Dangerous Functions vs. Safe Alternatives
| Dangerous Function | Problem | Safe Alternative |
|---|---|---|
gets(buf) |
No length limit | fgets(buf, size, stdin) |
strcpy(dst, src) |
No destination size check | strncpy(dst, src, size-1) |
sprintf(buf, fmt, ...) |
No destination size check | snprintf(buf, size, fmt, ...) |
strcat(dst, src) |
No remaining space check | strncat(dst, src, size-strlen(dst)-1) |
scanf("%s", buf) |
No input length limit | scanf("%99s", buf) or scanf_s |
Safe Functions in Detail
strncpy
char *strncpy(char *dest, const char *src, size_t n);
Copies at most n characters. If src has fewer than n characters, the rest of dest is filled with '\0'. But if src is exactly n characters long, dest will not automatically be null-terminated!
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[6];
strncpy(buf, "Hello World", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
printf("%s\n", buf);
return 0;
}
Hello
strncpy does not automatically add '\0'! You must manually add it after copying, or subsequent string operations may go out of bounds.
snprintf
int snprintf(char *str, size_t size, const char *format, ...);
Writes at most size-1 characters and automatically adds '\0'.
#include <stdio.h>
int main(void) {
char buf[10];
int n = snprintf(buf, sizeof(buf), "Value is %d", 12345);
printf("Output: \"%s\"\n", buf);
printf("Required length: %d, Actual space: %d\n", n, (int)sizeof(buf));
return 0;
}
Output: "Value is 1"
Required length: 14, Actual space: 10
Example
Safe input handling pattern:
#include <stdio.h>
#include <string.h>
int main(void) {
char name[32];
printf("Enter your name: ");
if (fgets(name, sizeof(name), stdin) == NULL) {
fprintf(stderr, "Input failed\n");
return 1;
}
name[strcspn(name, "\n")] = '\0';
if (strlen(name) == 0) {
fprintf(stderr, "Name cannot be empty\n");
return 1;
}
printf("Hello, %s!\n", name);
return 0;
}
fgets + strcspn to remove the newline is the standard safe pattern for handling user input. Never use gets.
Example
Safe string concatenation:
#include <stdio.h>
#include <string.h>
int main(void) {
char path[256] = "/home/user";
const char *subdir = "/documents/work/projects/2024";
size_t current_len = strlen(path);
size_t remaining = sizeof(path) - current_len - 1;
if (strlen(subdir) < remaining) {
strncat(path, subdir, remaining);
} else {
fprintf(stderr, "Path too long, cannot concatenate\n");
return 1;
}
printf("Path: %s\n", path);
return 0;
}
Path too long, cannot concatenate
Safe scanf Usage
scanf is the most commonly used input function for beginners, and also the most dangerous:
char buf[10];
scanf("%s", buf);
If the user enters more than 9 characters, it overflows. Safe version:
char buf[10];
scanf("%9s", buf);
Or use scanf_s on compilers that support C11:
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
scanf_s is part of C11's optional Annex K. MSVC supports it, but gcc/clang may not. For cross-platform code, the fgets + sscanf combination is recommended.
Integer Overflow
Signed integer overflow is undefined behavior:
#include <stdio.h>
#include <limits.h>
int main(void) {
int a = INT_MAX;
int b = a + 1;
printf("%d + 1 = %d\n", a, b);
return 0;
}
The output is not necessarily INT_MIN — the compiler may optimize away this addition.
Safe checking method:
#include <limits.h>
int safe_add(int a, int b) {
if (a > 0 && b > INT_MAX - a) return 0;
if (a < 0 && b < INT_MIN - a) return 0;
return a + b;
}
Other Common Pitfalls
sizeof with Array Parameters
void func(int arr[]) {
printf("%zu\n", sizeof(arr));
}
When an array is passed as a function parameter, it decays into a pointer. sizeof(arr) gives the pointer size (4 or 8), not the array size. You must pass the length as an additional parameter.
Macro Definition Pitfalls
#define SQUARE(x) x * x
SQUARE(2 + 3)
This expands to 2 + 3 * 2 + 3 = 11, not 25. Correct version:
#define SQUARE(x) ((x) * (x))
But SQUARE(i++) still has a problem (i gets incremented twice). An inline function is safer.
== and =
if (x = 5) {
}
This is assignment, not comparison! The value of the assignment expression is 5 (non-zero), so the condition is always true. Some compilers warn about this, but not all have this warning enabled.
❓ FAQ
valgrind --leak-check=full ./program. On Windows, you can use Visual Studio's debug heap or AddressSanitizer.-fsanitize=address to enable it. It can detect out-of-bounds access, use-after-free, memory leaks, and other issues.📖 Summary
- When in doubt about operator precedence, add parentheses — it's the simplest defense
- Array out-of-bounds and buffer overflow are the most common security issues in C
- Use
fgetsinstead ofgets,snprintfinstead ofsprintf,strncpyinstead ofstrcpy - Undefined behavior may "happen to work" but can behave differently across compilers or optimization levels
- Set pointers to NULL after free, check malloc return values, and watch for signed overflow
📝 Exercises
- Write a safe string copy function
safe_strcpy(char *dst, size_t dst_size, const char *src)that ensures no overflow and always null-terminates - Write a program that intentionally causes array out-of-bounds and integer overflow, compile and run with AddressSanitizer, and observe the output
- Audit the following code, find all security issues, and fix them:
char buf[8]; gets(buf); sprintf(buf, "Result: %d", value);



