freebsd-launchd-842 — porting Apple's launchd-842.92.1

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

Contents

  1. Starting point — what's in the stack today
  2. Source map — what's in launchd-842
  3. Mach / IPC surface — gap analysis
  4. PID-1 vs daemon-mode split
  5. launchctl + control-protocol surface
  6. Phase plan — I1, I2, checkpoint, I3
  7. Risks & open questions
  8. References

1. Starting point — what's in the stack today

Phase I1 is complete at freebsd-launchd-mach commit be8f444 (2026-05-16). The stack provides:

ComponentStatusInstall path
mach.koport-management traps, special-port traps, multiplexer slot 219, audit-trailer types definedkernel module
libsystem_kernel.somach_msg, mach_port_*, task_*_special_port, host_set_special_port/usr/lib/system/
libdispatch.sofull type system + Mach RECV polling backend/usr/lib/system/
libxpc.so.4type system round-trips; dictionary IPC round-trip green/usr/lib/system/
bootstrap_server daemonstandalone, 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.1launch_data_t API, vproc_*, bootstrap_*; bundles jobUser.c MIG stubs so consumers other than launchd itself dlopen cleanly/usr/lib/system/
/sbin/launchd235 KB ELF; execs + rejects non-PID-1 invocation with proper error/sbin/ (Apple-canonical)
libicucore / lib_FoundationICU.soApple'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.6swift-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/launchctl79 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 markers14 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:

2. Source map — what's in launchd-842

Apple's launchd-842.92.1 at apple-oss-distributions/launchd, tagged 2014-08-13. Total ~28,500 LOC of C across five directories.

DirectoryLOCProducesNotes
src/16,285launchd binarycore.c is 12,126 lines on its own. 7 MIG .defs files.
liblaunch/4,448liblaunch.dylibliblaunch.c + libvproc.c + libbootstrap.c. Public launch.h / bootstrap.h / vproc.h.
support/4,836launchctl + launchproxy + wait4pathlaunchctl.c alone is 1,847 lines (single-file CLI with 25 subcommands).
SystemStarter/1,939SystemStarter binaryLegacy startup-items runner; safe to skip entirely for our port.
man/, rc/doc + rc scriptsNo compiled code.

MIG (Mach Interface Generator) inventory

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.

FileSubsystemRoleGenerated headers consumed by
job.defs400vproc_mig_* client + job_mig_* servercore.c:126, runtime.c:77
protocol_jobmgr.defs400same wire as job.defs + ServerAuditToken trailersame demux table
internal.defs137000kernel → launchd kqueue notificationruntime.c:65
helper.defs4241011UserEventAgent downcalllibvproc.c
job_reply.defsinternal MIG reply marshalinginternal
job_forward.defsinternal MIG forward marshalinginternal
job_types.defstype definitions for the aboveshared

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.

Closed-source link dependencies

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 libFreeBSD replacement
libdyldld-elf rtld (no shim needed)
libcompiler_rtFreeBSD ships libcompiler_rt; usable directly
libsystem_kernelOur existing /usr/lib/system/libsystem_kernel.so
libsystem_platformcovered by FreeBSD libc
libsystem_pthreadFreeBSD libthr (-lpthread)
libsystem_mallocjemalloc in FreeBSD libc
libsystem_cFreeBSD libc
libquarantineStub macOS Gatekeeper attr-tracking; no-op shim is fine
libdispatchOur 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.

3. Mach / IPC surface — gap analysis

What Mach symbols does launchd-842 actually call, and how does that map against our coverage today?

SymbolCall sitesCoverageNotes
mach_msg~10+HaveCore dispatch at runtime.c:1022-1029.
mach_task_self, mach_host_self14HavePort-name queries.
mach_port_allocate / _deallocate / _insert_right~10HavePhase F shipped these.
task_get_special_port / _set_2HavePhase G prereq shipped these.
bootstrap_check_in / _look_up40+ MIG refsHaveHand-rolled subsystem 400 wire format matches.
host_reboot4 (core.c:4212, 4221, 7277)TrivialFits multiplexer slot 219 pattern.
mach_port_request_notification1 (core.c:5445)HardDead-name notifications; needs kernel mailbox infra.
mach_port_move_member2 (runtime.c:714, 722)HardPort-set membership; kernel port-set object.
mach_port_get_set_status1 (runtime.c:487)HardEnumerate port-set members.
mach_port_set_mscount1 (runtime.c:621)HardNo-senders-notification suppression.
mach_port_get_attributes / _set_attributes6 (TEMPOWNER, LIMITS, RECEIVE_STATUS)HardPer-port kernel state.
task_set_exception_ports1 (core.c:6458)HardEXC_CRASH / EXC_GUARD routing.
host_set_exception_ports1 (core.c:6468)HardHost-wide; PID-1 only, defers naturally.
host_statistics HOST_VM_INFO1 (runtime.c:1273)HardMemory introspection; can be stubbed to 0.
fileport_makeport / _makefd3 (core.c:11657, 11717, libvproc.c:1040)StubAlready stubbed in libxpc; ENOSYS degrades gracefully.
vproc_transaction_*declared, internal counters onlyStubAlready stubbed in libxpc.

