The Kernel ABI vs API Distinction

Two words that are constantly used interchangeably mean precisely opposite things at the kernel boundary, and confusing them is the single most common source of “why won’t this driver load on the new kernel?” frustration. An API (Application Programming Interface) is a source-level contract: the function names, signatures, header declarations, macros, and type definitions you write your code against and then compile. An ABI (Application Binary Interface) is a binary-level contract: the syscall numbers, the register that carries each argument, the byte-exact layout of a struct in memory, calling conventions, and symbol mangling that an already-compiled binary depends on at runtime. The API is what the compiler reads; the ABI is what the CPU executes. Linux makes a deliberate, asymmetric promise about these two contracts that surprises almost everyone on first contact: it keeps the userspace-facing syscall ABI frozen essentially forever, while explicitly refusing to provide any stable in-kernel API or ABI for loadable modules. That asymmetry — stable binary contract facing out, deliberately unstable everything facing in — is the architecture of the whole boundary, and it is argued at length in the kernel’s own Documentation/process/stable-api-nonsense.rst (stable-api-nonsense, v6.12).

This note draws the distinction sharply, shows where each contract lives, and explains why Linux chose to stabilize one and destabilize the other. The “do not break” half of the story — the enforcement, the patterns, the famous Linus rants — lives in its sibling The We Do Not Break Userspace ABI Promise; here the focus is the conceptual split itself.

API: the Source-Level Contract

An API is everything you can name when you write source code. When a C program calls open("/etc/passwd", O_RDONLY), the API it is using is: the spelling open, the fact that it takes a const char * and an int, the constant O_RDONLY defined in <fcntl.h>, and the promise that the return value is an int file descriptor or -1. None of that says anything about how the call is encoded in machine code. The API is satisfied entirely at compile time: the compiler needs the header to know the prototype, type-checks your arguments against it, and emits a call to a symbol named open. If the header changes — a parameter added, a struct field renamed, a macro redefined — your code may fail to compile, but a binary you compiled yesterday is completely unaffected, because the binary no longer contains any of those names.

Two programs that agree on an API can still be binary-incompatible. If I compile open against a header that declares its second argument as a 32-bit int and you compile a library that expects a 64-bit long, our source looks identical but our binaries disagree about how many bytes to push — and that disagreement is an ABI mismatch, not an API one.

ABI: the Binary-Level Contract

An ABI is everything an already-compiled binary assumes about the machine and the code it links or traps into, without recompiling. At the syscall boundary the ABI is unusually concrete and is documented per architecture in syscall(2). On x86-64 it consists of:

  • The syscall number, placed in the rax register. read is 0, write is 1, open is 2, and so on — a number, not a name. (See System Call Numbers and the ABI.)
  • The argument registers, in a fixed order: rdi, rsi, rdx, r10, r8, r9 for up to six arguments (syscall(2)). Note r10not rcx as the C function-call ABI would use — because the syscall instruction clobbers rcx to save the return address.
  • The return convention: the result comes back in rax; a value in the range [-4095, -1] is interpreted as a negated errno.
  • Struct layouts: when a syscall takes a pointer to a structure (say struct stat), the kernel and the binary must agree byte-for-byte on field order, sizes, and padding. A field inserted in the middle shifts every later field and breaks every compiled caller.

The defining property of an ABI is that it is consumed at runtime by code that will never be recompiled. A binary from 1995 still puts the read number in rax and the buffer pointer in rsi; if today’s kernel still honors that encoding, the binary runs. That is exactly the promise Linux makes for syscalls.

flowchart LR
  subgraph SRC["Compile time — the API"]
    H["headers:<br/>open(), O_RDONLY,<br/>struct stat decl"]
    C["your .c source"]
    H --> CC["compiler:<br/>type-check, resolve<br/>symbol names"]
    C --> CC
  end
  CC --> BIN["compiled binary<br/>(names are gone)"]
  subgraph RUN["Runtime — the ABI"]
    BIN --> REG["rax = nr,<br/>rdi/rsi/rdx/r10/r8/r9 = args,<br/>struct bytes laid out"]
    REG --> K["kernel honors the<br/>same encoding forever"]
  end

Two contracts, two lifetimes. What it shows: the API governs the left box (what the compiler reads — names, prototypes, constants) and is satisfied and then discarded at compile time; the ABI governs the right box (what the running binary and CPU exchange — numbers, registers, struct bytes) and must hold for the entire life of the binary. The insight: changing a header (API) can break a future build but never a shipped binary; changing a syscall number or struct layout (ABI) breaks every shipped binary instantly — which is why the ABI is the surface Linux freezes.

The Asymmetry: Stable Out, Unstable In

The boundary has two sides, and Linux treats them oppositely.

