← Back to Blog

February 15, 2026 · Deep Dive

How BSDulator Translates 500+ Syscalls

BSDulator's core job is deceptively simple to describe and brutally complex to implement: intercept every system call a FreeBSD binary makes, translate it to the Linux equivalent, and return the result as if nothing happened.

The Translation Pipeline

When a FreeBSD binary executes a syscall instruction, BSDulator's ptrace interceptor catches it at syscall-entry. The pipeline is:

  1. Syscall number translation: FreeBSD and Linux use completely different numbering. FreeBSD's open is syscall 5; Linux's is 2. We maintain a mapping table for 500+ syscalls.
  2. Argument translation: Even when syscalls share a name, the arguments often differ. FreeBSD's sigprocmask uses how values 1/2/3; Linux uses 0/1/2. FreeBSD's sigset_t is 16 bytes; Linux's is 8.
  3. Memory space awareness: Results must be written to the child's memory, not ours. This uses process_vm_writev() with ptrace(PTRACE_POKEDATA) as fallback.
  4. Return value translation: Error codes, structure layouts, and return conventions differ between the two kernels.

Three Categories of Syscalls

Direct translations (TRANS): Syscall numbers differ but semantics are identical. read, write, close, getpid. These just need number remapping.

Emulated syscalls (EMUL): These require custom handler functions because the semantics differ significantly. Examples include clock_gettime (FreeBSD has different clock IDs), sigprocmask (different sigset sizes and how values), sysarch (TLS setup via AMD64_SET_FSBASE), and all four jail syscalls.

FreeBSD-only syscalls: Calls like jail_set (507), jail_get (506), jail_attach (436), and jail_remove (508) have no Linux equivalent at all. These are fully emulated using Linux namespaces, cgroups, and chroot.

The Hardest Part: Structures

The trickiest bugs come from structure layout differences. FreeBSD's struct sigaction isn't the same as Linux's. FreeBSD's stat has different field offsets. The jail parameter structures (struct iovec arrays used by jail_set/jail_get) have their own encoding format.

Every one of these requires reading from the child process's memory, translating the structure in our address space, and writing the translated version back. One byte off in alignment and everything breaks silently.

Current Coverage

About 500 FreeBSD syscalls are mapped. The most critical ones — file I/O, memory management, process control, signals, threading primitives, and jail operations — have custom emulation handlers. Less common syscalls either map directly or return ENOSYS stubs with logging so we know when they're hit.

The source lives in src/syscall/syscall_table.c — currently the largest file in the project.

← BSDulator Phase 3: VNET Networking is LiveThe TLS Bug That Took Three Days to Find →