Frontend Labs
⋯

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.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36