OpenBSD Big Fish Exploit: 37‑Year‑Old Kernel Bug Leads to Local Root

TL;DR

The researchers at Calif uncovered a 37‑year‑old integer overflow in OpenBSD’s SysV shared‑memory attachment counter, combined it with a race condition and an out‑of‑bounds write, and obtained a local root shell despite modern mitigations such as KARL, SMAP and SMEP. The bug was fixed in OpenBSD 7.9 errata 026 and 018, and the write‑what‑where primitive reveals several avenues for hardening the kernel allocator and list‑unlinking code.


The Vulnerability

What happened: A signed 16‑bit shm_nattch counter in sys/shm.h overflowed when more than 32,767 shared‑memory attachments were created, wrapping to a negative value and enabling a use‑after‑free via a race condition. The overflow originates from the original BSD source code written around 1987.

Why it matters: The overflow allows an attacker to manipulate kernel memory structures, leading to a write‑what‑where primitive that bypasses SMAP/SMEP and grants kernel code execution, ultimately giving a root shell on a system that is otherwise considered highly secure.

Fixes applied: OpenBSD 7.9 errata patches 026 (sysvipc) and 018 (shmat) address the counter overflow and the out‑of‑bounds write respectively.


Exploit Overview

Core idea: The exploit drives the shared‑memory attachment counter into the negative range, then races IPC_RMID against a new attachment to free a kernel object while still holding a reference. An out‑of‑bounds write overwrites fields in a struct ip_moptions, which later triggers an atomic decrement that flips a page‑table entry from user‑ to supervisor‑accessible, allowing arbitrary kernel writes.

Step‑by‑step summary:

  1. Wrap the counter: Map 127 SysV shared‑memory segments, unmap them, and fork 259 processes to push shm_nattch to –32,643.
  2. Heap grooming: Use sysctl(KERN_MALLOCSTATS) to monitor kernel malloc statistics and time allocations.
  3. Race condition: In one process call IPC_RMID while a helper process attaches a size‑confused segment, creating a use‑after‑free.
  4. Prepare socket structure: Allocate a persistent struct ip_moptions via an IP socket and set IP_MULTICAST_TTL=2 to place controllable counters in the structure.
  5. Out‑of‑bounds write: Exploit a conditional ‑1 write to corrupt the imo_num_memberships and imo_max_memberships fields.
  6. Trigger atomic decrement: Closing the socket walks the membership list, causing two atomic decrements that rewrite the self‑referential PTE mapping, making a user‑controlled page supervisor‑accessible.
  7. Write‑what‑where: Overwrite a predictable kernel pointer (e.g., an ioctl handler) with a pointer to attacker‑controlled shellcode, then restore the pointer after gaining root.

Result: A stable local‑root shell on OpenBSD 7.9 with KARL, SMAP, and SMEP enabled, without needing any information‑disclosure bug.


Mitigation Landscape

Immediate patches

  • Errata 026 – fixes the signed‑short overflow in the shared‑memory attachment counter.
  • Errata 018 – patches the out‑of‑bounds write in shmat handling.

Structural hardening proposals

Proposal What it protects Implementation hint
Safe‑unlinking Prevents use‑after‑free via corrupted doubly‑linked lists (TAILQ_REMOVE). Add invariant checks before unlink; abort/panic on violation. FreeBSD/NetBSD already ship this in sys/queue.h.
Randomize self‑referential PTE map Stops deterministic atomic‑decrement attacks on the page‑table base. Choose a random PML4 slot at boot (320 + (rand & 127)) and install the recursive entry there.
Non‑deterministic kernel allocator Breaks predictable allocation addresses used for pointer overwrites. Randomize the boot‑time allocator base address and randomize allocation order at runtime (see CalANDif commits).
Compiler‑level sanitizers Catches out‑of‑bounds array writes and use‑after‑free before they become exploitable. Enable Clang’s Array Bounds Sanitizer (UBSan) in release builds; future AllocTokens can isolate pointer‑containing allocations.

Why OpenBSD’s existing mitigations were insufficient

  • KARL randomizes function and data addresses but does not randomize allocator base addresses, leaving early‑boot structures at predictable locations.
  • SMAP/SMEP block user‑mode access to supervisor memory, yet the exploit flips a PTE entry to supervisor‑accessible, bypassing these protections.

Community Insight

"It’s remarkable that a bug survived 37 years in one of the most heavily audited codebases, which shows how hard some kernel bugs are to catch by review alone. Do you think fuzzing or static analysis could realistically have found this class of bug, or did it need a human thinking about the exploit primitive?" – HN comment by @ozereray1

The discussion highlights that even extensive manual review cannot guarantee the absence of subtle integer‑overflow bugs, especially when they require a specific exploitation primitive. Automated fuzzing that exercises large numbers of shmat/shmdt sequences with varied fork patterns could, in theory, surface the overflow, but the race‑condition and out‑of‑bounds write would likely need targeted symbolic execution or manual reasoning.


Takeaways for Kernel Security Engineers

  1. Audit legacy data structures – Signed‑short counters and other narrow integer fields are prime candidates for overflow when legacy code is ported forward.
  2. Assume deterministic allocators are exploitable – Predictable allocation addresses enable write‑what‑where primitives; randomization at boot and runtime is a low‑cost mitigation.
  3. Layered defenses matter – Even with address‑space randomization, KASLR, and SMAP/SMEP, an attacker can still gain code execution by corrupting the page‑table hierarchy itself.
  4. Leverage existing safe‑unlink implementations – FreeBSD and NetBSD already protect against corrupted doubly‑linked list removals; OpenBSD can adopt the same technique with minimal impact.
  5. Adopt compiler sanitizers early – Enabling UBSan’s array‑bounds checks and preparing for AllocTokens can catch many out‑of‑bounds writes before they reach production kernels.

References

Sources