mach.ko — an out-of-tree FreeBSD kernel module for Mach IPC, shipped from freebsd-launchd

Feasibility study for a small kernel module (/boot/kernel/mach.ko) that re-introduces just enough Mach IPC to let launchd run a real bootstrap server and let configd boot unmodified. Built and released as a versioned artifact of the freebsd-launchd repo, alongside the live ISO. Targets only stable in-major-version FreeBSD KPIs so the artifact rebuilds at major bumps (15.x → 16.x), not minor releases.

Companion to freebsd-launchd-service-ordering.html. Where that doc surveys three options and broadly favours not porting Mach, this doc takes the "port Mach" path seriously and asks what a minimal, well-scoped implementation would actually look like. Written 2026-05-10.

Contents

  1. TL;DR — verdict and shape
  2. Scope — the "minimum useful Mach" target
  3. Why a kernel module, not a userspace shim
  4. Kernel KPI surface and the "rebuild only on major bumps" guarantee
  5. Architecture — mach.ko, libmach, launchd bootstrap server
  6. Userland API — what libmach.so exposes
  7. Changes to the freebsd-launchd daemon itself
  8. What configd needs, and what it doesn't
  9. Build pipeline — shipping mach.ko as a freebsd-launchd release artifact
  10. Milestones — A through E
  11. Risks and unknowns
  12. License posture
  13. Decision criteria — when to go, when to bail

1. TL;DR — verdict and shape

Feasible, scoped, ~6–9 months from zero to "configd boots." Not feasible as "implement Mach." Feasible as "implement the ~15-call Mach subset that configd and launchd's bootstrap server actually use, in kernel, because that subset has clean port-rights and queueing semantics that are awkward in userspace."

Companion doc context. The service-ordering doc evaluates "port Mach" as Option 2 and prices it at 6–24 months, ranking it lowest on effort/risk among the three options. This doc agrees with that range but argues the lower bound (6–9 months) is reachable if we accept a hard scope freeze: configd's API surface, nothing more. Go beyond that and the timeline blows up.

2. Scope — the "minimum useful Mach" target

The risk in any "port Mach" project is scope creep into Mach-the-microkernel: tasks, threads, exception ports, processor sets, virtual memory regions, the host abstraction, processor pinning, etc. Almost none of that is what configd or launchd actually want. They want named, queued message passing with kernel-mediated capability transfer. We define scope by enumeration:

Mach conceptIn scope?Justification
mach_port_t (receive right + send right semantics)YesRequired by every bootstrap_* call and every configd RPC. Core abstraction.
mach_msg() with SEND/RCV/TIMEOUT optionsYes138 direct call sites in configd; the whole MIG transport.
Inline message copy (header + body up to N KB)Yesconfigd's XML payloads travel inline.
Send-right transfer in messages (MACH_MSG_PORT_DESCRIPTOR)YesBootstrap hands send rights to clients; clients send reply ports back. Non-negotiable.
No-senders notification (MACH_NOTIFY_NO_SENDERS)Yes11 call sites in configd; how sessions are reaped when clients exit.
Dead-name notification (MACH_NOTIFY_DEAD_NAME)YesUsed for client tracking; small additional surface once no-senders is in.
Bootstrap server protocol (lookup, check-in, register)YesImplemented in launchd PID 1, not in mach.ko itself, but mach.ko has to support the underlying RPC.
Out-of-line memory descriptors (MACH_MSG_OOL_DESCRIPTOR)StubZero direct uses in configd, but MIG can emit them for large XML. Stub returns KERN_NOT_SUPPORTED initially; messages exceeding inline limit get an explicit error rather than silent truncation.
Memory entries (mach_make_memory_entry_64)NoZero call sites in configd. Skip.
Tasks, threads, exception ports, processor sets, host portsNoNot used by configd or launchd-bootstrap. The "port" is just the IPC name; we don't implement the rest of the microkernel.
XPC layer (xpc_connection_*, xpc_dictionary_*)No (separate)344 calls in configd's satellite daemons (IPMonitorControl, dnsinfo, network_information_server) but zero in the configd core bootstrap path. Layer XPC on later as libxpc.so over Mach; not part of mach.ko.
Audit tokens / peer credentialsYes, simplifiedCarry pid + uid + gid in the kernel-attached message metadata. Configd uses these for authorization.

The whole module fits in roughly: one cdev (/dev/mach) for control + a small handful of new syscalls registered via SYSCALL_MODULE for the hot mach_msg path. Estimated implementation budget: 4–6 KLoC of kernel code plus 2 KLoC of userland (libmach.so + bootstrap glue inside launchd).

