Kernel

Add a syscall

Add a new BSD-personality syscall to XNU — from the syscall table entry, through the handler, to userspace.

Adding a syscall to darwinOS touches three layers: the syscall table (where its number lives), the handler (what it does in the kernel), and libSystem (how userspace calls it). This guide walks all three for a trivial example: a hello_syscall(const char *name) that logs hello, <name> from kernel space.

It’s a toy — the point is the plumbing. Real syscalls look the same, they just do more work.

Prerequisites

Step 1: Assign a syscall number

Edit xnu/bsd/kern/syscalls.master. Each line is <num> <abi> <ret> <name>(<args>); append yours at the end of the BSD namespace:

540  ALL   STD  int  hello_syscall(const char *name);
  • 540 is the next free number in the BSD syscalls range (check the file — don’t reuse).
  • ALL means every architecture gets it.
  • STD = standard syscall (no special treatment).

The build system regenerates syscall_numbers.h, syscall_dispatch.c, and the companion libSystem stubs from this file, so don’t edit those by hand.

Step 2: Write the handler

Handlers go in xnu/bsd/kern/. By convention, name the file after the syscall or its subsystem — kern_hello.c:

#include <sys/systm.h>
#include <sys/kauth.h>
#include <sys/sysproto.h>

struct hello_syscall_args {
    const char *name;
};

int
hello_syscall(proc_t p, struct hello_syscall_args *uap, int *retval)
{
    char buf[64];
    size_t copied = 0;
    int err = copyinstr((user_addr_t)uap->name, buf, sizeof(buf), &copied);
    if (err != 0) {
        return err;
    }
    printf("hello, %s (from PID %d)\n", buf, proc_pid(p));
    *retval = 0;
    return 0;
}

Key points:

  • proc_t p is the calling process’s kernel handle. proc_pid(p) gives the PID.
  • uap is the argument struct — same layout as in syscalls.master.
  • Never dereference userspace pointers directly. Use copyin, copyout, or copyinstr to marshal across the boundary.
  • Return 0 on success, a errno-style value on failure.

Add the new file to xnu/bsd/conf/MASTER or whatever your tree’s equivalent file-list is, then rebuild:

make xnu

Step 3: Call it from userspace

Once the kernel is built, the libSystem stubs include _hello_syscall automatically. Headers are a separate step — add the prototype to libsystem/include/sys/hello.h:

#pragma once
int hello_syscall(const char *name);

And from your userland program:

#include <sys/hello.h>

int main(void) {
    hello_syscall("world");
    return 0;
}

Build, copy into the guest, run. The string you passed shows up on the kernel console.

Step 4: Testing

Unit tests for syscalls live under tests/syscalls/. Add hello.c:

#include <assert.h>
#include <sys/hello.h>

int main(void) {
    assert(hello_syscall("test") == 0);
    return 0;
}

Register it in tests/syscalls/Makefile. Then:

make test

Negative paths — what does your syscall do if the pointer is NULL? Misaligned? A kernel pointer? Tests for each.

Common pitfalls

“Unknown syscall” at runtime

The libSystem stub wasn’t rebuilt after you edited syscalls.master. make world (not just make xnu) regenerates both sides.

Kernel panic on invocation

Almost always a missed copyin. Trying to read userspace memory directly in a kernel context panics on arm64 (SMAP-equivalent) or returns stale/wrong data on x86_64.

Syscall number collision

Two PRs adding syscalls at the same number race at merge time. The number you chose became wrong the moment someone else’s PR landed. Re-run the build, let the codegen pick the next free number, and update your handler’s args accordingly.

Next steps

  • Trace a syscall — observe your new syscall with DTrace once it’s working.
  • Debug the kernel — set a breakpoint in your handler and inspect what userspace is sending.