Audit-trailer requirement

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.

The five hardest kernel-side items, ranked

  1. Audit-trailer materialization. Without this, no security-conscious operation works. Scope: ~150 LOC in mach.ko's ipc_kmsg_make_trailer path.
  2. Dead-name notifications (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.
  3. Port sets (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.
  4. Port attributes (TEMPOWNER, LIMITS, RECEIVE_STATUS). Used for queue-depth tuning and ownership transfer at exec. Scope: per-port kernel state additions.
  5. Exception ports. EXC_CRASH / EXC_GUARD / EXC_RESOURCE delivery. Without these, launchd can't intercept process crashes. Can defer entirely until we have a real crash-reporter daemon.

4. PID-1 vs daemon-mode split

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.

What PID-1 mode does that daemon mode doesn't

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.

5. launchctl + control-protocol surface

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/socknot 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.

SubcommandIPC mechanismNotes
helpnonePrints table. Phase I1 smoke marker.
listvproc_swap_complex(VPROC_GSK_ALLJOBS)Empty dict if no jobs. Phase I2 first IPC test.
load / unloadlaunch_msg + LAUNCH_KEY_SUBMITJOBRequires plist parsing.
start / stop / removelaunch_msg + LAUNCH_KEY_STARTJOB / STOPJOB / REMOVEJOBString payload (job label).
setenv / getenv / export / unsetenvvproc layer + LAUNCH_KEY_SETUSERENVIRONMENTGlobal env. Phase I2 later.
limit / umask / logvproc + LAUNCH_KEY_GET/SETRESOURCELIMITSResource controls.
shutdown / singleuserlaunch_msg + LAUNCH_KEY_SHUTDOWNDefer.
bsexec / bslist / bstreeMach bootstrap subset machineryDefer 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.

6. Phase plan

Phase I0 — research (this document)

Done Four parallel research passes through launchd-842; gap analysis above.

Phase I1 — build + exec only Done

Goal: launchd + launchctl binaries compile, link cleanly against our stack, and execute the no-side-effect CLI paths.

Tasks (all complete)

  1. Done Vendored launchd-842 into freebsd-launchd-mach/src/launchd/ (src/, liblaunch/, support/); SystemStarter/ skipped.
  2. Done MIG strategy: ported Apple's bootstrap_cmds. Installs /usr/bin/mig + /usr/libexec/migcom. MIG-BUILD-OK marker fires.
  3. Done FreeBSD shims at 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.
  4. Done Build at 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.
  5. Done Smoke markers: LAUNCHD-BUILD-OK (launchd execs + rejects non-PID-1) and LAUNCHCTL-BUILD-OK (launchctl exists + ldd resolves all libsystem deps including libCoreFoundation, lib_FoundationICU, liblaunch). Runtime invocation of launchctl is gated on the §7 mach.ko fix.

Smoke markers (CI-green)

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

Side benefits shipped during I1

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.

Phase I2 — core functionality (multiple test daemons)

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 scope — one test plist + one marker per feature

FeatureMarkerTest plistWhat it proves
Basic plist parse + spawnLAUNCHD-SPAWN-OKRunAtLoad+ProgramArguments writing to fileplist loader, fork/exec
KeepAlive supervisionLAUNCHD-KEEPALIVE-OKBinary exits after 2 s, KeepAlive=truerespawn loop
StartInterval timerLAUNCHD-INTERVAL-OKStartInterval=5 timer + log filekqueue timer dispatch
WatchPathsLAUNCHD-WATCH-OKTouches a file, launchd spawns reactorkqueue vnode events
Sockets (inetd-style)LAUNCHD-SOCKETS-OKEcho daemon on TCP, fd via launch_activate_socketsocket listener + fd inheritance
Stdout/StderrPathLAUNCHD-STDIO-OKWrites to redirected filesfd setup pre-exec
launchctl listLAUNCHCTL-LIST-OKread-only IPC roundtrip
launchctl load / unload / start / stopLAUNCHCTL-CTRL-OKOne plist exercises all fourwrite IPC + state mgmt

Required kernel work before I2

Everything else from the gap table can stay stubbed for I2.

Checkpoint — user sign-off before PID 1

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.

Phase I3 — PID 1 (deferred, scoped after checkpoint)

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.

7. Risks & open questions

MIG on FreeBSD — resolved Done

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

mach.ko hangs on send to MACH_PORT_NULL — blocks Phase I2 runtime smoke

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.

Kernel work creep

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.

Shape of "supervised binary that exits"

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.

vproc_* surface — resolved (with caveat)

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.

Apple's launchd-842 is pre-libxpc-split

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.

8. References

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.