freebsd-launchdFeasibility 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.
libmach.so exposesFeasible, 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."
mach_msg, mach_port_allocate/deallocate/mod_refs/insert_right/request_notification/construct/destruct, vm_allocate/deallocate, mach_task_self, bootstrap_check_in/look_up). Two MIG subsystems, ~20 routines in _config_subsystem. No out-of-line memory entries, no exception ports, no processor sets, no host ports in configd's hot path. (Verified by direct grep of configd/src/ at version 963.270.3.)malloc(9), mtx(9), sx(9), condvar(9), callout(9), sysctl(9), make_dev(9), kqueue(9) custom filter, SYSCALL_MODULE/syscall_helper_register, file(9), vm_map_* for inline copy, eventhandler(9) for proc-exit cleanup. Survey of sys/modules/ finds no __FreeBSD_version conditionals in comparable out-of-tree-style modules — the convention is to rebuild against each major.freebsd-launchd's existing build.sh already runs inside a FreeBSD VM via vmactions/freebsd-vm@v1, and already stages and builds the kmodloader userland tool. Adding a mach-kmod/ source tree and a make-mach-kmod.sh wrapper that invokes bsd.kmod.mk is a one-day pipeline change. The kmod ships both baked into the live ISO at /boot/kernel/mach.ko and uploaded as a standalone GitHub release asset per (FreeBSD-major × arch) tuple.liblaunch.c (preserved but unbuilt today), (2) parse MachServices in core.m, (3) implement bootstrap server inside launchd PID 1 (registration table + on-demand spawn). The AF_UNIX IPC stays as the launchctl control channel; Mach is layered alongside, not replacing it.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.
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 concept | In scope? | Justification |
|---|---|---|
mach_port_t (receive right + send right semantics) | Yes | Required by every bootstrap_* call and every configd RPC. Core abstraction. |
mach_msg() with SEND/RCV/TIMEOUT options | Yes | 138 direct call sites in configd; the whole MIG transport. |
| Inline message copy (header + body up to N KB) | Yes | configd's XML payloads travel inline. |
Send-right transfer in messages (MACH_MSG_PORT_DESCRIPTOR) | Yes | Bootstrap hands send rights to clients; clients send reply ports back. Non-negotiable. |
No-senders notification (MACH_NOTIFY_NO_SENDERS) | Yes | 11 call sites in configd; how sessions are reaped when clients exit. |
Dead-name notification (MACH_NOTIFY_DEAD_NAME) | Yes | Used for client tracking; small additional surface once no-senders is in. |
| Bootstrap server protocol (lookup, check-in, register) | Yes | Implemented 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) | Stub | Zero 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) | No | Zero call sites in configd. Skip. |
| Tasks, threads, exception ports, processor sets, host ports | No | Not 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 credentials | Yes, simplified | Carry 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).
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:
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.exec()'d.<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).NSMachPort) can be ported without further IPC plumbing.machd daemon is doable in 2–3 KLoC. It's slower and the right semantics are slightly off, but configd would not notice.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.
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:
| KPI | Used for | KBI stability |
|---|---|---|
malloc(9), free(9), MALLOC_DEFINE | Port object, message queue node, name table allocation | Stable |
mtx(9), sx(9), rmlock(9) | Per-port queue lock; namespace rwlock; refcount mtx | Stable |
condvar(9) | Blocking mach_msg(MACH_RCV) wait | Stable |
callout(9) | MACH_RCV_TIMEOUT, MACH_SEND_TIMEOUT | Stable |
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, audit | Stable (modern make_dev_s API since 11.x) |
SYSCALL_MODULE / syscall_helper_register | Hot path: mach_msg, mach_port_allocate, etc. as proper syscalls so MIG stubs see the right ABI | Stable (linux compat shim has used this pattern since 7.x) |
kqueue(9) custom filter | EVFILT_MACHPORT equivalent so libdispatch can wait on Mach receives alongside fd events | Stable (filter registration API documented in kqueue(9)) |
file(9), fget, fput, finstall | Port-as-fd handle so kqueue and poll work; file descriptor inheritance for posix_spawn hand-down | Stable |
copyin, copyout, fueword, suword | Userland message buffer transfer | Stable |
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_ucred | Tagging messages with sender pid/uid/gid | Stable |
eventhandler(9) — process_exit | Reaping ports owned by a dying task; firing no-senders / dead-name notifications | Stable |
uma(9) zone allocator | Optional optimization for hot port-object alloc; can be deferred | Stable |
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.
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) |
+----------------------+
mach_msg: blocking send/receive with timeout, port-descriptor copy semantics, sender credential attachment, queue-limit enforcement.allocate, deallocate, insert_right, mod_refs, request_notification, construct/destruct.eventhandler(9) on process exit and on refcount drop./dev/mach for: namespace introspection (debug only), bootstrap-port handoff at task spawn, and any RPC the bootstrap server needs that doesn't fit cleanly in a syscall.EVFILT_MACHPORT kqueue filter so libdispatch's DISPATCH_SOURCE_TYPE_MACH_RECV can multiplex Mach receives with regular fd events.<mach/mach.h> API surface so configd and Apple-source consumers compile and link unmodified.mach_msg/mach_port_* syscalls (the hot path).bootstrap_* family as RPC over a well-known port (the "bootstrap port"), which every task inherits at spawn time.mach_task_self() as a process-local read of the inherited self-port.MachServices dict in plists. For each entry, allocate a kernel port and store the mapping "name" → (port, owning Job).bootstrap_look_up("name"): if the owning Job is not running, posix_spawn it (handing it the receive right via posix_spawn_file_actions-like Mach extension); return a send right to the caller.bootstrap_check_in("name"): hand the receive right to the calling task (which is the just-spawned job).launchctl protocol unchanged. AF_UNIX is the control channel; Mach is the activation channel. They coexist.libmach.so exposesThe day-1 symbol export list. Configd's call-site grep gives us the closure; this list is the closure plus a small standard envelope.
| Symbol | Implemented as | Notes |
|---|---|---|
mach_msg | syscall | Hot path. SEND, RCV, SEND|RCV (round-trip), TIMEOUT options. Honors MACH_MSG_PORT_DESCRIPTOR in the body. |
mach_port_allocate | syscall | RECEIVE only on day 1. PORT_SET deferred (configd doesn't use sets). |
mach_port_deallocate | syscall | Drop one reference. |
mach_port_mod_refs | syscall | Adjust refcount on a right. Used heavily by configd for session bookkeeping. |
mach_port_insert_right | syscall | Attach a known port to a name in the calling task. |
mach_port_construct / mach_port_destruct | syscall | Modern grouped allocate+configure. Used by newer configd code paths. |
mach_port_request_notification | syscall | NO_SENDERS and DEAD_NAME only on day 1. PORT_DESTROYED, SEND_POSSIBLE deferred. |
mach_task_self | cached value | Set by libmach init from the task-self capability the kernel hands out at exec. |
vm_allocate / vm_deallocate | wraps mmap/munmap | configd uses these for inline buffer setup; not for OOL transfer. |
bootstrap_check_in | RPC to launchd PID 1 | Returns receive right. |
bootstrap_look_up / bootstrap_look_up2 | RPC to launchd PID 1 | Returns send right; triggers on-demand spawn. |
bootstrap_register | RPC to launchd PID 1 | Used by daemons that want to register a name not in their plist (rare, but configd uses it). |
mig_* support routines | thin wrappers | Allocate/deallocate, error reply, dispatch table walking. ~300 lines. |
Symbols explicitly not implemented in v1, returning KERN_NOT_SUPPORTED:
mach_make_memory_entry_64 — OOL memory entries; no caller in configdthread_*, task_* beyond task_selfhost_*, processor_*, processor_set_*exception_raise*<mach/mach_voucher*.h>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:
liblaunch.cApple'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.
MachServices in core.mAdd 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.
A new file src/src/bootstrap.c, ~600 lines:
bootstrap_look_up / bootstrap_check_in / bootstrap_register requests.DISPATCH_SOURCE_TYPE_MACH_RECV on the bootstrap port).job_spawn(j); reply with a freshly-inserted send right to the kernel port.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.
From the configd Mach-surface inventory:
| Demand | Met by |
|---|---|
Register com.apple.SystemConfiguration.configd at startup | bootstrap_check_in → launchd bootstrap server → mach.ko hands receive right |
~20 MIG routines in _config_subsystem | Apple's MIG-generated configMIGServer.c compiles unchanged once libmach exists; dispatcher calls handlers in configd.tproj/_*.c |
| Per-client session ports | mach_port_allocate + mach_port_insert_right in mach.ko; one port per session |
| NO_SENDERS notification on session port for client-died cleanup | mach_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_SYSTEM | Unrelated 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 tool | Talks to configd via SCDynamicStore → bootstrap_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.
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.
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/
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
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.
| FreeBSD major | Build matrix entry | Notes |
|---|---|---|
| 15.x (last stable before bump) | vmactions/freebsd-vm@v1 with release: 15.x-RELEASE | Built 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-RELEASE | Primary target; baked into the live ISO. |
| CURRENT (-CURRENT, KBI churn possible) | Optional experimental matrix entry; allowed to break | Useful 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).
Estimates assume one engineer at ~70% allocation. Multiply for vacation, calendar reality, and the fact that kernel work always surprises.
kldload (3 weeks)DECLARE_MODULE, MODULE_VERSION, sysctl tree, /dev/mach cdev with stub d_ioctl.mach_port_allocate / deallocate / mod_refs / insert_right as syscalls.mach_msg SEND, RCV, SEND|RCV with timeouts. Inline body up to 64 KB.MACH_MSG_PORT_DESCRIPTOR.port_allocate → child mach_msg(SEND) → parent mach_msg(RCV) → reply round-trip, repeated 1M times, no leaks (sysctl-reported port count returns to baseline).request_notification(NO_SENDERS) + (DEAD_NAME); eventhandler hook on process_exit.EVFILT_MACHPORT kqueue filter so libdispatch can drive a Mach receive on its dispatch_source.libmach.so: the symbol set in Section 6, plus <mach/mach.h> headers extracted/derived from XNU public headers (BSD-licensed parts).DISPATCH_SOURCE_TYPE_MACH_RECV works end-to-end.bootstrap.c in freebsd-launchd/src/src/: name registration table, lookup/check-in/register handlers.core.m: parse MachServices, allocate ports at job-load time, fire on-demand spawn from lookup handler.liblaunch.c; write minimal bootstrap.h / vproc.h.MachServices plist; scutil-style client looks up the name, sends a message, daemon was launched on demand.MachServices plist for org.freebsd.netconfigd.scutil against it; expect at least list, get, set to work.scutil --get HostName returns the right answer.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.
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| 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) | Medium | High | 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 | Medium | High | 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 | Low | Medium | 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 | Medium | Medium | 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 addressed | High | 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 v1 | Low | 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 |
Confirmed | Medium | 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 | Likely | High | 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 inspection | Medium | 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. |
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).
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.
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.