← Back · NextBSD Research · companion to Debian userland: debootstrap and the apt wrapper and Mounts, fstab and DiskArbitration §5

Linux ABI mounts

Who mounts /proc, /sys, /dev/fd and /dev/shm for Linux programs on NextBSD now that no /etc/fstab ships? The plan must also cover the case where the Linux root does not exist yet. The answer is a single boot-time LaunchDaemon that does what FreeBSD’s rc.d/linux does. It creates the Linux root’s mount points and always mounts them, whether or not a Linux userland has been installed.

2026-09-21. Three research agents read FreeBSD releng/15.0 (rc.d/linux, the Linuxulator, vfs_lookup.c, fdescfs, tmpfs), debootstrap 1.0.145 and the FreeBSD port’s patches, dpkg, Chromium’s shared-memory code, Apple launchd-258 through -842, and NextBSD’s launchd at main. Nothing here has been run on a NextBSD box yet; §7 lists the tests that would prove it. Planning only.

Implemented (2026-09-21): nextbsd-userland#244, fixing #190. The mounts are made by linux_abi_mounts() inside launchctl bootstrap, next to the other boot-time mounts. That replaced the earlier draft of a one-shot LaunchDaemon plus a /usr/libexec tool, which would have shown up in launchctl list. CI proved it end to end on amd64 and arm64: pkg install claude-code into the pre-mounted /compat/linux, a Linux uname -s, and claude --help. That test was then removed to keep CI fast; a one-second boot check stays. Sections below are updated to match.

The short answers.

  1. Always mount the compat set, and pre-create it. At every boot, launchctl bootstrap’s linux_abi_mounts() (support/launchctl.c) creates $emul/{proc,sys,dev,tmp} and mounts all five filesystems plus a nullfs of /tmp. Here $emul is compat.linux.emul_path, default /compat/linux. It does this even when no Linux userland is installed, exactly as stock FreeBSD’s rc.d/linux does with linux_enable=YES (mkdir -p, then mount, unconditionally, rc.d/linux:17-26, 74-80). There is no LaunchDaemon and no separate tool.
  2. debootstrap works with the mounts in place, with one step afterwards. debootstrap does not require an empty target. It stacks its own devfs, fdescfs and linprocfs during stage 2 and removes them on exit. The catch is that it first unmounts our $emul/proc, so the apt wrapper remounts it (mount -t linprocfs linprocfs $emul/proc) when debootstrap finishes (§3).
  3. No trigger is needed. Because the mounts always exist, a root that appears later (debootstrap, an untarred image, linux_base) is already mounted; CI proved this with pkg install claude-code. WatchPaths is inert on this launchd, and restoring it is optional (§5).
  4. The native set is optional, not required. An empty but mounted /compat/linux gives every Linux process Linux-style /proc, /sys, /dev/fd and /dev/shm through the alt-root lookup. That includes static AppImage runtimes with no Linux userland at all. Mounting linprocfs on the real /proc is safe for native binaries but not needed (§4).

Contents

  1. How the Linuxulator finds Linux paths
  2. Design: pre-create and always mount
  3. debootstrap into a mounted root
  4. The native set
  5. Triggering, and restoring WatchPaths
  6. /dev/shm sizing
  7. Acceptance tests
  8. Implementation: linux_abi_mounts()
  9. What changes in the existing tickets
  10. Open questions

1. How the Linuxulator finds Linux paths