3. Why a kernel module, not a userspace shim

The service-ordering doc's Option 1 is "rewrite configd to use AF_UNIX, drop Mach." That works and is cheaper. The argument for going to the kernel anyway:

What the kernel buys you

  • Capability transfer with kernel-enforced rights. Send-right transfer over a Unix socket via SCM_RIGHTS exists, but it transfers file descriptors with full POSIX semantics. Mach's send/receive distinction (you can hold a send right without being able to receive on the port; multiple processes can share a send right) is not expressible in SCM_RIGHTS. Doing it in userspace means a trusted broker process mediates every right transfer — that's a Mach implementation in userland, with extra IPC.
  • Atomic, kernel-queued message delivery. "Send X, receive Y, hand Z to a third party" can complete without the recipient or third party being scheduled. A userland broker would need to wake up to forward.
  • One namespace, kernel-rooted. No "where did the broker socket go?" race at boot. mach.ko's namespace is available the moment the module loads — before launchd PID 1 is even exec()'d.
  • configd-source compatibility, no patches. Apple's configd source compiles against <mach/mach.h> and links against a libmach.so with the standard symbols. A kernel-backed implementation is API-identical; a userland broker is not (broker-RPC stubs leak in everywhere).
  • Future Apple-source ports come along free. Once mach.ko + libmach is in place, anything else from Darwin that wants Mach (notifyd, distnoted, asl, parts of CoreFoundation, anything that uses NSMachPort) can be ported without further IPC plumbing.

What you pay

  • Kernel code is kernel code. Bugs panic the box. Memory has to be accounted. Lock orders matter. We need a real test harness inside a VM, not unit tests.
  • KBI maintenance per major. Even with a stable-only KPI diet, every FreeBSD major needs a fresh build and a smoke test in CI.
  • Userland alternative is genuinely smaller. A trusted-broker libmach over a single Unix socket to a machd daemon is doable in 2–3 KLoC. It's slower and the right semantics are slightly off, but configd would not notice.
  • Audit surface. A new kernel-resident name registry that any process can publish to and look up against is a new attack surface. Has to be designed with default-deny ACLs, not "anyone can register any name."

The recommendation in this doc: start kernel-resident, because the service-ordering doc's Section 4.4 makes the case that bootstrap-mediated activation is the only ordering primitive that scales. Anything that has to be "running before launchd" or "available before any process exists" wants to be in the kernel.

4. Kernel KPI surface and the "rebuild only on major bumps" guarantee

FreeBSD's KBI policy: stable within a major release, may break across majors. __FreeBSD_version in sys/sys/param.h is currently 1600018 (major 16, the encoding is MMmmRXX). Out-of-tree modules are expected to be rebuilt for each major. We commit to that; we want to be able to not commit to anything tighter.

The exhaustive list of KPIs mach.ko would touch, classified:

KPIUsed forKBI stability
malloc(9), free(9), MALLOC_DEFINEPort object, message queue node, name table allocationStable
mtx(9), sx(9), rmlock(9)Per-port queue lock; namespace rwlock; refcount mtxStable
condvar(9)Blocking mach_msg(MACH_RCV) waitStable
callout(9)MACH_RCV_TIMEOUT, MACH_SEND_TIMEOUTStable
sysctl(9)kern.mach.* introspection (port count, message bytes queued, namespace size)Stable
make_dev_s(9), cdevsw, d_ioctl/dev/mach control device for namespace setup, debug, auditStable (modern make_dev_s API since 11.x)
SYSCALL_MODULE / syscall_helper_registerHot path: mach_msg, mach_port_allocate, etc. as proper syscalls so MIG stubs see the right ABIStable (linux compat shim has used this pattern since 7.x)
kqueue(9) custom filterEVFILT_MACHPORT equivalent so libdispatch can wait on Mach receives alongside fd eventsStable (filter registration API documented in kqueue(9))
file(9), fget, fput, finstallPort-as-fd handle so kqueue and poll work; file descriptor inheritance for posix_spawn hand-downStable
copyin, copyout, fueword, suwordUserland message buffer transferStable
vm_map_lookup, vm_map_protect (read-only paths)Validating user buffers; bounded inline message copy. We do not implement OOL memory entries.Stable (read-only KPIs)
proc(9): curproc, p_pid, p_ucredTagging messages with sender pid/uid/gidStable
eventhandler(9)process_exitReaping ports owned by a dying task; firing no-senders / dead-name notificationsStable
uma(9) zone allocatorOptional optimization for hot port-object alloc; can be deferredStable

