C for People Who Already Know How to Code
Strings and Buffer Footguns
A C string is a convention, not a type
char * doesn't know its own length. "String" in C means "keep reading bytes until you hit a \0" Every string function is walking a buffer on that promise, and every buffer overflow is that promise being broken.
The classics: gets, strcpy, sprintf, strcat
gets() reads a line with no length limit at all and was removed from the C standard entirely in C11, not deprecated, deleted, because there is no way to call it safely. strcpy, strcat, and unbounded sprintf all share the same flaw: they write until the source runs out, with zero awareness of how big the destination actually is.
char buf[8];
strcpy(buf, user_input); // writes exactly as many bytes as user_input has, regardless of buf's sizebuf[8], 6-byte input
buf[8], 16-byte input
Nothing here "corrupts" in some abstract sense. It's a plain sequential write that doesn't stop at the buffer's edge, into whatever memory happens to sit next: other locals, saved registers, a return address.
Try it yourself: see the overflow caught by a sanitizer instead of by luck›
gcc -fsanitize=address -g -o overflow overflow.c && ./overflowCompile the strcpy example above with AddressSanitizer and run it with an oversized input. ASan will report the exact out-of-bounds write with a stack trace, instead of the program silently corrupting memory and failing somewhere unrelated ten calls later, which is what happens without it.
strncpy's null-terminator lie
strncpy looks like "the safe version of strcpy" and isn't quite: if the source is n bytes or longer, it copies exactly n bytes and does not null-terminate the destination. Whatever reads that buffer next as a string keeps going past its end:
char dest[8];
strncpy(dest, "eight!!!", sizeof dest); // no room for a terminator, dest is NOT a valid C string now
dest[sizeof(dest) - 1] = '\0'; // this line is not optionalThe reliably-terminated alternative most people actually want is snprintf(dest, sizeof dest, "%s", src). It always null-terminates within the given size, at the cost of silently truncating rather than failing loudly on overflow (check its return value if you need to detect truncation).
String literals are read-only, even though the type says otherwise
char *s = "hello"; gives you a pointer to a string literal with static storage duration. The type is char *, not const char *, for historical reasons, but modifying it is undefined behavior anyway, and on many platforms it lives in a genuinely read-only mapped segment and segfaults immediately:
char *s = "hello";
s[0] = 'H'; // undefined behavior: may crash, may silently corrupt a literal shared elsewhere
char arr[] = "hello";
arr[0] = 'H'; // fine, arr is a real, mutable, stack-allocated copy of the literal's bytesIf you intend to modify it, declare it as an array (char arr[] = "..."), never as a barechar *pointing at a literal.