Adding a New System Call to Linux
Adding a system call is the most consequential thing you can do to the kernel’s userspace contract: a syscall, once shipped in a release, “forms part of the API of the kernel, and has to be supported indefinitely” (adding-syscalls.rst). Because of the “we do not break userspace” rule, a syscall’s number, signature, and semantics are frozen the moment a stable kernel goes out the door — you can never reuse the number, never change the argument layout, and never break a program that already calls it. The official process, documented in
Documentation/process/adding-syscalls.rst, is therefore as much about forethought — is a syscall even the right interface, and is its signature future-proof? — as about the mechanics of writingSYSCALL_DEFINE, allocating a number in every architecture’s table, wiring upunistd.h, handling 32-bit compatibility, testing it, and shipping a man page. This note walks the whole pipeline as of Linux 6.12 LTS (the doc is stable across the 6.x series, but the number-allocation step changed mechanically in 6.11, noted below).
This note is the kernel-developer counterpart to The syscall() Generic Wrapper Function (which is how userspace reaches a syscall that has no libc wrapper yet) and builds directly on SYSCALL_DEFINE and Syscall Handler Wrappers (the macro that defines the entry point) and The System Call Table (where the number lands).
Mental Model
A syscall is not a function you add in one place; it is a number wired to a handler, replicated across every architecture the kernel supports, with a userspace contract attached. Adding one means threading a single new identity through several independent tables and headers, plus a body of forward-compatibility discipline so that the signature you pick today still works on the kernels and userspaces of a decade from now.
flowchart TB Q{"Is a syscall even<br/>the right interface?"} ALT["Alternatives:<br/>new fs/device · sysfs/proc<br/>· fcntl/prctl command"] API["Design signature:<br/>flags arg · size-versioned struct<br/>· fd handles · loff_t offsets<br/>· capability check"] IMPL["Generic implementation:<br/>SYSCALL_DEFINEn · prototype in<br/>syscalls.h · CONFIG option<br/>· COND_SYSCALL stub"] NUM["Allocate the number:<br/>generic scripts/syscall.tbl<br/>+ x86 syscall_64.tbl / _32.tbl"] COMPAT["32-bit compat layer<br/>(only if struct/64-bit/ptr-to-ptr<br/>layout differs): COMPAT_SYSCALL_DEFINEn"] TEST["selftest in<br/>tools/testing/selftests/<br/>(calls via syscall())"] MAN["man page to<br/>linux-man@vger.kernel.org"] Q -->|"yes"| API Q -->|"maybe not"| ALT API --> IMPL --> NUM --> COMPAT --> TEST --> MAN
The end-to-end pipeline for adding a syscall. What it shows: the work splits into a design phase (is this a syscall at all, and is its signature extensible?) and a wiring phase (define it, number it in every arch table, add compat shims, test, document). The insight: the irreversible decisions are at the top — once the number and signature ship, they are permanent — so the doc front-loads the “should you even?” and “is it future-proof?” questions before any code.
Step 0 — Is a Syscall Even the Right Interface?
The doc opens by insisting you consider alternatives first, because every syscall is permanent overhead on the ABI (adding-syscalls.rst):
- A filesystem-like object or device. If your operations can be modeled as objects you read/write, create a new filesystem or device instead. This is more easily encapsulated in a kernel module (a syscall must be built into the core kernel), and if the object yields a file descriptor, userspace gets
poll/select/epollnotification for free. The downside the doc flags: operations that don’t map toread/writeend up asioctl(2)requests, “which can lead to a somewhat opaque API.” sysfsor/proc. For merely exposing runtime system information, a node in sysfs or procfs is more appropriate — but the doc warns these “require that the relevant filesystem is mounted, which might not always be the case (e.g. in a namespaced/sandboxed/chrooted environment),” and explicitly forbids usingdebugfsas a production userspace API.- An
fcntl(2)command for operations specific to a file descriptor, or aprctl(2)command for operations specific to a task/process. Both are multiplexing syscalls, “best reserved for near-analogs of existing” commands or simple flag get/set — not a dumping ground for unrelated functionality.
Only if none of these fit is a fresh syscall justified. And because it is permanent, the doc says: “it’s a very good idea to explicitly discuss the interface on the kernel mailing list,” and to CC linux-api@vger.kernel.org on the proposal.
Step 1 — Design the Signature for Extension
This is the part that most distinguishes a good syscall from a regret. The doc points at the table’s own scar tissue — the pairs of syscalls that exist because the first one wasn’t extensible: eventfd/eventfd2, dup2/dup3, inotify_init/inotify_init1, pipe/pipe2, renameat/renameat2. Each …2/…1 exists only because the original forgot a flags argument. The forward-compatibility discipline:
Take a flags argument, and reject unknown bits. For simple syscalls, a flags parameter is the preferred extension point. You must validate it so that a program built against a newer kernel cannot silently get old behaviour on an older one (adding-syscalls.rst):
if (flags & ~(THING_FLAG1 | THING_FLAG2 | THING_FLAG3))
return -EINVAL;If no flags are defined yet, check the argument is zero. The point is that an unknown flag must fail loudly with EINVAL, never be ignored — otherwise the meaning of a flag bit is ambiguous across kernel versions.
For many arguments, use a size-versioned struct. Encapsulate the parameters in a structure passed by pointer, with a size field as the first member:
struct xyzzy_params {
u32 size; /* userspace sets p->size = sizeof(struct xyzzy_params) */
u32 param_1;
u64 param_2;
u64 param_3;
};As long as every later-added field defaults to “zero == old behaviour,” this handles version mismatch in both directions: a newer userspace calling an older kernel works because the kernel checks that any memory beyond the struct size it knows is zero (i.e. the new param_4 == 0); an older userspace calling a newer kernel works because the kernel zero-extends the smaller struct it receives. The doc cites perf_event_open(2) and its perf_copy_attr() function (kernel/events/core.c) as the reference implementation of this pattern.
Other signature rules, each citing a real failure the kernel learned from:
- Use file descriptors as object handles. “Don’t invent a new type of userspace object handle when the kernel already has mechanisms and well-defined semantics for using file descriptors.”
- Provide an
O_CLOEXEC-equivalent flag for any syscall returning a new fd, so userspace can close the race window between getting the fd and settingFD_CLOEXEC— a window where afork()+execve()in another thread could leak the descriptor. (But don’t reuse the literalO_CLOEXECvalue, which is architecture-specific and lives in a crowdedO_*numbering space.) - Consider an
*at()variant if there’s a filename argument:xyzzyat(dfd, path, …, flags)is more flexible, and withAT_EMPTY_PATHit gives anfxyzzy()-on-an-fd operation for free. - Use
loff_tfor file offsets so 64-bit offsets work even on 32-bit architectures. - Govern privileged operations with a
capable()check against a specific capability bit, “avoid adding new uses of the already overly-generalCAP_SYS_ADMIN” (the doc is blunt that lumping things underCAP_SYS_ADMINdefeats the purpose of capabilities — see Linux Security MOC). If the syscall manipulates another process, gate it withptrace_may_access(). - Put explicitly-64-bit parameters on odd-numbered argument slots (parameter 1, 3, 5). Some non-x86 architectures pass a 64-bit value as a contiguous pair of 32-bit registers, and odd-numbered placement lets that pair align cleanly. (This is the kernel mirror of the userspace 64-bit-argument caveat in The syscall() Generic Wrapper Function.)
Step 2 — Write the Generic Implementation
The entry point is added with the SYSCALL_DEFINEn() macro, not a hand-written function, because the macro also emits metadata used by other tooling (see SYSCALL_DEFINE and Syscall Handler Wrappers for what it expands to and why x86 decodes struct pt_regs on the fly):
SYSCALL_DEFINE3(xyzzy, int, arg1, char __user *, arg2, unsigned int, flags)
{
/* ... */
}The n (here 3) is the argument count, followed by (type, name) pairs. Alongside it you add:
- A prototype in
include/linux/syscalls.h,asmlinkage-marked to match the call convention:asmlinkage long sys_xyzzy(...); - A
CONFIGoption (typically ininit/Kconfig) so the feature is optional — and verify the kernel “still builds with the new CONFIG option turned off.” - A fallback stub in
kernel/sys_ni.cviaCOND_SYSCALL(xyzzy);, which provides a-ENOSYS-returning implementation when the feature is compiled out. This is what makessyscall(__NR_xyzzy)returnENOSYSrather than crashing on a kernel built without the feature.
A hard rule from the doc’s final section: never call sys_xyzzy() from inside the kernel. Since v4.17 on x86-64, the syscall calling convention decodes struct pt_regs on the fly in a wrapper, so only the parameters a syscall actually uses are passed; calling the sys_ entry directly would feed it garbage and “may cause serious trouble down the call chain.” Shared logic goes in a ksys_xyzzy() helper that both the syscall stub and other kernel code call.
Step 3 — Allocate the Number in Every Architecture Table
Uncertain
Verify: the precise generic-table mechanism name at 6.12. Reason: the prose of
adding-syscalls.rstat the v6.12 tag still instructs editinginclude/uapi/asm-generic/unistd.hdirectly, but the kernel also shipsscripts/syscall.tbl(confirmed present at v6.12), introduced around 6.11, which several architectures (arc, arm64, csky, hexagon, loongarch, nios2, openrisc, riscv) now consume to generate theirunistd.h— so the doc text lags the actual mechanism for those arches. To resolve: readscripts/syscall.tbl,scripts/syscallhdr.sh/syscalltbl.sh, and a consuming arch’sMakefile.syscallsat the v6.12 tag. uncertain
There is no single global syscall number — each architecture has its own. You must wire the new call into each table you support.
The generic table (consumed by the architectures that share one — arc, arm64, csky, hexagon, loongarch, nios2, openrisc, riscv, and others). As of 6.11+ this is scripts/syscall.tbl, whose format is <number> <abi> <name> <native-entry> [<compat-entry>] — confirmed at v6.12, e.g. its first line is 0 common io_setup sys_io_setup compat_sys_io_setup (scripts/syscall.tbl). You add a line like 468 common xyzzy sys_xyzzy. (Historically, and as the older doc text still describes, this was done by hand-editing include/uapi/asm-generic/unistd.h with __SYSCALL(__NR_xyzzy, sys_xyzzy) and bumping __NR_syscalls; that header is now generated from the table for those arches.)
The x86 tables, which x86 maintains separately. For a syscall that’s not “special,” add a common entry (covering both x86_64 and x32) to arch/x86/entry/syscalls/syscall_64.tbl — whose format header reads <number> <abi> <name> <entry point> [<compat entry point> [noreturn]], with the abi being common, 64, or x32 (syscall_64.tbl):
468 common xyzzy sys_xyzzy
and an i386 entry to arch/x86/entry/syscalls/syscall_32.tbl:
468 i386 xyzzy sys_xyzzy
The doc warns twice that “these numbers are liable to be changed if there are conflicts in the relevant merge window” — two patches in flight may both grab 468, and the maintainer renumbers one. This is why you never depend on a pending number and why, once merged and released, the number is sacred (see System Call Numbers and the ABI).
Step 4 — Compatibility Layer (Only Sometimes)
For most syscalls, the same 64-bit implementation serves 32-bit userspace transparently — even an explicit pointer argument is handled automatically. A compat_sys_xyzzy() is needed only when 32-bit and 64-bit layouts genuinely differ, specifically when an argument is (adding-syscalls.rst):
- a pointer to a pointer,
- a pointer to a struct containing a pointer (e.g.
struct iovec __user *), - a pointer to a varying-sized integral type (
time_t,off_t,long, …), - a pointer to a struct containing a varying-sized integral type,
or when an argument is explicitly 64-bit even on 32-bit (e.g. loff_t, __u64), because a 32-bit app splits that into two 32-bit halves the compat layer must reassemble. Notably, a pointer to an explicit 64-bit type does not need compat — the doc gives splice(2)’s loff_t __user * as the counter-example.
When needed, you write COMPAT_SYSCALL_DEFINEn(xyzzy, …), add an asmlinkage prototype to include/linux/compat.h, and (if a struct differs) define a struct compat_xyzzy_args there using compat_ types (compat_uptr_t, compat_long_t, …) for the variable-width fields, keeping fixed-width fields like u64 as-is. The compat stub typically converts to 64-bit and calls the shared ksys_/inner helper. You then point the 32-bit table column at the compat entry (on x86, __ia32_compat_sys_xyzzy in syscall_32.tbl; for x32, decide whether the layout matches the 32-bit or 64-bit version — if a pointer-to-pointer is involved, x32 is ILP32 so it follows the 32-bit layout via __x32_compat_sys_xyzzy). See The Compat Syscall Layer for 32-bit Binaries.
Step 5 — Syscalls That “Return Elsewhere,” Testing, and the Man Page
Special control-flow syscalls. A few syscalls don’t return to the next instruction with the same stack/registers/address space: rt_sigreturn returns to a different location, fork/vfork/clone change the memory space, execve/execveat change the whole program image. These need arch-specific assembly entry points that save/restore extra registers — on x86_64 a stub_xyzzy in arch/x86/entry/entry_64.S referenced from the table, with a stub32_xyzzy counterpart in entry_64_compat.S for 32-bit. (For user-mode Linux, a #define stub_xyzzy sys_xyzzy in arch/x86/um/sys_call_table_64.c keeps the UML build working since it simulates registers.) The doc also flags the audit subsystem as a special case: if your syscall is analogous to open/exec/socketcall, audit’s arch-specific classifiers need updating.
Testing. Add a self-test under tools/testing/selftests/. Because there is by definition no libc wrapper for a brand-new syscall, the test must invoke it via [[The syscall() Generic Wrapper Function|syscall()]] — this is the canonical reason that generic wrapper exists. Verify it across ABIs: compiled as x86_64 (-m64), x86_32 (-m32), and x32 (-mx32). For deeper coverage, contribute to the Linux Test Project, or xfstests for filesystem changes.
The man page. “All new system calls should come with a complete man page,” ideally in groff markup (plain text accepted), CC’ed to linux-man@vger.kernel.org (adding-syscalls.rst; man-pages patch guide).
Patch structure. Split the series into distinct commits: (1) core implementation — CONFIG, SYSCALL_DEFINEn, prototype, generic number, fallback stub; (2) the x86 wiring; (3) the selftest; (4) the man-page draft — and CC linux-api@vger.kernel.org on the whole proposal.
Common Misunderstandings and Failure Modes
- Forgetting the
flagsargument. The single most common regret, immortalized by the…2syscall pairs. If there is any chance the syscall will grow, take aflags(or size-versioned struct) from day one. - Ignoring unknown flag bits. Accepting (rather than
EINVAL-rejecting) unknown bits makes a flag’s meaning version-dependent and breaks forward compatibility — the exact thing flags were supposed to provide. - Assuming a pending number is stable. A number can be renumbered during the merge window; only a released number is frozen. Conversely, never reuse a retired number (see System Call Numbers and the ABI).
- Wiring only x86 and forgetting the generic table (or vice versa), leaving the syscall returning
ENOSYSon architectures you thought you supported. - Calling
sys_xyzzy()from kernel code. Forbidden on x86-64 since v4.17 due to thept_regsdecoding convention; use aksys_helper. - Shipping a syscall that should have been sysfs/fcntl/prctl. The doc’s whole first section exists because permanent ABI surface is expensive; reviewers will push back hard if an alternative fits.
See Also
- SYSCALL_DEFINE and Syscall Handler Wrappers — the macro that defines the entry point and the
pt_regsdecoding it generates - The System Call Table — where the allocated number dispatches to the handler
- System Call Numbers and the ABI — why numbers are append-only and never reused
- The We Do Not Break Userspace ABI Promise — the rule that makes every syscall permanent
- The syscall() Generic Wrapper Function — how the selftest (and userspace generally) calls a syscall with no libc wrapper yet
- The Compat Syscall Layer for 32-bit Binaries — the 32-bit-on-64-bit shims of Step 4
- Linux Security MOC — capability design (
capable(), avoidingCAP_SYS_ADMIN) - Linux System Call Interface MOC — parent map