The bet: every entry above is documented in share/man/man9/ on FreeBSD-CURRENT and has been present and source-compatible since 13.x. We use none of: vnode internals, scheduler internals, network stack internals, USB stack, sound stack, GEOM internals, ZFS, jails (beyond cred read), capsicum hooks, NUMA topology APIs. A grep of sys/modules/cuse, sys/modules/sysvipc, sys/modules/linux finds zero __FreeBSD_version conditionals — that's the bar we hold ourselves to.

If we ever need a KPI that turns out to be major-version-sensitive, the policy is: add a thin per-major shim file under mach-kmod/compat/ and select it via the Makefile's SRCS, not via #if inside the main code. This keeps the main module readable and pushes all version churn into one file per major.

5. Architecture — mach.ko, libmach, launchd bootstrap server

Three components, each with a distinct responsibility:

+----------------------------------------------------------+
|  Userland                                                |
|                                                          |
|  +----------+   +---------------+   +-----------------+  |
|  |  scutil  |   |  netconfigd   |   | other clients   |  |
|  | client   |   |  (configd)    |   |                 |  |
|  +----+-----+   +-------+-------+   +--------+--------+  |
|       |                 |                    |           |
|       |  bootstrap_     |  bootstrap_        |           |
|       |  look_up()      |  check_in()        |           |
|       v                 v                    v           |
|  +-----------------------------------------------------+ |
|  |              libmach.so  (libdispatch-compatible)   | |
|  |  mach_msg, mach_port_*, vm_alloc, bootstrap_*       | |
|  +-----------------------------------------------------+ |
|                       |                |                 |
|              syscall  |                | UNIX socket     |
|              (mach_*) |                | to PID 1 (boot) |
|                       v                v                 |
+-----------------------|----------------|-----------------+
|  Kernel               |                |                 |
|                       v                |                 |
|  +-----------------------------------------------------+ |
|  |   mach.ko  (port namespace, message queues, rights, | |
|  |             notifications, /dev/mach, EVFILT_MACH)  | |
|  +-----------------------------------------------------+ |
|                                        |                 |
+----------------------------------------|-----------------+
                                         |
                                         v
                              +----------------------+
                              |  launchd PID 1       |
                              |  - bootstrap server  |
                              |  - MachServices regs |
                              |  - on-demand spawn   |
                              |  (AF_UNIX still      |
                              |   serves launchctl)  |
                              +----------------------+

5.1 mach.ko responsibilities

5.2 libmach.so responsibilities

5.3 launchd PID 1 responsibilities

6. Userland API — what libmach.so exposes

The day-1 symbol export list. Configd's call-site grep gives us the closure; this list is the closure plus a small standard envelope.

