C Security: Buffer Overflows and Safe Coding
Learn about buffer overflow exploits, format string vulnerabilities, and how to write secure C code using safe functions.
Buffer Overflow
A buffer overflow occurs when a program writes past the end of an allocated buffer, corrupting adjacent memory — stack frames, return addresses, or heap metadata. It is the most exploited class of vulnerability in C programs and the root cause of countless CVEs. The fix is always the same: know your buffer sizes and use length-limited functions.
/* VULNERABLE — do not write code like this */
#include <stdio.h>
#include <string.h>
void vulnerable_copy(const char *input) {
char buf[16];
strcpy(buf, input); /* no bounds check — overflows if input > 15 chars */
printf("buf: %s\n", buf);
}
void greet(const char *name) {
char greeting[32];
sprintf(greeting, "Hello, %s!", name); /* overflows if name > 25 chars */
puts(greeting);
}
/* SAFE versions — always specify the buffer size */
void safe_copy(const char *input) {
char buf[16];
strncpy(buf, input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0'; /* strncpy does not guarantee null termination */
printf("buf: %s\n", buf);
}
void safe_greet(const char *name) {
char greeting[32];
int n = snprintf(greeting, sizeof(greeting), "Hello, %s!", name);
if (n < 0 || (size_t)n >= sizeof(greeting)) {
fprintf(stderr, "Name too long\n");
return;
}
puts(greeting);
}
Stack Smashing Demo
This example shows how a stack buffer overflow can overwrite adjacent variables. On real systems without protections, it can overwrite the return address and redirect execution to attacker-controlled code.
#include <stdio.h>
#include <string.h>
/* On a real system without stack protections, overflowing 'buf' can overwrite
the return address, redirecting execution to arbitrary code.
Modern defenses: stack canaries (-fstack-protector), ASLR, NX bit. */
void check_password(const char *input) {
char buf[8];
int authenticated = 0; /* sits adjacent to buf on the stack */
/* Bug: copies input without checking length */
strcpy(buf, input); /* if input > 7 chars, overwrites 'authenticated' */
if (authenticated) {
printf("Access granted!\n"); /* reachable via overflow */
} else {
printf("Access denied.\n");
}
}
int main(void) {
check_password("admin"); /* "Access denied." — correct */
check_password("AAAAAAAAAAAA"); /* may trigger "Access granted." — overflow! */
return 0;
}
Compile with stack protection disabled to observe the overflow:
gcc -fno-stack-protector -z execstack -no-pie -o demo demo.c
Always compile production code with: gcc -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2
Format String Vulnerabilities
Format string vulnerabilities occur when user-controlled data is passed directly as the format string to printf or a related function. The attacker can use format specifiers like %x to read stack memory, or %n to write to arbitrary addresses. The fix is simple: always use a literal format string.
#include <stdio.h>
/* VULNERABLE: user controls the format string */
void log_message_bad(const char *user_input) {
printf(user_input); /* if user_input = "%x %x %x", leaks stack memory! */
}
/* SAFE: user input is always treated as data, never as a format string */
void log_message_good(const char *user_input) {
printf("%s", user_input);
}
void format_bugs(void) {
char buf[128];
char *user = "data from network";
/* DANGEROUS — user controls format */
sprintf(buf, user);
printf(user);
fprintf(stderr, user);
/* SAFE — user input is the data argument, not the format */
snprintf(buf, sizeof(buf), "%s", user);
printf("%s", user);
fprintf(stderr, "%s\n", user);
}
Format string attacks can:
- Read arbitrary memory with
%x,%p,%s - Write arbitrary memory with
%n(writes the count of bytes written so far) - Lead to arbitrary code execution
Rule: Never pass user-controlled data as a format string to any printf-family function.
Integer Overflow
Integer overflow is subtle because signed overflow is undefined behavior in C — the compiler is free to assume it never happens and optimize accordingly, producing surprising results. Unsigned overflow wraps predictably but can still cause security bugs in size calculations.
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
void integer_issues(void) {
/* Signed integer overflow — undefined behavior */
int max = INT_MAX;
int overflow = max + 1; /* UB — compiler may assume this never happens */
printf("%d\n", overflow);
/* Unsigned wraps predictably (defined behavior) */
unsigned int u = UINT_MAX;
printf("%u\n", u + 1); /* 0 — wraps to 0 */
/* Overflow in size calculation — classic heap overflow setup */
int count = 1073741824; /* 2^30 */
size_t size = count * 4; /* overflows on 32-bit! becomes 0 */
/* malloc(0) succeeds, then you write 4GB into a 0-byte buffer */
/* SAFE: check for overflow before multiplying */
if ((size_t)count > SIZE_MAX / sizeof(int)) {
fprintf(stderr, "Size overflow\n");
return;
}
size = (size_t)count * sizeof(int);
}
/* Safe addition with explicit overflow detection */
int safe_add(int a, int b, int *result) {
if ((b > 0 && a > INT_MAX - b) ||
(b < 0 && a < INT_MIN - b)) {
return -1; /* would overflow */
}
*result = a + b;
return 0;
}
Use-After-Free and Double-Free
Use-after-free occurs when code reads or writes memory through a pointer after it has been freed. Double-free occurs when free is called on the same pointer twice. Both corrupt the allocator’s internal metadata and can be exploited to execute arbitrary code. Setting pointers to NULL after freeing them is the primary defense.
#include <stdio.h>
#include <stdlib.h>
void use_after_free(void) {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
/* p is now a dangling pointer — the memory may be reallocated */
*p = 100; /* UNDEFINED BEHAVIOR — may corrupt other allocations */
printf("%d\n", *p); /* may print garbage, crash, or appear to work */
}
void double_free(void) {
int *p = malloc(sizeof(int));
free(p);
free(p); /* UNDEFINED BEHAVIOR — corrupts allocator metadata */
}
/* Prevention: set pointer to NULL after freeing */
void safe_free_pattern(void) {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
p = NULL; /* now any accidental dereference will crash immediately (SIGSEGV) */
if (p) { /* this check prevents the use */
*p = 100;
}
free(p); /* free(NULL) is a no-op — safe */
}
Safe String Functions Cheat Sheet
Every unsafe string function has a safe alternative. The pattern is always the same: specify the maximum number of bytes to read or write.
#include <stdio.h>
#include <string.h>
void safe_string_ops(void) {
char dst[32];
const char *src = "Hello, World!";
/* AVOID strcpy — use snprintf instead */
/* strcpy(dst, src); */
snprintf(dst, sizeof(dst), "%s", src); /* preferred: always null-terminates */
/* AVOID strcat — compute remaining space explicitly */
/* strcat(dst, " More"); */
size_t used = strlen(dst);
snprintf(dst + used, sizeof(dst) - used, " More");
/* AVOID gets — removed in C11 */
/* gets(line); */
char line[64];
if (fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\n")] = '\0'; /* trim newline */
}
/* AVOID unbounded scanf %s */
/* scanf("%s", buf); */
char word[32];
scanf("%31s", word); /* limit to buffer size - 1 */
}
Static Analysis and Runtime Hardening
Use multiple layers of defense. Each tool catches a different class of bug, and together they eliminate the vast majority of C security vulnerabilities before code reaches production.
# AddressSanitizer: detects buffer overflows, use-after-free, leaks at runtime
gcc -Wall -std=c11 -g -fsanitize=address,undefined -o prog prog.c
# Stack protector: inserts canary values to detect stack-based overflows
gcc -Wall -std=c11 -fstack-protector-strong -o prog prog.c
# Fortify Source: replaces unsafe functions with bounds-checked versions at compile time
gcc -Wall -std=c11 -O2 -D_FORTIFY_SOURCE=2 -o prog prog.c
# Static analysis with cppcheck
cppcheck --enable=all --inconclusive prog.c
# Static analysis with clang's built-in analyzer
clang --analyze prog.c
# Valgrind for memory errors and leak detection
valgrind --leak-check=full --error-exitcode=1 ./prog
Enable all four in your CI pipeline: AddressSanitizer, UBSanitizer, -fstack-protector-strong, and -D_FORTIFY_SOURCE=2. They catch the majority of C security bugs before they reach production.