Memory Safety
In C and C++, an array index is address arithmetic, and a freed block is only a promise not to touch it again. Nothing checks either one. When the promise breaks, the program reads or writes memory that belongs to something else, and an attacker who controls the input can often choose what gets read or written.
This one class of bug dominates the security record. Microsoft and the Chromium project each found that about 70% of their serious security bugs are memory-safety bugs, and it is why security agencies in the United States and elsewhere now urge vendors to write new code in memory-safe languages. This topic explains the mechanism, and why a service written in Python still has the problem.
What C Does Not Check
Indexing an array in C compiles to "the start of the array plus the index times the element size", the array arithmetic of Chapter 5, with no comparison against the array's length. Freeing a block hands it back to the allocator (see The Stack and the Heap) without touching any pointer that still holds its address. Both choices save a few instructions per access.
They also leave correctness entirely to the programmer, on every access, in every line, for the life of the program. The language assumes each index is in range and each pointer still points at a live object. When either assumption fails, the program does not stop. It carries on with memory it has no right to.
Buffer Overflows
Suppose a program keeps a 16-byte buffer for a user's name, and a flag that says whether the user is an administrator stored right after it. Copying a 40-byte name into the buffer without checking the length writes 16 bytes into the buffer and the next 24 bytes over whatever follows: the flag, another field, the allocator's bookkeeping, or, if the buffer is on the stack, the return address that Chapter 3 showed sits in every frame.
Reading past the end leaks instead of corrupting. Heartbleed, the 2014 bug in the OpenSSL library, trusted a length field in a request and returned up to 64 kibibytes of the server's memory per request to anyone who asked, private keys and passwords included. Researchers demonstrated recovering a server's private key through it within days of disclosure.
Use-After-Free and Double Free
After a block is freed, the allocator hands the same block to the next request of that size, because that is its job. A stale pointer to the old block now reads and writes someone else's object. If an attacker can trigger the right allocation in between, their own data is what sits there when the stale pointer is used.
Freeing the same block twice is the related bug. The allocator threads free blocks onto a list, the linked list of Chapter 5, and a double free puts one block on that list twice. Two later requests can then be handed the same block, and each one's writes corrupt the other's data.
Why They Are Security Bugs
An attacker who triggers one gets more than a crash: a write to memory of their choosing, which can redirect what the program executes next. Operating systems and compilers add mitigations: addresses randomized at every start, canary values that detect a smashed stack frame, and memory marked non-executable. Each one raises the price of an exploit without removing the bug, and exploit chains still get through them.
The evidence is in the numbers. Microsoft reported in 2019 that about 70% of the vulnerabilities it assigns a CVE each year are memory-safety issues, and the Chromium project reported in 2020 that about 70% of its high-severity security bugs are. Both figures were measured with every mitigation already in place. Android shows the other side: as its new code moved to memory-safe languages, memory-safety bugs fell from 76% of its vulnerabilities in 2019 to 24% in 2024, by Google's count.
What Safe Languages Buy
A bounds check turns an overflow into an exception. It costs a compare and a branch that the branch predictor almost always gets right (Chapter 3), and compilers remove many checks they can prove redundant, such as those in a loop that runs from zero to the length. Garbage collection (see Garbage Collection), or Rust's ownership rules checked at compile time, makes use-after-free impossible outside code explicitly marked unsafe.
What safety costs is a small runtime check, a collector's memory and pauses, or a stricter compiler. Every such language also keeps an escape hatch where the old rules return: unsafe blocks in Rust, the foreign-function module and C extensions in Python. Rust has been accepted in the Linux kernel since version 6.1 in 2022, which says how the systems world now weighs that price.
The Native Code Under a Safe Service
A Python or Java service is memory-safe in its own code and not in its dependencies. The TLS library, the image decoder, the XML parser, the compression library and the interpreter itself are written in C. In 2023, a heap overflow in libwebp, the WebP image library, was exploited in the wild; it reached any program that decoded an untrusted WebP image, Python services that used the Pillow imaging library's bundled copy among them, until Pillow shipped version 10.0.1 with the fixed library.
So the exposure of a service written in a safe language sits wherever bytes from a stranger meet native code: uploads, parsers, decoders and decompressors. A service is as safe as the least safe library that touches untrusted input, and patching those libraries promptly is part of the price of running it.
- "Our service is written in Python, so memory-safety bugs are someone else's problem." The native libraries under it parse the untrusted input, and a heap overflow in an image decoder is exploitable through a Python upload handler.
- "A memory bug shows up as a crash, so we would notice." Heartbleed crashed nothing. An out-of-bounds read returns plausible bytes, and a crafted write is designed not to crash.
- "Address randomization and stack canaries solved buffer overflows." They make exploitation harder and leave the bug in place. The 70% figures from Microsoft and Chromium were measured on code that already had them.
- "Bounds checks make safe languages too slow for systems code." A check is a compare and a well-predicted branch, often removed by the compiler. Android and the Linux kernel now accept that cost for new code.
- "Rewriting in a memory-safe language removes all vulnerabilities." It removes one class, the largest. Injection, logic and authorization bugs remain, and every unsafe block or foreign call reopens the old class locally.
- Patch the native libraries that parse untrusted input first. Image, font, compression, XML and TLS code is where a managed-language service's memory bugs live.
- Write new code that handles untrusted input in a memory-safe language. The Android data shows the bug class shrinking as new code shifts.
- Treat a crash in native code on external input as a security incident until shown otherwise. A crash is the visible edge of an out-of-bounds write.
- Keep unsafe code small and at the edges, whatever the language. Fewer lines that can break the rules are fewer lines to audit.
Knowledge Check
Heartbleed only read past the end of a buffer; it never wrote anything. Why was it still severe?
- Reading out of bounds always crashes the server, so it caused outages
- The bytes it returned could hold private keys and users' passwords
- A read past the end silently rewrites the server's configuration
- It let attackers run their own code through the returned bytes
How does a use-after-free let an attacker place their own data where the program expects its object?
- The freed block is copied to disk, where the attacker can edit it
- The attacker's code runs inside the allocator and edits the block
- The operating system gives the freed block to the attacker's process
- The allocator reuses the block for an allocation the attacker triggers
What do stack canaries and address randomization change about a buffer overflow bug?
- They make exploiting it harder, and the bug itself remains in place
- They remove the bug, because the overflowing write is now blocked entirely
- They turn the overflow into a compile error in affected programs
- They have no effect on overflows and only protect against leaks
A Python service accepts image uploads and resizes them. Where does its memory-safety exposure actually sit?
- In its own request handlers, where Python lists can overflow
- In the native image decoder that parses the uploaded bytes
- Nowhere, because the interpreter checks every native library too
- In the database driver, since queries are the only native calls
A safe language inserts a bounds check before every array access. What does that cost, and why is it usually small?
- A system call per access, made cheap by caching the array's length
- A full copy of the array, made cheap because copies are streamed
- A lock around the array, made cheap because locks are rarely held
- A compare and a branch that the processor predicts correctly
You got correct