Facing userspace (outward): the ABI is frozen. The kernel’s own stable-ABI documentation states that userspace interfaces such as syscalls “are expected to never change and always be available,” and that documented stable interfaces carry a backward-compatibility guarantee of “at least 2 years” — with syscalls in practice promised forever (abi-stable, v6.12). stable-api-nonsense.rst is blunt about it: “The kernel to userspace interface is the one that application programs use, the syscall interface. That interface is very stable over time, and will not break.” The evidence it cites is that binaries built against pre-0.9 kernels still run on modern releases.

Facing modules (inward): there is deliberately no stable contract — neither API nor ABI. This is the part that shocks people coming from Windows or Solaris, where a driver compiled once keeps loading across OS updates. Linux refuses to make that promise on purpose. The document’s title — “stable-api-nonsense” — is the thesis: a stable in-kernel interface is, in the maintainers’ view, not merely unprovided but actively undesirable.

flowchart TB
  APP["userspace binary<br/>(1995 or 2026)"]
  subgraph KERN["kernel"]
    SYS["syscall ABI<br/>FROZEN — numbers,<br/>registers, struct layout"]
    INT["in-kernel API + ABI<br/>UNSTABLE — function sigs,<br/>struct layout, EXPORT_SYMBOL"]
    MOD["loadable module / driver"]
    SYS --- INT
    INT --- MOD
  end
  APP -->|"stable contract,<br/>honored forever"| SYS
  MOD -->|"recompiled against<br/>THIS kernel's source"| INT

The two faces of the kernel. What it shows: outward, toward userspace binaries, the syscall ABI is a frozen contract any binary of any age can rely on; inward, toward modules, both the API (function signatures) and ABI (struct layouts, exported-symbol set) are explicitly volatile and a module must be recompiled against the exact kernel it loads into. The insight: Linux trades inward instability for the freedom to refactor internals, and pays for it by requiring in-tree drivers; it trades that freedom away outward to keep userspace working forever.

Why No Stable In-Kernel ABI — the Three Obstacles

stable-api-nonsense.rst gives three concrete reasons a binary in-kernel interface is impossible even if anyone wanted one (v6.12 text):

  1. Compiler variation. “Depending on the version of the C compiler you use, different kernel data structures will contain different alignment of structures.” Struct padding and field alignment are a function of the compiler and its flags; a module built with one GCC cannot trust the layout a differently-built kernel uses.
  2. Build-option variation. “Depending on what kernel build options you select, a wide range of different things can be assumed by the kernel.” A CONFIG_* option can add or remove fields from a struct, inline-or-not a function, or change locking — so the same source compiled with different .config files produces incompatible binaries. struct task_struct is the canonical example: its size and field offsets depend on dozens of config knobs.
  3. Architecture incompatibility. “Linux runs on a wide range of different processor architectures. There is no way that binary drivers from one architecture will run on another architecture properly.” Word size, endianness, and calling conventions differ.

The combinatorial product of compiler versions × config options × architectures × kernel releases is astronomically large, so a single binary module that loads everywhere is not just hard — it is mathematically hopeless. This is why module loading checks vermagic and symbol CRCs: the kernel refuses a module whose build assumptions don’t match, rather than silently running a layout-mismatched binary.

Why No Stable In-Kernel Source API Either

A weaker promise would be a stable source API: internal function signatures never change, so at least you only need to recompile. Linux declines even this, and the reasoning is about velocity, not feasibility. “Linux kernel development is continuous and at a rapid pace, never stopping to slow down” (stable-api-nonsense, v6.12). Freezing internal signatures would freeze internal design: the document points to the USB subsystem being reworked roughly three times to move from a synchronous to an asynchronous model and to fix deadlocks — reworks that necessarily changed the interfaces every USB driver used. Security fixes are the sharpest case: closing a vulnerability sometimes requires restructuring internal data flow in a way that touches every caller, and a frozen API would forbid the fix.

The kernel’s prescribed remedy is structural, not a stable interface: put your driver in the mainline tree. “If your driver is in the tree, and a kernel interface changes, it will be fixed up by the person who did the kernel change in the first place” (v6.12 text). The person who renames an internal function is responsible for fixing every in-tree caller in the same patch, so in-tree drivers never bit-rot. Out-of-tree modules carry the maintenance burden themselves — by design, as an incentive to upstream.

How the Two Contracts Coexist in One Codebase

The same kernel source simultaneously implements a frozen outward ABI and a churning inward one, and the boundary between them is the SYSCALL_DEFINEn macro and the user-copy accessors. Consider read:

/* fs/read_write.c (illustrative, abridged) */
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
        return ksys_read(fd, buf, count);
}

