JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Buffer Overflows and Why gets() Is Banned

A buffer overflow is what happens when you pour more bytes into a box than the box can hold — and in C, the bytes do not just spill on the floor, they overwrite whatever lived next door. This guide shows exactly what gets smashed, why one ancient function got hunted out of the C standard for it, and how an overflow turns a harmless typo into a security hole.

The box that does not push back

By now you know the shape of the danger. The third guide in this rung walked you through the out-of-bounds access as one of C's classic traps, and you have met undefined behavior head-on. This guide zooms all the way in on the single most consequential out-of-bounds bug in the history of computing — the buffer overflow — and on the one library function so reliably dangerous that the C committee did something it almost never does: it deleted the function from the language. To see why, we have to watch what happens byte by byte.

A buffer is just a region of memory you set aside to hold data — most often an array. When you write `char buf[8];`, you are asking for a box exactly 8 bytes wide. A buffer overflow is what happens when code writes past the end of that box: a 9th byte, a 20th byte, bytes that were never yours. Here is the part that trips up everyone coming from a friendlier language: C does no bounds checking at all. The box does not push back. There is no wall, no exception, no "index out of range." The hardware happily writes byte number 9 into whatever address sits 9 bytes from the start of buf, and that address belongs to something else entirely.

Smashing the neighbor: the stack overflow up close

To feel why this is so dangerous, you have to remember where local arrays live. A `char buf[8]` declared inside a function lives on the call stack, inside that function's stack frame. And sitting in that same frame, just a few bytes above buf, is one of the most important values in the whole program: the return address — the address of the instruction the CPU will jump back to when this function returns. The frame is a tightly packed neighborhood, and the return address is the house next door.

Now picture the disaster. The function copies a 20-byte string into our 8-byte buf. Bytes 0 through 7 fill the box correctly. Byte 8, byte 9, byte 10 spill upward into the frame, and somewhere in that spill they land squarely on top of the return address, overwriting it with whatever happened to be in the input. When the function finishes and executes its `ret` instruction, the CPU loads that corrupted value into rip and jumps there. If the overwriting bytes were garbage, rip points into nonsense and you get a segmentation fault. But if an attacker chose those bytes carefully, rip now points wherever the attacker wants — and the attacker is running the show. That is the entire mechanism behind decades of "stack smashing" exploits, including the 1988 Morris Worm.

stack frame, low address at top:

  [ buf[0] buf[1] ... buf[7] ]   <- the 8-byte box you asked for
  [ saved rbp                 ]   <- the caller's base pointer
  [ return address            ]   <- where ret jumps back to

writing 20 bytes into buf[8] runs straight
upward through saved rbp and INTO the return address.
A local array and the return address are neighbors; overflowing the first quietly rewrites the second.

Why gets() was hunted out of the language

Some functions make this disaster easy; one made it almost unavoidable. The classic offender is gets(), a library call whose whole job was to read a line of input from the keyboard into a buffer. You called it like `gets(buf)`. Look closely at that call and you will spot the fatal flaw: you handed gets() a pointer to your buffer, but you never told it how big the buffer is. gets() had no way to know buf was only 8 bytes wide, so it simply kept copying input bytes until it hit a newline — 8 bytes, 80 bytes, 8000 bytes, straight past the end of the box and over the return address. There was no safe way to call it. The size information it needed to protect you was not even a parameter it accepted.

This made gets() a uniquely indefensible member of the family of dangerous string functions. The committee's response was unusually severe. gets() was formally deprecated, and then in the C11 standard (2011) it was removed from the language entirely — a fate almost no other standard-library function has ever suffered. The lesson the committee drew, and the one to carry with you, is sharp: any function that writes into a buffer without being told the buffer's size is a buffer overflow waiting for the wrong input. The size of the destination must travel with the call.

The replacement is fgets(), and the difference is exactly the missing piece: `fgets(buf, sizeof buf, stdin)`. That middle argument is a hard cap — fgets() will read at most sizeof buf minus one bytes and then stop, leaving room for the terminating null byte no matter how long the input is. The contract is the cure: by passing the size, you turn "copy until something stops me" into "copy until the box is full." Notice that even fgets() leans on you using sizeof on the array itself; pass the wrong size and you are back in danger, which is the honest catch we return to in the last section.

The quiet cousin: off-by-one and the missing null

Not every overflow is a spectacular twenty-byte cannonade. The most common one in real code overshoots by a single byte — the off-by-one error — and it hides inside one of C's most-used idioms. Remember that in C a string is a null-terminated string: the text plus one extra byte holding the value 0x00 that marks the end. The trap is that beginners size the buffer for the visible characters and forget the invisible terminator. The word "system" is 6 characters, so a tempting `char name[6]` feels right — but storing it needs 7 bytes, six letters plus the null, and that 7th byte lands one past the end.

An off-by-one of a single byte sounds harmless next to a stack-smashing exploit, and often the program runs fine for years — which is exactly what makes it treacherous. It is still out-of-bounds access and still undefined behavior; it just happens to land on a byte that does not matter yet. Change the compiler, change the optimization level, rearrange the variables, and that one stray byte starts clobbering something live. This is the deeper truth of memory in C: it is a question of memory safety, and C gives you none of it for free. A program can be wrong and still appear to work, right up until the day it does not.

What an overflow really teaches you

Step back and the whole shape becomes clear. A buffer overflow is not an exotic security topic bolted onto the side of C — it is the direct, inevitable consequence of three things you already know meeting at once: arrays are just bare bytes with no built-in length, C performs no bounds checking, and a write past the end is undefined behavior. Put those together and you get a language where a single forgotten size turns input into the program's instructions. The attacker's input becomes your control flow. There is no clever trick being played; the door was simply left open.

It is worth being honest about how far the defenses go. Modern systems add layers — a stack canary (a secret value placed just before the return address that gets checked on return), non-executable stacks, and address-space randomization — and they make the classic stack smash much harder than it was in 1988. But these are speed bumps, not walls: they raise the cost of an exploit without making the underlying overflow safe, and attackers have answers for each. The only true fix is to not overflow in the first place. This is also precisely the class of bug that Rust was built to abolish in its safe subset, by attaching a length to every slice and checking it — a different set of tradeoffs we will weigh later, not a magic wand.

All of this points at one habit that the final guide in this rung makes its whole subject: treat every byte of input you did not generate yourself as hostile until proven otherwise. The overflow only fires because some input was longer than the buffer assumed; the cure is to validate length before you copy, at the boundary where untrusted data enters. The next guide turns that instinct into a discipline — defensive coding and the handling of untrusted input — and you now carry the concrete, byte-level picture of exactly what you are defending against.