my notes on software, security, systems, & whatever else crosses my mind i guess

Chris Jagdeo

back to notes

why old bugs never die

the same four vulnerability classes have been getting people breached since 1988. a working theory on why they keep coming back, and the one thing that actually kills them

In 1988, a grad student named Robert Morris released a worm that took down something like a tenth of the internet. one of the ways it got in was a buffer overflow in a program called fingerd. the code used gets(), which reads input into a buffer without ever checking how big the buffer is. send it more than it expects, overwrite the stack, and run whatever you want.

that was thirty-eight years ago. in 2023, a buffer overflow in glibc (the C library that basically every linux box on earth links against) gave anyone with a local shell root. same class of bug. same language. same fundamental mistake of trusting that the input would be the size you assumed.

i've been thinking about this a lot, because it's weird. we know about these bugs. we've known for decades. there are entire textbooks. and they keep showing up anyway.

the list is short and it doesn't change

if you look at the CWE top 25 (a ranking of the most dangerous software weaknesses, updated yearly), the top three have been the same three things for years, just shuffled around: out-of-bounds writes, cross-site scripting, and SQL injection. nothing new. nothing clever. the same three ideas that were being written up in Phrack in the late 90s.

the pattern holds when you look at the big incidents, too.

  • Heartbleed (2014) was a missing bounds check in OpenSSL. the heartbeat extension trusted a length field the client sent and read that many bytes back out of memory, whether or not that many bytes belonged to the request.
  • Equifax (2017) was injection. an Apache Struts bug let you put an expression in a Content-Type header and have the server evaluate it.
  • Citrix (2019) was path traversal. a couple of ../ in a url got you past the auth layer on an appliance that was sitting on the edge of thousands of corporate networks.
  • Log4Shell (2021) was injection again. a logging library would see ${jndi:...} in a string and go fetch and run code from wherever the string pointed.
  • MOVEit (2023) was SQL injection. in 2023.

different decades, different languages, different companies, and the same four bug classes. every one of those is something a security course covers in the first month.

so why don't they die

here's my working theory, and it's not really one thing. it's a few things stacked on top of each other.

the bugs live in the language, not the code. C doesn't have a way to say "this pointer belongs to a buffer of size n." it just doesn't. so every bounds check is something a human has to remember to write, and humans forget. microsoft and google have both published numbers saying roughly 70% of their serious security bugs are memory safety issues. that's not a training problem. that's the tool.

the fix is always local, the mistake is always structural. when a SQL injection gets found, the patch is to parameterize that one query. it doesn't stop the next developer from string-concatenating the next query, because the thing that let them do that (the database driver happily accepting a raw string) is still there. we fix instances and leave the pattern.

the vulnerable code is old and nobody wants to touch it. most of the internet is running on libraries written in the 90s and 2000s by a handful of people. i wrote a whole post about abandoned code. the stuff that isn't abandoned is often maintained by one or two people who are terrified of breaking it. rewriting glibc's string handling isn't a weekend project.

new code copies old code. every tutorial, every stack overflow answer, every autocomplete suggestion is trained on the last thirty years of software, and the last thirty years of software is full of these bugs. you learn to build a login form by looking at login forms, and a shocking number of login forms are wrong.

and the incentives point the wrong way. a bounds check costs you nothing when it's right and costs you a breach when it's missing, but the person who skips it ships faster, and the breach happens to someone else, later. nobody gets promoted for the buffer overflow that didn't happen.

the one thing that's actually working

the only approach i've seen that kills a bug class instead of a bug instance is making the mistake impossible to write.

parameterized queries did this for SQL injection, sort of. if your ORM won't let you concatenate strings into a query, you can't have that bug. (people still find ways. but it's harder.)

memory-safe languages are doing it for overflows. google published a number that stuck with me: the share of android's security bugs that were memory safety issues dropped from 76% in 2019 to 24% in 2024, and the main thing that changed is that new code was being written in Rust instead of C++. they didn't fix the old bugs. they just stopped writing new ones, and the old ones aged out.

that's the whole trick, i think. you don't kill old bugs by finding them faster. you kill them by taking away the tool that makes them possible and then waiting a very long time.

why i care about this

i'm a CS major, and security is the part of it i keep gravitating toward. a lot of what i've been reading on my own is basically the history of the same four mistakes. that used to feel discouraging. now it feels like the opposite. if the list is short and the list doesn't change, then the list is learnable. the goal isn't to memorize every CVE. it's to recognize the shape of a bounds check that isn't there, or a string that's about to be handed to something that will interpret it, and to catch it before it's a headline.

thirty-eight years is a long time to keep making the same mistake. but it's also a long time to get good at spotting it.

comments

leave a comment

2026 · chris jagdeo · new york city