Guide · Backend
OpenBSD's October 2026 Big Fish Bug Explained: When a 16-Bit Counter Wraps
18 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The OpenBSD Big Fish bug is a flaw in the kernel, the core of the operating system. A counter of attachments to one block of shared memory could wrap past 32,767 to a negative number.
- Calif, the security firm that published it, says the wrong count could lead to a use-after-free: memory freed while still in use. Combined with a second kernel bug, it gave a normal user root.
- It is local privilege escalation: someone who can already run code on the machine becomes root, the all-powerful admin account. Nobody can use it over the network alone.
- Fixes ship as errata, OpenBSD's numbered patches: 018 and 026 on 7.9, and 054 and 062 on 7.8. Run syspatch, the patch tool, then reboot. As of 19 October 2026, no CVE, the public ID for a flaw, is listed.
- Beginners can stop after the patching section. Engineers then get what most write-ups skip: how the two errata map to the two bugs, and why the usual sanitizer, a compiler run-time check, misses this exact wrap.
What goes wrong when a counter that guards memory overflows?
The program can free memory while someone is still using it. That is the lesson of the OpenBSD Big Fish bug, which Andrew Griffiths of the security firm Calif published on 16 October 2026.
Picture a hall with a door counter that only shows four digits. After 9,999 guests it rolls back to zero. The caretaker sees zero, thinks the hall is empty, and locks up with people still inside.
Where the picture breaks: the Big Fish counter does not roll to zero. It jumps to a negative number, and the kernel acts on that wrong number, not a caretaker.
Picture a common setup. A small hosting team runs a dozen OpenBSD 7.9 firewalls and four shared build servers. Forty customer developers have their own logins on the build servers. The setup is made up, but the bug is real.
The team has two questions. Which machines are exposed, and what must be patched today? This guide answers both, then shows engineers how to stop the same bug in their own code.
Which words do you need before reading about Big Fish?
- Kernel: the core of the operating system. It controls memory and hardware, so whoever controls it has root, the master key.
- Local privilege escalation: a normal user on the machine turns themselves into root. The attacker needs a login or some code already running.
- Shared memory segment: one block of memory that several processes, meaning running programs, can see at once, like a shared whiteboard in an office.
- Fork: a process makes a copy of itself, called a child. Calif's write-up says repeated forks of a process with shared memory attached push the count up.
- Reference count: a number that tracks how many users hold an object. When it reaches zero, the object is freed.
- Use-after-free: code keeps using memory after it was freed and perhaps handed to something else.
- Integer overflow: a number grows past the largest value its type can hold and wraps round to a wrong value.
- Errata: OpenBSD's numbered fixes for one release, listed on a page per release, such as the 7.9 errata page.
What is the OpenBSD Big Fish bug?
It is a kernel bug in OpenBSD's System V shared memory code, the classic Unix way for processes to share memory. A counter called shm_nattch counts current attaches to a segment. Calif describes it as a signed 16-bit short, a C number type.
Calif traces that type roughly 37 years back, to the original BSD sources. The OpenBSD shmctl manual page, last changed on 2 September 2026, before the errata, lists shm_nattch as a short. We could not check whether the fixes changed the type, as of 19 October 2026.
Repeatedly forking a process that has shared memory attached could push the counter past 32,767. It then wrapped to a negative value. Calif says this overflow could lead to a use-after-free through a race condition, where the result depends on timing.
A race alone did not give root. Calif combined it with a separate out-of-bounds write in the kernel, a write past the end of a block of memory. Together they gave local root.
| Fact | Detail |
|---|---|
| Write-up | By Andrew Griffiths at Calif, published 16 October 2026 |
| Counter | shm_nattch, a short; Calif calls it signed 16-bit, maximum 32,767 |
| Impact | Local privilege escalation to root, combined with a separate out-of-bounds write |
| Kernel defences that were on | KARL, SMAP and SMEP (kernel hardening, explained below); no information leak bug was needed |
| Fixes on OpenBSD 7.9 | Errata 018 (14 September) and 026 (30 September) |
| Fixes on OpenBSD 7.8 | Errata 054 and 062, same titles and dates |
| CVE | None listed by Calif or on the errata pages |
For the hosting team, the answer is quick. All 16 machines run OpenBSD 7.9, and none has the September errata yet.
How does System V shared memory count its users?
The kernel keeps one attach count per segment. The shmat call maps a segment into a process's memory, and shmdt unmaps it. The shmctl manual page describes shm_nattch as the number of current attaches.
A segment marked for removal with IPC_RMID is destroyed only when no process uses it any more. So the count is the kernel's answer to one question: is anyone still here? On OpenBSD, new callers may even attach to a segment already marked for removal.
You can see these counts yourself. On OpenBSD, ipcs -mo lists active shared memory segments and, per its manual page, the number of processes attached to each. This shows use, not exposure. An attacker can create a segment of their own, so an empty list is no reason to skip the patch.
How one segment tracks its users
Drag to exploreNotice that every path into the segment raises one number, and only that number tells the kernel the segment is still in use.
Diagram connections:
- Build job process to shmat call (calls)
- shmat call to Shared memory segment (maps)
- Build job process to Forked child (forks)
- Forked child to shm_nattch counter (adds 1)
- Shared memory segment to shm_nattch counter (tracked by)
- shm_nattch counter to IPC_RMID removal (zero users means free)
- Build job process: A customer's job on a shared build server
- shmat call: Maps the segment and adds 1 to the count
- Shared memory segment: One block of memory many processes can see
- shm_nattch counter: A short; Calif says signed 16-bit
- Forked child: Keeps the attachment, so the count rises
- IPC_RMID removal: Segment destroyed once no process uses it
How does a 16-bit counter wrap past 32,767?
A signed 16-bit number holds values from -32,768 to 32,767. Add 1 to 32,767 and the bits roll over to -32,768. C has no built-in check that stops this, so someone has to write one.
Pause and predict: a count sits at 32,767 and one more process attaches. What number does the kernel see next?
Python's own integers never overflow, so the example below uses ctypes, Python's module for C number types, to copy a C short. The second function refuses the attach at the limit.
import ctypes
def unsafe_attach(count):
return ctypes.c_int16(count + 1).value # wraps like a C short
def safe_attach(count, limit=32_767):
if count >= limit:
raise OverflowError("attach limit reached, refusing new attach")
return count + 1
count = 32_767 # one shared segment on a build server (made up)
print("unsafe:", unsafe_attach(count)) # prints -32768
try:
safe_attach(count)
except OverflowError as err:
print("safe:", err)Run it with python3 and you get two lines. The first is unsafe: -32768. The second is safe: attach limit reached, refusing new attach. We ran it on Python 3.13.
What just happened? The unsafe version produced a count no real segment can have. The safe version refused the request instead. A refused request beats a wrong number.
Why does a wrong reference count lead to use-after-free?
Because code that uses a reference count frees memory based on the count, not on who really holds it. If the count says nobody is left, the memory can be released while processes still map it. Any later use touches memory that may now belong to something else.
The general bug class, step by step
Drag to exploreNotice that the damage happens two steps after the wrap. Calif says the real Big Fish path also needed a race and a separate out-of-bounds write.
Diagram connections:
- Count climbs to Limit reached (more forks)
- Limit reached to Count goes negative (one more attach)
- Count goes negative to Kernel misreads users (wrong count)
- Kernel misreads users to Memory freed too early (frees early)
- Memory freed too early to Use-after-free (old user returns)
- Count climbs: Each attach adds 1; a fork adds 1 per attach held
- Limit reached: The count hits 32,767 and no check stops it
- Count goes negative: The next add wraps it to -32,768
- Kernel misreads users: Lifetime decisions now use a wrong number
- Memory freed too early: Released while real users remain
- Use-after-free: A remaining user touches reused memory
Who is exposed to the Big Fish bug in 2026?
Anyone running an unpatched OpenBSD 7.9 or 7.8 machine where an untrusted person can run code. The attacker needs a normal account or a running program first. Calif names only the 7.9 patches, but the 7.8 errata page lists matching fixes.
Only the two most recent releases get security fixes, according to the OpenBSD FAQ. As of 19 October 2026, until OpenBSD 8.0 ships, that means 7.9 and 7.8. Calif does not list affected versions, but older releases likely share the decades-old counter and get no errata, so upgrade them.
Shared machines come first
The four build servers are the worry. Forty outside developers run their own code there, so any one of them could try the local path to root.
Admin-only firewalls can wait for the next window
The 12 firewalls have only 3 admin logins. Nobody untrusted runs code on them, so the team schedules them for the next maintenance window, not today.
Which machines to patch first
Drag to exploreNotice that the release decides which errata you need, and untrusted code decides how soon. Every unpatched machine gets the fixes in the end.
Diagram connections:
- Running OpenBSD 7.9 or 7.8? to Older release: upgrade (No)
- Running OpenBSD 7.9 or 7.8? to Untrusted users run code? (Yes)
- Untrusted users run code? to Patch today (Yes)
- Untrusted users run code? to Patch in next window (No)
- Running OpenBSD 7.9 or 7.8?: Run uname -r on each machine
- Older release: upgrade: Only 7.9 and 7.8 get errata, as of October 2026
- Untrusted users run code?: Logins, build jobs or customer scripts
- Patch today: The 4 shared build servers
- Patch in next window: The 12 admin-only firewalls
How do you patch OpenBSD for the Big Fish bug?
Install the errata for your release, then reboot into the patched kernel. On 7.9 that means 018 and 026; on 7.8 it means 054 and 062. Erratum 018 is listed as a reliability fix, and 026 as a security fix. Calif still names 018 among the Big Fish fixes, so never skip an erratum because of its label.
The errata pages also list two related System V fixes. On 7.9 they are 005 (16 July) and 029 (30 September); on 7.8 they are 041 and 065. Calif does not mention them, but syspatch installs them too.
Check each machine's release
Run uname -r. It prints the release, such as 7.9. The team's list has 12 firewalls and 4 build servers, all on 7.9.
Read the errata page for that release
Confirm 018 and 026 are listed for 7.9, or 054 and 062 for 7.8.
Patch shared machines first
Start where untrusted users run code: the four build servers.
Run syspatch as root
With no options, syspatch applies all missing patches. Its manual says patches are cumulative, so it cannot install only some of them. Full binary updates exist for the amd64, i386 and arm64 processor families. Elsewhere, build from the -stable branch, the release plus its errata, instead.
Reboot into the new kernel
The running kernel stays the old, unpatched one until the machine restarts.
Verify and record
Run syspatch -l, which lists installed patches, and look for 018 and 026 (054 and 062 on 7.8). Then run syspatch -c, which lists missing patches, and expect an empty list.
The 7.9 errata page says -stable is maintained for one year after release. Each errata entry also links a signed source patch with its own instructions.
Beginners can stop here. You know what broke, who is exposed and how to patch. The rest is for engineers who write or review low-level code.
Which erratum fixes which part of the Big Fish chain?
Calif, which found both bugs, gives the mapping. It writes that the issues were addressed in errata 026 and 018, respectively. By that order, 026 fixed the counter bug and 018 the separate out-of-bounds write.
The errata titles do not spell out the link. Erratum 026 fixes a reference counting bug that could lead to use-after-free, which closely matches Calif's counter bug. Erratum 018 prevents integer overflow in shmat, the attach call.
Neither title names the out-of-bounds write, so that half of the mapping rests on Calif's word. We could not open the patch files to check it. Either way the action is the same: install both.
On 7.8, the 062 entry adds that the ipcs command stopped working after an ABI change, meaning a change to a binary interface programs rely on. If you rely on ipcs there, read that entry before patching.
Why did kernel defences not stop the Big Fish bug?
Calif reached root with KARL, SMAP and SMEP all enabled, and without an information leak bug, one that reveals kernel memory such as addresses. These defences make attacks harder, but none of them fixes a wrong count.
KARL is OpenBSD's kernel relinking: it builds a freshly shuffled kernel at install, upgrade and boot, so function and data addresses change. SMEP stops the kernel running code from user-space memory. SMAP stops it reading or writing user-space memory except where it explicitly allows that.
Calif says its chain got past SMAP and SMEP by making its own user-space mapping accessible to the kernel. Allocators are the parts that hand out kernel memory. Calif says KARL shuffles the kernel binary, but the boot-time and best-fit allocators lack base address randomisation.
Why does the usual sanitizer flag miss a 16-bit wrap?
Because in C this wrap is not undefined behaviour, meaning code the C standard gives no meaning at all. The standard leaves the result to each compiler, and gcc and clang wrap it. The -fsanitize=undefined flag looks for undefined code only.
C first promotes the short to an int. Where an int is 32 bits, as on mainstream compilers, the addition itself cannot overflow.
The bug happens when the result is squeezed back into 16 bits. The clang documentation says issues caught by its implicit conversion checks are not undefined behaviour, and that the undefined group leaves them out.
int printf(const char *format, ...);
int main(void) {
short nattch = 32767; /* a 16-bit attach count at its maximum */
nattch++; /* one more attach */
printf("after one more attach: %d\n", nattch);
return 0;
}We built this toy file, not kernel code, with gcc 13.3 and clang 18.1.3 on Linux. The table shows what each build did.
| Build | What happened | Caught? |
|---|---|---|
| gcc -O2 -Wall | Printed after one more attach: -32768 | No, and no warning |
| gcc -fsanitize=undefined | Printed -32768 with no report | No |
| clang -fsanitize=undefined -fsanitize-trap=all | Printed -32768 with no report | No |
| clang -fsanitize=implicit-conversion -fsanitize-trap=all | Stopped at the wrap with Illegal instruction, exit code 132 | Yes |
The -fsanitize-trap=all flag makes a failed check stop the program with a trap instruction, so no sanitizer runtime library is needed. The clang documentation calls these conversions often unintentional, not always, so review each report before changing code.
How should engineers write counters that never overflow?
Give every lifetime counter a type wide enough that it cannot realistically fill, and still check its limit. When the limit is reached, refuse the new reference or stop the count at its maximum. Never let it wrap.
| Approach | What happens at the limit | Safe for lifetimes? |
|---|---|---|
| Wrap | A signed 16-bit count jumps from 32,767 to -32,768 | No, this is the Big Fish pattern |
| Wider type | A signed 32-bit count needs 2,147,483,647 increments to fill | Better, but still add a check |
| Saturate | The count sticks at a fixed saturation value and the object is never freed | Yes, it leaks memory instead of corrupting it |
| Refuse | The new attach or reference fails with an error | Yes, the caller gets a clear failure |
Saturation is a real design, not a thought experiment. The Linux kernel's refcount_t type pins the count at a saturated value on overflow, and its source comment says this avoids spurious use-after-free bugs.
Refusing is just as short in C. The gcc and clang builtin __builtin_add_overflow reports whether the result fits, so the function can refuse instead of storing a wrapped value.
int printf(const char *format, ...);
/* Refuse the increment instead of wrapping. */
static int attach(short *count) {
short next;
if (__builtin_add_overflow(*count, 1, &next))
return -1; /* refuse: real code returns an error code */
*count = next;
return 0;
}
int main(void) {
short count = 32766;
int rc = attach(&count);
printf("first attach: rc=%d count=%d\n", rc, count);
rc = attach(&count);
printf("second attach: rc=%d count=%d\n", rc, count);
return 0;
}Built with gcc 13.3 or clang 18.1.3, it prints first attach: rc=0 count=32767, then second attach: rc=-1 count=32767. The second attach is refused, and the count never wraps.
Why was the counter only 16 bits wide?
Calif traces the short back to the original BSD sources. POSIX, the standard Unix interface, now names the type shmatt_t and requires an unsigned integer at least as large as unsigned short.
A signed short meets neither the unsigned rule nor the full range. Old types often linger because changing a public structure can break programs built against it.
What Calif suggests beyond the patch
Fuzzing throws huge numbers of random inputs and call sequences at code. Sanitizers add run-time checks that catch bad behaviour as it happens.
Calif suggested the array bounds sanitizer, a subset of the undefined behaviour sanitizer, to FreeBSD as a possible release-build defence. It privately reported 21 FreeBSD issues that this sanitizer would have caught.
Calif also suggests safe unlinking, a check on linked-list pointers before removing an item. It notes FreeBSD and NetBSD already do this in their list code. Its other proposals randomise allocator base addresses and the order of allocations.
How do you explain the Big Fish bug in a system design interview?
Tell it as a story about object lifetimes and invariants, rules that must always hold. Use this frame.
Name the bug class
An integer overflow in a reference count, which turns into a use-after-free.
State the invariant
The count must always equal the real number of users. A signed 16-bit count broke this past 32,767.
Give the trigger at a high level
Repeated forks of a process with shared memory attached kept raising the count until it wrapped.
Size the risk honestly
Local only. With a separate out-of-bounds write, it reached root with KARL, SMAP and SMEP on. Shared machines matter most.
Describe fix and defence
Install both errata and reboot. Use wide, checked counters, test with clang's implicit conversion checks, and fuzz rare states such as a count near its limit.
Questions people ask
- Can someone attack my OpenBSD firewall over the network with Big Fish?
- No, not on its own. Big Fish is local privilege escalation, so the attacker must already run code on the machine. An admin-only firewall faces much less risk, but still install the errata in your next maintenance window.
- Does the Big Fish bug have a CVE number?
- Not as of 19 October 2026. Neither Calif's write-up nor the OpenBSD 7.9 and 7.8 errata pages list one. Track it by the errata numbers instead: 018 and 026 on 7.9, or 054 and 062 on 7.8.
- Which erratum fixes Big Fish, 018 or 026?
- Install both. Calif, which found the bugs, says 026 fixed the counter bug that led to use-after-free and 018 the separate out-of-bounds write. The 026 title matches that counter bug, while 018's title names an integer overflow in shmat. OpenBSD's errata pages do not spell out the link, as of 19 October 2026.
- Is OpenBSD 7.8 affected too?
- Treat it as affected. Calif names only 7.9 patches, but the 7.8 errata page lists 054 and 062 with the same titles and dates as 7.9's 018 and 026. Run syspatch on 7.8 machines as well, then reboot.
- Do Linux or other BSD systems have the same bug?
- Calif's write-up does not say, so do not assume either way. The errata cover OpenBSD 7.9 and 7.8. If you run another system, read its own security advisories rather than guessing from this one.
- Do fuzzing and sanitizers really catch bugs like this?
- Some do, if you pick the right check. In our test, the usual undefined behaviour sanitizer missed a 16-bit wrap, while clang's implicit conversion check stopped it. gcc 13.3 has no such check. Calif reported 21 FreeBSD issues that an array bounds sanitizer would have caught.
Sources and further reading
- Calif: Big Fish, Exploiting a 37-Year-Old Kernel Bug in OpenBSD, by Andrew Griffiths (16 October 2026)
- OpenBSD 7.9 errata (entries 005, 018, 026 and 029; syspatch and -stable notes)
- OpenBSD 7.8 errata (entries 041, 054, 062 and 065)
- OpenBSD manual: shmctl(2), struct shmid_ds and IPC_RMID
- OpenBSD manual: shmat(2) and shmdt(2)
- OpenBSD manual: syspatch(8)
- OpenBSD FAQ: Building the System from Source (releases that receive fixes, -stable)
- OpenBSD manual: ipcs(1)
- POSIX.1-2024: sys/shm.h and the shmatt_t type
- Clang documentation: UndefinedBehaviorSanitizer, implicit conversion checks
- LWN: Randomizing the kernel, on OpenBSD KARL (12 July 2017)
- Linux kernel include/linux/refcount.h at commit ccb79116c372, saturation comment