NextBSD: Linux support — the unified design
How a user runs any AppImage (and any Linux program) on NextBSD, end to end. This page unifies the existing Linux plans with what we learned running Cool Retro Term and 86Box on a real NextBSD box, and settles the open decisions — base distro, the /compat layout, and user identity (NSS).
The three detailed plans already exist and are the source of truth for their layer; this document does not re-derive them. It adds the connective tissue, the real-box findings, and the decisions. Read top to bottom for the end-to-end story, or jump in:
- The three layers
- The rootfs — one root, Devuan, and the
/compatlayout - The kernel — and the PT_INTERP→SIGABRT defect
- User identity (NSS) — so any user can run in it
- The AppImage runtime
- End to end: a user runs an AppImage
- Phased plan & ticket map
- Open decisions
1. The three layers
Everything stacks in three independent layers. A layer below is a prerequisite for the one above; a layer is inert (harmless, unmounted/absent) if the one below is missing.
| Layer | What it is | Who provides it | State |
|---|---|---|---|
| L1 Kernel | Linuxulator: linux64 + linux_common modules translate Linux syscalls; sets kern.features.linux64 | nextbsd-kernel (baked in) | planned §4b |
| L2 Rootfs + mounts | One Linux root (Devuan minbase) at emul_path; the six ABI filesystems always mounted | nextbsd-linux bootstrap (debootstrap) + launchctl bootstrap linux_abi_mounts() | planned §4, §8 |
| L3 Identity + apps | User/group resolution (NSS) + the AppImage/GUI runtime (client libs, FUSE, D-Bus session, GPU) | nextbsd-linux run / .app wrappers + curated packages | planned §4, §5 |
2. The rootfs — one root, Devuan, and the /compat layout
2a. The base: Devuan
The debootstrap plan already made this choice; it holds. Devuan Excalibur is the default because it has the same Mesa 25.0.7 / libdrm 2.4.124 (built against 6.12) as Debian 13 trixie — an exact match for NextBSD’s drm-kmod 6.12-lts — but no systemd at all, and systemd’s postinst is the one thing that has actually failed under the Linuxulator.
| Base | systemd | glibc | Valid-Until | Mesa/DRM match | Note |
|---|---|---|---|---|---|
| Devuan Excalibur (default) | none | tracks its Debian base (≈ trixie-era) | current | 6.12, exact | No systemd postinst; self-provisions messagebus (§4). Recommended. |
| Debian trixie | present (inert) | 2.41 (newest) | current | 6.12, exact | What we used for the hands-on session; the messagebus provisioning gap bites here (§4). |
| Debian bookworm | present | 2.36 (older) | past (LTS) | 6.12 | Older glibc; a -o Acquire::Check-Valid-Until=false flag on every update. Oldest, most AppImage-compatible of the three by glibc age only. |
2b. The /compat layout — one root, and why
The constraint is in the kernel, not in our layout. In sys/compat/linux/linux_util.c: linux_emul_path is one global string (:71, tunable as compat.linux.emul_path, CTLFLAG_RWTUN). On every Linux-ABI exec, linux_pwd_onexec() (:77) looks up that one global and calls pwd_altroot() to set the thread’s alt-root to it (:91; if the path is missing it falls back to the native root, :87). The alt-root is per-thread, but its value is that single global — there is no per-binary or per-process root selector. Hence one active Linux root: /compat/linux and /compat/devuan cannot both be live. Two simultaneous roots would require a kernel change to linux_pwd_onexec (a per-credential dispatch).
That one root serves three axes; only two of them force a choice of base:
| You run… | Depends on the root’s base? | Why |
|---|---|---|
| Self-contained AppImages (FUSE or extracted) | No — base-agnostic | Brings its own runtime; needs only the compat mounts (§2d, under whatever emul_path is) + /dev/fuse + identity (§4). Rides any root, even a minimal one. |
USES=linux / linux_base-rl9 ports (claude-code, chrome) | Yes — pinned to rl9 | The wrapper execs the program by native path; its ld/libc then resolve under the alt-root, so the root must hold the rl9 base at /compat/linux. |
Portable programs / portable AppImages (link system libs; 86Box’s libfluidsynth/librtmidi) | Yes — a curated base | Needs the specific system .sos present → the curated client-lib set (§5), hence a chosen base (Devuan). |
2c. /lib64 — required, minimal, and why
Every unpatched x86-64 glibc binary carries PT_INTERP = /lib64/ld-linux-x86-64.so.2. The Linuxulator resolves that path under the alt root (a kernel lookup, not ld.so.conf), so the root must contain /lib64. Verified on the box: remove /compat/linux/lib64 and a bare chroot /compat/linux /bin/ls dies with SIGABRT (rc 134); restore it and rc=0. Minimal layout:
/compat/linux/lib64 -> usr/lib64 (symlink) /compat/linux/usr/lib64/ld-linux-x86-64.so.2 (real file) /compat/linux/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 (real file, multiarch)
2d. The six ABI mounts (always present)
From the ABI-mounts plan, made by linux_abi_mounts() inside launchctl bootstrap at every boot (nextbsd-userland #244, fixing #190), even when no userland is installed:
| Mount | Where | Supplies |
|---|---|---|
devfs | $emul/dev | /dev/* (incl. dri), and devfs auto-creates dev/fd |
devfs (shm) | $emul/dev/shm | by linux_dev_shm_create() at ABI init — Chromium’s shared-memory fallback |
linprocfs | $emul/proc | Linux-style /proc (debootstrap unmounts it; the apt wrapper remounts) |
linsysfs | $emul/sys | Linux-style /sys |
fdescfs | $emul/dev/fd | the fd table |
tmpfs + nullfs | $emul/tmp | a Linux /tmp (nullfs of the host /tmp) |
$emul = compat.linux.emul_path. These are the compat instances; a path that is missing under $emul falls through to the native root (one extra negative lookup), which is how a self-contained AppImage runs with no userland.
3. The kernel — and the PT_INTERP→SIGABRT defect
The necessary kernel work is in the Linuxulator plan: bake linux_common + linux64 (sets kern.features.linux64), and build NVIDIA’s kext with the Linux-ioctl compat off. That is the whole of L1. What this section adds is a defect found on the real box that the plans did not anticipate.
4. User identity (NSS) — so any user can run in it
This is the layer that makes it any user, not just the one who built the root. Two distinct problems live here; conflating them is what muddied the earlier discussion.
4a. Resolution: “I am uid 5000” → “I am admin”
A host user (say admin, uid 5000, home /Local/Users/admin) execs a Linux ELF. It runs as uid 5000, and glibc calls getpwuid(5000) → NSS files → the root’s /etc/passwd. That file is the 20-line Devuan/Debian base — 5000 is not in it → NULL. The app cannot find its username or $HOME and misbehaves or fails.
The host resolves users with NextBSD’s own nss_directory_services module (/usr/lib/nss_directory_services.so.1, source in nextbsd-userland/src/nss_directory_services/; the host nsswitch is passwd: directory_services files). It is a FreeBSD nsdispatch(3) NSS source resolving from the DirectoryServices store.
4b. The options
| Option | Mechanism | Pros | Cons |
|---|---|---|---|
| v1 — generated identity (default) | At bootstrap and on every nextbsd-linux run, copy the invoking host user’s passwd+group entries (uid/gid/home/shell) into the root’s /etc/passwd+/etc/group. Stock files NSS. | Any user; no custom module; zero new code paths; exactly the FreeBSD Handbook’s “add your user to the root’s passwd” recipe — already the debootstrap plan’s recommendation. | Per-user entries accumulate; the host home (/Local/Users/*) is outside the root, so $HOME needs a nullfs mount or a root-local dir. |
| v2 — glibc module + host bridge the “integrated” path | A Linux glibc NSS module in the root (nsswitch: passwd files <src>) that resolves by reaching the host. Two flavours: (a) nullfs-mount the host’s DirectoryServices store//etc into the root and read it; (b) a host-side daemon (FreeBSD libc — may use libds/nss_directory_services) answering uid/gid↔name over a unix socket; the glibc module is a thin client. | The root sees the same live users/groups as native NextBSD — true integration; no per-user file churn. | Real new code (a glibc NSS module +, for (b), a host daemon + socket protocol); the socket variant is the only one that survives chroot isolation cleanly. |
4c. Provisioning: creating the accounts the base needs (the messagebus gap)
Distinct from resolution: some Linux packages need a system account to exist. The canonical one is messagebus. The dbus-system-bus-common postinst provisions it with a branch that keys on the base:
if command -v systemd-sysusers >/dev/null; then
systemd-sysusers dbus.conf # Debian: has systemd
else
adduser --system --quiet --group messagebus # Devuan: no systemd
fi
- On Devuan (no
systemd-sysusers) → theadduserarm runs at install time →messagebusis created in place → no gap. This is one more reason Devuan is the right base: it removes the failure by construction. - On Debian (has
systemd-sysusers, which only runs at boot under a live systemd — never in a chroot) → the account wasn’t present whendbus’s core postinst ran itsdpkg-statoverride --add root messagebus …underset -e→ half-configured cascade (dbus,dbus-user-session,dconf-*,libgtk-3-0). Fix (reference, for a Debian root):groupadd -r messagebus useradd -r -g messagebus -d /var/run/dbus -s /usr/sbin/nologin messagebus dpkg --configure -a
Verified on the box: after the three lines, dpkg --audit is clean and all five packages are ii; systemd is in the tree only because dbus-user-session Depends: … libpam-systemd, systemd, and it is inert (no PID-1) in a root. Devuan sidesteps all of it.
5. The AppImage runtime
AppImages are self-contained by design — the payload bundles its own runtime. So an AppImage needs only the compat mounts (under the single emul_path, §2d) + /dev/fuse (FUSE) or just the ld-fix (extracted, §3) + identity (§4). It does not need a curated base — it rides whichever root the mounts are under, coexisting with the rl9 ports (§2b). The “curated client-lib set” in §5b is therefore for portable (system-lib) software and the rare non-self-contained AppImage — not for ordinary self-contained ones.
5a. Delivery: FUSE vs extracted
| FUSE (self-mount) | Extracted (--appimage-extract) | |
|---|---|---|
| Needs | fuse + /dev/fuse + the compat mounts; the payload is self-contained in the squashfs | the compat mounts + the ld-fix (§3); a portable image additionally wants the root’s curated client libs (§5b) |
| Root userland? | No (brings its own runtime) | Self-contained: no. Portable (system-lib) image: yes — loads $emul’s libs |
| 86Box caveat | its relative interp still SIGABRTs unless the in-image ld symlink is a real file | same — needs the real-file ld workaround |
5b. What a portable GUI program needs from the root
(A self-contained AppImage needs none of this — it brings its own libs. This list is for portable programs, and the rare non-self-contained AppImage that links system libraries.)
- Curated client-lib set (nextbsd/nextbsd #526): the
filesNSS base accounts + the libraries the apps link (libGL/Mesa+/dev/dri, X11/XCB, fontconfig/freetype, and the app-specific SONAMEs). Defined against the installed base’s index; where the base ships a newer SONAME, pull the older one’s.sofrom a matching older codename’s.deb(apt-get download+dpkg -x) — the #526 recipe. (86Box specifically wantslibfluidsynth.so.2+librtmidi.so.5, both older than what trixie-class bases ship.) - D-Bus — the session bus, the root’s own. GUI apps (GTK/Qt) use the D-Bus session bus (gsettings, portals), not the system bus. Two facts settle it: (1) a native FreeBSD
dbus-daemonand a Linux ELF are different ABI/namespace — don’t bridge them; (2) these two apps don’t even need a running bus to launch (GTK/Qt degrade gracefully). If you want full gsettings, run the app under the root’s own Linux bus:dbus-run-session -- ./app. The native NextBSD dbus stays separate, for native consumers. - GPU: NVIDIA’s Linux-ioctl compat is off, so a Linux app on an NVIDIA card gets llvmpipe; on Intel/AMD, Mesa/DRI drives NextBSD’s
drm-kmod 6.12-ltsthrough/dev/dri(provided by the root’s devfs) — that path works. - Display: the app connects to the NextBSD display (X/Wayland) over the socket; the window appears on the desktop. (The GUI-server bridge is a separate track; a headless run only needs the above.)
| App | Toolkit | Base libs | Bus | Special | Status |
|---|---|---|---|---|---|
| Cool Retro Term | Qt | GL + X11 + fontconfig (bundles most) | session (optional) | — | runs; verify on Devuan base |
| 86Box | GTK3 | GL + X11 + libfluidsynth.so.2 + librtmidi.so.5 (§5b) | session (optional) | real-file ld fix (§3, §5a) | runs after the ld fix; re-verify on Devuan |
6. End to end: a user runs an AppImage
admin(uid 5000) double-clicks86Box.AppImage→ the.appwrapper callsnextbsd-linux run ./86Box.AppImage.- The kernel Linuxulator (L1) translates;
linux_pwd_onexec()sets the alt root toemul_path= the Devuan root (L2). - The six ABI mounts are already up (L2) —
/proc,/sys,/dev/dri,/dev/shm,/dev/fd,/tmp. - Identity (L3, §4a v1): the wrapper has ensured
5000 admin …is in the root’spasswd/group(and mounted/Local/Users/adminif the app wants its real home).getpwuid(5000)→admin. - Runtime (L3, §5): curated libs load from the root; the relative-interp
ldis a real file (so no SIGABRT);dbus-run-sessiongives a session bus; Mesa drives/dev/dri. - The window appears on the NextBSD desktop.
7. Phased plan & ticket map
| Phase | Deliverable | Tracks |
|---|---|---|
| P0 | This design doc — on pkgdemon.github.io, linked from NextBSD Research | — (this page) |
| P1 | Bake linux64+linux_common into the kernel (kern.features.linux64); NVIDIA compat off | Linuxulator plan §4 · nextbsd-kernel |
| P2 new | Kernel defect: unresolvable PT_INTERP → clean exec error / re-apply the compat prefix on symlink follow (kills 86Box today, §3) | new nextbsd-kernel ticket → Epic #461 |
| P3 | One Devuan root at emul_path + the six mounts + the /lib64 layout + curated client-lib set → CRT + 86Box run (fixed user) | debootstrap plan (#241) · abi-mounts (#244/#190) · #526 |
| P4 | User identity v1 — generated passwd/group on bootstrap+run → any user (Model A) | debootstrap plan §5 · nextbsd-userland |
| P5 | AppImage delivery — FUSE (/dev/fuse) and/or extracted+real-file-ld; dbus-run-session; .app wrappers | debootstrap plan (§ AppImages, #242) · #460 |
| P6 future | User identity v2 (glibc module + host bridge); rl9-ports coexistence (Model B, emul_path-switch); NVIDIA Linux userland | new tickets → Epic #461 / E13 |
8. Open decisions
| Decision | Recommendation | Why / what closes it |
|---|---|---|
| Base distro | Devuan Excalibur (confirms the debootstrap plan) | No systemd (the Linuxulator failure) + exact 6.12 DRM match + self-provisions messagebus. |
/compat layout | Model A — one curated root | emul_path is one global; two live roots is impossible. Closes the “/compat/linux vs /compat/devuan” question. |
| User identity | v1 generated identity to ship; design v2 (bridge) next | v1 = Handbook recipe, any user, no new ABI. v2 = the integrated nss_directory_services bridge (over the §4a ABI wall). |
| AppImage delivery | extracted + real-file-ld now; FUSE once /dev/fuse is proven | extracted is robust pre-kernel-fix; the P2 kernel fix then makes FUSE the clean “no root” path. |
| Home mapping | nullfs-mount /Local/Users/<user> into the root (or a root-local $HOME) | the host home is outside the alt root; pick the mount vs local-dir policy in P4. |