freebsd-launchd-842 — porting Apple's launchd-842.92.1A data-grounded porting plan for Apple's last open-source launchd (launchd-842.92.1, 2014) onto our freebsd-launchd-mach stack. Phase I1 is complete at commit be8f444 (2026-05-16): /sbin/launchd and /bin/launchctl build, install, and pass ldd verification; LAUNCHD-BUILD-OK + LAUNCHCTL-BUILD-OK markers green; full CoreFoundation + ICU surface available via the vendored libCoreFoundation + swift-foundation-icu pair. Phases I2 (core functionality, multiple test daemons) and I3 (deferred PID-1) follow. The one remaining blocker for I2's runtime smoke is a mach.ko null-port-send hang documented in §7.
Phase I1 is complete at freebsd-launchd-mach commit be8f444 (2026-05-16). The stack provides:
| Component | Status | Install path |
|---|---|---|
mach.ko | port-management traps, special-port traps, multiplexer slot 219, audit-trailer types defined | kernel module |
libsystem_kernel.so | mach_msg, mach_port_*, task_*_special_port, host_set_special_port | /usr/lib/system/ |
libdispatch.so | full type system + Mach RECV polling backend | /usr/lib/system/ |
libxpc.so.4 | type system round-trips; dictionary IPC round-trip green | /usr/lib/system/ |
bootstrap_server daemon | standalone, hand-rolled message-ID dispatch (no MIG), host-bootstrap-port fallback | /usr/local/sbin/ |
MIG (mig + migcom) | Apple bootstrap_cmds ported; generates MIG client+server stubs for job.defs, helper.defs, mach_exc.defs, notify.defs | /usr/bin/mig + /usr/libexec/migcom |
liblaunch.so.1 | launch_data_t API, vproc_*, bootstrap_*; bundles jobUser.c MIG stubs so consumers other than launchd itself dlopen cleanly | /usr/lib/system/ |
/sbin/launchd | 235 KB ELF; execs + rejects non-PID-1 invocation with proper error | /sbin/ (Apple-canonical) |
libicucore / lib_FoundationICU.so | Apple's swift-foundation-icu ICU 74 vendored at src/swift-foundation-icu/; U_DISABLE_RENAMING=1, full CLDR data via .incbin restructure (~40 MB installed) | /usr/lib/system/ |
libCoreFoundation.so.6 | swift-corelibs-foundation pure-C CF, 84 source files including all 16 ICU-using files restored; full CFPropertyList + plist binary/XML round-trip | /usr/lib/system/ |
/bin/launchctl | 79 KB ELF; built + dynamic-linker-verified against the full stack. Runtime invocation hangs in mach_msg pending the kernel fix at §7 below; ldd-only smoke at this phase | /bin/ (Apple-canonical) |
| Smoke markers | 14 markers green on CI: MACH-SMOKE, LIBSYSTEM-KERNEL, MACH-PORT, TASK-SPECIAL-PORT, HOST-BOOTSTRAP, BOOTSTRAP, BOOTSTRAP-REMOTE, LIBDISPATCH, LIBDISPATCH-MACH, LIBXPC, MIG-BUILD, LAUNCHD-BUILD, COREFOUNDATION, LAUNCHCTL-BUILD | — |
What we do not have:
fileport_make{port,fd} — stubs returning ENOSYS in libxpc.protocol_vproc.defs — Apple ships it; launchd-842 doesn't include it. We use job.defs (which has the same userprefix vproc_mig_; and overlapping routine set) and link jobUser.c into liblaunch.so so the vproc_mig_* symbols resolve at dlopen time.mach_msg hangs on send to MACH_PORT_NULL (the bootstrap_port pre-PID-1), blocking every launchctl subcommand. Fix lives in mach.ko's ipc_kmsg.c early-return path. See §7.launchd-842Apple's launchd-842.92.1 at apple-oss-distributions/launchd, tagged 2014-08-13. Total ~28,500 LOC of C across five directories.
| Directory | LOC | Produces | Notes |
|---|---|---|---|
src/ | 16,285 | launchd binary | core.c is 12,126 lines on its own. 7 MIG .defs files. |
liblaunch/ | 4,448 | liblaunch.dylib | liblaunch.c + libvproc.c + libbootstrap.c. Public launch.h / bootstrap.h / vproc.h. |
support/ | 4,836 | launchctl + launchproxy + wait4path | launchctl.c alone is 1,847 lines (single-file CLI with 25 subcommands). |
SystemStarter/ | 1,939 | SystemStarter binary | Legacy startup-items runner; safe to skip entirely for our port. |
man/, rc/ | — | doc + rc scripts | No compiled code. |
Seven .defs files; MIG generates RPC client+server stubs as .c + .h pairs at build time. We do not ship MIG on FreeBSD. Three handling options exist; the plan recommends a hybrid.
| File | Subsystem | Role | Generated headers consumed by |
|---|---|---|---|
job.defs | 400 | vproc_mig_* client + job_mig_* server | core.c:126, runtime.c:77 |
protocol_jobmgr.defs | 400 | same wire as job.defs + ServerAuditToken trailer | same demux table |
internal.defs | 137000 | kernel → launchd kqueue notification | runtime.c:65 |
helper.defs | 4241011 | UserEventAgent downcall | libvproc.c |
job_reply.defs | — | internal MIG reply marshaling | internal |
job_forward.defs | — | internal MIG forward marshaling | internal |
job_types.defs | — | type definitions for the above | shared |
Important wire fact: message IDs for subsystem 400 are 400 + routine_offset; e.g. bootstrap_check_in = 408, bootstrap_register = 412, bootstrap_look_up = 416. Our standalone bootstrap_server is wire-compatible with subsystem 400 — same Mach message layout, same trailer expectations. We hand-rolled what MIG would have generated for the subset we need; launchd-842 uses MIG output for the full set.
Apple links liblaunch.dylib against the closed-source libsystem_* family. Reading xcconfigs/liblaunch.xcconfig:
-umbrella System -L/usr/lib/system
-ldyld -lcompiler_rt -lsystem_kernel -lsystem_platform
-lsystem_pthread -lsystem_malloc -lsystem_c -lquarantine -ldispatch
For our port:
| Apple lib | FreeBSD replacement |
|---|---|
libdyld | ld-elf rtld (no shim needed) |
libcompiler_rt | FreeBSD ships libcompiler_rt; usable directly |
libsystem_kernel | Our existing /usr/lib/system/libsystem_kernel.so |
libsystem_platform | covered by FreeBSD libc |
libsystem_pthread | FreeBSD libthr (-lpthread) |
libsystem_malloc | jemalloc in FreeBSD libc |
libsystem_c | FreeBSD libc |
libquarantine | Stub macOS Gatekeeper attr-tracking; no-op shim is fine |
libdispatch | Our existing /usr/lib/system/libdispatch.so |
Apple-private headers that need stubs or replacement: <TargetConditionals.h> (define for FreeBSD), <System/sys/spawn.h> + <System/sys/spawn_internal.h> (private posix_spawn extensions), <sandbox.h> (no-op shim — we don't have macOS Sandbox), <libkern/OSAtomic.h> + <libkern/OSByteOrder.h> (replace with C11 atomics + FreeBSD endian.h), <asl.h> (port Apple System Logger later or stub to syslog), <_simple.h> (Apple's mini-allocator — trivial port), <quarantine.h> (no-op), <responsibility.h> (no-op). <CoreFoundation/CFPriv.h> is only used by SystemStarter which we skip entirely. <IOKit/IOKitLib.h> + <DiskArbitration/…> are also SystemStarter-only.
What Mach symbols does launchd-842 actually call, and how does that map against our coverage today?
| Symbol | Call sites | Coverage | Notes |
|---|---|---|---|
mach_msg | ~10+ | Have | Core dispatch at runtime.c:1022-1029. |
mach_task_self, mach_host_self | 14 | Have | Port-name queries. |
mach_port_allocate / _deallocate / _insert_right | ~10 | Have | Phase F shipped these. |
task_get_special_port / _set_ | 2 | Have | Phase G prereq shipped these. |
bootstrap_check_in / _look_up | 40+ MIG refs | Have | Hand-rolled subsystem 400 wire format matches. |
host_reboot | 4 (core.c:4212, 4221, 7277) | Trivial | Fits multiplexer slot 219 pattern. |
mach_port_request_notification | 1 (core.c:5445) | Hard | Dead-name notifications; needs kernel mailbox infra. |
mach_port_move_member | 2 (runtime.c:714, 722) | Hard | Port-set membership; kernel port-set object. |
mach_port_get_set_status | 1 (runtime.c:487) | Hard | Enumerate port-set members. |
mach_port_set_mscount | 1 (runtime.c:621) | Hard | No-senders-notification suppression. |
mach_port_get_attributes / _set_attributes | 6 (TEMPOWNER, LIMITS, RECEIVE_STATUS) | Hard | Per-port kernel state. |
task_set_exception_ports | 1 (core.c:6458) | Hard | EXC_CRASH / EXC_GUARD routing. |
host_set_exception_ports | 1 (core.c:6468) | Hard | Host-wide; PID-1 only, defers naturally. |
host_statistics HOST_VM_INFO | 1 (runtime.c:1273) | Hard | Memory introspection; can be stubbed to 0. |
fileport_makeport / _makefd | 3 (core.c:11657, 11717, libvproc.c:1040) | Stub | Already stubbed in libxpc; ENOSYS degrades gracefully. |
vproc_transaction_* | declared, internal counters only | Stub | Already stubbed in libxpc. |
The audit trailer is mandatory. runtime.c:999-1000 casts the post-message trailer area to mach_msg_audit_trailer_t* and calls audit_token_to_au32() to extract caller {euid, egid, uid, gid, pid, asid}. core.c access-control checks at lines ~9033-9050 gate check_in and register calls on these fields. If we don't materialize a real audit trailer in mach.ko, every cross-task lookup would either crash on uninitialized memory or accept the request with caller PID 0 — effectively root.
This is the single biggest kernel-side work item that must happen before launchd-842 can supervise non-trivial workloads. The trailer types are already defined in our <mach/message.h>; what's missing is the trailer-write path inside mach.ko's receive routine. Scoped at one new file in src/mach_kmod/ referencing the calling thread's td_ucred at message-receive time.
ipc_kmsg_make_trailer path.mach_port_request_notification). Without this, launchd can't tell when a supervised process's reply port goes away — supervision degrades to polling. Scope: kernel mailbox queue per requesting port, dead-port event delivery.mach_port_move_member, _get_set_status). Launchd's runtime.c demand-dispatch uses a port set to batch-poll. Without it, we route every receive through its own kqueue…which is actually how libdispatch already works on FreeBSD. May be possible to skip by routing each Mach receive through its own dispatch_source; needs validation.Good news: launchd-842 has a clean single-gate split. The whole codebase keys off one boolean, pid1_magic, set by getpid() == 1 at runtime.c:1387. There is no separate init/ heritage subdirectory; the legacy BSD init code Apple inherited got fully absorbed into the core path long before launchd-842.
There is also a built-in non-PID-1 mode: per-user launchd. When pid1_magic == false, the code runs the per-user path: socket in /tmp/launchd-<pid>.XXXXXX/, per-user database under /private/var/db/launchd.db/com.apple.launchd.peruser.<uid>, idle-exit timer enabled, no console output, no audit-session initialization. This is the natural shape for our daemon-mode port.
/dev/console (launchd.c:177-182) and routes log lines to it.AU_SESSION_FLAG_IS_INITIAL (launchd.c:469-487).update_thread that calls sync() every 30 s (launchd.c:298-309).host_set_exception_ports for jobs that declare LAUNCH_JOBKEY_MACH_HOSTEXCEPTIONPORT (core.c:6466-6468).kern.bootargs sysctl for verbose-boot flags (runtime.c:1421)./etc/rc.deferred_install if present at shutdown (core.c:11860-11908).kill -TERM on every PID not in the supervised set (core.c:6710-6718).launchd.c:227-228).VPROCMGR_SESSION_SYSTEM instead of VPROCMGR_SESSION_BACKGROUND.All of these are isolated behind pid1_magic gates. For Phase I2 we override pid1_magic = false unconditionally (or run as non-root and let the natural test fire), turning every gated branch off automatically. No surgery in core.c is needed — the codebase already knows how to be a non-PID-1 launchd.
launchctl is a single 4,549-line file at support/launchctl.c. It dispatches 25 subcommands via a command table at launchctl.c:228-265. The control protocol uses liblaunch's launch_msg() over a Unix socket at /var/run/launchd/sock — not Mach. Plist parsing is via CoreFoundation's CFPropertyList; the CF2launch_data converter at launchctl.c:2001-2066 walks the CF tree and rebuilds it as launch_data_t.
| Subcommand | IPC mechanism | Notes |
|---|---|---|
help | none | Prints table. Phase I1 smoke marker. |
list | vproc_swap_complex(VPROC_GSK_ALLJOBS) | Empty dict if no jobs. Phase I2 first IPC test. |
load / unload | launch_msg + LAUNCH_KEY_SUBMITJOB | Requires plist parsing. |
start / stop / remove | launch_msg + LAUNCH_KEY_STARTJOB / STOPJOB / REMOVEJOB | String payload (job label). |
setenv / getenv / export / unsetenv | vproc layer + LAUNCH_KEY_SETUSERENVIRONMENT | Global env. Phase I2 later. |
limit / umask / log | vproc + LAUNCH_KEY_GET/SETRESOURCELIMITS | Resource controls. |
shutdown / singleuser | launch_msg + LAUNCH_KEY_SHUTDOWN | Defer. |
bsexec / bslist / bstree | Mach bootstrap subset machinery | Defer to post-PID-1 phase. |
The launchd-side handlers live in ipc.c via ipc_readmsg2() at lines 360-457. Each handler is small (3-10 LOC) and dispatches to core.c functions for the actual work.
Implication for plist parsing — resolved. The "hand-roll a minimal XML-plist parser" path floated in an earlier draft of this plan was abandoned. We vendor swift-corelibs-foundation's CoreFoundation at src/libCoreFoundation/ built standalone (non-Swift refcount path) and link it as /usr/lib/system/libCoreFoundation.so.6. launchctl calls CFPropertyListCreateFromStream and the local CFPropertyListCreateFromFile wrapper as-shipped. The CF runtime needed ICU for its grapheme / locale / timezone surface; we vendor Apple's swift-foundation-icu at src/swift-foundation-icu/ with a .incbin restructure of icu_packaged_data.cpp to keep the compile within the 8 GB CI VM. Total cost: roughly the same effort as the hand-rolled parser would have been, but every future Apple-source consumer (configd, IPConfiguration, mDNSResponder) inherits a working CF surface instead of needing the same workaround.
Done Four parallel research passes through launchd-842; gap analysis above.
Goal: launchd + launchctl binaries compile, link cleanly against our stack, and execute the no-side-effect CLI paths.
launchd-842 into freebsd-launchd-mach/src/launchd/ (src/, liblaunch/, support/); SystemStarter/ skipped.bootstrap_cmds. Installs /usr/bin/mig + /usr/libexec/migcom. MIG-BUILD-OK marker fires.src/launchd/freebsd-shims/: TargetConditionals.h (TARGET_OS_MAC/OSX flipped to 0 for CF consumers), asl.h, libinfo.h, libproc.h, libproc_internal.h, libkern/, os/, spawn_private.h, quarantine.h, util.h, _simple.h, AvailabilityMacros.h, bsm/, plus launchctl-specific shims IOKit/IOKitLib.h, NSSystemDirectories.h, mach-o/getsect.h, dns_sd.h, bootfiles.h, and the force-included launchctl_freebsd_compat.h compat header.src/launchd/src/Makefile + src/launchd/support/Makefile. Install paths follow the project's no-/usr/local rule + Apple's shipping layout: /sbin/launchd (matches Apple) and /bin/launchctl (matches Apple). PID-1 promotion is a separate Phase I3 concern; the binary at /sbin/launchd is the same whether it's started by init or runs as init.LAUNCHD-BUILD-OK — /sbin/launchd 235 KB ELF, runs zero-IPC code paths cleanly.
LAUNCHCTL-BUILD-OK — /bin/launchctl 79 KB ELF, ldd verifies all libsystem deps resolve. Runtime launchctl help deferred (mach.ko hang — §7).
The CF + ICU vendoring work that landed during I1 is reusable by every future Apple-source daemon (configd, IPConfiguration, mDNSResponder, notifyd, asl, DiskArbitration). The cost of the swift-corelibs CF + swift-foundation-icu pair is paid once; consumers inherit a working CF runtime + plist parser + locale surface.
Goal: launchd runs in daemon mode (per-user codepath, pid1_magic == false), loads plists via launchctl load, supervises real processes. One test daemon per feature; failures map cleanly.
| Feature | Marker | Test plist | What it proves |
|---|---|---|---|
| Basic plist parse + spawn | LAUNCHD-SPAWN-OK | RunAtLoad+ProgramArguments writing to file | plist loader, fork/exec |
| KeepAlive supervision | LAUNCHD-KEEPALIVE-OK | Binary exits after 2 s, KeepAlive=true | respawn loop |
| StartInterval timer | LAUNCHD-INTERVAL-OK | StartInterval=5 timer + log file | kqueue timer dispatch |
| WatchPaths | LAUNCHD-WATCH-OK | Touches a file, launchd spawns reactor | kqueue vnode events |
| Sockets (inetd-style) | LAUNCHD-SOCKETS-OK | Echo daemon on TCP, fd via launch_activate_socket | socket listener + fd inheritance |
| Stdout/StderrPath | LAUNCHD-STDIO-OK | Writes to redirected files | fd setup pre-exec |
launchctl list | LAUNCHCTL-LIST-OK | — | read-only IPC roundtrip |
launchctl load / unload / start / stop | LAUNCHCTL-CTRL-OK | One plist exercises all four | write IPC + state mgmt |
dispatch_source and skip mach_port_move_member? If yes, drop port-set work; if no, kernel port-set object (~400 LOC).mach_port_request_notification NOTIFY_DEAD_NAME) — or accept supervision-via-kqueue and stub the trap. If we stub, launchd loses some prompt cleanup but a periodic reaper covers it.Everything else from the gap table can stay stubbed for I2.
Hold here. Discuss whether to graduate to PID 1, how to coordinate with FreeBSD's existing init(8) and rc.d handoff, whether to keep freebsd-launchd as a fallback for non-Mach builds.
Out of this plan's scope; flag the work without committing to it. Likely items: ISO build swaps init to launchd, console / audit-session / exception-port paths come back online, single-user mode integration, shutdown-stray-process sweep, host exception port wiring, kern.bootargs verbose-boot handling. Many of these reactivate when the same code runs as PID 1; the gates are already in place.
Apple's bootstrap_cmds ported to FreeBSD; /usr/bin/mig + /usr/libexec/migcom install during the build. The pre-generated-output and hand-roll fallbacks were not needed. MIG-BUILD-OK marker fires on the boot smoke. The investment amortizes across every future Apple-source daemon that ships .defs files (configd, notifyd, mDNSResponder).
Known kernel bug, discovered 2026-05-16 during LAUNCHCTL-BUILD-OK runtime invocation. When userland calls mach_msg() with a SEND descriptor whose remote port is MACH_PORT_NULL (port name 0), mach.ko's ipc_kmsg.c logs "ipc_entry_lookup failed on 0" at line 1318 but mach_msg(2) does NOT return MACH_SEND_INVALID_DEST. Userland blocks indefinitely.
The trigger is launchctl-842's main() calling vproc_swap_integer(NULL, VPROC_GSK_IS_MANAGED, NULL, &is_managed) at the very top of main(). Without launchd running as PID 1, bootstrap_port is MACH_PORT_NULL; the MIG-generated vproc_mig_swap_integer client stub does mach_msg(SEND | RCV, ...) — hangs.
Fix: early-return in mach.ko's ipc_kmsg_get_from_kernel_send / ipc_kmsg_send path when the destination port is MACH_PORT_NULL. Reference: XNU's ipc_kmsg_send() in osfmk/ipc/ipc_kmsg.c returns MACH_SEND_INVALID_DEST for this case.
Blocking impact: every launchctl runtime path. Phase I2 markers (LAUNCHCTL-LIST-OK, LAUNCHCTL-CTRL-OK) all hit this. Audit-trailer materialization can land in parallel; the null-port early-return is independent.
The audit-trailer + dead-name + port-set work remains for Phase I2 sign-off. The null-port hang above is a fourth item, smallest of the four (~20 LOC change). Budget: 1-2 weeks of focused mach.ko work. Prior phases (libdispatch RECV backend) landed two latent mach.ko bugs along the way; expect similar.
For the KeepAlive test the supervised binary is trivial. For the Sockets test we need Apple's launch_activate_socket API — part of liblaunch which is vendored, so it comes for free.
liblaunch's libvproc.c compiles against our IPC layer and is in /usr/lib/system/liblaunch.so.1. The vproc_mig_* client stubs were the gap — Apple ships protocol_vproc.defs that MIG-generates them; launchd-842 doesn't include that .defs file (the file in our tree is protocol_jobmgr.defs from a pre-launchd-842 era, with a different routine set and importing nonexistent bootstrap_public.h). We work around it by relying on job.defs's userprefix vproc_mig_; + overlapping routine set, and linking jobUser.c into liblaunch.so so the symbols resolve at dlopen time. Caveat: a handful of vproc_mig_* calls invoke routines that job.defs doesn't carry — those will return ENOSYS-like errors when actually exercised. Phase I2 may need to add a real protocol_vproc.defs (or augment job.defs) for the full surface.
From OS X 10.10 onward Apple folded launchd's job-management into closed-source libxpc. launchd-842 predates that split, which is exactly why we can use it. The downside is that some "modern" Apple-source daemons assume modern launchd behavior we don't have (e.g., XPC service activation). Not our problem for Phase I2; flag for the configd / asl / mDNSResponder follow-on phases.
apple-oss-distributions/launchd at tag launchd-842.92.1 (2014-08-13). 28.5k LOC, last open source. Apache 2.0 license.apple-oss-distributions/bootstrap_cmds — ships MIG. Apple Public Source License.freebsd-launchd-mach-plan — the underlying mach.ko + Mach-IPC stack this plan builds on.freebsd-libxpc-plan — the libxpc port, where Phase 5 references this launchd-842 import.freebsd-launchd-plan — the minimal scratch-rewrite launchd we ship today; kept as fallback for non-Mach builds.freebsd-libxpc-foundation-spike — the CoreFoundation / Foundation analysis for configd / asl / mDNSResponder./Users/jmaloney/Documents/launchd/launchd-842/ (verbatim Apple), /Users/jmaloney/Documents/launchd/nextbsd/sbin/launchd/ (NextBSD port reference), /Users/jmaloney/Documents/launchd/ravynos/sbin/launchd/ (ravynOS port reference).Drafted 2026-05-13 from four parallel agent passes over the verbatim launchd-842.92.1 tree. Findings are file:line-cited throughout; the underlying agent reports live in the project's session transcript.