The kernel is still C
An OpenBSD kernel use-after-free (CWE-416, CVSS 7.8) escalates a local user to root via pool reclaim and cr_uid overwrite. Mechanism, exploit path, and the BSD telemetry gap.
OpenBSD frees a kernel object, keeps the pointer, and hands a local user uid 0. The bug class is CWE-416, use-after-free, resolved inside the kernel address space. The impact is local privilege escalation from an unprivileged account to root. CVSS v3 base 7.8. Vector AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. No network vector. No user interaction. A shell on the host is the only precondition. This maps to MITRE ATT&CK T1068, exploitation for privilege escalation.
The framing matters more than the specific CVE. OpenBSD ships with pledge, unveil, W^X enforced kernel-wide, SMEP and SMAP on supported hardware, KARL relinking the kernel at every boot, and a pool allocator with freelist integrity checks. It is the reference implementation for defensive OS engineering. And it still has use-after-free in the kernel, because the kernel is written in C, and C does not enforce object lifetime. Predictable memory management is an assumption. The assumption is false on every operating system that manages its own heap.
The mechanism is lifetime desynchronisation. A kernel subsystem allocates a structure from a pool - pool(9) backs most fixed-size kernel objects, malloc(9) the variable ones. Something frees that structure. A second reference to the same address survives the free. The pointer is dangling. The allocator does not zero the pointer on the holding side; free returns the backing memory to the pool freelist and moves on. The next allocation of the same pool size reclaims that exact chunk. Now two logical objects occupy one physical allocation. One is the stale reference the code still trusts. The other is whatever the attacker just placed there.
The common root causes are narrow. A reference count decremented one time too many, so the object frees while a consumer still holds it. A concurrent close or ioctl racing an in-flight operation - classic TOCTOU inside the kernel - where one thread frees the descriptor’s backing while another dereferences it. An error path that frees on failure but leaves the caller’s pointer live. In each case the code is structurally correct for the single-threaded, no-error case and wrong the moment concurrency or a failed allocation reorders the lifetime. This is the exact failure mode that memory-safe languages eliminate at compile time and C cannot express.
Exploitation is reclaim-and-control. The attacker triggers the free, then races to allocate a replacement object of matching pool size before the stale reference is used. Kernel heap grooming makes the race deterministic. Pool caches are per-size and per-CPU. An attacker who can drive allocations of the target size - through sockets, mbufs, file descriptors, or any syscall that pulls from that pool - shapes the freelist so the freed chunk is the next one returned. The replacement is attacker-controlled data. When the kernel dereferences the stale pointer, it reads the attacker’s bytes as a valid object.
What that buys depends on the object. A structure containing a function pointer - think a fileops table, a callback in a timeout, a protocol handler - turns the stale dereference into control-flow hijack. SMEP and SMAP block the old shortcut of pointing execution at a userland page, so the classic ret2usr is dead on hardened hardware. That pushes exploitation toward data-only paths. The higher-value target is not a function pointer at all. It is the credential structure. Every process holds a struct ucred through p_ucred. cr_uid, cr_gid, the group set. Overwrite cr_uid with 0 in the running process’s credentials and the escalation is complete without ever redirecting execution. No shellcode. No code page. A single controlled write to the right offset in a reclaimed object, and an unprivileged process is root. Data-only privilege escalation is harder to detect precisely because nothing executes that should not.
The attacker controls three things: when the free happens, what refills the chunk, and when the stale use fires. The gap between free and use is the race window. Kernel heap grooming widens it and makes the outcome repeatable. KARL and pool randomisation raise the cost - the attacker cannot assume static kernel addresses, and pool layout shifts - but reclaim-based UAF does not require a leaked kernel address. It requires the freed chunk and the refill chunk to be the same size class. That constraint is a property of the allocator, not the randomisation, and it holds regardless of KARL.
The real-world context is that OpenBSD is not exempt. Qualys demonstrated this directly. In December 2019 they published a chain of OpenBSD authentication and privilege escalation bugs - CVE-2019-19521 authentication bypass, CVE-2019-19519 local privesc through su, CVE-2019-19520 through xlock, CVE-2019-19522 through S/Key and YubiKey paths. Weeks earlier, CVE-2019-19726 gave local root through the dynamic loader ld.so mishandling LD_LIBRARY_PATH on set-user-ID binaries. None of those were kernel memory corruption. They were logic and trust-boundary failures in the base system. The point stands harder for a kernel UAF: the most audited free operating system in production still ships memory-lifetime bugs, because auditing reduces their density and does not reach zero. A UAF in the kernel is reachable through a legitimate syscall from a legitimate process. Pledge and unveil constrain what userland can name and call. They do not fix a bug that lives on the other side of a syscall the process is allowed to make.
The telemetry reality on BSD is where defenders lose. There is no Sysmon. There is no mature EDR agent for OpenBSD emitting Event ID 10 on cross-process access or flagging anomalous handle behaviour. The Windows and Linux detection playbooks do not port. What exists is dmesg, the kernel message buffer, and it only speaks when something crashes. A failed exploitation attempt often panics the kernel - a reclaim that lands wrong dereferences garbage and faults in supervisor mode. That produces a panic trace, a reboot, and a line in the log. That is the loud failure. The successful exploitation produces nothing. cr_uid changes from a non-zero value to 0 inside kernel memory. No syscall audit records it as anomalous, because on stock OpenBSD there is no syscall audit layer running by default the way auditd or eBPF instrumentation runs on Linux. ktrace and kdump can trace a process, but they are targeted tools, not always-on telemetry, and an attacker who reaches this stage is not running under ktrace.
The observable signal, when it exists, is indirect. A process whose effective uid transitions to 0 without passing through setuid, su, doas, or a known set-user-ID binary is the anomaly. Detecting that requires monitoring credential transitions at the process level and correlating them against the sanctioned escalation paths. Most BSD deployments do not collect that. The second signal is the panic itself. Repeated kernel panics with faults in pool or copy routines, clustered on one host, is the fingerprint of an exploit being tuned - the attacker iterating the race, losing it, and crashing the kernel until the grooming stabilises. A SIEM correlation rule on reboot frequency and panic strings from remote syslog catches the noisy attempts. It does not catch the clean success. That is the gap. The defender sees the failures and is blind to the win.
Network telemetry contributes nothing here. This is local. There is no C2 beacon required to escalate, no lateral movement packet, no IOC on the wire at the moment of exploitation. The escalation happens entirely inside one host’s kernel. Detection has to live on the host, and on OpenBSD the host-based detection stack is thin by design. The operating system’s security model assumes a small trusted base and correct code. It does not assume a rich instrumentation layer watching itself, and adding one cuts against the minimalism that makes the base auditable in the first place.
The patch boundary for a UAF is narrow and specific. The fix nulls the pointer on free, corrects the reference count, reorders the free relative to the last use, or adds the lock that closes the race. It is a small diff - often a handful of lines - because the bug is a lifetime error, not a design error. Applying it closes that instance. It does not close the class. The residual exposure after patch is every other object in the kernel with the same allocate-free-reference pattern and no proof of correct lifetime. KARL, pool freelist hardening, SMEP, and SMAP are cost multipliers. They make a given UAF harder to weaponise and force attackers into data-only reclaim techniques. They do not make the class unreachable, and cr_uid does not care about control-flow mitigations.
The structural fix is memory safety in the components that manage lifetime, and OpenBSD’s base kernel is C. Rewriting kernel subsystems in a language that enforces ownership at compile time removes the class rather than the instance. That work is not in the OpenBSD base tree. Until it is, the correct read is the one the topic states plainly. This is not OpenBSD being broken. This is a demonstration that lifetime safety is not a property any C kernel can guarantee, and that the assumption of predictable memory management is the vulnerability underneath the vulnerability. The CVE is one instance. The pattern is every kernel that frees its own objects and trusts the pointer afterward.
Keep Reading
openbsdOpenBSD use-after-free hands local users root
An OpenBSD use-after-free escalates a local user to root. Confirmed: the privilege boundary was crossable. Reputation is not enforcement.
qualcommCVE-2024-43047 hit live targets in 2024
CVE-2024-3679 maps to no Qualcomm bug. The real 2024 Snapdragon zero-day is CVE-2024-43047 - a DSP/FastRPC use-after-free, CVSS 7.8, exploited in the wild.
cuda-securityEvery kernel launch is an ioctl into ring 0
Launching a CUDA kernel crosses into ring 0 via the NVIDIA kernel-mode driver - the real attack surface behind GPU compute, its CVE class, and detection gaps.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.