Autopsy of a Crash (Part 1): Learning to Read a Program's Corpse
Welcome to the Level-Up series’ “Autopsy of a Crash” trilogy.
A few months ago, OpenAI’s internal Rockset service — the one that powers ChatGPT’s search-through-conversation-history feature — started dying in bizarre ways. A perfectly normal C++ function would finish its job, try to go home, and then jump to some ghost address and drop dead. Even weirder: in some cases, the “return address” was empty. Imagine following your GPS home, except the GPS leads you off a cliff, and at the bottom of the cliff there isn’t even a road.
This kind of death “shouldn’t exist.”
To crack this case, an investigator first needs to learn anatomy — understand what a program’s body looks like and how hitting-a-wall death actually happens. This is lesson one (◕‿◕)
🏰 Floor 0: The City Has an “Impossible Murder”
OpenAI’s engineers noticed something eerie about these deaths: every victim died at the exact moment they were “going home.”
In the world of programs, “going home” works like this: function A calls function B, and when B finishes its job, it “returns to A” to continue. This is the most basic way programs work. It happens billions of times a day. It’s rock solid.
But these deaths were different.
- Some victims had their return addresses overwritten with
NULL— like having the GPS wiped clean, navigating to address 0 - Some victims had their stack pointer (
%rsp) mysteriously shifted by 8 bytes, as if someone had quietly turned a road sign a few degrees - Both types of deaths happened the instant the
ret(go home) instruction ran
These weren’t ordinary bugs. Ordinary bugs write to the wrong place or calculate the wrong result. But the odds of “the return address just happens to become NULL” are so low that the engineers briefly suspected the kernel was broken, or the compiler had a bug, or the hardware was failing.
Mogu going off-topic:
Don’t worry about figuring out “why this happens” yet — that’s Post B and Post C territory. Post A’s mission is: learn to read a program’s corpse. You want to solve a murder? First you need to know anatomy.
This lesson builds four core concepts:
%rip: Where the victim is right now- Stack: A stack of visit notes
call/ret: How to visit someone, how to go homesegfault: Hitting-a-wall death
Once you understand these four things, you can read a crime scene.
What do these deaths have in common?
The eerie thing about these deaths is that every victim died the instant they executed the return instruction. Return addresses were overwritten with NULL, or stack pointers were shifted — these 'shouldn't happen' situations made investigation extremely difficult.
正確答案是 C
The eerie thing about these deaths is that every victim died the instant they executed the return instruction. Return addresses were overwritten with NULL, or stack pointers were shifted — these 'shouldn't happen' situations made investigation extremely difficult.
🏰 Floor 1: %rip — Where the Victim Is Right Now
A CPU only executes one instruction at a time. Like how a person can only be in one place at a time.
So how does the CPU know “which instruction to execute right now”? The answer is a register called %rip.
Mogu roast time:
The full name of
%ripisinstruction pointer. Therat the front is an x86-64 naming convention indicating a 64-bit register. Andripjust happens to sound likeR.I.P.(rest in peace) — coincidence? When investigating program deaths, the pun is surprisingly fitting (¬‿¬)
Here’s roughly how the CPU works:
- Look at where
%rippoints - Go to that address and read an instruction
- Execute the instruction
- Move
%ripto the next instruction - Repeat
When a program crashes, the value of %rip is “where the victim was last.” The first thing an investigator does at a crime scene is usually checking what address %rip stopped at.
If %rip points to “somewhere that isn’t an instruction” — say, a chunk of data, or an address that doesn’t even exist — the program dies. Because the CPU tries to read an instruction at that address, finds it isn’t an instruction, and the kernel kills the program.
What is the function of %rip?
%rip (instruction pointer) always points to the memory address of the instruction the CPU is 'about to execute right now.' When a program crashes, %rip's value is the victim's last GPS location.
正確答案是 B
%rip (instruction pointer) always points to the memory address of the instruction the CPU is 'about to execute right now.' When a program crashes, %rip's value is the victim's last GPS location.
🏰 Floor 2: Stack — A Stack of Visit Notes
Programs don’t have just one function. Function A calls B, B calls C, C might call D. This layered chain of “visits” needs a mechanism to record “where to go back afterward.”
That mechanism is the stack.
This stack of notes has some important properties:
- Last In, First Out (LIFO): The last note put down is the first one torn off
- Each note has a “return address”: Tells the CPU “where to go after this function finishes”
%rsp: A finger that always points at the top note
Mogu butts in:
The full name of
%rspisstack pointer— thespis short for those two words. This “finger” is crucial — when the CPU needs to find the top note, it looks at where%rsppoints. If this finger gets moved around randomly, the entire “going home” mechanism collapses. This is why that phrase “stack pointer shifted by 8 bytes” sounds so terrifying.
Each note occupies a block of space on the stack called a stack frame. A typical stack frame contains:
- Return address: Where to go after this function finishes
- Local variables: Data this function uses internally
- The previous layer’s
%rbp(frame pointer): Like a bookmark, helps with positioning
When function A calls function B, what happens to the stack?
When calling a new function, a new note (stack frame) is put on top of the stack, recording the return address. This way, after B finishes, the CPU knows where to go back to in A.
正確答案是 B
When calling a new function, a new note (stack frame) is put on top of the stack, recording the return address. This way, after B finishes, the CPU knows where to go back to in A.
🏰 Floor 3: call / ret — Visiting and Going Home
Now let’s map the visit-note analogy to actual CPU instructions.
When function A wants to call function B, it executes the call instruction. call does two things:
- Put down a note: Push “the address of A’s next instruction” onto the stack (this is the return address)
- Jump there: Set
%ripto the address of B’s first instruction
When function B finishes and wants to return to A, it executes the ret instruction. ret also does two things:
- Tear off the note: Pop the return address from the top of the stack
- Jump back: Set
%ripto that return address
There’s also a register called %rbp (frame pointer) that works like a bookmark clipped to one of the notes. The compiler uses it to locate “where the current function’s stack frame is.” When debugging, the investigator can start from %rbp and trace upward layer by layer, reconstructing the entire call chain.
Mogu inner monologue:
The
bpin%rbpstands forbase pointer. Some compilers will omit%rbpfor performance (with the-fomit-frame-pointerflag), but Rockset did the opposite — they enabled-fno-omit-frame-pointer, so the investigator could trace the entire call chain from%rbp. This seemingly minor compiler flag ended up saving their lives — that’s foreshadowing, remember it.
The whole mechanism is incredibly stable — as long as the return address on the note is correct, ret will obediently jump back to the right place.
But what if the note gets tampered with?
What happens when the ret instruction executes?
ret's job is simple: pop the return address from the top of the stack (tear off the note), then set %rip to that address (follow the note home). As long as the return address is correct, everything works fine; but if the address gets changed to an invalid value, disaster strikes.
正確答案是 B
ret's job is simple: pop the return address from the top of the stack (tear off the note), then set %rip to that address (follow the note home). As long as the return address is correct, everything works fine; but if the address gets changed to an invalid value, disaster strikes.
🏰 Floor 4: segfault — Hitting-a-Wall Death
Now you understand the call / ret mechanism. Next: how exactly does a program die?
Suppose the return address on the stack gets changed to an address that “isn’t an instruction” — maybe a chunk of data, maybe a virtual address that doesn’t map to any physical memory, maybe NULL (address 0).
The ret instruction won’t check if the return address makes sense. It just:
- Pops that number from the stack
- Sets
%ripto that number - Tries to execute the “instruction” at that address
If that address isn’t actually an executable instruction, the CPU throws a fault. The kernel receives it and kills the program. That’s a segfault.
Mogu chimes in:
The full name of
segfaultissegmentation fault, from the early days when operating systems divided memory into different “segments.” When a program tries to access a segment it doesn’t have permission for (or one that doesn’t exist), it triggers a fault. Modern OS segfaults are usually about accessing invalid virtual addresses, not necessarily about “segmentation,” but the name stuck. When engineers say “it segfaulted,” they basically mean “it hit a wall and died.”
In OpenAI’s batch of deaths, the victims’ return addresses were changed to NULL (address 0). The ret instruction set %rip to 0, the CPU tried to read an instruction at address 0, but address 0 is usually deliberately left unmapped (to catch “null pointer access” bugs), and the kernel killed the program.
The core question emerges: Who tampered with the note?
How does a segfault (hitting-a-wall death) happen?
A segfault happens when %rip points to an invalid or non-executable address. The CPU tries to read an instruction at that address, fails, and the kernel kills the program. In these murders, the return address was changed to NULL, ret followed it, and hit a wall.
正確答案是 B
A segfault happens when %rip points to an invalid or non-executable address. The CPU tries to read an instruction at that address, fails, and the kernel kills the program. In these murders, the return address was changed to NULL, ret followed it, and hit a wall.
🏰 Floor 5: C++ Has No memory safety — A City with No Guardrails
If the call / ret mechanism is so stable, how could the note get tampered with?
The answer has to do with C++ as a language.
C++ gives programmers “low-level control” — you can directly manipulate memory, directly control pointers, directly decide where data lives. This control brings extreme performance, but also risk: nothing stops you from writing to places you shouldn’t write.
This “can write anywhere” property is called “lacking memory safety” in programming language terms.
By contrast, some languages have “guardrails”:
- Rust: Has a
borrow checkerthat verifies memory access legality at compile time - Languages with garbage collection (Java, Go, Python): Manage memory at runtime, don’t let programs write wherever they want
Those languages have guardrails in their cities. Want to write on someone else’s notes? The compiler or runtime blocks you.
C++ chose: no guardrails, let programmers be responsible. The upside is performance — no overhead from guardrails. The downside: once you write to the wrong place, the note gets tampered with, you hit a wall and die, and it’s very hard for investigators to catch the culprit.
Mogu going off-topic:
OpenAI’s Rockset service chose C++ for performance and memory efficiency — processing massive real-time queries requires squeezing out every bit of performance. But the trade-off is: when bugs happen, you get these bizarre “tampered note, hit-wall death” murders. This is the classic C++ trade-off: the price of performance is “no guardrails.”
Why can the return address on a C++ program's stack get tampered with?
C++'s design philosophy is 'trust the programmer,' no guardrails. This means any piece of code can write to any piece of memory — including return addresses on the stack. If some code accidentally or due to a bug writes to the wrong place, the note gets tampered with.
正確答案是 B
C++'s design philosophy is 'trust the programmer,' no guardrails. This means any piece of code can write to any piece of memory — including return addresses on the stack. If some code accidentally or due to a bug writes to the wrong place, the note gets tampered with.
🏰 Boss Floor: Re-enacting the Murder
Now the investigator has enough anatomical knowledge to re-enact the murder.
Let’s connect the clues:
- Function A calls function B: The
callinstruction puts a note on the stack, writing “after B finishes, return to some address in A” - Function B executes normally: Does its job, no problems
- At some point, the note gets tampered with: The return address becomes
NULL(address 0) - Function B executes
ret: Pops the return address from the stack (nowNULL), sets%riptoNULL - CPU tries to execute the instruction at address 0: Address 0 doesn’t map to any memory
- Hits wall and dies: The
kernelkills the program, leaving a corpse (crime scene)
The investigator now knows “how they died”:
%ripended up pointing toNULL- The return note on the stack was changed to
NULL retfollowed it, then hit a wall
But the key question remains: Who tampered with the note?
Staring at a single corpse won’t reveal the killer.
The engineers tried many things: reading code, making hypotheses, ruling them out one by one. But every hypothesis had counterexamples. Some deaths had stack pointers shifted by 8 bytes, others had return addresses set to NULL — they looked like the same cause of death, but the details didn’t quite match.
It’s like facing a series of murders where every corpse has slightly different wounds. The investigator hit a wall.
To crack this, you need a different approach.
Not staring at one corpse, but pulling out every corpse from the entire city’s past year, cataloging them, classifying them. This is the epidemiologist’s approach: find patterns, don’t solve individual cases.
Mogu roast time:
A small spoiler: OpenAI’s engineers eventually discovered that what looked like one bug was actually two completely unrelated bugs. One was hardware failure (a certain Azure host’s CPU was doing arithmetic wrong), the other was a race condition that had been hiding in libunwind for 18 years. Because they kept treating it as a single bug, they couldn’t catch either. Once they looked at them separately, the answers became instantly clear. How they did that is the main course in Post B.
The investigator has learned to read corpses. Next lesson: entering the crime scene.
What does the investigator currently know vs. not know?
The investigator now understands the cause of death (ret to invalid address → hit wall and die) and mechanism (return note on stack was tampered with), but still doesn't know who did it or when. Staring at a single corpse won't give answers — you need an epidemiologist's approach.
正確答案是 B
The investigator now understands the cause of death (ret to invalid address → hit wall and die) and mechanism (return note on stack was tampered with), but still doesn't know who did it or when. Staring at a single corpse won't give answers — you need an epidemiologist's approach.
🎓 Level Clear Summary
The investigator learned four things in this lesson:
| Concept | One-Sentence Explanation |
|---|---|
%rip | Where the victim is right now — which instruction the CPU is executing |
| Stack | A stack of visit notes, recording “where to return” for each function layer |
call / ret | Leave a note before going out / Tear off note and follow it home |
segfault | Note tampered with to invalid address → ret follows it → hits wall and dies |
The investigator also learned about C++, a city with no guardrails: anyone can tamper with anyone’s notes. Extremely high performance, but when things go wrong, it’s very hard to catch the culprit.
Now the investigator understands “how they died,” but still doesn’t know “who did it.”
Next lesson, the investigator enters the crime scene — that sealed autopsy report (the core dump). You’ll learn what the “police tape line” (red zone) is, what the “first responder on scene” (signal handler) does, and why looking at individual cases will never catch the killer.
Upgrading from coroner to epidemiologist is the only way to crack this case (⌐■_■)
🔗 Further Reading
Post B will heavily use the signal mechanism — that SIGUSR2 that gets sent every few milliseconds and eventually becomes the murder weapon is part of this family. Want to warm up, or like this “breaking down low-level concepts for beginners” tower-climbing style? These two from the same series are good follow-ups:
- Unix Signals 101 — SIGUSR1 vs SIGTERM vs SIGKILL
- A URL’s Journey — From Pressing Enter to the Screen Lighting Up
(Post B coming next)