I know that exploiting NULL dereferences is a thing of the past and that modern systems prohibit user processes from mapping NULL page, but I also know that back in the day, a NULL dereference in the kernel could be exploited from userland by mapping the NULL page, writing a shellcode to it and triggering in the kernel the function call that dereferences the NULL page to achieve LPE.

My question is: Since the kernel address space is supposed to be distinct from the any user-mode process' address space, when the user-mode process maps the NULL page into its address space and writes to it, the way I understand it is that should have no effect on the the kernel's address space; that is the NULL page should not become mapped and if it is, the data that exists in it should be different from the payload written by the user-mode process.

How come the exploits (used to) work?

  • Having address space reserved for the kernel doesn't imply that the user address space is unmapped while running kernel code.
    – Ben Voigt
    Jun 6 at 21:41
  • @BenVoigt I agree. What I mean is that say both the kernel and a usermode process dereference the same address, 0xdeadbeef for the sake of example. Then that virtual address points to two different physical addresses with differenr contents depending on the context by virtue of memory isolation. So VA 0x0 from usermode should be different from VA 0x0 from kernel mode.
    – user9128
    Jun 7 at 0:21
  • But see, that's not what happens. Two different user-mode processes will map the same VA to two different physical addresses. But a user-mode process and kernel-mode code running within that process use the same mapping table. Due to the different privilege level, there are some mappings that result in access violation when used outside kernel code and succeed when used from kernel code. But within the context of that single process, there's only one translation table hierarchy.
    – Ben Voigt
    Jun 7 at 14:21

1 Answer 1


This is mostly a question about operating system architecture. Fundamentally, though, the reason is simply "because the CPU is allowed to access user-mode addresses even while the current thread is in kernel mode", although there is now a feature to prevent this.

You say

Since the kernel address space is supposed to be distinct from the any user-mode process' address space

This is confused, bordering on wrong. "The kernel" is not really a process in the same sense as you seem to be thinking, with its own address space and so on (there typically exists one or more system processes that are launched from kernel-mode code and are distinct processes in the usual sense, but they aren't "the kernel"). There are addresses (typically though not always the upper half of the address space) that can't be accessed while the CPU is in user mode (typically "ring 3" on x86-based CPUs), but they aren't outside of the process' address space. In fact, quite the opposite; large chunks of shared kernel memory are mapped into each process' kernel address space. (This is a performance optimization, and is part of the reason that vulnerabilities like Meltdown were so severe; the kernel does - or did - store a lot of data, even sensitive data, within the address space of untrusted processes).

When a process executes a system call (or "syscall", a special instruction that flips the CPU into kernel mode - ring 0 - and jumps execution to a handler function defined in the kernel-only range), the OS needs some way to receive data from the process' user-mode memory (for example, if you invoke NTCreateProcess/execve, the kernel needs to be able to read the path and other data necessary for creating the new process, and that's too large to put in a register). The OS doesn't want to have to do a bunch of expensive context switches between the user process and some kernel process (which involves changing hardware memory mappings and process-specific data structures), and even if context switches were free, there's nowhere that the user-mode code can write the data such that only kernel-mode code can read it. In practice, the kernel will typically start syscall execution by verifying that the user-supplied address is both readable and within the user-accessible address space, and copy the incoming data to a kernel-mode buffer before operating on it (to prevent other user-mode threads in the process from modifying the data during syscall execution, which could cause a time-of-check-to-time-of-use vulnerability). A similar process (writing to user space instead) typically happens at syscall conclusion, copying data back into the user-accessible address space of the process, typically at a user-specified process-specific virtual address which the kernel has to verify is valid for user-mode code, and writable.