Line-by-line:

  • SYSCALL_DEFINE3(read, ...) — the macro that pins the outward ABI for read: it wires this handler into sys_call_table[0] and establishes that argument 1 (fd) arrives in rdi, argument 2 (buf) in rsi, argument 3 (count) in rdx. Those register assignments are part of the frozen contract; they cannot change.
  • char __user *buf — the __user annotation marks buf as a userspace pointer that must be validated, not dereferenced directly (see copy_to_user and copy_from_user). This is an ABI-level concern: the binary handed the kernel a raw address.
  • ksys_read(...) — an internal function. Its name, signature, and everything below it are the inward API, and the maintainers are free to rename, split, or re-parameterize it tomorrow. They did, in fact, introduce the ksys_* family precisely to refactor the internal call paths without touching the SYSCALL_DEFINE3 ABI surface.

So the macro is the seam: above it (the macro’s name and argument registers) is frozen forever; below it (the helper it calls) is fair game. New functionality is added outward not by editing SYSCALL_DEFINE3(read, ...) but by defining an entirely new syscall — the flags-argument and versioned-struct patterns covered in The We Do Not Break Userspace ABI Promise and Adding a New System Call to Linux. renameat2 is the textbook case: rather than add a flag to rename, the kernel shipped a new syscall whose “zero flags argument is equivalent to renameat()” (renameat2(2)), preserving the old ABI exactly while extending the API.

The ABI Documentation Taxonomy

Not every userspace-facing interface is equally frozen. The kernel sorts them into four buckets under Documentation/ABI/, each a directory with a stability promise (ABI/README, v6.12):

BucketPromise
stable”backward compatibility for them will be guaranteed for at least 2 years”; most (syscalls especially) “expected to never change and always be available.”
testingBelieved stable, main development done; “can be changed to add new features, but the current interface will not break by doing this, unless grave errors or security problems are found.”
obsoleteStill present but marked for eventual removal; the docs state the reason and expected removal window.
removedAlready gone.

The table is a genuine enumeration, so a list is appropriate here; the teaching is the prose around it. The key point: “stable” is not a single binary switch but a gradient, and syscalls sit at the most-frozen end. /sys (sysfs) attributes, by contrast, frequently live in “testing” and can shift — a reminder that “ABI” at the kernel boundary spans more than syscalls: it includes the byte/text format of /proc and /sys files, ioctl numbers, netlink message layouts, and /dev semantics, all of which compiled userspace tools parse and depend on.

Common Misunderstandings

  • “Stable ABI means stable source — I can recompile my driver and it’ll work.” No. There is neither a stable in-kernel ABI nor a stable in-kernel source API. Recompiling against a new kernel’s headers can fail outright (signature changed) or, worse, succeed but mismatch a struct layout. Only in-tree, recompiled-with-the-kernel drivers are safe.
  • “The userspace ABI promise covers libc functions.” No — the kernel promises the syscall ABI. The libc API (fopen, printf) is glibc/musl’s contract, governed by its own versioning (glibc symbol versioning). The kernel guarantees write the syscall; glibc guarantees fwrite the function. See libc Syscall Wrappers and errno Translation.
  • “A stable ABI means the numbers are the same on every architecture.” No. Syscall numbers are per-architecture — read is 0 on x86-64 but 63 on arm64. The ABI is stable within an architecture, not across them (syscalls(2)). See Syscall ABI Differences Across Architectures.

Uncertain

Verify: the claim that documented “stable” ABI interfaces carry a minimum 2-year guarantee while syscalls are promised effectively forever. Reason: the Documentation/ABI/README and abi-stable.rst wording was read at the v6.12 tag and via the rendered docs, but the “at least 2 years” figure historically applied to the broader stable-ABI set (sysfs etc.), and the forever status of syscalls is a cultural/enforced norm rather than a numbered SLA. To resolve: re-read Documentation/ABI/README and Documentation/admin-guide/abi-stable.rst at the v6.12 and v6.18 tags and confirm the exact figures. uncertain

Alternatives and the Closed-Source Contrast

Other operating systems made the opposite trade. Windows maintains a stable kernel-mode driver ABI within a driver model generation (the Windows Driver Model / Windows Driver Frameworks), so a signed binary driver loads across many OS builds. Solaris had a Device Driver Interface (DDI) committed as stable. The benefit is binary third-party drivers that survive OS upgrades; the cost, as stable-api-nonsense.rst argues, is that the kernel is then locked into legacy interfaces and cannot freely refactor internals or apply structural security fixes. Linux took the other branch: maximal internal freedom, paid for by requiring drivers in-tree, and a frozen external ABI so end users never pay for that internal churn. The asymmetry is the entire point — the instability is pushed onto out-of-tree module authors (who are nudged to upstream) and away from the userspace binaries the kernel exists to serve.

See Also