RC RANDOM CHAOS

The kernel keeps its word

OpenBSD's use-after-free is not a bypass but the kernel honoring a reference it validated once and never rechecked, the same pattern behind X.509 certificate trust.

· 9 min read
The kernel keeps its word

A pointer in the OpenBSD kernel is an address, and an address is a promise the system keeps without stopping to ask whether the promise is still true. When kernel code holds a reference to an object, it dereferences that reference to read or write the memory the reference names. A use-after-free is not the moment that promise is broken by force from outside. It is the moment the kernel honors a promise that has already expired, and honors it with full authority, because nothing in the execution path is responsible for asking whether the object behind the pointer still exists. The privilege escalation to root is not the kernel being defeated. It is the kernel doing precisely what a valid-looking pointer instructs it to do.

OpenBSD is engineered on the premise that memory errors will happen and that their consequences should be expensive to weaponize. That premise is visible in the platform’s mitigations: W^X enforcement, the Kernel Address Randomized Link that reorders the kernel on every boot, guard pages in the allocator, pledge(2) and unveil(2) constraining what a process may even attempt. These raise the cost of turning a corrupted reference into reliable control. They do not change the contract underneath. A use-after-free lives below all of them, at the level where a pointer is just a number and dereferencing that number is the most ordinary operation the kernel performs.

The boundary being crossed here is the one between a local user and the root of the machine, and it is worth being precise about what carries that authority. The kernel is the most trusted component in the system. A reference held by kernel code inherits kernel privilege by virtue of where it lives. When such a reference outlives the object it was created to name, the privilege attached to it does not decay along with the object. The authority is a property of the executing context, not of the memory the context happens to touch. So a stale reference in a privileged path is not a weaker version of a valid one. It is an equally powerful one pointed at the wrong thing.

The assumption underneath all of this is the contract of the allocator itself. When kernel code receives a pointer from the memory allocator, it treats that pointer as a durable name for a valid object, one that remains a valid name until the code that owns the allocation explicitly releases it. Lifetime is assumed to be singular, knowable, and coordinated. Trust in the pointer is treated as persistent across every operation that receives it and transferable to every function it is passed into. The number that came back from allocation is taken to mean what it meant at the instant it was returned, indefinitely, until some owner decides otherwise.

This works only because the kernel is a web of shared references. Many code paths may hold the same pointer to the same object at the same time. The design assumes that ownership and lifetime are coordinated well enough that no reference is ever used after the object it names has been freed. That coordination is a property of the whole system’s behavior over time. It is not a property the pointer carries within itself. The pointer has no field that records whether its object is alive. It cannot be interrogated about the state of what it points to, because it is only an address, and an address knows nothing.

That assumption is not particular to OpenBSD. It is the assumption of the entire C memory model on which the kernel is written. A reference’s validity is external to the reference. Nothing in the bits of the address encodes whether a live object still sits behind it. The system trusts that validity was established once, at allocation, and preserved thereafter by convention and discipline. This is a trust model built on reference rather than validation. The pointer is honored because it exists and because it was once legitimate, not because anything re-confirms its legitimacy at the moment of use.

What changed is not an attacker growing more capable and not a lapse by anyone maintaining the code. What changed is the validity of the assumption, and it changed the instant free() ran. The object was returned to the allocator. The pointer that named it still exists somewhere in a live code path. The number is byte-for-byte identical to what it always was. What changed is what that number refers to. The system did not re-evaluate the pointer at the point of use. It inherited the pointer’s validity from a past state, the state in which the reference was created, and carried that inherited validity forward into a present where it no longer holds.

Freed memory in the kernel does not sit empty waiting to be noticed. The allocator reclaims it and hands the same address to the next request. A different object takes up residence at the location the stale pointer still names. Now that pointer references a live object it was never meant to reference. Dereferencing it is not an error the kernel is positioned to catch. It is a correctly executed read or write to a valid, mapped address that happens to hold contents the reference does not expect. The kernel validated the address, which is genuinely live. It never validated the object, which is genuinely gone. Identity of the location stood in for integrity of the content, and the substitution was invisible because the two were indistinguishable to the code performing the access.