SymbolImplemented asNotes
mach_msgsyscallHot path. SEND, RCV, SEND|RCV (round-trip), TIMEOUT options. Honors MACH_MSG_PORT_DESCRIPTOR in the body.
mach_port_allocatesyscallRECEIVE only on day 1. PORT_SET deferred (configd doesn't use sets).
mach_port_deallocatesyscallDrop one reference.
mach_port_mod_refssyscallAdjust refcount on a right. Used heavily by configd for session bookkeeping.
mach_port_insert_rightsyscallAttach a known port to a name in the calling task.
mach_port_construct / mach_port_destructsyscallModern grouped allocate+configure. Used by newer configd code paths.
mach_port_request_notificationsyscallNO_SENDERS and DEAD_NAME only on day 1. PORT_DESTROYED, SEND_POSSIBLE deferred.
mach_task_selfcached valueSet by libmach init from the task-self capability the kernel hands out at exec.
vm_allocate / vm_deallocatewraps mmap/munmapconfigd uses these for inline buffer setup; not for OOL transfer.
bootstrap_check_inRPC to launchd PID 1Returns receive right.
bootstrap_look_up / bootstrap_look_up2RPC to launchd PID 1Returns send right; triggers on-demand spawn.
bootstrap_registerRPC to launchd PID 1Used by daemons that want to register a name not in their plist (rare, but configd uses it).
mig_* support routinesthin wrappersAllocate/deallocate, error reply, dispatch table walking. ~300 lines.

Symbols explicitly not implemented in v1, returning KERN_NOT_SUPPORTED:

7. Changes to the freebsd-launchd daemon itself

From the IPC inventory: today's launchd has 6 plist keys, an AF_UNIX framed protocol, and a liblaunch.c that's checked in but excluded from src/Makefile. The Mach work adds, narrowly:

7.1 Build liblaunch.c

Apple's src/liblaunch/liblaunch.c already contains the launch_data_t serializers, the launch_mach_checkin_service() path (currently calls bootstrap_check_in, line ~1010), and the Mach port marshalling (launch_data_set_machport, launch_data_new_machport). Adding it to src/Makefile's SRCS is a one-line change once libmach.so exists.

Two missing headers from the Apple-private set: bootstrap.h, vproc.h, vproc_priv.h. We write these from scratch, declaring just the symbols liblaunch.c references. The MIG stubs (vproc_mig_set_security_session, etc.) become thin RPC wrappers calling launchd over its existing AF_UNIX socket — they don't all have to be Mach.

7.2 Parse MachServices in core.m

Add to the recognized-keys table in core.m:258:

// In jobmgr_load_plist_file(), after the existing key parsing:
NSDictionary *machServices = [plist objectForKey:@"MachServices"];
if (machServices) {
    j->mach_services = [machServices retain];
    j->run_at_load = false;  // MachServices implies launch-on-demand
}

And on jobmgr_insert(j), walk j->mach_services, allocate a kernel port via mach_port_allocate, register the (name, port, j) tuple in launchd's bootstrap table.

7.3 Bootstrap server loop

A new file src/src/bootstrap.c, ~600 lines:

7.4 Hand the bootstrap port to spawned jobs

Tiny mach.ko-supported extension to posix_spawn: an ioctl on /dev/mach that says "the next task this process spawns inherits port X as its bootstrap port." launchd does this immediately before each posix_spawn for a job. Cleaner than a Mach-specific syscall.

8. What configd needs, and what it doesn't

From the configd Mach-surface inventory:

DemandMet by
Register com.apple.SystemConfiguration.configd at startupbootstrap_check_in → launchd bootstrap server → mach.ko hands receive right
~20 MIG routines in _config_subsystemApple's MIG-generated configMIGServer.c compiles unchanged once libmach exists; dispatcher calls handlers in configd.tproj/_*.c
Per-client session portsmach_port_allocate + mach_port_insert_right in mach.ko; one port per session
NO_SENDERS notification on session port for client-died cleanupmach_port_request_notification(MACH_NOTIFY_NO_SENDERS) — mach.ko fires via eventhandler(9) proc_exit
Inline XML payload transfer (typical 200 B – 8 KB)Inline mach_msg body; mach.ko bounded queue, configurable upper bound (default 64 KB matching XNU)
Plugin process notifications via PF_SYSTEMUnrelated to mach.ko. KernelEventMonitor uses BSD route sockets and PF_SYSTEM; FreeBSD has PF_ROUTE and an event loop. Separate porting work, but not in this plan's scope.
scutil command-line toolTalks to configd via SCDynamicStorebootstrap_look_up → MIG over Mach. Works once libmach + bootstrap server are running.
XPC clients (IPMonitorControl, dnsinfo, network_information_server)Out of scope for v1. XPC daemons need libxpc.so over Mach, which is a separate implementation effort. Without XPC, the core configd boots; satellite features are degraded.

The verdict here is the load-bearing claim of the whole plan: core configd's hard dependency on Mach is small and well-bounded. Most of the 690 grep hits collapse to repeated use of the same ~12 calls. The 344 XPC hits are concentrated in code that can be left out of v1.

9. Build pipeline — shipping mach.ko as a freebsd-launchd release artifact

Per the user's framing: mach.ko should be built and released by the freebsd-launchd repo, not as a separate downstream project. Today the repo's CI already builds inside a FreeBSD VM via vmactions/freebsd-vm@v1 and publishes a continuous release per push.

9.1 Source layout addition

freebsd-launchd/
├── boot/
├── build.sh                  <-- modify to invoke make-mach-kmod.sh
├── configd/
├── kmodloader/
├── mach-kmod/                <-- NEW
│   ├── Makefile              (KMOD=mach, SRCS=...)
│   ├── compat/
│   │   ├── freebsd15.c       (per-major shim, if needed)
│   │   └── freebsd16.c
│   ├── mach_dev.c            (/dev/mach cdevsw)
│   ├── mach_msg.c            (mach_msg syscall)
│   ├── mach_port.c           (port objects, refs, notifications)
│   ├── mach_namespace.c      (per-task name table)
│   ├── mach_kqueue.c         (EVFILT_MACHPORT filter)
│   ├── mach_module.c         (DECLARE_MODULE, MODULE_VERSION)
│   └── tests/                (kyua tests run inside the build VM)
├── make-launchd.sh
├── make-mach-kmod.sh         <-- NEW
├── make-configd.sh
└── overlays/

9.2 Build script integration

make-mach-kmod.sh is a wrapper of the same shape as make-launchd.sh: stage mach-kmod/ into the chroot, run make with FreeBSD's bsd.kmod.mk, install the resulting mach.ko to ${DESTDIR}/boot/kernel/mach.ko. build.sh calls it after kmodloader and before launchd, so the live ISO contains /boot/kernel/mach.ko ready to be preloaded.

Live ISO loader.conf gains:

# /boot/loader.conf inside the live ISO
mach_load="YES"      # load mach.ko before init starts
mach_modules=""      # reserved for future submodules

9.3 Standalone artifact upload

The CI job that today uploads FreeBSD-15.0-amd64-launchd-YYYYMMDD.iso as the continuous release also uploads, in parallel:

mach.ko-FreeBSD-15.4-amd64.tar.gz
mach.ko-FreeBSD-16.0-amd64.tar.gz
mach.ko-FreeBSD-16.0-aarch64.tar.gz   # if/when arm64 build is added

Each tarball contains: mach.ko, a README with the exact __FreeBSD_version built against, the SHA256 of the build-tree kernel, and a tiny install.sh that drops the file into /boot/kernel/ and updates /boot/loader.conf. Users on a stock FreeBSD install can fetch the tarball, install, reboot, and have a kernel that's ready to host launchd + configd from the freebsd-launchd userland.

9.4 Per-major build matrix

FreeBSD majorBuild matrix entryNotes
15.x (last stable before bump)vmactions/freebsd-vm@v1 with release: 15.x-RELEASEBuilt once per major-stable release; rebuilt only if KPIs we use shifted (rare; caught by CI smoke test).
16.x (current target)Same, release: 16.0-RELEASEPrimary target; baked into the live ISO.
CURRENT (-CURRENT, KBI churn possible)Optional experimental matrix entry; allowed to breakUseful early-warning; not promised to users.

The artifact naming embeds the exact __FreeBSD_version the module was built against. If a user's running kernel reports a different __FreeBSD_version, kldload refuses — FreeBSD's normal KBI safety net. Users see a clear error instead of a panic; we publish a fresh artifact for the new minor only if a real KBI bump occurred (which our KPI choice should make rare).

10. Milestones — A through E

Estimates assume one engineer at ~70% allocation. Multiply for vacation, calendar reality, and the fact that kernel work always surprises.

Phase A — mach.ko skeleton boots and survives kldload (3 weeks)

Phase B — ports, messages, rights (8 weeks)

Phase C — notifications, kqueue, libmach.so (5 weeks)

Phase D — bootstrap server in launchd (4 weeks)

Phase E — configd boots (4 weeks)

Total: 24 weeks (~5.5 months) elapsed at 70% allocation. Add 6–8 weeks of "configd actually works for real use cases including KernelEventMonitor PF_ROUTE port" before this is shippable to end users. Total realistic budget: 7–9 months.

11. Risks and unknowns

RiskLikelihoodImpactMitigation
Some "rare" Mach call turns out to be on configd's hot path after all (e.g., MIG emits OOL descriptors for messages over inline limit) MediumHigh Phase B exit gate includes "compile MIG-generated configMIGServer.c and link." Any unsatisfied symbols surface immediately, not at Phase E.
libdispatch on FreeBSD relies on Mach-specific dispatch_source semantics we underestimate MediumHigh Phase C explicitly tests DISPATCH_SOURCE_TYPE_MACH_RECV end-to-end; if it doesn't work we redesign the kqueue filter before Phase D, not after.
FreeBSD KBI shifts in a minor release because we hit a less-stable corner of a "stable" KPI LowMedium CI matrix includes the latest minor of each supported major. Failures show up as red builds, not as user-reported panics. Per-major compat/ shim keeps churn isolated.
Send-right semantics are subtly wrong vs Apple, configd works but later imports (e.g., notifyd) don't MediumMedium Build a portable test suite from XNU's tests/mach_*.c early. Run it in CI from Phase B onward.
Security model: any process can register any bootstrap name — trivial spoofing High if not addressedHigh Bootstrap server rejects bootstrap_register from non-root unless the name is in a per-user namespace (mirrors Apple's user vs system bootstrap). MachServices registration is launchd-only.
Performance: kernel-resident port queues add a syscall per message vs Apple's optimized fastpath Low for v1Low configd is not a high-throughput service. Defer fastpath work to a later phase. Initial benchmark target: 50k round-trips/sec single-threaded, comfortably above configd's working load.
Apple-source MIG stub generation requires the mig tool, which is Apple-only ConfirmedMedium Use the open-source MIG re-implementation from Darling, or pre-generate stubs once and check them into the repo. Phase A includes a one-time decision on which.
Scope creep: someone wants exception ports, processor sets, host ports for an unrelated import LikelyHigh Hard rule, written into mach-kmod/README.md: any new symbol added to libmach.so requires a justification in the form "<named caller> uses it for <named purpose>." No speculative implementations.
The XPC layer (which configd's satellite daemons need) turns out to be much larger than Mach itself Confirmed by inspectionMedium v1 ships without XPC. Satellite daemons (IPMonitorControl, dnsinfo, network_information_server) are explicitly out of scope until libxpc is a separate workstream. Document this loudly.

12. License posture

We re-implement the Mach API from public documentation. We do not import APSL-2.0 code from XNU. The <mach/*.h> headers in XNU are largely BSD-licensed (CMU/Mach origin) and can be used with attribution; the implementation files (osfmk/ipc/*) are APSL and we leave them alone.

This puts mach.ko's license at BSD-2-Clause matching the rest of freebsd-launchd. The headers we ship in mach-kmod/include/mach/ retain CMU notices where applicable. Apple's liblaunch.c and the configd source remain under their original Apache-2.0 / APSL headers per file, exactly the existing repo policy (per NOTICE).

13. Decision criteria — when to go, when to bail

Go signal: Phases A–C complete on schedule (16 weeks), and the libdispatch DISPATCH_SOURCE_TYPE_MACH_RECV test in Phase C passes without significant kqueue redesign. Phases D–E are then mostly userland glue.

Pause-and-reassess signal: Phase B reveals a Mach semantic we hadn't budgeted (e.g., port sets are needed by MIG in a way we missed; OOL is on configd's hot path; send-once rights are required by some bootstrap path). Add 4 weeks; if the new estimate exceeds 10 months total, switch to the service-ordering doc's Option 1 (rewrite configd to AF_UNIX) for the netconfigd milestone and keep mach.ko as a longer-horizon project.

Bail signal: Either (a) the FreeBSD KPIs we depend on turn out to require __FreeBSD_version conditionals every minor release (would be very surprising given the survey, but if so the per-major artifact promise is dead), or (b) a kernel panic in mach.ko proves to require deep VM-subsystem knowledge to fix. The contingency is the same: ship Option 1, treat configd as a one-off, defer mach.ko.

Even in the bail case, the work is not wasted: libmach.so with all entry points stubbed to return KERN_NOT_SUPPORTED still lets us compile-test Apple-source ports, and the MachServices parsing in core.m + the bootstrap server in launchd still solves the service-ordering problem (Sections 4.4 and 10 of the companion doc) by giving us a name registry, even if the actual port handed out is backed by an AF_UNIX socket rather than a kernel port.

Closing summary

An out-of-tree mach.ko shipped from freebsd-launchd is feasible if and only if we hold ruthlessly to the "configd's call set, nothing more" scope. The kernel-side surface fits in ~5 KLoC of stable-KPI code; the userland glue is another ~2 KLoC; the launchd changes are bounded to building liblaunch.c, parsing MachServices, and adding a bootstrap server loop.

The shipping story matches the existing repo: a mach-kmod/ source tree, a make-mach-kmod.sh wrapper, integration into build.sh, and a per-FreeBSD-major release artifact uploaded alongside the live ISO. The KBI bet — rebuild only on major bumps — holds because the KPIs we touch are documented stable and because comparable out-of-tree-style modules in the FreeBSD tree carry no __FreeBSD_version conditionals.

The realistic budget is 7–9 months. The realistic risk is scope creep into Mach-the-microkernel; the discipline is documented in Section 11.

Research basis: direct inspection of /Users/jmaloney/Documents/launchd/freebsd-launchd (launchd port, configd 963.270.3 import, build pipeline) and /Users/jmaloney/Documents/launchd/freebsd-src (FreeBSD kernel module patterns, KPI documentation, __FreeBSD_version at 1600018). Companion to freebsd-launchd-service-ordering.html and freebsd-launchd-plan.html.