AI / CYBERSECURITY · BY AARON DEMBY JONES · 15 MIN READ
Two mistakes, one weird trick, twenty-seven years
In early 2026, an AI system called Claude Mythos Preview went hunting for software vulnerabilities and found a lot of them, including one that had been living in OpenBSD's networking code since 1998. The coverage so far has been entirely about the AI. I want to talk about the bug. Is it something elaborate that only an AI could understand? Or is it the software equivalent of using 'password' as your password? And why did no one find it for 27 years?
The paranoid operating system
OpenBSD is a security-focused operating system created in 1995. It descends from the BSD line developed at UC Berkeley starting in the late 1970s. Engineers often use it to control firewalls, routers, and other critical infrastructure. A bug in OpenBSD hits differently than a bug in a random software package.
So how exactly did the vulnerability work? To really get what's going on, we need to understand three things: how computers talk to each other over networks, how a linked list can turn into a black hole, and how the past can also be the future.
How computers talk
When computers exchange information across a network, that data is broken up into packets—small fragments of the data, each taking their own route across the network. If you download a file from the internet, the sender actually breaks the file into hundreds of packets and sends them to you. Then your operating system reassembles these packets into the actual file that you wanted.
This sounds simple enough, but problems can arise: what if a packet arrives out of order, or not at all?
Fortunately, people have thought about this already. The strategy to handle unordered or missing packets is called TCP: Transmission Control Protocol. This works by assigning every packet a sequence number. Then the receiver can reorder their packets according to that number, and if they're missing any packets, they can talk back to the sender and let them know.
We'll come back to the finer details of how the sender and receiver coordinate when packets go missing later. But in broad strokes, the receiver sends updates on which packets it has received so far, and the sender infers from those updates a list of missing ranges—we'll call them holes—so that they can re-send them in a future batch. This list of holes is implemented as a linked list. ("What is a linked list?", you might ask.) Fear not: next on our agenda is a crash course in linked lists.
Linked lists, pointers, and a black hole
To understand how a linked list is implemented, we need to talk about how computers keep track of things in memory. On a computer, memory is a giant array of numbered slots—you can think of these as mailboxes with addresses printed on the outside, and contents on the inside. Every piece of data your computer stores lives inside one or more of these mailboxes, each of which has an address, like slot 100. This data can be anything, so sometimes, programmers create fun little scavenger hunts: you might open up mailbox 100 and find regular data inside, like the letter "C," but also a note that says "to find the next letter, look at mailbox 482." These notes that refer to mailbox addresses are called pointers.
A linked list is exactly this: a scavenger hunt of mailboxes chained together. We call each pointer to the next mailbox a next pointer.
We could go on like this forever, in theory. In practice, however, we'll want our list to end eventually. What should the next pointer in the last mailbox say? We could just opt to not have a next pointer at all in the very last box, but that creates a clumsy asymmetry that might be hard to work with. Programmers instead have opted to use a clever convention: we always assign the next pointer in the last box to address 0. This is called null. It's similar to how in arithmetic, it's more convenient to have an actual number, like 0, to represent nothing, than to not use a number at all. The operating system knows to treat mailbox zero with trepidation: you can look at it from the outside, but you can never try to open it up to read or write data from it. If you do, the system will crash. Here's an example of our linked list ending after the letter T.
Why use a linked list at all? It makes it easy to add or remove an element to our list: doing either of those operations will only require updating a couple of next pointers. For example, if we want to add a new piece of data at mailbox 42 with the letter 'S' at the end of our existing CAT list, all we have to do is grab the tail mailbox (the one with 'T' in it) open it up, and rewrite its next pointer to point to 42. Then we just update mailbox 42 to have a next pointer to null. (Grabbing a CAT by the tail rarely ends well in other contexts, but it's effective in linked list land.)
There's one gotcha worth knowing. Suppose we have a list with one element, as shown below:
Now if we remove T, the list is conceptually empty, but the null mailbox is still sitting there. This is an important point: an empty linked list mechanically isn't nothing; it's just mailbox #0 all by itself.
Since you never can be too safe, and there is always an off chance that your current mailbox is somehow null, paranoid code always checks before opening up any mailbox.
Numbers…on a clock?!
From our crash course on linked lists, you now have a sense of how memory works on a computer. Now let's talk about numbers. Numbers, like everything else on a computer, get stored in memory. The problem is, memory is finite. So in a typical computer implementation of numbers, there is actually a biggest number! Here's what that looks like in practice if you tell your computer to start counting from 1, one at a time:
Wait, what just happened?
This is called integer overflow. In this example, the computer is using a maximum of 32 bits to store its numbers. (Imagine a car odometer with 32 slots, each one only showing 0 or 1.) Once all those slots are all at the max, if you keep going, the next value will read all zeros.
A useful visual that can help is to view our numbers as living on a really big clock. So that I can actually draw it without exploding my computer, I'm going to use 16 positions on the clock instead of ~4 billion:
The numbers are smaller, but the principle is the same: if you start counting on this clock, you'll go 1 2 3 4 5 … 14 15 0 1 2 … etc. The clock just makes the wraparound more visually intuitive.
Hold onto this clock idea for later—we'll see that TCP sequence numbers live on a clock just like this, and that comparing two numbers on a clock is trickier than it looks.
Selective acknowledgment
Okay, now we have everything we need to understand the protocol that tracks the running list of hole positions. This protocol is called Selective ACKnowledgment, or SACK for short. The receiver reports exactly which packets it has using byte ranges. These look like [start, end] pairs. We call each pair a SACK block, and a single report can carry up to four of them at once.
Since file exchanges can be large, the sender and receiver work in batches. At any given moment, the sender keeps track of two things:
- The send window: the current batch of data the sender is working with.
- The highest confirmed byte: the largest byte number the receiver has confirmed receiving in the past.
The receiver has the easier job: they just report the data they've received so far. For example, the receiver might report [110, 150] and [160, 200].
Looking at that diagram, the gaps jump out immediately—you can see them. But the sender isn't looking at a picture. It's comparing numbers, one block at a time. It needs an explicit algorithm to detect the same gaps you spotted at a glance. The algorithm processes each block through a decision loop—a validation step and three named cases cover every situation:
- Validation: if the end of the block is past the end of the send window, the data must be invalid. Reject the block.
- Q0: if the hole list is empty, create the first hole on the spot and skip Q1 and Q2.
- Q1: does this block patch an existing hole? If so, remove it.
- Q2: does this block push past the frontier (the highest confirmed byte)? If so, append a new hole.
Step by step
The sender starts with an empty hole list, highest confirmed byte at 99, and the receiver's first report waiting:
Block [110, 150]. The hole list is empty, so Q0 fires: the gap between the highest confirmed byte (99) and the block's start (110) becomes a new hole, [100, 109]. The sender records it, advances the highest confirmed byte to 150, and skips Q1 and Q2:
hole list: [100, 109]
Block [160, 200]. The list now has one entry, so Q0 is skipped. Q1: does [160, 200] patch the hole at [100, 109]? No—160 is past 109. Q2 fires: 160 is past the highest confirmed byte (150). A new hole, [151, 159], is appended and the highest advances to 200:
hole list: [100, 109], [151, 159]
The sender retransmits both holes. A moment later, the receiver sends a second report:
[100, 150], [160, 200]
Block [100, 150]. Q0: the list isn't empty—skip. Q1 fires: the block starts at 100, matching the hole's start, and ends at 150—past 109. The hole [100, 109] is removed.
One block can settle multiple holes, so Q1 checks the next entry too: [151, 159]. The block ends at 150, which falls short of 151—no patch. Q2: is 100 past the highest confirmed byte (200)? No. Block done.
Block [160, 200] matches no hole and isn't past the highest—nothing changes. Our hole list is now just [151, 159]. We'll have to re-retransmit that data.
Two mistakes
The walkthrough above stayed on the happy path. Two corners of the algorithm are quietly fragile—each harmless in isolation, but together they're the entire vulnerability.
You may have noticed that the flowchart's Validation step runs before anything else—before Q0, before Q1, before Q2. A receiver's SACK blocks are supposed to sit inside the sender's send window, so the code checks: is the block's end value in range? If not, it rejects the block immediately:
That check makes sense—the receiver claiming data you haven't sent yet is obviously wrong. But the flowchart only shows one check. The code validates the end value and stops there. It never validates the start. This is the first of our two mistakes:
At first glance, this looks harmless. After all, if the receiver acknowledges receiving data from the past that we've previously transmitted, it just seems redundant, not broken. Still, it's a foot in the door for an attacker.
That's the first foothold. Mistake #2 is more subtle—it lives in a structural property of Q1 and Q2 that the original code quietly relied on. Under normal traffic, Q1 and Q2 can never both fire for the same SACK block. Think about where the holes live: they're gaps in previously-sent data, sitting behind the highest confirmed byte. Q1 fires when the block's start is at or before some hole, back in the old territory. Q2 fires when the block's start is past the highest confirmed byte, out at the new frontier. A legitimate block can't be in both places at once. It either fills in old gaps (Q1) or reports new data further out (Q2), never both.
The 1998 programmer leaned on this guarantee. Q0 handles the empty list up front, and Q1/Q2 never fire together on a single block—so the list is guaranteed non-empty by the time Q2 runs. Q2 grabs the tail and opens it up without a null check.
But if the list is empty when Q2 fires, that "tail" is mailbox #0, and opening it crashes the system.
Before or After
Q1 and Q2 both ask the same kind of question: is byte a before byte b? You'd think the answer would be obvious—just subtract. On a number line, if b - a is positive, then b comes after a.
But remember, with computer integer arithmetic, we're on a clock, not a line. For example, on our clock with 16 numbers, if a = 15 and b = 2, is b before or after a? You can get from a to b by taking 3 steps clockwise (into the future), so maybe a is before b. But you can also get from a to b by taking 13 steps counterclockwise (into the past), so maybe a is after b. On a clock, mathematically speaking, there is no absolute notion of 'before' or 'after' at all. Time is a flat circle, and the past and the future smear into each other.
So how does the TCP algorithm pull off the before/after comparisons? It cheats! Here's how the cheat works. At any given moment, the send window is a tiny arc relative to the clock (the real clock has about 4 billion positions). So when the sender compares two byte numbers that both live inside the send window, they're always close together relative to the full circle. That means we can do the following: trace from a to b along the shorter arc. If the path is clockwise, a came before b. If it's counterclockwise, a came after b.
As long as the send window stays small compared to the full circle, this is fully consistent. But if the send window ever grows past the halfway mark, the cheat starts lying. The shorter way between two byte numbers in the window now cuts across empty space outside the window entirely and returns the wrong answer. The past can become the future, or vice versa.
Fortunately, in practice the send window is never even close to half the circle, so the cheat always works—for legitimate traffic. But an attacker doesn't have to play fair.
One weird trick
Let's take stock of our two mistakes:
- Mistake #1: The start value of a SACK block is never checked against the send window. An attacker can lie about where their SACK block starts.
- Mistake #2: When Q2 appends a new hole to the end of the list, it opens the tail mailbox without first checking whether it's #0. If the list is empty, the tail is #0, and opening it crashes the machine.
Neither mistake is dangerous on its own. The missing check on the start value seems harmless—the receiver's just claiming to have data we already know about, big deal. And the missing null check seems unnecessary, because there's no way the hole list could be empty when Q2 fires.
Right?
Right???
Not so fast. What if I told you you could put your start value so far in the past that it also became the future?
Then Q1 would fire because of the past data, but Q2 would also fire because of the future data! Under just the right circumstances, this could actually lead to a crash.
To pull it off, the attacker has to slip past two guards. The first is the validation step, which would normally reject any SACK block whose values fall outside the send window. Mistake #1 takes care of that—the start value is never checked. The second is Q0's safety net: when the hole list is empty, Q0 intercepts the block and skips Q2 entirely. To bypass it, the attacker has to plant a hole in the list first. That's what Block 1 is for.
We'll stick with the 16-position toy clock for the walkthrough—real TCP has roughly four billion positions, but the trick scales the same way.
Block 1: plant the hole. Mid-connection—bytes 0–7 already fully acknowledged, the current send window covering bytes 8–12—the attacker sends a single SACK block reporting [10, 12]. The target sees that bytes 8–9 haven't been confirmed, and creates a hole for them. The hole list now has exactly one entry, and the highest confirmed byte advances to 12. The target resends bytes 8–9.
Block 2: the exploit. The attacker sends [1, 9]. This is our one weird trick: we choose a start value far enough in the past that it could also be the future. On the number line, it looks like stale data. Mistake #1 means nothing rejects it.
The server processes it. Q1 compares the start value (1) to the start of the hole (8): since 1 comes before 8, the block [1, 9] covers the hole at [8, 9]. So Q1 fires and deletes the hole. The hole list is now empty. Then Q2 asks: is the highest confirmed byte (12) less than the start value (1)? On a straight number line, obviously not—12 is bigger than 1. Q2 should stay quiet. But the numbers live on a circle:
Interactive—tap to animate
From Q1's perspective, the hole starting at 8 is a short arc clockwise from 1, so 1 is in the past. This means Q1 fires and removes the hole. The hole list is now empty.
From Q2's perspective, the highest confirmed byte at 12 is a short arc counterclockwise from 1, so 1 is in the future. This means Q2 fires and tries to append to nothing. It opens the forbidden mailbox #0, crashing the system.
How did we choose our special number so that something far enough in the past also ended up in the future? It's simpler than it sounds: we just need a number on the clock opposite the send window. In a sense, it's the send window's mirror image, reflected across the clock. An attacker only needs to aim for that mirror window.
So what's the actual damage?
This bug doesn't steal your passwords or let the attacker read your files. All it does is crash the machine. That might sound mostly harmless...until you think about what's running OpenBSD.
Remember, OpenBSD is the paranoid operating system, the one chosen for firewalls, routers, etc.— the infrastructure that other infrastructure depends on. A crashed firewall means the network behind it is either exposed or offline. A crashed router means everything behind it goes dark.
Okay, but the system can recover from a crash, right? Well, once the machine reboots, nothing stops the attacker from repeating the same attack over and over. The target never stays up long enough to be useful. This is a Denial of Service attack that can take an OpenBSD machine offline indefinitely.
The fix
Either mistake, fixed alone, would have prevented the crash. But, true to their paranoid style, OpenBSD fixed both.
Fix 1—check the start value
The original code already validated the end of each SACK block against the send window. The fix adds the same kind of check for the start: if sack.start falls before the oldest unacknowledged byte, reject the block immediately. Now the attacker can't place sack.start on the far side of the circle. The packet gets thrown away before it ever touches the hole list.
Fix 2—check for null
Before appending a new hole to the list inside Q2, the code now checks that the pointer to the last entry is actually valid. Even if some future bug finds another way to empty the list unexpectedly, the code won't try to open mailbox #0. It'll just…not append, and move on.
This is defense in depth. Fix 1 prevents the attack. Fix 2 prevents the crash even if someone finds a way around Fix 1 someday.
Why it survived for twenty-seven years
The bug was easy to overlook because it lived inside the interaction between separate components. Each component individually was sound. And yet, a skilled human auditor going over the code with adversarial intent would probably spot it. Discovering the bug doesn't require a rare insight. It just requires a thorough read of one protocol.
The likely explanation is simpler: no one was doing that read. Even a project as focused on security as OpenBSD can't review every line of code with full adversarial focus every release cycle. They just don't have the bandwidth. Furthermore, old code that's run without any issues for years accumulates trust. After a certain point, no one was looking at this code at all.
So Claude Mythos didn't use its mythical AI powers to find this bug—in theory, a skilled human could have found it in exactly the same way. The difference was time and prioritization. Anthropic was able to point Mythos at the code and tell it to scrutinize the relevant subsystem, and Mythos could oblige within hours.
There are a lot of pieces of old, unexamined code like this one out there.
Aaron Demby Jones is a musician, artist, and educator based in San Diego. He has a PhD in media arts and technology from UC Santa Barbara, where he worked on computational fluid dynamics and sound synthesis. More of his work is at studiodemby.com.