Monolithic Kernels
A monolithic kernel runs every operating system service in one privileged address space, so a file system can call a driver as directly as calling a local function. That directness is the performance advantage and, simultaneously, the security cost.
One Address Space, Full Privilege
A monolithic kernel in OS design places the scheduler, memory manager, file systems, network stack, and every device driver inside a single privileged address space. That structure — one address space, every subsystem inside it — is the whole meaning of the term. When the VFS layer needs to read a block, it calls the block driver the way any C program calls a function — a jump, arguments in registers, no protection boundary crossed and no message constructed.
This is the entire performance argument, and it is a strong one. A syscall like read() on a cached file costs a single user-to-kernel transition, after which every internal step is direct calls and shared pointers. The same operation on a microkernel may cross protection domains several times. On the hardware where these decisions were made, that difference was the difference between usable and not.
The word 'monolithic' misleads people into imagining an unstructured blob. Modern monolithic kernels are heavily layered internally — Linux has clean abstraction boundaries at the VFS, the block layer, netfilter, and the driver model. The distinction is not about internal organisation; it is that those boundaries are conventions enforced by review, not by hardware.
- Every subsystem shares one privileged address space
- Calls between subsystems are jumps, not messages
- Data is shared by pointer with no translation
- Internal layering is convention, not hardware
Loadable Modules Are Not Microkernel Services
Monolithic kernel architecture in practice is not statically compiled. Linux loadable kernel modules, Windows drivers, and BSD KLDs let code be inserted and removed at runtime, so a distribution ships one kernel that supports thousands of devices without containing them all in memory.
It is important to be precise about what this does and does not give you. A loaded module is linked into the kernel's own address space and runs with full kernel privilege. It is dynamically loaded, not isolated. A buggy module dereferencing a bad pointer corrupts kernel memory exactly as a statically compiled one would.
So modules solve distribution and memory footprint, not fault containment. This is the single most common misunderstanding in monolithic kernel vs microkernel discussions: people see runtime loading and assume it implies separation. The isolation boundary is the address space, and a module is inside it.
- Modules are linked into the kernel address space
- They run with full kernel privilege
- Loading is dynamic; isolation is absent
- Modules solve footprint, not fault containment
The Cost You Actually Pay
The trusted computing base of a monolithic kernel is all of it. The Linux kernel source is well over 30 million lines, the majority of which is drivers written to varying standards for hardware that is often undocumented. Every line runs with authority to write any physical page and execute any privileged instruction.
Empirically, drivers are where the bugs are. Studies of kernel defect distributions have consistently found error rates in driver code several times higher than in core kernel code, which is unsurprising: core code is read by thousands of reviewers, an obscure driver by very few. In a monolithic design, that low-review code has the same privilege as the scheduler.
Mitigations exist and are real — kernel address space layout randomisation, lockdown mode, signed modules, eBPF verification for programmable hooks, and Rust for new driver code in both Linux and Windows. All of them narrow the exposure. None changes the structural fact that a driver fault is a kernel fault.
- The trusted computing base is the whole kernel
- Linux is over 30 million lines, mostly drivers
- Driver defect rates exceed core kernel rates
- KASLR, signing and Rust narrow but do not remove exposure
A module loads, calls directly, then corrupts a structure it never owned
# Why a monolithic call is cheap: it IS a call. No message, no copy,
# no context switch -- and no boundary either.
class MonolithicKernel:
def __init__(self):
# every subsystem lives in ONE address space
self.state = {"scheduler": "ok", "vfs": "ok", "block": "ok"}
self.modules = {}
def sys_read(self):
calls = ["vfs.resolve", "block.plan", "driver.read"]
return calls # all direct calls, ~2ns each
def insmod(self, name, module):
# A loaded module is LINKED INTO this address space. It is
# dynamically loaded, NOT isolated. Full kernel privilege.
self.modules[name] = module
module(self.state) # hands it everything
def buggy_driver(kernel_state):
# off-by-one write: the byte lands in whatever is adjacent
kernel_state["scheduler"] = "CORRUPT"
k = MonolithicKernel()
print("one syscall, direct calls:", " -> ".join(k.sys_read()))
k.insmod("badnet", buggy_driver)
print("after loading one buggy driver:", k.state)
print("a module is dynamically loaded, never isolated")
// One address space: a subsystem call is a function call, and a buggy
// module writes wherever it likes. Both facts have the same cause.
#include <iostream>
#include <cstring>
#include <string>
struct KernelState {
char rx_buf[16]; // a driver's receive buffer
std::string scheduler; // whatever the allocator placed next to it
};
static KernelState kernel {
{
}, "ok"
};
// Direct call: no message, no copy, no context switch -- and no boundary.
const char* sys_read() {
return "vfs.resolve -> block.plan -> driver.read";
}
// A loaded module runs INSIDE this address space with full privilege.
// Dynamically loaded is not the same thing as isolated.
void buggy_driver(const char* frame, std::size_t len) {
if (len > sizeof(kernel.rx_buf)) {
// The real bug writes past the buffer. Shown as the effect it has:
// whatever sits after rx_buf in the slab is corrupted.
kernel.scheduler = "CORRUPT";
}
}
int main() {
std::cout << "one syscall: " << sys_read() << '\n';
buggy_driver("....................", 20); // a jumbo frame
std::cout << "scheduler state after one driver bug: "
<< kernel.scheduler << '\n';
}// Expressed as a Java program, the structure is obvious: one process,
// shared references, no isolation anywhere.
class MonolithicKernel {
Scheduler scheduler = new Scheduler();
VFS vfs = new VFS();
BlockLayer blockLayer = new BlockLayer();
Map<String, Driver> drivers = new HashMap<>();
byte[] sysRead(int fd, int count) {
Inode inode = vfs.resolve(fd); // direct invocation
var plan = blockLayer.plan(inode, count);
return drivers.get(inode.device()).read(plan);
}
void insmod(Driver d) {
d.init(this); // hands the module a reference to EVERYTHING
drivers.put(d.device(), d);
}
}
// The analogy holds exactly where it matters: passing `this` gives the
// module reach into the scheduler, the VFS, and every other driver.
// Java would at least keep it memory-safe -- C does not, which is why
// Rust was merged into the kernel specifically for new driver code.Step through it
Running on insmod badnet.ko — a network driver with an off-by-one write
Read all 14 Steps
- one address space, every subsystem inside it The scheduler, VFS, block layer, network stack and memory manager all live in a single privileged address space. Any one of them can reach any other's data by pointer, with no translation and no permission check. That is the definition of monolithic, and everything else follows from it.
- a syscall enters — one crossing, then nothing read() transitions from user to kernel mode. That is the only protection boundary in the entire operation. From this point every step is an ordinary function call.
- VFS calls the block layer — a jump, not a message The VFS resolves the descriptor and calls into the block layer. Arguments go in registers, the callee reads the caller's structures directly through pointers. The cost is a few nanoseconds. A microkernel would need two protection-domain crossings and a message copy for exactly this step.
- block layer calls the driver — again just a jump Same again into the device driver. Three subsystems have now cooperated on one request with zero boundary crossings between them, sharing data structures by reference. This is the performance argument for monolithic design, and it is a genuinely strong one.
- the request completes — total cost, 2 crossings Data returns up the same path by direct return, and one transition back to user mode completes it. Two crossings for the whole operation. The same read on a microkernel took eight.
- a new device appears — insmod loads a module A network driver is loaded at runtime with insmod. The kernel allocates memory for it, resolves its symbol references against exported kernel symbols, runs its init function, and registers it. No reboot, no recompile.
- the misconception worth killing Runtime loading looks like isolation and is not. The module was linked into the kernel's own address space and runs with full kernel privilege. It can write any physical page and execute any privileged instruction. Modules solve distribution and memory footprint; they do nothing whatsoever for fault containment.
- the module runs normally for hours It handles packets correctly. Its receive buffer is 1500 bytes, sized for a standard Ethernet frame, and every frame it has seen has fit. The bug is present in every one of those successful executions and has simply not been triggered.
- a 1501-byte frame arrives A jumbo frame arrives one byte over the buffer. memcpy writes 1501 bytes into a 1500-byte allocation. In a userspace program this is a heap overflow that might crash the process. Here there is no process boundary to crash into.
- the overwritten byte belonged to the scheduler Whatever occupied the next bytes in the slab is now corrupted. In this run it happens to be a task_struct field belonging to the scheduler — a subsystem the network driver has no relationship with, wrote no code against, and was never intended to touch. Nothing in hardware prevented the write, because both live in the same address space.
- the failure surfaces somewhere unrelated The system does not fault at the memcpy. It continues until the scheduler reads the corrupted field, then panics — or worse, does not panic and silently schedules incorrectly. The reported symptom points at the scheduler; the cause is in a network driver, and the two are connected by nothing except having shared an address space.
- why drivers are where this keeps happening Defect studies of kernel source consistently find error rates in driver code several times higher than in core kernel code. The reasons are structural, not cultural: drivers are written against undocumented hardware, tested on few configurations, reviewed by very few people, and make up the majority of kernel source by volume. In a monolithic design, that least-reviewed code carries the same privilege as the scheduler.
- what the mitigations actually do KASLR randomises kernel addresses, lockdown restricts what root can do to a running kernel, module signing stops unsigned code loading, eBPF verifies programs before they run, and Rust is being adopted for new driver code in both Linux and Windows. Every one of these narrows the exposure and is worth having. None of them changes the structural fact: a driver fault is a kernel fault, because the driver is in the kernel.
- why nearly everything ships this way anyway Linux, the BSDs, Solaris and AIX are monolithic. Windows NT and macOS are called hybrid, but XNU runs a Mach microkernel with BSD and drivers alongside it in kernel space, which makes it monolithic in the only sense that matters for fault isolation. The most telling data point is Windows NT 4.0: Microsoft shipped the graphics subsystem in user space in NT 3.5, measured the cost, and moved it into the kernel. That is a team that had built the isolated version deciding, with numbers, that the trade was not worth it for a desktop OS. The conclusion is not that one design won. General-purpose systems, where workloads are unpredictable and throughput is what users perceive, chose the monolithic trade. Systems where a crash is unacceptable chose the other one. Both are correct for their constraints.
Hybrids and Why Nearly Everything Is One
Monolithic kernel examples cover most of what runs today: Linux, FreeBSD, OpenBSD, NetBSD, Solaris, and AIX. Windows NT and macOS are usually called hybrid — XNU literally contains a Mach microkernel with a BSD layer and drivers running alongside it in kernel space, which makes it monolithic in the only sense that matters for fault isolation.
The hybrid label describes lineage more than behaviour. Windows moved the graphics subsystem into kernel space in NT 4.0 for performance, having shipped it in user space in NT 3.5. That is the monolithic trade being made deliberately, by a team that had already built the microkernel version and measured it.
The reasonable conclusion is not that one design won. It is that general-purpose systems, where workloads are unpredictable and throughput is what users perceive, have chosen the monolithic trade — while systems where a crash is unacceptable have chosen the other. Both are engineering, and both are correct for their constraints.
- Linux, the BSDs, Solaris and AIX are monolithic
- Windows and macOS are hybrids in lineage only
- NT 4.0 moved graphics into the kernel for speed
- General-purpose systems chose throughput deliberately