The assumption was that a reference established as valid remains valid until its owner says otherwise. That assumption no longer holds the moment the object is freed while any other reference to it survives. And the failure generalizes past this one flaw in this one system. Every operation that receives a stale pointer trusts it for the same reason the operation before it did: because a prior operation trusted it. Trust propagates forward from the point of allocation and is never revisited along the way. The system resolves the reference once, at creation, and treats that single resolution as permanent. Validity is decided in one state of the world and consumed in another, with nothing in between asking whether anything has changed. This is what inherited trust looks like when it is written in memory, and OpenBSD’s use-after-free is one exact instance of it: a system that binds trust to a reference and is not built to make the reference prove itself a second time.

When the privileged path reaches the stale pointer, what follows is the most ordinary operation the kernel performs. It issues a read or a write to the address the pointer holds. The address is mapped, so the access completes and no fault is raised. The operation returns. From the outside there is no seam, no anomaly, no moment where the kernel pauses over the reference and finds it wanting. The only condition the access depends on is that the address is usable, and the address is usable, because the allocator handed that exact location to a new request after free() returned the old object to it. Reference stood in for validation here not because a check was skipped but because a check of the object was never a step in the operation. The kernel confirmed the address. It was never positioned to confirm the object, because the object is not a thing the pointer can be asked about.

What is observable is a substitution. A local user performs operations that are permitted to a local user, and the effect that lands is a write into memory that a local user’s operations were never meant to reach. Identity of the location has replaced integrity of its contents. The address is genuinely valid; a live, mapped object sits behind it. That object is simply not the one the reference was created to name. The write passes into the fields of whatever now occupies the reclaimed slot, and it passes with kernel authority, because authority in OpenBSD is a property of the executing context and not of the memory the context happens to touch. Nothing was defeated. The write was authorized the moment it was issued from a kernel path, and it succeeded because the target was a real address holding real contents. The escalation to root is the completion of that ordinary write, not the subversion of it.

This is why the failure is invisible to the code that produces it. The two conditions that would have to be told apart, a valid address and the intended object, are indistinguishable at the point of access. The kernel resolves the address and finds it live, which is true. It does not resolve the object and find it stale, because resolving the object is not something a dereference does. A use-after-free in OpenBSD is therefore not an exception the system handles badly. It is the system handling nothing at all, executing an expected instruction against an unexpected occupant of a valid location. The mechanism is not a bypass of the contract. It is the contract, honored exactly, in a state of the world the contract assumed could not arrive.

The abstraction underneath is simple and it does not stay inside a kernel. A system that binds an outcome to a reference will honor that reference whenever it is presented, because the reference was legitimate at the moment it was created. Validity is established once and consumed later, and nothing between those two moments is responsible for asking whether the world still matches the reference. Execution follows the reference, not a fresh act of verification. Where this pattern lives, the freshness of trust is never a variable. Trust is resolved a single time and treated as a standing fact until something downstream is exhausted or overwritten by it.

The same mechanism runs in X.509 certificate validation, on entirely different substrate. A certificate is a signed assertion that binds an identity to a key, valid from the moment it is issued. When a client opens a TLS session, it walks the chain and confirms the signature, the expiry window, and the name. What it confirms is that the certificate was validly issued and names the party the client expected. It authenticates the reference. It does not re-establish that the binding behind the reference is still alive at the instant of use. Revocation is the mechanism that was built to ask exactly that question, and it is delivered through CRLs and OCSP, and in practice the revocation check is frequently absent or configured to fail open. So a certificate whose binding is dead is presented, and the client honors it, because it was once legitimately issued. The certificate is the pointer. Revocation is the free(). The binding has been returned to the void, and the reference still resolves, and the system executes trust across it.

It is one pattern, not two. Identity of source, a validly issued certificate or a validly allocated pointer, stands in for integrity of content, a binding that is still valid or an object that is still alive. In both, the system resolved trust at one point in time and carried that single resolution forward into a present it never re-examined. Whether the reference is a 64-bit address in kernel memory or a signed assertion inside a TLS handshake, the executing system does the same thing with it. It follows the reference. It does not make the reference prove, a second time, that what it names is still there.

The system resolves the reference once, and it does not revalidate. The pointer is honored, the certificate is honored, and neither is asked to demonstrate that the object behind it is still real. The control exists. The outcome does not.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.