← Back · foundation for eliminating loader.conf (#180)

mach.ko → kernel built-in

Compile 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).

2026-06-03. Grounded in a 5-agent code scope: the nextbsd/src/mach_kmod build (sources, MIG, Makefile, syscall wiring) + the FreeBSD 15.x sys/conf/{options,files} + SYSINIT mechanism, then a deep pass on exact build wiring, header/namespace collisions, and link/init correctness (§11). Tracked on nextbsd#181 (child of #180).

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.

Contents

  1. Why build it in
  2. What mach is today (inventory)
  3. The compile-in mechanism
  4. Source migration & the wiring patch
  5. C deltas (smaller than expected)
  6. Boot ordering: ready before PID 1
  7. CI / pipeline changes
  8. Risk & the safety net
  9. Steps & sequencing
  10. Open decisions
  11. Implementation-ready wiring (deep scope)

1. Why build it in

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.

2. What mach is today (inventory)

Out-of-tree module at nextbsd/src/mach_kmod/, built by make-mach-kmod.shmake -C src/mach_kmodbsd.kmod.mk, against the ingested kernel source (SYSDIR) with the NEXTBSD build’s opt_*.h for KBI match.

GroupFilesNotes
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.cNo 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.

3. The compile-in mechanism

FreeBSD’s standard way to gate a subsystem on a kernel option:

  1. 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.)
  2. 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.
  3. 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.
  4. The kernel build compiles the mach sources into 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.)

4. Source migration & the wiring patch

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.

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.

5. C deltas (smaller than expected)

The init path already uses the SYSINIT machinery that fires for compiled-in code, so most of it carries over verbatim:

6. Boot ordering: ready before PID 1

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.

7. CI / pipeline changes

RepoChange
nextbsd-kernelAdd 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.
nextbsdOnce 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.

8. Risk & the safety net

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).

9. Steps & sequencing

  1. Stage the source in nextbsd-kernel (sys/compat/mach/ + headers under sys/sys/mach/).
  2. Write 0004-compat-mach-builtin.patch (options + files + config) and the build-step that lays the source into the clone.
  3. Convert/keep init: tidy mach_module.c (SYSINIT or keep DECLARE_MODULE), correct the stale EVFILT comment.
  4. Open the PR → boot smoke test must pass; verify mach syscalls live with no mach.ko.
  5. Merge on green (both gh exit code and API conclusion); continuous kernel republishes with mach built in.
  6. nextbsd follow-up PR: drop the mach_kmod build + mach_load="YES". Verify the ISO boots.
  7. Then the rest of #180 (ROOTDEVNAME/INIT_PATH options, trim unused .ko) proceeds on the now-clean built-in tier.

10. Open decisions

11. Implementation-ready wiring (deep scope)

From a 3-agent code-grounded pass (build wiring, header/namespace audit, link/init audit). These are the literal artifacts to apply.

11.1 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

11.2 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.

11.3 Header placement (zero collisions, confirmed via cgit)

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)excludedconfig(8) generates them (gotcha 1)

11.4 Source delivery + CI step

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.

11.5 Verdict

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).