🐧 Linux Binary Compatibility: Running Linux Programs on FreeBSD
FreeBSD can run Linux programs directly, with no virtual machine. Switch it on, install a Rocky Linux userland, run real Linux software, and find where the compatibility ends
What it is
The FreeBSD kernel contains a Linux ABI layer, nicknamed the linuxulator. A Linux program makes system calls to the kernel to open files, allocate memory and send data, and it expects a Linux kernel to answer. The layer sits in the FreeBSD kernel, translates each Linux system call into the matching FreeBSD one, and answers in the way the Linux program expects.
This is not emulation of a processor and not a virtual machine. The Linux program runs natively on the CPU, at full speed, as an ordinary FreeBSD process: you see it in ps, you can limit it, trace it and kill it like any other. (Windows' WSL 1 used a similar idea.) Compare the other ways to run Linux on FreeBSD:
- A Linux virtual machine with bhyve or vm-bhyve: a complete Linux kernel of its own. Full compatibility, with the weight of a whole machine.
- The Linux compatibility layer (this page): no second kernel, nothing to boot, and Linux programs sit beside FreeBSD programs. The price is that the translation is not complete: some Linux features do not exist, as you will see.
Two ingredients are needed: the kernel modules that do the translating, and a Linux userland: the libraries and programs of a Linux distribution, kept in a folder, /compat/linux. Everything below was run for real on FreeBSD 15.0-RELEASE in a virtual machine.
Step 1: switch the layer on
root@practice:~ # uname -sr FreeBSD 15.0-RELEASE root@practice:~ # kldstat | grep -i linux root@practice:~ # ls /compat ls: /compat: No such file or directory root@practice:~ # sysctl compat.linux.osname sysctl: unknown oid 'compat.linux.osname'
A fresh FreeBSD has no Linux anything: no modules loaded, no /compat folder, and the Linux settings do not exist (unknown oid).
root@practice:~ # sysrc linux_enable=YES linux_enable: NO -> YES root@practice:~ # service linux start root@practice:~ # kldstat | grep -i linux 10 1 0xffffffff8305a000 32320 linux.ko 12 4 0xffffffff83094000 ce68 linux_common.ko 13 1 0xffffffff830a1000 2e3f0 linux64.ko root@practice:~ # mount | grep -E "linprocfs|linsysfs|compat" linprocfs on /compat/linux/proc (linprocfs, local) linsysfs on /compat/linux/sys (linsysfs, local) devfs on /compat/linux/dev (devfs) fdescfs on /compat/linux/dev/fd (fdescfs) tmpfs on /compat/linux/dev/shm (tmpfs, local) root@practice:~ # sysctl compat.linux.osname compat.linux.osrelease compat.linux.osname: Linux compat.linux.osrelease: 5.15.0
- sysrc linux_enable=YES makes the layer start at every boot, and service linux start starts it now.
- It loaded three kernel modules: linux (32-bit Linux programs), linux_common (shared code) and linux64 (64-bit Linux programs). This is the translator.
- It also mounted the pretend Linux file systems under /compat/linux: linprocfs (a Linux-style /proc), linsysfs (/sys), a /dev, and a tmpfs for /dev/shm. Linux programs expect to find these.
- Two Linux settings now exist: compat.linux.osname is Linux, and compat.linux.osrelease is 5.15.0. That is the Linux kernel version that Linux programs are told they are running on. A program that insists on a minimum kernel version looks at this number.
Step 2: install a Linux userland
⚠️ Newer package repository (checked October 2026): the package repository now targets FreeBSD 15.1, while 15.0-RELEASE is end of life. On a 15.0 machine pkg may stop with Newer FreeBSD version for package ... [y/N], and the version numbers in the output below may differ from yours. To carry on, run export IGNORE_OSVERSION=yes first, or better, move to a supported release (see the upgrading guide).
root@practice:~ # pkg install -y linux_base-rl9 2>&1 | grep -E "linux|Number|download|space" linux_base-rl9: 9.8 [FreeBSD-ports] Number of packages to be installed: 1 The process will require 361 MiB more space. 57 MiB to be downloaded. [1/1] Fetching linux_base-rl9-9.8: .......... done [1/1] Installing linux_base-rl9-9.8... [1/1] Extracting linux_base-rl9-9.8: .......... done root@practice:~ # ls /compat/linux afs bin dev etc lib lib64 opt proc run sbin srv sys usr var root@practice:~ # cat /compat/linux/etc/os-release | head -4 NAME="Rocky Linux" VERSION="9.7 (Blue Onyx)" ID="rocky" ID_LIKE="rhel centos fedora" root@practice:~ # du -sh /compat/linux 206M /compat/linux root@practice:~ # ls -l /compat/linux/bin /compat/linux/lib64 | head -6 lrwxr-xr-x 1 root wheel 7 Nov 3 2024 /compat/linux/bin -> usr/bin lrwxr-xr-x 1 root wheel 9 Nov 3 2024 /compat/linux/lib64 -> usr/lib64
linux_base-rl9 is the base set of a Rocky Linux 9 userland (package version 9.8; its os-release still says 9.7) (the same family as the Rocky page in Linux 101: a free rebuild of RHEL). One package, 57 MiB to download, which unpacks into /compat/linux. A CentOS 7 base set is also offered (linux_base-c7: from the package list, not installed here).
/compat/linux now looks like the root of a Linux machine: bin, etc, lib64, usr, var and so on. Its os-release says Rocky Linux 9.7 (Blue Onyx). bin and lib64 are links into usr, as on modern Linux. The folder takes about 206 MB. Why Rocky 9? Software for Red Hat's family is built for the same libraries, so it is the most widely compatible base (from the documentation, not run here).
Step 3: run Linux programs
root@practice:~ # uname -a FreeBSD practice 15.0-RELEASE FreeBSD 15.0-RELEASE releng/15.0-n280995-7aedc8de6446 GENERIC amd64 root@practice:~ # /compat/linux/bin/uname -a Linux practice 5.15.0 FreeBSD 15.0-RELEASE releng/15.0-n280995-7aedc8de6446 GENERIC x86_64 x86_64 x86_64 GNU/Linux root@practice:~ # file /compat/linux/bin/uname | cut -c1-118 /compat/linux/bin/uname: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib root@practice:~ # brandelf /compat/linux/bin/uname File '/compat/linux/bin/uname' is of brand 'SVR4' (0). root@practice:~ # brandelf /bin/ls File '/bin/ls' is of brand 'FreeBSD' (9). root@practice:~ # chroot /compat/linux /bin/bash -c 'cat /etc/os-release | head -2; uname -sr; echo bash $BASH_VERSION' NAME="Rocky Linux" VERSION="9.7 (Blue Onyx)" Linux 5.15.0 bash 5.1.8(1)-release root@practice:~ # chroot /compat/linux /usr/bin/ldd /bin/bash linux-vdso.so.1 (0x00007fffffffd000) libtinfo.so.6 => /lib64/libtinfo.so.6 (0x00000008011b7000) libc.so.6 => /lib64/libc.so.6 (0x0000000801200000) /lib64/ld-linux-x86-64.so.2 (0x0000000001021000) root@practice:~ # cat /compat/linux/proc/version Linux version 5.15.0 (des@freebsd.org) (gcc version FreeBSD Clang 19.1.7 (https://github.com/llvm/llvm-project.git llvmorg-19.1.7-0-gcd708029e0b2)) #4 Sun Dec 18 04:30:00 CET 1977 root@practice:~ # chroot /compat/linux /bin/cat /proc/meminfo | head -3 MemTotal: 3102180 kB MemFree: 1998464 kB Buffers: 0 kB
Read this one from the top:
- The native uname -a says FreeBSD. The Linux uname program, run by its path in /compat/linux, says Linux practice 5.15.0 ... x86_64 GNU/Linux. The first words, Linux and 5.15.0, are the lie the layer tells, and the middle, FreeBSD 15.0-RELEASE releng/15.0-..., is the real kernel's build string peeking through. The program is the genuine Rocky uname.
- file describes it as an ELF executable that wants the Linux loader (/lib64/ld-linux-x86-64.so.2) and for GNU/Linux 3.2.0. brandelf reads the brand in an ELF file: the Linux program says SVR4, the generic one, and FreeBSD's own ls says FreeBSD. How does the kernel know to use the Linux layer for a program with the generic brand? It looks at things such as the loader path and notes inside the file (from the documentation, not run here).
- chroot /compat/linux ... steps inside the Rocky folder, where the Linux programs see it as their whole system: /etc/os-release says Rocky Linux 9.7, uname -sr says Linux 5.15.0, and the shell is bash 5.1.8, the Rocky one.
- ldd lists the libraries that the Linux bash uses: Linux's own C library (libc.so.6), found under /lib64, and the Linux loader. None of this is FreeBSD's.
- /compat/linux/proc/version is the pretend /proc at work: the "kernel version" text it prints includes the FreeBSD compiler and a made-up date. /proc/meminfo shows real numbers (2 GB, the memory of this virtual machine), because linprocfs fills in the Linux-style files from FreeBSD's real data.
The translation, caught in the act
root@practice:~ # truss /compat/linux/bin/uname 2>&1 | head -9
linux_brk(0x0) = 16949248 (0x102a000)
linux_arch_prctl(0x3001,0x7fffffffbba0) = 0 (0x0)
linux_mmap2(0x0,0x2000,0x3,0x22,0xffffffff,0x0) = 34376921088 (0x801063000)
linux_newuname(0x7fffffffb790) = 0 (0x0)
linux_access("/etc/ld.so.preload",R_OK) ERR#-2 'No such file or directory'
linux_openat(0xffffff9c,0x801055266,0x80000,0x0) = 3 (0x3)
linux_newfstat(3,0x7fffffffadd0) = 0 (0x0)
linux_mmap2(0x0,0x256b,0x1,0x2,0x3,0x0) = 34376929280 (0x801065000)
close(3) = 0 (0x0)
truss is FreeBSD's system-call tracer. Run on a Linux program, it shows the Linux system calls by name: linux_brk, linux_mmap2, linux_newuname, linux_openat. These are the translator's entry points. The program asked for uname, and linux_newuname answered. A call that FreeBSD already does the same way (close) is handled directly, and ERR#-2 'No such file or directory' is just the program looking for an optional file that is not there. This is what "running Linux programs natively" means: every system call goes through a small adapter, and everything else runs as it is.
A real Linux program, and two real snags
Now something useful: the Linux curl, built for Rocky. FreeBSD's base system has no curl at all (which curl found nothing), so this is a Linux program doing something the host cannot do by itself. We installed the package linux-rl9-curl (25 MiB with its libraries, from the package list), copied the host's /etc/resolv.conf into the Linux folder (it had none, and Linux programs need it to look up names), and then:
root@practice:~ # chroot /compat/linux /usr/bin/curl --version | head -2 /usr/bin/curl: error while loading shared libraries: libevent-2.1.so.7: cannot open shared object file: No such file or directory root@practice:~ # chroot /compat/linux /usr/bin/ldd /usr/bin/curl | grep 'not found' libevent-2.1.so.7 => not found libevent-2.1.so.7 => not found root@practice:~ # pkg search -x 'linux-rl9.*libevent' linux-rl9-libevent-2.1.12_2 API for executing callback functions on events or timeouts (Rocky Linux 9.8) root@practice:~ # pkg install -y linux-rl9-libevent 2>&1 | grep -E "linux|Number" linux-rl9-libevent: 2.1.12_2 [FreeBSD-ports] Number of packages to be installed: 1 [1/1] Fetching linux-rl9-libevent-2.1.12_2: .......... done [1/1] Installing linux-rl9-libevent-2.1.12_2... [1/1] Extracting linux-rl9-libevent-2.1.12_2: .......... done root@practice:~ # chroot /compat/linux /usr/bin/curl --version | head -2 curl 7.76.1 (x86_64-redhat-linux-gnu) libcurl/7.76.1 OpenSSL/3.5.5 zlib/1.2.11 brotli/1.0.9 libidn2/2.3.0 libpsl/0.21.1 (+libidn2/2.3.0) libssh/0.10.4/openssl/zlib nghttp2/1.43.0 Release-Date: 2021-04-14 root@practice:~ # chroot /compat/linux /usr/bin/curl -sSI https://techcommune.org 2>&1 | head -3 curl: (77) error setting certificate file: /etc/pki/tls/certs/ca-bundle.crt root@practice:~ # chroot /compat/linux /usr/bin/getent hosts techcommune.org 47.204.6.48 techcommune.org root@practice:~ # pkg install -y linux-rl9-ca-certificates 2>&1 | grep -E "linux|Number" linux-rl9-ca-certificates: 2025.2.80 [FreeBSD-ports] linux-rl9-libtasn1: 4.16.0_2 [FreeBSD-ports] linux-rl9-p11-kit: 0.26.2 [FreeBSD-ports] Number of packages to be installed: 3 [1/3] Fetching linux-rl9-libtasn1-4.16.0_2: .......... done [2/3] Fetching linux-rl9-p11-kit-0.26.2: .......... done [3/3] Fetching linux-rl9-ca-certificates-2025.2.80: .......... done [1/3] Installing linux-rl9-libtasn1-4.16.0_2... [1/3] Extracting linux-rl9-libtasn1-4.16.0_2: .......... done [2/3] Installing linux-rl9-p11-kit-0.26.2... [2/3] Extracting linux-rl9-p11-kit-0.26.2: .......... done [3/3] Installing linux-rl9-ca-certificates-2025.2.80... [3/3] Extracting linux-rl9-ca-certificates-2025.2.80: .......... done root@practice:~ # chroot /compat/linux /usr/bin/curl -sSI https://techcommune.org | head -4 HTTP/1.1 200 OK Server: nginx Date: Sat, 24 Oct 2026 23:53:09 GMT Content-Type: text/html root@practice:~ # which curl || echo "no curl on the FreeBSD side" no curl on the FreeBSD side
Two typical snags, one after another:
- error while loading shared libraries: libevent-2.1.so.7. A Linux program needs a library that is not in /compat/linux. This is the most common problem. The way to find out what is missing is ldd inside the Linux folder: libevent-2.1.so.7 => not found. Then search for the FreeBSD package that holds a Linux library: pkg search -x 'linux-rl9.*libevent' finds linux-rl9-libevent. Install it, and curl --version prints curl 7.76.1 (x86_64-redhat-linux-gnu): the genuine Rocky build.
- curl: (77) error setting certificate file: /etc/pki/tls/certs/ca-bundle.crt. The program is fine, but a Linux program looks for the Linux list of trusted certificates, in the Linux place, and FreeBSD's own list is in the wrong folder. The Linux package linux-rl9-ca-certificates brings it. (Name lookups were never the problem: getent hosts techcommune.org from inside the Linux folder answered at once, 47.204.6.48.)
After that, the Linux curl fetched this very website over HTTPS and got HTTP/1.1 200 OK. The pattern is always the same: run it, read the error, find the missing Linux package, install it. The linux-rl9-* packages cover a long list of libraries, from graphics and fonts to compilers (from the package list).
Where the compatibility ends
root@practice:~ # ls /compat/linux/usr/bin | wc -l
263
root@practice:~ # chroot /compat/linux /usr/bin/systemctl status 2>&1 | head -2
chroot: /usr/bin/systemctl: No such file or directory
root@practice:~ # chroot /compat/linux /bin/ls /sys/fs/cgroup 2>&1 | head -2
/bin/ls: cannot access '/sys/fs/cgroup': No such file or directory
root@practice:~ # chroot /compat/linux /bin/ls /proc/self/ns 2>&1 | head -2
/bin/ls: cannot access '/proc/self/ns': No such file or directory
- The Rocky base set has 263 programs in usr/bin. It is a minimal system. There is no systemctl, because there is no systemd here: FreeBSD's own rc starts your services, and the Rocky base set we installed has no systemctl.
- /sys/fs/cgroup does not exist: no Linux control groups.
- /proc/self/ns does not exist: no Linux namespaces.
- Together, those last two mean that Linux's own container tools (Docker and the Linux build of Podman) cannot run on the compatibility layer: they are built on cgroups and namespaces. FreeBSD does have its own way to run containers, and it can even run simple Linux images as jails: the containers page does that for real.
- Other gaps, from the documentation, not run here: Linux system calls that have not been written (new ones such as io_uring are the usual suspects), programs that need Linux kernel modules or special device files, and anything that talks to the kernel through /sys or /proc files that linprocfs does not provide. Some programs run and then quietly misbehave, so test the exact program you need.
And the boot-time setting, as we saw it:
root@practice:~ # grep -n "linux" /etc/rc.conf 5:linux_enable="YES" root@practice:~ # service linux describe 2>&1 | head -2 Enable Linux ABI
Other things to know
All from the documentation, not run here:
- Jails: a jail can have its own Linux userland and run Linux programs, if the jail is allowed to mount linprocfs and friends. This is a neat way to keep a Linux application and its libraries apart.
- Other Linux userlands: besides the Rocky and CentOS base sets, people build Debian or Ubuntu folders with debootstrap, and use those the same way.
- Graphical applications: work with the Linux graphics libraries (linux-rl9-dri, gtk3 and so on) from the package list. Several well-known commercial programs are run this way.
- Updates: the linux-rl9-* packages are normal packages, so pkg upgrade updates them and pkg audit checks them for known vulnerabilities (see the poudriere and upgrade pages for those tools). A Linux program does not become safer for running on FreeBSD: patch the Linux libraries too.
When to choose which
- A native FreeBSD port exists: use it. It is faster to update and fully supported.
- A Linux-only program, and you need it on this machine: try the compatibility layer. It is light, and often just works.
- It needs systemd, cgroups, namespaces, Docker or a specific Linux kernel: use a Linux virtual machine (bhyve or vm-bhyve).
⚠️ Test before you rely on it. Compatibility is very good, not perfect. A program that starts and runs is not proof that every feature of it works. Try the actions you need (and watch truss for unimplemented calls) before putting a Linux program into production on FreeBSD.
Someone had told it the kernel was 5.15.0,<br>
and it did not ask any more questions.
🧪 Try it yourself
On a FreeBSD machine:
1. Enable the layer with sysrc linux_enable=YES and service linux start. Which modules loaded, and what do the compat.linux sysctls say? 2. Install linux_base-rl9 and read /compat/linux/etc/os-release. 3. Run the Linux uname -a and compare it with the FreeBSD one. Then chroot /compat/linux /bin/bash and look around. 4. Use truss on the Linux uname and find a linux_ system call. 5. Install linux-rl9-curl. When it complains about a missing library, find it with ldd and pkg search. 6. Fetch a web page over HTTPS with it. What was missing the second time? 7. Look for systemctl, /sys/fs/cgroup and /proc/self/ns in the Linux folder. What does that tell you about running Docker?
The cheat sheet
- Switch on: sysrc linux_enable=YES, service linux start (modules linux, linux64; mounts under /compat/linux).
- Userland: pkg install linux_base-rl9 (Rocky Linux 9). More: pkg search -x 'linux-rl9-'.
- Run: /compat/linux/bin/PROGRAM or chroot /compat/linux /bin/bash.
- Look: sysctl compat.linux.osrelease, brandelf FILE, file FILE, truss PROGRAM.
- Missing library: chroot /compat/linux /usr/bin/ldd PROGRAM | grep 'not found', then pkg search -x 'linux-rl9.*NAME'.
- Network: copy /etc/resolv.conf into /compat/linux/etc/, and install linux-rl9-ca-certificates for HTTPS.
- Not available: systemd, cgroups, namespaces (so no Linux Docker or Podman; FreeBSD's own Podman runs simple Linux images, see the containers page).
← ipfw: The Other FreeBSD Firewall · ↑ FreeBSD Corner · The Boot Loader and Kernel Tuning: loader.conf, sysctl and Single-User Mode →