The BSD personality: how XNU pretends to be Unix
Processes, file descriptors, signals, sockets — the FreeBSD-derived layer that sits on top of Mach and makes macOS pass POSIX.
XNU is two kernels in one binary. The bottom is Mach: tasks, threads, ports, virtual memory. The top is BSD: processes, files, signals, sockets, system calls. When you type ls, every syscall you make goes through the BSD layer; the BSD layer turns each one into operations on the Mach objects underneath.
This is the article about that top half. It exists, and it's not a thin shim.
Where the BSD code came from
The BSD subset in XNU was forked from FreeBSD around the 4.x → 5.x transition, then carried forward selectively. Apple pulls fixes and features from upstream FreeBSD when they're useful, but the codebase has diverged enough that you should think of it as its own thing now.
apple-oss-distributions/xnubsd/kern/init_main.c:1bsdinit_task — the first BSD process. Read this once and a lot of boot will make sense.View on GitHub(line 1)If you grep bsd/ you'll see structures that look exactly like FreeBSD: struct proc, struct vnode, struct mbuf, struct ucred. The function names match. The macros match. What's missing is FreeBSD's scheduler, its VM, and its IPC — XNU does those in Mach, and the BSD code calls down.
The proc is a thin wrapper over a Mach task
Every BSD process is a struct proc. The first interesting field is the one you'd expect:
struct proc {
LIST_ENTRY(proc) p_list;
pid_t p_pid;
struct proc *p_pptr;
...
void *task; // points to a Mach task_t
...
};
apple-oss-distributions/xnubsd/sys/proc_internal.h:180struct proc — the BSD process. Notice that `task` is just a pointer.View on GitHub(line 180)
task is a task_t from Mach. The BSD layer doesn't own the address space, the threads, the port rights — Mach does. The BSD proc is the part with a pid_t, a ucred, a file descriptor table, signal handlers, and the parent/child links every Unix process tree needs.
Conversely, a Mach task has a bsd_info pointer that lets the kernel walk the other way: from a task_t you can reach the proc. That bidirectional pointer is the entire bridge between the two halves of the kernel.
System calls land here
When userspace executes SVC #0x80 (arm64) or SYSCALL (x86_64), the trap handler picks the table to dispatch from based on the syscall number's sign:
- Positive numbers → BSD syscalls. Dispatched through
sysent[]. - Negative numbers → Mach traps. Dispatched through
mach_trap_table[].
apple-oss-distributions/xnubsd/kern/init_sysent.csysent[] — every positive-numbered POSIX syscall, indexed by its number.View on GitHub(line —) apple-oss-distributions/xnuosfmk/kern/syscall_sw.cmach_trap_table[] — the negative-numbered Mach traps.View on GitHub(line —)
The BSD sysent is the auto-generated table that maps SYS_open, SYS_read, SYS_kqueue and so on to the C function that implements them. The build system pulls this from bsd/kern/syscalls.master, which is the human-readable source of truth.
VFS — the file system you actually touch
Open /etc/hosts and you're going through the BSD VFS layer. VFS gives every filesystem (APFS, HFS+, NFS, devfs, fdesc) a shared API: vnode_t, vnop_* calls, namespaces glued together with mount points.
apple-oss-distributions/xnubsd/vfs/vfs_syscalls.cVFS syscall entry points — open, close, read, write, stat all start here.View on GitHub(line —) apple-oss-distributions/xnubsd/sys/vnode.hvnode — the in-core handle for a file/directory regardless of filesystem.View on GitHub(line —)
A vnode is reference-counted. When the count hits zero it's eligible for reuse from a global LRU. There's a fixed number of them (controlled by kern.maxvnodes), and exhausting them is a real failure mode on heavy workloads — vnode_pager is one of the busier sources of pressure on the VM compressor.
File descriptors are not vnodes
int fd indexes into a per-process descriptor table (fdt). Each slot points to a struct fileproc, which points to a struct fileglob, which contains a generic fg_ops and an fg_data payload. For a regular file, fg_data is the vnode. For a kqueue, it's a kqueue struct. For a socket, it's a socket. The same syscalls (read, write, close) work because everything's dispatched through fg_ops.
apple-oss-distributions/xnubsd/kern/kern_descrip.cfdtable — file descriptor allocation, dup, dup2, fcntl, close.View on GitHub(line —) apple-oss-distributions/xnubsd/sys/file_internal.hfileproc / fileglob / fileops — the abstraction that lets sockets, files, and kqueues share read/write.View on GitHub(line —)
This is the same UNIX pattern you'd see in any BSD; it's named slightly differently in XNU but the shape is identical.
Signals (and why they're harder than they look)
POSIX signals are per-process, but delivery is per-thread — POSIX says a signal can be handled by any thread that doesn't have it masked. XNU implements this by keeping a process-wide pending mask plus per-thread masks, walking the threads to find a candidate at delivery time.
apple-oss-distributions/xnubsd/kern/kern_sig.cSignal generation, masking, delivery, default actions.View on GitHub(line —)The interesting part is that signal delivery has to interact with Mach. Killing a process needs to abort threads; threads might be blocked in mach_msg_receive on a Mach port. The BSD signal code calls down into Mach's thread state to push a delivery frame onto the user stack — and the AST mechanism (Asynchronous System Trap) is how it gets the thread to notice on its way back to userspace.
Networking lives here, mostly
The sockets and protocol stacks (TCP, UDP, IPv6, the routing table) are BSD code — derived from FreeBSD, kept close enough to upstream that diffs against FreeBSD are tractable. The interesting macOS-specific bits are the network kernel extensions (NKEs) and the filter framework that lets things like Little Snitch intercept connections.
apple-oss-distributions/xnubsd/netinet/tcp_input.cTCP receive path — the busiest function in the stack on most systems.View on GitHub(line —) apple-oss-distributions/xnubsd/net/kpi_protocol.cProtocol KPI — the supported interface for third-party network filters/protocols.View on GitHub(line —)
What this layer doesn't do
A lot of "operating system" things you'd expect from BSD code are missing here, because Mach does them:
- Memory allocation for processes (
mmap, anonymous pages, the heap) ends up callingvm_map_enterin Mach. - Threads —
pthread_createis implemented inlibpthreadagainst the Machthread_*calls; BSD doesn't have its own thread type. - Inter-process communication beyond pipes and sockets: anything richer (XPC, services, sharing memory) is Mach.
This is why beginners get confused: the BSD layer looks like a complete Unix until you read bsd/kern/init_main.c and notice it's bootstrapping into a Mach task. The Unix you're using is a personality. Mach is the host.
What to read next
- Mach ports and messages — the IPC the BSD layer calls down to.
- IOKit and the driver model — how the kernel actually talks to hardware.
And read bsd/kern/init_main.c end-to-end at least once. It's short. It explains how the two halves of the kernel meet.