46. How NOT to write C: a bad-code/good-code anti-pattern catalog
Every entry below compiles (the guide's opening promise holds here too —
"BAD" blocks compile, several with a warning your flags should be
catching; run them past -Wall -Wextra yourself to see your compiler
already knows about most of these).
46.1 Ignoring a return value that reports failure
// BAD
FILE *f = fopen("config.json", "r");
fread(buf, 1, size, f); // if fopen failed, f is NULL — this dereferences NULL
// GOOD
FILE *f = fopen("config.json", "r");
if (!f) { perror("fopen"); return -1; }
fread(buf, 1, size, f);
46.2 Unbounded string functions
// BAD
char name[32];
strcpy(name, user_supplied); // no bound — a longer input overflows `name` (Section 29.1)
// GOOD
char name[32];
snprintf(name, sizeof name, "%s", user_supplied);
46.3 Trusting scanf("%s", ...) with no width limit
// BAD
char name[32];
scanf("%s", name); // unbounded — identical risk to strcpy above
// GOOD
char name[32];
scanf("%31s", name); // explicit width, leaving room for the '\0'
46.4 Format-string injection
// BAD
void log_bad(const char *user_input) {
printf(user_input); // if user_input contains "%s"/"%n", this reads or WRITES
} // arbitrary memory through printf's own varargs machinery
// GOOD
void log_good(const char *user_input) {
printf("%s", user_input); // user_input is always DATA, never interpreted as a format string
}
Modern compilers already catch this one for you: Clang and GCC both emit
-Wformat-security (part of -Wall) — printf(user_input) triggers
"format string is not a string literal (potentially insecure)" at
compile time, which is exactly why checklist rule 11 says turn that
warning into a hard build failure with -Werror.
46.5 Shelling out with unsanitized input
// BAD
char cmd[256];
snprintf(cmd, sizeof cmd, "grep %s /var/log/app.log", user_pattern);
system(cmd); // user_pattern = "foo; rm -rf ~" runs a second, attacker-chosen command
// GOOD — avoid the shell entirely; call the tool directly with fork/exec (Section 26)
pid_t pid = fork();
if (pid == 0) {
execlp("grep", "grep", user_pattern, "/var/log/app.log", (char *)NULL);
_exit(127);
}
// (parent: waitpid as in Section 26)
execlp/execvp pass each argument as a separate, non-reinterpreted
string — there is no shell in between to reinterpret ;, |, `,
or $() — which is the entire class of "shell injection" vulnerability
system()/popen() are exposed to whenever any part of the command
line comes from outside the program.
46.6 Off-by-one and unsigned-underflow loop bounds
// BAD
void print_reversed_bad(const char *s) {
size_t len = strlen(s);
for (size_t i = len - 1; i >= 0; i--) { // size_t is UNSIGNED: when s is "", len-1
putchar(s[i]); // wraps to SIZE_MAX, and `i >= 0` is ALWAYS true
} // — this reads far out of bounds, forever
}
// GOOD
void print_reversed_good(const char *s) {
size_t len = strlen(s);
for (size_t i = len; i > 0; i--) {
putchar(s[i - 1]);
}
}
46.7 Returning a pointer to a local variable
// BAD
int *make_answer_bad(void) {
int answer = 42;
return &answer; // answer's stack frame is gone the instant this function returns
}
// GOOD — either return by value, or have the caller own the storage
int make_answer_good(void) { return 42; }
void make_answer_out_param(int *out) { *out = 42; } // when you need more than one value back
46.8 sizeof on a decayed array parameter
// BAD
void print_len_bad(int arr[]) {
printf("%zu\n", sizeof(arr)); // arr decayed to int* the moment it crossed the function
} // boundary (Section 6.1) — this prints 8, not the array's length
// GOOD — pass the length explicitly; there is no other reliable way in C
void print_len_good(int *arr, size_t len) {
printf("%zu\n", len * sizeof(*arr));
}
Clang and GCC flag the bad version directly: -Wsizeof-array-argument
(again, part of -Wall) — *"sizeof on array function parameter will
return size of 'int ' instead of 'int[]'." Another entry for
checklist rule 11's file.
46.9 Comparing floating-point numbers with ==
// BAD
int equal_bad(double a, double b) {
return a == b; // 0.1 + 0.2 == 0.3 is FALSE — binary floating point can't represent
} // most decimal fractions exactly, so rounding error accumulates
// GOOD
#include <math.h>
int equal_good(double a, double b) {
return fabs(a - b) < 1e-9; // compare within a tolerance appropriate to your value range
}
46.10 Macros without full parenthesization, or with side-effecting arguments
// BAD
#define SQUARE(x) x * x
int bad = SQUARE(1 + 2); // expands to 1 + 2 * 1 + 2 = 5, not 9
// BAD (still, even parenthesized)
#define SQUARE2(x) ((x) * (x))
int i = 2;
int also_bad = SQUARE2(i++); // expands to ((i++) * (i++)) — UB: i modified twice with no
// sequence point between the two modifications (Section 8.4, 28)
// GOOD — a real function; the compiler still inlines it, and it evaluates i++ exactly once
static inline int square(int x) { return x * x; }
46.11 Double free and use-after-free from an un-nulled pointer
// BAD
free(conn->socket_buf);
// ... fifty lines later, in an error path that re-checks conn->socket_buf ...
if (conn->socket_buf) free(conn->socket_buf); // conn->socket_buf still holds the OLD address —
// "if not NULL" doesn't protect against double free
// GOOD
free(conn->socket_buf);
conn->socket_buf = NULL; // now every later `if (conn->socket_buf)` check is actually meaningful
46.12 Feeding malloc an unchecked multiplication
// BAD
int *matrix = malloc(rows * cols * sizeof(int)); // rows*cols can overflow size_t for
// attacker-influenced dimensions, wrapping
// to a tiny allocation that later writes overflow
// GOOD
int *matrix = calloc((size_t)rows * (size_t)cols, sizeof(int)); // calloc checks the
// multiplication for overflow internally
46.13 Mutable global state that quietly breaks under threads or re-entrancy
// BAD — works fine single-threaded, corrupts data the moment two threads call it,
// with no compiler warning to point at the problem
static char scratch[256];
char *format_message_bad(const char *name) {
snprintf(scratch, sizeof scratch, "hello, %s!", name);
return scratch; // every caller gets a pointer to the SAME shared buffer
}
// GOOD — caller owns distinct storage; no shared mutable state to race on
void format_message_good(const char *name, char *out, size_t out_cap) {
snprintf(out, out_cap, "hello, %s!", name);
}
This is precisely the bug class Section 20.1 demonstrated with a shared
counter, generalized: any file-scope or static mutable object that a
function reads or writes is a potential data race the moment that
function is called from more than one thread, or re-enters itself
(a signal handler calling back into code already running).
46.14 Skipping the warnings instead of fixing them
# BAD — silences the exact class of bug this whole section demonstrates
clang -w -o app app.c
# GOOD — the one flag change that would have caught 46.4, 46.8, and more, for free
clang -std=gnu17 -Wall -Wextra -Wpedantic -Werror -o app app.c
If there's one takeaway from this entire catalog, it's this last one:
sections 46.4 and 46.8 above are not hypothetical dangers you have to
remember to look for by eye — they're warnings your compiler already
emits by default under -Wall. The single highest-leverage habit in
this whole guide is never disabling that.