Calif published a write-up on 16 October called Big Fish, with exploit code, demonstrating local root on OpenBSD 7.9 with KARL, SMAP and SMEP enabled. In the authors' words, they "managed to get local root in OpenBSD with KARL / SMAP / SMEP enabled without the use of an information disclosure vulnerability".

That last clause is the whole story, and almost every write-up of it led with the age of the bug instead.

The two bugs

The first is a counter. OpenBSD's shared memory attachment count, shm_nattch, was defined as a signed 16-bit short in the original BSD sources, which the project inherited roughly 37 years ago. Push it past 32,767 and it wraps negative.

Calif does that with arithmetic rather than cleverness: map 127 SysV shared memory segments, unmap them, and fork 259 processes to drive the attach count to -32,643. Combined with a race, the wrap produces a use-after-free. Fixed in errata 026.

The second is a separate out-of-bounds write, described as a conditional write of the value -1 landing outside its intended bounds. Fixed in errata 018.

Neither carries a CVE.

The chain, and why the mitigations did not stop it

The interesting engineering is what happens after the use-after-free.

Calif grooms the kernel heap using sysctl memory statistics, watching allocation totals to see when new pages are taken. Then it wins a race between removing a shared memory segment and attaching a size-confused one, which lets it place a chosen object in freed space.

That object is a struct ip_moptions, a 16-byte allocation obtained by setting the IP multicast time-to-live on a socket to 2. Its final word packs two 16-bit counters: the number of multicast memberships and the maximum. The out-of-bounds write overwrites both.

Then the trick. Calling close on the socket makes the kernel walk the membership list and perform atomic decrements. With the counters corrupted, those decrements land on the page table's self-referential mapping, and they land on it twice — which changes a mapping the process already owns from user-mode to supervisor-accessible.

That is the SMAP and SMEP bypass, and it is why no information leak was needed. The attacker does not have to learn where kernel memory is. They convert memory they already have into memory the kernel will execute from.

The final step overwrites a kernel function pointer, and Calif says the exploit "reliably targets" the ioctl handlers for the lo0 and enc0 interfaces, using one for control flow and the other to put the original pointer back.

Reliably is doing work in that sentence

Why can an exploit written elsewhere predict where a kernel pointer lives on your machine? Because, as Calif explains, OpenBSD's kernel allocators are deterministic. The boot-time and best-fit allocators have no base address randomisation, so early allocations land at the same addresses across boots on the same hardware.

KARL randomises the kernel layout at each boot by relinking it. It is a real mitigation and it was enabled. It does not help when the thing you need is not a kernel symbol but an allocation whose address is a function of allocation order, and allocation order at boot is the same every time.

This is the same shape as the Chrome chain we covered on Wednesday, where the middle link was rated as having no security impact on its own. A mitigation that holds against the bug class it was designed for can still be walked around by a chain that never attacks it directly.

The authors did the useful part

Calif did not stop at the exploit. It proposes base address randomisation for the allocators, which it calls "effectively free", and randomisation of runtime allocations, which "does incur some overhead". It also proposes randomising the page table self-referential mapping and safe unlinking for the list operation that provided the write, with example implementations rather than finished patches, and notes that if page table randomisation is unsuitable there are other options, such as making the mapping read-only and toggling the write-protect bit during modification.

That is what a research write-up should look like. The bug reports went in, the errata shipped, and the proposed hardening addresses the technique rather than the two bugs.

What to do

  • Run syspatch. Both errata are out for 7.9 on amd64, i386 and arm64, and the chain needs both bugs.
  • If you run OpenBSD anywhere that untrusted users get a local shell, treat this as urgent rather than interesting. The exploit code is public.
  • Do not read "no CVE" as "not serious". OpenBSD fixes through errata and does not routinely request identifiers; the absence of one says nothing about the bug.
  • If you maintain anything with a counter inherited from the 1980s, the lesson is not that old code is bad. It is that a type chosen when 32,767 was an unreachable number becomes a bug when the thing counting it becomes cheap.

What is not established

  • The release dates of errata 018 and 026. Calif's page links the patches but does not date them, and the dates circulating come from secondary summaries.
  • Whether either bug has been exploited outside research. No evidence of in-the-wild use has been published.
  • Whether other BSD derivatives that inherited the same declaration are affected. Calif addresses OpenBSD only, and nobody has published an answer for the others.
  • Whether OpenBSD will adopt any of the proposed hardening. The example implementations are Calif's, not the project's.