roadmap
week 4 · day 23

Strings: Bytes with a Zero

The null terminator and every trap it sets

Hard 32 min 180 xp
After this you can
  • Explain that a C string is a byte array ending in a '\0', with no length field
  • Trace how strlen, strcpy and friends walk memory until the terminator
  • Predict the off-by-one and overflow bugs that the null-terminator convention causes
  • See why unbounded string functions are the classic source of buffer overflows

A string is not a type — it's a convention

C has no string type. A "string" is just a char array (day 15/17) that, by *convention*, ends with a zero byte — the null terminator, written '\0' (value 0). There's no stored length. The end of the string is wherever the first 0 byte appears. That single convention makes strings simple to represent and a minefield to handle — and understanding it is understanding a huge class of real bugs.

the string "Hi" in memory
   char s[] = "Hi";   // occupies 3 bytes, not 2

   index:   0    1    2
          ┌────┬────┬────┐
          │'H' │'i' │'\0'│    'H'=0x48  'i'=0x69  '\0'=0x00
          └────┴────┴────┘
   The 0x00 is the string. Remove it and s is no longer a valid string —
   functions would keep reading past index 2 into whatever follows.

Every string function walks to the zero

Because there's no length field, the standard functions scan for the terminator. strlen is literally "count bytes until you hit 0":

mystrlen.cc
1size_t my_strlen(const char *s) {
2 size_t n = 0;
3 while (s[n] != '\0') { // walk until the null terminator
4 n++;
5 }
6 return n; // number of bytes BEFORE the '\0'
7}

strcpy(dst, src) copies bytes from src into dst including the terminating 0, and stops when it copies that 0. Note what it does *not* do: it never checks how big dst is. It trusts that src has a terminator and that dst is large enough. Both assumptions are where things go wrong.

Key idea
Two traps fall out of "length = position of the first zero." (1) The off-by-one: a string of N visible characters needs N+1 bytes of storage (the extra one for '\0'). Allocate exactly N and the terminator overwrites the byte after your buffer. (2) The overflow: strcpy and gets keep writing until *the source's* terminator — with no regard for the destination's size — so a source longer than the destination writes right past the end. Recall day 14: past the end of a stack buffer lies the saved rbp and the return address.

The canonical vulnerability

Here is, essentially, the most famous bug in computing:

vuln.cc
1void greet(const char *name) {
2 char buf[16];
3 strcpy(buf, name); // if name is longer than 15 chars + '\0', this
4 // writes past buf — into saved rbp and the return
5 // address (day 14). Classic stack buffer overflow.
6 printf("Hello, %s\n", buf);
7}

buf holds 16 bytes; strcpy will copy however long name is. Feed a 100-byte name and 84 bytes spill past buf, over the saved registers and the return address the function will ret to. You learned the mechanism on day 12/14; this is the C code that opens the door. The fix is to use bounded functions that take the destination size — strncpy, snprintf, or better, length-aware handling — and to always reserve room for the terminator.

Predict first
char buf[8]; strcpy(buf, "abcdefgh"); — the literal "abcdefgh" is 8 visible characters. How many bytes does strcpy write, and what goes wrong with an 8-byte buffer?
Note
strncpy is safer but has its own trap: if the source is as long as or longer than the size, it does not write a terminator, leaving dst un-terminated — so the *next* strlen/printf runs off the end. snprintf(dst, size, "%s", src) always terminates and is usually the better choice. The lesson: bounded functions help, but you must still understand the terminator, because the bugs hide in its edges.
finished reading?
Your task, you write the code

Feel the terminator, then break a buffer safely

In strings.c: (1) implement my_strlen and my_strcpy from scratch (walking to '\0'), and test them against the real ones. (2) Show the off-by-one: allocate exactly strlen(src) bytes, copy into it, and watch valgrind/ASan report the terminator write past the end; then fix it with +1. (3) Write the greet()-style vulnerable function with a small stack buffer and, in YOUR OWN program only, pass it an over-long string and run under `-fsanitize=address` to see the overflow detected. Uses only this lesson plus days 14/17.

deliverable: strings.c (+ the ASan report for the off-by-one and the overflow)
build & run
$ gcc -O0 -g -Wall -fsanitize=address strings.c -o strings && ./strings
self-review before running
  • my_strlen and my_strcpy match the standard versions on several inputs
  • You reproduced the +1 off-by-one and fixed it by reserving room for '\0'
  • AddressSanitizer reported your deliberate overflow
  • You can explain why strcpy is unbounded and what to use instead
stretchReplace strcpy with snprintf(buf, sizeof buf, "%s", name) and confirm the overflow is gone. Then try strncpy with a source that exactly fills the buffer and show the resulting string is NOT terminated — reproducing strncpy's own trap.

Self-check

01What marks the end of a C string?
02A string with 5 visible characters needs how many bytes of storage?
03Why is strcpy(dst, src) dangerous?
04How does a string function like strlen find the length with no length field?
0/4 correct · 0/4 checked