Break the OS: Predict, Break, Diagnose
Six buttons that push a simulated system past a limit — memory, locks, threads, disk, descriptors, the stack — each paired with what the real symptom looks like in logs and tools, and the one diagnostic question that separates the failure from its look-alikes.
The problem
How to use the six buttons
Each button injects one fault into the running model from The OS Simulator: Cores, Processes, RAM, I/O and Locks. The discipline is the same every time. Predict: before pressing, write down which panel will move first and in which direction. Break: press it and watch. Diagnose: using only what the panels show — not the label on the button — answer the diagnostic question for that fault. Then read the “real symptom” column and find the same shape in the tool output you would see on a Linux host.
The point of the diagnostic question is that real failures do not come with a button label. “High CPU” is four different faults (The OS Debugging Playbook: Four Symptoms, Fourteen Causes); a hung process is three. The simulator lets you learn the *distinguishing* observation for each while you know the answer, so that in production you can run the distinction the other way round.
- Every value shown is
Simulated; the symptom column describes real Linux tools and log lines, and is labelledlinux. - Reset between faults, or stack them deliberately — a deadlock under memory pressure is a realistic incident.
The six faults
Each row is one button. The simulator column is what the panels do; the real column is what you would see on a Linux host; the question is what you must be able to answer from evidence alone.
Read the rows in pairs. Exhaust memory and Spawn 10,000 threads both raise memory (frames vs stacks) but differ in CPU; Create deadlock and Block disk I/O both stop a process cold but differ in the state letter; Exhaust file descriptors and Trigger stack overflow both fail a single operation but one leaves the process alive and one kills it. The diagnostic question in the last column is always the observation that separates the row from its neighbour.
| Button | What the simulator shows | What the real symptom looks like (Linux) | The diagnostic question |
|---|---|---|---|
| Exhaust memory | frames in use → 100%, page faults spike, throughput collapses, then the largest process is killed and memory frees | dmesg: Out of memory: Killed process 4121 (node) total-vm:… anon-rss:…; exit code 137; container OOMKilled: true; before the kill, vmstat shows si/so and high %wa | Is memory growing without bound (leak) or is the working set legitimately larger than RAM (undersized)? Look at the growth curve: linear-forever vs plateau. |
| Create deadlock | two threads move to blocked, each waiting on the lock the other holds; their wait time grows forever; the cores go idle; throughput of the affected process → 0 | process in S state with ~0% CPU; thread dump (gdb -p, jstack, py-spy dump) shows two threads inside lock acquisition with opposite lock orders; strace -p shows futex(…, FUTEX_WAIT, …) and nothing else | Are the blocked threads waiting on each other (cycle) or on something external (I/O, a remote)? A cycle in the wait-for graph is a deadlock; a chain that ends outside is a hang. |
| Spawn 10,000 threads | ready queue explodes, switch overhead % climbs past 30%, throughput falls below the 4-thread baseline; memory rises with per-thread stacks | top shows 100% CPU with high %sy; top -H shows thousands of threads each at ~0%; pthread_create eventually fails with EAGAIN; cat /proc/sys/kernel/threads-max, ulimit -u; vmstat cs column in the hundreds of thousands per second | Is the CPU doing work or switching? Compare user time to system time and look at cs in vmstat: high %sy + high cs + falling throughput is switching, not work. |
| Block disk I/O | threads pile into blocked-on-I/O; cores idle; I/O queue length grows; latency per operation climbs | process in D (uninterruptible sleep) state; kill -9 has no effect until the I/O returns; load average climbs while CPU is idle; iostat -x shows %util at 100 and await in seconds; cat /proc/<pid>/stack shows a filesystem or block-layer wait | Is the process waiting for the kernel (D state, disk) or for another process or the network (S state, recv/futex)? The state letter in ps answers it. |
| Exhaust file descriptors | the descriptor table fills; new opens and accepts fail; existing connections keep working; the socket count stops rising at the limit | accept: Too many open files / EMFILE in logs; ls /proc/<pid>/fd | wc -l equals ulimit -n; lsof -p shows what they are — often thousands of sockets in CLOSE_WAIT; ss -s | Is the count a leak (steadily rising, dominated by one kind — CLOSE_WAIT sockets, one file path) or legitimate load exceeding a 1024 default? Look at what the descriptors *are*. |
| Trigger stack overflow | one thread’s stack pointer walks down past its limit, the thread faults on the guard page, and the process dies | Segmentation fault (core dumped) with exit 139; dmesg: segfault at 7ffc… ip … sp 7ffc… error 6; core file backtrace shows the same frame thousands of times; JS RangeError: Maximum call stack size exceeded; Python RecursionError | Is this a SIGSEGV from a bad pointer or from a full stack? A backtrace with one function repeated thousands of times, or a fault address just below the stack mapping in /proc/<pid>/maps, is the stack. |
What each fault teaches about the real mechanism
Exhaust memory is Memory Pressure, Swap and the OOM Killer end to end: reclaim first (page cache dropped, then anonymous pages swapped), the thrashing knee, then the OOM killer choosing by oom_score — largest RSS wins, adjusted by oom_score_adj. In a container it is the cgroup OOM killer (Containers Are Processes With the Kernel’s View Narrowed) and the host is fine. The log line names the victim, its total-vm and anon-rss; a victim whose anon-rss had been growing linearly for days is a leak, one that plateaued is undersizing.
Create deadlock is Deadlocks: four conditions, and the observation that identifies it is a cycle in the wait-for graph (Cycle Detection). The simulator draws the graph; in production you build it from a thread dump by hand. Spawn 10,000 threads is Context Switching plus per-thread memory — each thread reserves a stack (8 MB virtual by default with glibc, committed on touch), so 10,000 threads is 80 GB of virtual address space and the scheduler’s run queue is now a large data structure. The C10K: Ten Thousand Connections, Then a Million lesson is the cure.
Block disk I/O is the D state from Process States and Follow a File Read: a thread inside the kernel waiting for a device cannot be interrupted, even by SIGKILL, until the I/O completes — which is why the load average rises while CPU sits idle (Linux counts D-state tasks in load) and why a dying NFS mount can make a machine look hung. Exhaust file descriptors is File Descriptors at its limit: ulimit -n (often a 1024 soft default), EMFILE for the per-process limit, ENFILE for the system-wide one, and the near-universal cause — sockets the peer closed that the application never did (too-many-open-files). Trigger stack overflow is Stack Overflow: the guard page below the stack mapping turns runaway recursion into a SIGSEGV, and language runtimes convert it into a catchable error only because they check depth before the guard page is reached.
Look-alikes
The reason to drill the diagnostic questions is that each fault has a twin with a different fix. Exhausted memory and a large legitimate cache both fill RAM; only one is a bug. A deadlock and a thread blocked on a dead database both look like a hung process at 0% CPU; D vs S and the thread dump separate them. 10,000 threads and an infinite loop both show 100% CPU; %us vs %sy and vmstat’s cs separate them. EMFILE from a leak and EMFILE from 2,000 legitimate connections against a 1024 limit have opposite fixes. A stack overflow and a null dereference are both SIGSEGV; the fault address and the backtrace separate them.
Run each button, then its twin from the playground’s controls (add a large cache instead of a leak; add I/O instead of a lock; add legitimate connections instead of leaking them), and check that you can tell them apart from the panels alone. That skill is the whole of The OS Debugging Playbook: Four Symptoms, Fourteen Causes.
Key points
- Predict which panel moves first; break; diagnose from evidence, not from the button label.
- OOM:
dmesgnames the victim, exit 137; the growth curve separates a leak from undersizing. - Deadlock:
Sstate, ~0% CPU,futexinstrace, a cycle in the wait-for graph from a thread dump. - Thread explosion: high
%sy, hugecsinvmstat, falling throughput; 10,000 stacks is 80 GB virtual. - Blocked disk:
Dstate, immune tokill -9, load average up with CPU idle,iostatawaitin seconds. - Descriptor exhaustion:
EMFILE,/proc/<pid>/fdcount atulimit -n, usuallyCLOSE_WAITsockets. - Stack overflow: exit 139, one frame repeated thousands of times, fault address just below the stack mapping.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why break a simulator instead of a test machine?
A real OOM kill or D-state hang takes minutes to set up and can take the machine with it; a simulated one is instant, repeatable and shows the internal state — the wait-for graph, the frame table — that a real kernel does not expose directly.
▸Why does every fault come with a diagnostic question?
Because in production the fault is unlabeled and has a twin. The question is the observation that distinguishes the pair; learning the fault without the question leaves you guessing at 3 a.m.
▸Why does a hung process in `D` state ignore `kill -9`?
The thread is inside the kernel holding resources that cannot be released until the device answers. Delivering the signal happens on the way back to user mode, and there is no way back until the I/O completes.
Break the OS
All six subsystems are healthy. Pick a way to break the machine.
How it fails
What the failure looks like from inside real software.
- Restarting the process on every OOM kill without reading
dmesg: the leak returns in an hour, and the log line already saidanon-rsswas 15 GB. - Treating a
D-state hang as a deadlock and hunting for locks while a dying disk or NFS mount is the actual cause. - Raising
ulimit -nto 1,000,000 to “fix”EMFILEwhenlsofwould have shown 60,000CLOSE_WAITsockets — the leak now takes a day instead of an hour. - Adding cores to a thread-explosion incident;
%syandcsgo up, throughput does not. - Catching
RecursionErrorand retrying; the recursion is unbounded and the retry overflows again.