← Back · foundation for eliminating loader.conf (#180)
mach.ko → kernel built-inCompile the out-of-tree Mach kernel module into the NEXTBSD kernel (options COMPAT_MACH) instead of loading it as a .ko. This is the answer to “how do we load mach.ko once loader.conf is gone?” — we don’t load it; it’s part of the kernel image, present from the first instruction, long before launchd (PID 1). It is the foundational step of #180 (eliminate loader.conf).
Verdict Feasible and clean — mostly build-system wiring, not a C rewrite. Two things that usually make “module → built-in” painful don’t apply here: the MIG server stubs are pre-generated and committed (no codegen in the kernel build), and the syscalls already register through a SYSINIT via kern_syscall_register() (no syscalls.master regen). The mach source is ours, not freebsd-src, so it lives in nextbsd-kernel and is dropped into the builder’s throwaway clone — the fork is never touched.
NextBSD’s running image loads exactly two things — kldstat shows kernel + mach.ko. mach.ko is preloaded by loader.conf.d (mach_load="YES") so Mach syscalls exist before any userland. But mach is genuinely boot-tier: launchd (PID 1) needs Mach traps, and the on-demand loader (kextd) is itself launched by launchd. So mach can never be an on-demand load — it must be present before PID 1.
When #180 retires loader.conf, the mach_load="YES" line goes away. The only correct place for a boot-tier component is then the kernel image itself — exactly how FFS and the disk drivers already ride (they’re built into GENERIC, which is why nothing loads them). Building mach in puts it on that same tier.
This is not a blocker today. mach_load="YES" keeps working through the entire transition; the built-in is the end state, reached when loader.conf is removed. The two states are interchangeable for correctness — built-in is simply the one that survives loader.conf removal.
Out-of-tree module at nextbsd/src/mach_kmod/, built by make-mach-kmod.sh → make -C src/mach_kmod → bsd.kmod.mk, against the ingested kernel source (SYSDIR) with the NEXTBSD build’s opt_*.h for KBI match.
| Group | Files | Notes |
|---|---|---|
Hand-written, top-level src/ | ~20 .c (mach_module.c, mach_syscall_wire.c, mach_traps.c, mach_busystate.c, mach_event_bridge.c, mach_{clock,host,host_priv,task,thread,vm,processor,semaphore,convert,misc,stats,test}.c, proc_info.c, compat_stubs.c) | core logic |
| MIG server stubs (pre-committed) | 7 .c from src/defs/*.defs: {clock,host_priv,mach_host,mach_port,mach_vm,task,vm_map}_server.c | No mig run at build — checked in. The Makefile just lists them in SRCS. |
ipc/ | 17 .c (ipc_*.c, mach_msg.c, mach_port.c, mach_debug.c) | hand-written |
kern/ | 4 .c (ipc_host.c, ipc_tt.c, task.c, thread_pool.c) | hand-written |
Include tree & flags (from the Makefile): -I include (resolves <sys/mach/…> and <sys/mach_debug/…>), -I include/apple (Apple-ism headers), -I . (the shipped opt_*.h), and a force-included -include include/compat_shim.h (aliases p_machdata→p_emuldata, defines EVFILT_MACHPORT, Apple stubs). CFLAGS: -DCOMPAT_MACH -DMACH_INTERNAL -DAUDIT plus a couple of -Wno-error= demotions.
Syscalls: mach_syscall_wire.c registers the traps from a SYSINIT (SI_SUB_INTRINSIC) via kern_syscall_register(), dynamically allocating slots from the widened lkmnosys band (patch 0001; slots 599–646). 15 Phase B traps wired today (mach_reply_port, mach_msg_trap, the _kernelrpc_* port traps, task/thread/host_self_trap, register/unregister_event_bell, mach_wait_quiet, …); ~39 more declared for later phases.
Existing kernel patches (nextbsd-kernel/patches/series): 0001 widen lkmnosys band, 0002 device_match quiesce hook, 0003 reserve EVFILT_MACHPORT. All merged.
FreeBSD’s standard way to gate a subsystem on a kernel option:
sys/conf/options: register the option so config(8) emits a header — COMPAT_MACH opt_compat_mach.h. (mach already compiles under -DCOMPAT_MACH with a shipped opt_compat_mach.h, so we reuse that name — the generated header replaces the hand-shipped one.)sys/conf/files: list every mach .c as optional compat_mach, e.g. compat/mach/mach_msg.c optional compat_mach. Per-file compile-with carries the extra flags (-DMACH_INTERNAL, -include …/compat_shim.h, the include/apple path) that the Makefile applies today.config/NEXTBSD: add options COMPAT_MACH. config(8) generates opt_compat_mach.h (#define COMPAT_MACH 1) and pulls the optional compat_mach files into the kernel build.kernel; the SYSINITs fire at boot. No .ko, no loader entry.No MIG in the kernel build. Because the *_server.c are pre-committed, we add them as ordinary optional compat_mach files — none of the vnode_if.c-style dependency/compile-with/before-depend codegen machinery is needed. (If we ever regenerate from .defs, that pattern is available, but it’s out of scope here.)
The mach source is NextBSD-original code, not part of freebsd-src. So it moves into nextbsd-kernel as a first-class kernel subsystem, and the build lays it into the builder’s freebsd-src clone — the same discipline as the patches (the fork is never edited).
nextbsd/src/mach_kmod/src/*.c → sys/compat/mach/*.c
nextbsd/src/mach_kmod/src/ipc/*.c → sys/compat/mach/ipc/*.c
nextbsd/src/mach_kmod/src/kern/*.c → sys/compat/mach/kern/*.c
nextbsd/src/mach_kmod/include/sys/mach/* → sys/sys/mach/* (so <sys/mach/…> resolves)
nextbsd/src/mach_kmod/include/sys/mach_debug/* → sys/sys/mach_debug/*
nextbsd/src/mach_kmod/include/apple/* → sys/apple/* (-I$S/apple → <sys/proc_info.h> resolves)
nextbsd/src/mach_kmod/include/compat_shim.h → sys/compat/mach/compat_shim.h (force-included)
nextbsd/src/mach_kmod/opt_*.h → NOT COPIED (config(8) generates them — see gotchas)
Gotcha 1 Do not lay the shipped opt_*.h into the tree. The module ships opt_compat_mach.h/opt_capsicum.h/opt_ntp.h/opt_audit.h (GENERIC-matching) for the out-of-tree build. In-kernel, config(8) generates these from the real NEXTBSD config; a shipped copy on the include path would shadow the generated one and options COMPAT_MACH would be silently ignored. Exclude them.
Gotcha 2 compat_shim.h redefines EVFILT_MACHPORT. It does #define EVFILT_MACHPORT (-16), but patch 0003 now defines it in sys/sys/event.h — so built against the patched kernel this is a hard redefinition error. Guard it: #ifndef EVFILT_MACHPORT … #endif (the same pattern as the NO_SYSCALL guard added in #178). The kn_ext alias and the S_IFPORT/DTYPE_MACH_IPC constants were checked and are collision-free.
Delivery mechanism: two parts.
nextbsd-kernel (e.g. under src/sys-overlay/); the kernel build cp -Rs it into the freebsd-src clone’s sys/ before buildkernel. Bulk source as files, not a giant diff.patches/0004-compat-mach-builtin.patch — small, touching only sys/conf/options, sys/conf/files (or a new sys/conf/files.compat_mach included from it), and config/NEXTBSD.This keeps nextbsd-kernel a patches-plus-our-additions repo (no full freebsd tree), honors the never-touch-the-fork rule, and keeps the wiring reviewable as a tiny diff.
The init path already uses the SYSINIT machinery that fires for compiled-in code, so most of it carries over verbatim:
DECLARE_MODULE(mach, …, SI_SUB_KLD, …) still fires its MOD_LOAD event for a built-in (the module framework runs events for compiled-in modules too). We can keep it, or convert mach_mod_init() to a plain SYSINIT — either works; converting is tidier and drops the now-meaningless MOD_UNLOAD→EBUSY path.mach_syscall_wire is already a SYSINIT doing dynamic kern_syscall_register(). No syscalls.master regen, no fixed slot numbers.kqueue_add_filteropts(EVFILT_MACHPORT, &machport_filtops) now succeeds (slot reserved by 0003). The stale “stock FreeBSD rejects -16” comment in mach_module.c can be corrected as a drive-by.ipc_*/mach_*/machport_*-prefixed or a clearly-named MIG server entry (mach_host_server, …); the scan found no collision with base-kernel globals. (Two unprefixed names — mach_debug_enable, trailer_template — are still collision-free in the kernel namespace.)SI_ORDER to add. ipc_bootstrap_sysinit (zone setup) and the mach module init both fire at SI_SUB_KLD, SI_ORDER_ANY; today their order falls out of link order (IPC first — correct). Make it explicit (IPC SI_ORDER_FIRST, mach init SI_ORDER_MIDDLE) so it can’t regress. The syscall-wire SYSINIT (SI_SUB_INTRINSIC) already runs after both and before PID 1.kqueue_add_filteropts call now succeeds (slot reserved by 0003); guard the shim’s duplicate #define (gotcha 2) and correct the stale comment.opt_*.h. The shipped opt_compat_mach.h/opt_capsicum.h/… are replaced by config(8)-generated ones from the real NEXTBSD config — which is more correct (guaranteed KBI match), removing the hand-maintained copies.The traps must exist before launchd runs. SYSINIT subsystem ordering (from <sys/kernel.h>) guarantees this: SI_SUB_INTRINSIC (current syscall-wire) and SI_SUB_KLD (current module init) both run well before SI_SUB_CREATE_INIT (PID 1) and SI_SUB_KICK_SCHEDULER. The one ordering invariant to preserve: IPC zone init must precede syscall wiring (it does today — init at SI_SUB_KLD, wire at SI_SUB_INTRINSIC). We keep that relative order; the boot smoke test confirms it empirically.
| Repo | Change |
|---|---|
nextbsd-kernel | Add sys/compat/mach/ source + 0004-compat-mach-builtin.patch + options COMPAT_MACH. The kernel build now compiles mach in. PR-gated by the boot smoke test (#6). On green main, the continuous kernel ships with mach built in. |
nextbsd | Once the built-in kernel is in continuous: drop the out-of-tree mach_kmod build from build.sh (steps 3a/3b) and remove mach_load="YES" from overlays/boot/loader.conf.d/freebsd-launchd-mach.conf. The ISO then boots mach from the kernel image. |
Ordering matters (per the pipeline rule): kernel PR merges and continuous kernel republishes first; only then the nextbsd change that depends on the built-in kernel.
Blast radius This changes the shipping kernel’s boot path. A mistake = a kernel that doesn’t boot. Mitigations: (1) land as a nextbsd-kernel PR — the boot smoke test (#6) boots the kernel in a VM and must pass before merge; (2) the transition keeps mach_load="YES" valid, so even a built-in that fails to init can’t be worse than today until we remove the loader line; (3) verify on the booted PR kernel that the mach syscalls work with mach.ko not loaded (kldstat shows only kernel).
nextbsd-kernel (sys/compat/mach/ + headers under sys/sys/mach/).0004-compat-mach-builtin.patch (options + files + config) and the build-step that lays the source into the clone.mach_module.c (SYSINIT or keep DECLARE_MODULE), correct the stale EVFILT comment.mach.ko.continuous kernel republishes with mach built in.mach_kmod build + mach_load="YES". Verify the ISO boots.ROOTDEVNAME/INIT_PATH options, trim unused .ko) proceeds on the now-clean built-in tier..ko too? Built-in-only is the simpler pipeline (any mach change = full kernel rebuild). Keeping the .ko build alongside lets developers kldload a rebuilt mach without a kernel build, at the cost of maintaining both paths. Leaning: built-in for release; optionally retain the .ko Makefile for dev iteration.COMPAT_MACH (matches the existing -DCOMPAT_MACH and shipped opt_compat_mach.h — least churn) vs. a fresh MACH. Leaning: reuse COMPAT_MACH.<sys/mach/…> implies sys/sys/mach/; confirm no clash with any base header namespace.DECLARE_MODULE (works built-in) vs. convert to SYSINIT (tidier). Leaning: convert.From a 3-agent code-grounded pass (build wiring, header/namespace audit, link/init audit). These are the literal artifacts to apply.
sys/conf/options + config/NEXTBSD# sys/conf/options (one line)
COMPAT_MACH opt_compat_mach.h
# config/NEXTBSD (today just "include GENERIC")
include GENERIC
ident NEXTBSD
+options COMPAT_MACH
sys/conf/files.compat_mach (per-file flags)One optional compat_mach line per source file. The MIG *_server.c compile with the default rule; the hand-written files carry the Makefile’s flags via compile-with. Factor the shared flags into a macro to keep it readable:
# sys/conf/Makefile (or top of files.compat_mach via make var)
COMPAT_MACH_C= ${NORMAL_C} -DCOMPAT_MACH -DMACH_INTERNAL= -DAUDIT \
-I$S/apple -I$S/sys/mach -include $S/compat/mach/compat_shim.h \
-Wno-error=missing-prototypes -Wno-error=visibility
# sys/conf/files.compat_mach (sketch — ~45 files)
compat/mach/host_priv_server.c optional compat_mach # MIG stubs: default rule
compat/mach/mach_host_server.c optional compat_mach
compat/mach/mach_port_server.c optional compat_mach
compat/mach/mach_vm_server.c optional compat_mach
compat/mach/task_server.c optional compat_mach
compat/mach/vm_map_server.c optional compat_mach
compat/mach/clock_server.c optional compat_mach
compat/mach/mach_module.c optional compat_mach compile-with "${COMPAT_MACH_C}"
compat/mach/mach_syscall_wire.c optional compat_mach compile-with "${COMPAT_MACH_C}"
compat/mach/mach_busystate.c optional compat_mach compile-with "${COMPAT_MACH_C}"
compat/mach/mach_event_bridge.c optional compat_mach compile-with "${COMPAT_MACH_C}"
compat/mach/mach_traps.c optional compat_mach compile-with "${COMPAT_MACH_C}"
# … mach_{clock,convert,host,host_priv,misc,processor,semaphore,task,thread,vm,stats,test}.c,
# proc_info.c, compat_stubs.c — all compile-with "${COMPAT_MACH_C}"
compat/mach/ipc/ipc_*.c optional compat_mach compile-with "${COMPAT_MACH_C}" # 17 files
compat/mach/ipc/mach_{msg,port,debug}.c optional compat_mach compile-with "${COMPAT_MACH_C}"
compat/mach/kern/{ipc_host,ipc_tt,task,thread_pool}.c optional compat_mach compile-with "${COMPAT_MACH_C}"
# sys/conf/files (one include line)
files "../compat/mach/files.compat_mach"
Flags decoded from src/mach_kmod/Makefile: -DMACH_INTERNAL= must be the bare form (its include-guard usage), and -include compat_shim.h stays force-included. ${NORMAL_C} already supplies -nostdinc -I$S -D_KERNEL.
| From (module) | To (in-kernel) | Why |
|---|---|---|
include/sys/mach/ (122 .h) | sys/sys/mach/ | <sys/mach/…> resolves unchanged ($S on the path) |
include/sys/mach_debug/ (8) | sys/sys/mach_debug/ | <sys/mach_debug/…> unchanged |
include/apple/ (97) | sys/apple/ + -I$S/apple | <sys/proc_info.h> → $S/apple/sys/proc_info.h |
opt_*.h (4) | excluded | config(8) generates them (gotcha 1) |
Store the tree in nextbsd-kernel under src-overlay/sys/ (real files, not a diff). The build copies it into the throwaway freebsd-src clone right after the patch loop, before buildkernel:
# .github/workflows/build.yml — new step after "Apply patches"
- name: Install Mach source overlay
run: |
cp -R "$GITHUB_WORKSPACE/src-overlay/sys/compat/mach" /usr/src/sys/
cp -R "$GITHUB_WORKSPACE/src-overlay/sys/sys/mach" /usr/src/sys/sys/
cp -R "$GITHUB_WORKSPACE/src-overlay/sys/sys/mach_debug" /usr/src/sys/sys/
cp -R "$GITHUB_WORKSPACE/src-overlay/sys/apple" /usr/src/sys/
cp "$GITHUB_WORKSPACE/src-overlay/sys/conf/files.compat_mach" /usr/src/sys/conf/
The sys/conf/{options,files} + config/NEXTBSD edits ride as patches/0004-compat-mach-builtin.patch (tiny). This keeps nextbsd-kernel a patches-plus-overlay repo, the fork untouched, and the wiring reviewable.
Approved Link-time and init correctness pass: no symbol collisions, syscalls wired before PID 1, kern_syscall_register works at boot, machport_filtops complete. The two compile-breakers (shipped opt_*.h shadowing; compat_shim.h EVFILT_MACHPORT redefinition vs. patch 0003) are known and fixed in the steps above. Ready to implement as a nextbsd-kernel PR, boot-test-gated.
mach-into-kernel plan, 2026-06-03. Foundation for eliminating loader.conf (#180). Sourced from the nextbsd/src/mach_kmod build (Makefile, make-mach-kmod.sh, mach_module.c, mach_syscall_wire.c) and FreeBSD 15.x sys/conf/{options,files} + SYSINIT(9) / config(5).