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

Safe String Handling in C

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

Why it's dangerous (buffer overflow)

A buffer overflow is writing 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;
}
Result: the stack frame is corrupted β†’ segmentation fault, or worse: arbitrary code execution. C's classic library functions do not check lengths, so the caller must guarantee safety.

Four rules to memorize

  1. Always pass the buffer size explicitly (sizeof or a constant).
  2. Prefer the n-suffix / 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 does not add '\0'. You must put it there 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");
}
Preference order: 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);
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. 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 line is longer than 64 bytes, fgets reads only a prefix β€” line[len-1] won't be '\n'. Decide whether to loop to read the rest or report an error.

atoi β†’ strtol

atoi cannot report errors and the behavior on overflow is undefined. strtol lets you detect "conversion failed" and "out of range" correctly.

❌ atoi

int n = atoi(s);
// s="abc" also returns 0 β†’ can't tell from error
// s="99999999999" is undefined

βœ… 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, strtoul for unsigned long. Same pattern.

Cheat sheet

AvoidUse insteadNotes
strcpysnprintf / strncpy+NULNo size check
strcatstrncat / snprintfSame
sprintfsnprintfDetect truncation via return
getsfgetsgets removed in C11
atoi / atofstrtol / strtodError-detecting
scanf("%s", ...)fgets+sscanf or width %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. -fstack-protector-strong aborts 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
Copy an 18-char string with strcpy into an 8-byte buffer. Compile with -fstack-protector-strong and observe stack smashing detected.
Challenge 2: Rewrite with snprintf
Write a function that builds "Hello, [name]! You are [age]." using strcpy/strcat and then using snprintf. Compare how each handles long input.
Challenge 3: Safer integer parser
Extend parse_int: one strict version that rejects "123abc", one 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.