πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English

Safe String Handling in C

Compare dangerous functions with their safe replacements. Buffer overflows have been a top security vulnerability for decades.

Why it's dangerous (buffer overflow)

A buffer overflow is when code writes past the end of a buffer. It can crash the program, corrupt nearby stack data, or let an attacker execute arbitrary code.
#include <string.h>

int main(void) {
    char buf[8];                      // only 8 bytes
    strcpy(buf, "This is too long!"); // writes 18 bytes β†’ overflow
    return 0;
}
The result: a corrupted stack frame, leading to a segmentation fault or β€” worse β€” arbitrary code execution. C's classic library functions don't check lengths, so the caller must guarantee safety.

Four rules to memorize

  1. Always pass the buffer size explicitly (via sizeof or a constant).
  2. Prefer the n-suffix or bounded variants (strncpy, snprintf, fgets, strtol).
  3. Check the return value to detect truncation or errors.
  4. Make sure the result is NUL-terminated.

strcpy β†’ strncpy / snprintf

❌ strcpy (unsafe)

char dst[8];
strcpy(dst, src);
// no size check β†’ overflow if src >= 8 bytes

βœ… strncpy + explicit NUL

char dst[8];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';
// always terminate explicitly
strncpy gotcha: When it writes n bytes, it doesn't add a '\0' terminator. You must add it yourself.

snprintf is the cleanest

char dst[8];
int n = snprintf(dst, sizeof(dst), "%s", src);
// snprintf always NUL-terminates (when size > 0)
// return n is "chars it wanted to write"; truncation if n >= sizeof(dst)
if (n >= (int)sizeof(dst)) {
    fprintf(stderr, "warning: truncated\n");
}
Order of preference: snprintf > strncpy+NUL >> strcpy.

sprintf β†’ snprintf

❌ sprintf

char buf[32];
sprintf(buf, "Hello, %s!", name);
// long name overflows buf

βœ… snprintf

char buf[32];
snprintf(buf, sizeof(buf), "Hello, %s!", name);

Detecting truncation via the return value

int n = snprintf(buf, sizeof(buf), fmt, ...);
if (n < 0)                 { /* encoding error */ }
else if (n >= sizeof(buf)) { /* truncation */ }
else                       { /* OK: n bytes written */ }

gets β†’ fgets

gets has no size parameter and was removed in C11. Always use fgets instead.

❌ gets (removed in C11)

char line[64];
gets(line);   // NG: no size limit

βœ… fgets

char line[64];
if (fgets(line, sizeof(line), stdin) == NULL) {
    // EOF or error
}

Stripping the newline

char line[64];
if (fgets(line, sizeof(line), stdin)) {
    size_t len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') {
        line[len - 1] = '\0';
    }
}
Note: If the input line is longer than 64 bytes, fgets reads only a prefix, so line[len-1] won't be '\n'. Decide whether to loop to read the rest or report an error.

atoi β†’ strtol

atoi can't report errors, and its behavior on overflow is undefined. strtol lets you properly detect "conversion failed" and "out of range."

❌ atoi

int n = atoi(s);
// s="abc" also returns 0 β†’ you can't tell it apart from an error
// s="99999999999" produces undefined behavior

βœ… strtol

#include <stdlib.h>
#include <errno.h>

errno = 0;
char *end;
long v = strtol(s, &end, 10);

if (end == s)             { /* no digits consumed */ }
else if (*end != '\0')   { /* trailing junk */ }
else if (errno == ERANGE) { /* out of range */ }
else                      { /* success: value is v */ }

Practical wrapper

#include <errno.h>
#include <limits.h>
#include <stdlib.h>

int parse_int(const char *s, int *out) {
    errno = 0;
    char *end;
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return -1;
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return -1;
    *out = (int)v;
    return 0;
}
See also: strtod for doubles and strtoul for unsigned long follow the same pattern.

Cheat sheet

AvoidUse insteadNotes
strcpysnprintf / strncpy+NULNo size check
strcatstrncat / snprintfSame issue as strcpy
sprintfsnprintfDetects truncation via return value
getsfgetsgets was removed in C11
atoi / atofstrtol / strtodSupports proper error detection
scanf("%s", ...)fgets + sscanf, or a width like %31sUnbounded %s is unsafe

Recommended compile flags

gcc -Wall -Wextra -Wformat-security -Wstack-protector \
    -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 prog.c
-D_FORTIFY_SOURCE=2 adds runtime checks via glibc, and -fstack-protector-strong aborts the program on stack corruption.
Static analysis: Run cppcheck, clang-tidy, or clang --analyze to catch buffer bugs before they ship.

Challenges

Challenge 1: Experience strcpy overflow
Use strcpy to copy an 18-character string into an 8-byte buffer. Compile with -fstack-protector-strong and observe the stack smashing detected message.
Challenge 2: Rewrite with snprintf
Write a function that builds "Hello, [name]! You are [age]." using strcpy/strcat, then again using snprintf. Compare how each handles long input.
Challenge 3: Safer integer parser
Extend parse_int: write a strict version that rejects "123abc" and a lenient version that accepts "3.14" and keeps the integer part. Add 10 test cases.
Challenge 4: Safe line reader
Implement char *read_line(FILE *fp, char *buf, size_t size);. If the input exceeds size, discard the rest so the next read starts clean. Return buf, or NULL on EOF.

Review Quiz

Check your understanding of this lesson.

Q1. What should you use instead of strcpy?

Length-bounded variants such as strncpy / snprintf / strlcpy
memcpy_safe
copy_string

strcpy doesn't check the buffer size, so it's a classic cause of buffer overflows. Use length-bounded variants or snprintf.

Q2. What's the main cause of a buffer overflow?

Writing beyond the allocated size
Slow write speed
Variable name collision

Writing past the allocated region is undefined behavior and can cause segfaults or open the door to arbitrary-code-execution vulnerabilities.

Q3. Which function should you use for formatted output into a fixed-size buffer?

snprintf
sprintf
printf

sprintf takes no size argument and is dangerous. snprintf accepts a buffer size, making it safe to use.