So with an empty but mounted $emul, /proc/*, /sys/* and /dev/* resolve to the compat instances, and every other path (/lib64/ld-linux…, /etc/passwd, /tmp/.mount_*) returns ENOENT under $emul and is retried on the real root. That is today’s behaviour with no alt root, plus one negative lookup.

A likely correction to #54. #54 records that AppImages “need the native set”. The mechanism says otherwise. The probable explanation is that in those tests /compat/linux/proc existed as an unmounted empty directory created by linux_base-rl9. /proc/self was then ENOENT under $emul and fell back to the native /proc, which only worked once linprocfs was mounted there. Test T3 settles it.

2. Design: pre-create and always mount

B1: pre-create, always mount (chosen)B2: mount only when the root exists
Precedentstock rc.d/linux#190’s current draft
Static Linux binary / AppImage, no userlandworks through the alt rootneeds the native set, or /proc fails
Root created later (debootstrap, tar, linux_base)already mounted; the wrapper re-runs the tool for procmust be started after the root appears; easy to forget, and races with the wrapper
Root moved (emul_path=/compat/debian)next run creates and mounts the new rootsame, once the root exists
Cost/compat/linux exists on every image; a mistyped emul_path creates a junk treebehaviour depends on state, and is hard to test in CI

Mount order (§8 has the full spec):

  1. linprocfs on $emul/proc
  2. linsysfs on $emul/sys
  3. devfs on $emul/dev
  4. fdescfs -o linrdlnk on $emul/dev/fd. linrdlnk is required, because Bun and claude-code resolve cwd through readlink("/dev/fd/N") (#54).
  5. tmpfs mode=1777,size=… on $emul/dev/shm

devfs goes first, and no mkdir is needed for dev/fd or dev/shm: devfs supplies fd, and linux_dev_shm_create() puts shm/ in every devfs instance at ABI init (sys/compat/linux/linux.c:699-709). The tool also re-asserts kern.elf64.fallback_brand=3 only if it reads -1. The kernel default is already 3 (patch 0009). It never touches elf32.

Where the root comes from. The root is sysctl -n compat.linux.emul_path. To move it persistently, write the line unquoted in /etc/sysctl.conf, as compat.linux.emul_path=/compat/debian. launchctl bootstrap applies that file before load -D all (launchctl.c:2342). sysctl does not strip quotes, so a quoted value would include them. --root PATH overrides for one run, and the wrapper uses it before the sysctl is set.

Pi 5. The linsysfs boot panic mentioned in #171 is fixed (nextbsd-kernel#209, patches 0056/0057). It happened at init time and was unrelated to mounting. nextbsd-kernel#212, where the RP1 PCI subtree is missing from /sys, is still open.

3. debootstrap into a mounted root

This was the deciding question. Here is what debootstrap does, with our five mounts already in place:

StepSourceEffect
Target checkdebootstrap:68, 552-558The “not empty” error applies only to --make-tarball. The FreeBSD sanity check is just mkdir -p (functions:1853-1859).
Stage 1: gtar -k -x of each .debfunctions:1085-1154base-files’ ./dev/ ./proc/ ./sys/ already exist. pseudofs ignores unchangeable attributes (pseudofs_vnops.c:1080-1091).
setup_devicesfunctions:1330-1353No-op on FreeBSD (no mknod).
Stage 2: mount -t devfs $TARGET/dev, fdescfs -o linrdlnk $TARGET/dev/fdfunctions:1356-1366Stacks on top of ours (no nocover; vfs_mount.c:1163-1170).
setup_proc: umount $TARGET/proc, then mount its own linprocfsfunctions:1271-1283Unmounts ours.
dpkg installs base packagesdpkg archives.c:896-903, 1065An existing directory is “do nothing”. base-files’ postinst never touches proc, sys or dev.
Exit: unmounts /proc, /dev/fd, /dev (its own)functions:1232-1263Our dev, dev/fd, dev/shm and sys reappear; $emul/proc stays unmounted.

Workaround: after debootstrap exits, whether it succeeded or failed, the wrapper remounts $emul/proc with one mount -t linprocfs. No pre-unmount is needed. If a maintainer script ever misbehaves because linsysfs is visible at $TARGET/sys during bootstrap, the fallback is the Handbook’s exact order: unmount first, bootstrap, then remount. Stock FreeBSD never has linsysfs mounted there during bootstrap.

Bootstrap in place. Do not bootstrap into a temporary directory and rename it into place: the root carries mounts, so it cannot be replaced by a rename. Bootstrapping in place is what makes B1 work.

4. The native set

Decision: not mounted. Under B1, Linux processes don’t need it. If /dev/fd/N parity for native shells is ever wanted, it is a few more lines in linux_abi_mounts().

5. Triggering, and restoring WatchPaths

6. /dev/shm sizing

Policy. size is a percentage of hw.physmem, page-rounded, with a 128 MiB floor:

There is no override today. A cap turns an OOM kill (#326) into ENOSPC inside the offending Linux process.

7. What CI proved

These ran on PR #244, on amd64 and arm64, in the userland CI image boot:

That end-to-end test (pkg from the FreeBSD mirror, pinned to the release image’s FreeBSD:15:<arch> ABI) was removed after it passed, to keep userland CI fast. What stays is the one-second LINUX-MOUNTS boot gate (§8).

Still to prove (with #241): debootstrap into the mounted root and the proc remount; Chrome/Edge/VS Code windows; AppImages; the Pi 500+ with linsysfs.

8. Implementation: linux_abi_mounts()

This is C in support/launchctl.c, called from system_specific_bootstrap(). It is about 130 lines.

  1. Read compat.linux.emul_path. If the sysctl doesn’t exist, the kernel has no Linuxulator, so return.
  2. Create the root and {proc,sys,dev,tmp} (mkdir each path component), then realpath() it.
  3. For each of the following, skip it if statfs shows that filesystem already mounted exactly there; otherwise run /sbin/mount -t … -o nocover,… via fwexec(). On failure, log to the console and continue.
    • linprocfs on proc
    • linsysfs on sys
    • devfs on dev
    • fdescfs linrdlnk on dev/fd
    • tmpfs mode=1777,size=… on dev/shm
    • nullfs of realpath("/tmp") on tmp
  4. dev/shm is sized at hw.physmem / 2, or / 4 when / is unionfs (live media), with a 128 MiB floor.

Deliberately left out of the earlier tool draft:

CI gate (LINUX-MOUNTS): all six mounts present after boot; linrdlnk checked by behaviour (dev/fd/0 is a symlink); dev/shm capped below RAM.

9. What changes in the existing tickets

TicketChange
userland#190 (B2, parked)Done: implemented in #244 (launchctl bootstrap, no LaunchDaemon, no tool).
userland#189 (B1, manual docs)Shrinks to documenting the tool, emul_path (unquoted) and the /tmp rule. The fstab lines become the fallback for admins who want them.
userland#54Moves from option H (manual) to option A (LaunchDaemon). The “AppImages need the native set” note awaits T3.
userland#171Closes once #190 lands; its /dev/shm size question and Chrome test are §6 and T5.
nextbsd-userland#243 (optional)Restore native WatchPaths/PathState in launchd from 392.39 using O_PATH (§5). This also unblocks nextbsd#278 Lever D and the getty guard.

10. Open questions

  1. Is the native set on by default? Resolved: not mounted (§4).
  2. T3 decides whether AppImages really need anything beyond the compat set.
  3. Unverified: nullfs over linprocfs and fdescfs (T6), mounts over lower-only unionfs directories (T8), and whether /sys is a dangling symlink on NextBSD images.

Evidence: FreeBSD releng/15.0 (libexec/rc/rc.d/linux, sys/compat/linux/{linux_util,linux,linux_file}.c, sys/kern/{vfs_lookup,vfs_mount,kern_descrip}.c, sys/fs/{fdescfs,pseudofs,tmpfs,nullfs}); debootstrap 1.0.145 (debootstrap, functions, scripts/debian-common) and the FreeBSD port’s patches; dpkg archives.c; Chromium shared-memory code; Linux tmpfs.rst; Apple launchd-392.39 launchd_core_logic.c and -842; nextbsd-userland main (launchctl.c, core.c, ipc.c, libxpc); nextbsd-kernel#209/#212. Related: E13 (#461), E15 (#471), #460.