Now, obviously this requires that the kernel have the user-mode process' virtual addresses mapped during at least these first and last steps of any system call, otherwise the user-supplied addresses would be meaningless! Indeed, the OS (typically) stays within the user process context during the whole syscall execution, which is important because sometimes the kernel-mode code needs to access other user-mode memory. Historically, the only differences between user-mode and kernel-mode are in what virtual addresses can be accessed, and whether privileged commands can execute. Thus, until the thread that made the syscall returns from the syscall, it can access addresses above the user/kernel boundary (that is, the CPU won't block access to those high addresses), and also execute commands that are not permitted to user-mode code. Once the syscall completes (including writing return data into user-mode addresses), the CPU thread switches back to ring 3 and resumes execution at the instruction after the syscall instruction, similar to returning from a normal function call.

Now, in typical cases with a monolithic kernel (such as Linux), the thread spends all the time between the start and the end of the system call executing code that is mapped at kernel-only addresses. However, this is not enforced by the hardware! Indeed, "microkernels" exist (mostly academic or experimental like Mach and GNU HURD, but NT started as one even though these days it's mostly monolithic as well) where nearly all execution of system-level code actually happens in user space, just in privileged system processes rather than in the user's process. In a microkernel, the actual kernel handles a system call by copying the data from the calling process into kernel mode, suspending that thread in the calling process and context-switching to the system process (which also has the same kernel-mode inbound-syscall buffer mapped), copying the data back to user-mode but this time in the system process, and exiting kernel mode into the system process' handler function. The system process services the system call without actually being in the kernel, and then calls back into the kernel when it's done; the result data is then copied back from the system process' user-mode address to the kernel, the kernel context-switches back to the original user-mode syscall-calling process and thread, copies the data into the user-mode result buffer there, and returns to user mode at the next instruction in that thread. Obviously, this approach requires at least two additional context switches beyond the monolithic approach (although there's ways to optimize that; I'm giving you a very basic version of the design that also leaves out some bits), which has a performance impact (on earlier CPUs, a rather severe one).

Also, the OS jumps into kernel mode for reasons beyond syscalls too. For example, any time there's incoming I/O data (be it from a storage device, a network device, a human interaction device like a mouse or keyboard, or something else), the OS may need to switch to kernel mode for the relevant driver to operate (and, potentially, pass received data to a user process). However, as with syscalls, the OS does not necessarily need to perform a context switch to do this (unless the driver is implemented in a user-mode process, microkernel-style).

An astute reader may note that there's still no point even in the microkernel flow where code that is resident in user-mode addresses executes while the CPU thread is in kernel mode. I honestly don't have a good answer for you as to why this isn't something that the CPU/memory controller doesn't block; it seems like it would be a small amount of silicon and minimal additional execution cost on each syscall to enforce a condition of "while in ring 0, the instruction pointer can't go below address [user/kernel boundary], else throw an interrupt" but as far as I know, this is not a feature any common CPU has. (If anybody wants to point out either an exception or a reason for this in the comments, I'd love to see it.)

In fact, this feature does exist, and has on Intel, at least, since 2012; AMD has it too since at least 2017 but I couldn't figure out the first chip released with it. (H/T @Poopoo, thank you!) It's called Supervisor Mode Execute Prevention (SMEP), or Intel OS Guard. Using it requires kernel support, but it's a pretty easy patch at least for kernels running on "bare metal" rather than in a hypervisor, and I expect all modern operating systems support and use it (Windows certainly does as of Win10, but I couldn't find when this feature was first adopted). It works very straightforwardly: if a certain flag in a control register is set, then when the CPU is in ring 0, the current thread is not allowed to execute any code below the user/kernel address boundary. Kernels could technically implement this on legacy processors using legacy memory mapping features - while in kernel, remap all user-mode memory addresses to be non-executable, then restore them before either returning to user mode due either to kernel execution finishing or a context switch - but that's a lot of expensive operations. In fact, since memory mappings are process-specific rather than thread-specific, it would also require halting all other threads executing in that process during the kernel-mode operation! That would obviously be an unacceptable performance hit, plus add complexity to a very sensitive part of the kernel.

So, SMEP protects against executing code that a user-mode program mapped into a low page. However, there are still risks if the kernel dereferences a NULL or near-NULL pointer while trying to access non-executable data. If the kernel-mode code expects to be loading a kernel-mode address, it might fail to perform the checks that kernel-mode code must make when dealing with (untrusted) user-mode data, or simply implicitly trust the untrustworthy data. For example, if the kernel is creating a new process and accidentally reads from user-mode memory when attempting to fetch the security token to assign to the process, then malicious user-mode code could pass the token of a highly-privileged process and end up launching what was supposed to be a low-privilege process as SYSTEM/root.

Fortunately, this can also be mitigated. In addition to SMEP, there's a more recent feature that has existed on at least some CPUs since at least 2017, called Supervisor Mode Access Prevention (SMAP). Intended as the complement to SMEP, SMAP disables all access to user-mode memory while in kernel mode. Now, as discussed above, this is obviously not viable to have active constantly - the kernel needs to be able to read data out of user-mode memory, and write data back, at certain points in executing system calls and other kernel-mode functionality - but such user-mode accesses can generally happen at known points. In between such accesses, the kernel can and should enable SMAP, such that a vulnerability in kernel code where it attempts to access user-mode memory will merely cause the system to crash, rather than let a malicious process gain elevated privileges.

SMAP support is also OS-dependent. Unfortunately, enabling SMAP is a somewhat more complicated patch than enabling SMEP, as kernel-mode code needs to be aware of SMAP, and disable it temporarily whenever interacting with user-mode addresses for a legitimate reason. The common use cases - system calls and so on - can all flow through common functionality that takes care of this enabling and disabling, but drivers and similar need to be SMAP-aware, or else the kernel needs to preemptively disable SMAP whenever executing code that is part of such drivers, lest they innocently attempt to make a user-mode memory access without temporarily disabling SMAP themselves. SMAP support is present on some Unix-like operating systems, but I'm not sure about Windows.

Anyhow, given what all I've said, there are obvious implications for developers when writing kernel-mode code (either components built into the kernel such as the process creation interface, or optional/installable components such as hardware or storage drivers). It is essential to validate memory handling very carefully, even more so than in most user-mode processes, because a user process (usually) doesn't share an address space with an untrusted thread. Never trust data from user mode to be well structured, or pointers from user mode to be valid. If you receive a complex data structure from user mode that contains additional pointers, you must behave in the same way as the syscall handler when retrieving those pointers: make sure they're valid in the user-mode context and readable without faulting, and copy them to kernel-mode addresses before performing any validation lest it be invalidated between use and check. And, obviously, make sure you're never attempting to dereference a NULL pointer - especially for the address of the next instruction - since the NULL address (and addresses near it, which might be at a great enough offset to still be mappable on modern systems!) are user-mode writable.

Additionally, with modern systems where SMAP is present, the kernel code author needs to wrap any access to user-mode addresses in a pair of operations to first disable, then re-enable, SMAP.

Once again, please remember that this is a high-level view intended to help you understand the architecture; there's details I've deliberately left out, and stuff that I'm sure I've missed in the years since I last dug into the guts of any major OS (let alone all of them; different ones take slightly different approaches even aside from micro/monolithic kernel distinctions).

  • 1
    x86 CPUs do have Supervisor Memory Execute Prevention, which prevents the processor from executing code in user pages while in supervisor mode.
    – Poopoo
    Jun 7 at 6:25
  • HUH! So it does, since 2012, at least on Intel. Wild. Do operating systems use this, in practice? Microsoft says that Win10 uses it (on supported hardware) but I can't find much information about this, like when it was introduced, exactly what CPUs support it, and so on. The original presentation makes it sound like the sort of extremely straightforward thing I thought it would be, though: enable a CPU control register flag, and then, if you're ring 0, you can't execute any instructions from below the user/kernel boundary. Null deref for data could still be a risk, but for code it won't work.
    – CBHacking
    Jun 7 at 10:20
  • Edited in SMEP (and also SMAP, which closes the loophole of poisoned data access rather than executing malicious code). Thanks for the tip, I learned something today!
    – CBHacking
    Jun 7 at 11:17

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .