Working notes on the cross-service ordering problem on freebsd-launchd — what makes varrun → cron, syslogd-first, dhcpcd-after-kmodloader work declaratively rather than by alphabetical-filename luck. netconfigd is one downstream consumer of the same primitives; the doc walks both layers.
The central topic of this document is cross-service ordering on freebsd-launchd. The launchd port today recognizes 6 plist keys (Label, Program, ProgramArguments, Disabled, RunAtLoad, KeepAlive) and orders services by alphabetical filename scan. That works coincidentally on the live ISO but breaks the moment any real cross-service dependency has to be expressed:
cron wants /var/run to exist before writing its PID file. varrun sorts after cron alphabetically. Saved today only by base.txz pre-populating /var/run.syslogd sorts after cron / dhcpcd / kmodloader; their early log lines go to the kernel ring buffer instead of /var/log/messages.dhcpcd sorts before kmodloader, but kmodloader is what loads the NIC drivers. dhcpcd survives only because it watches PF_ROUTE for late-arriving interfaces.Solving this is the foundation work covered in the recommendation (Section 10) and the plist-key roadmap (Section 11). It's mechanical engineering inside core.m: grow the plist key vocabulary — WatchPaths, Sockets, KeepAlive subkeys, LaunchEvents — so dependencies can be declared instead of inferred from filename order.
netconfigd is one downstream consumer of those same primitives. It's the most-discussed consumer because the gershwin-on-freebsd variant has imported Apple's configd source (configd/src/, version 963.270.3) and a build script (make-configd.sh) intended to produce a daemon called netconfigd. Whether that import actually ships, and in what form, is a real architectural fork (Sections 5–7) — but it sits on top of the ordering work, not parallel to it.
The hard problem inside the netconfigd-specific question: configd is built end-to-end on Mach IPC. Every plugin talks to the core via Mach messages; every client (scutil, SCDynamicStore consumers) reaches the daemon via the bootstrap server's Mach port lookup; the modern XPC layer sits on top of Mach. FreeBSD has no Mach. So you can compile configd, but you can't run it usefully without doing real work first.
That framework manages, on macOS:
IPConfiguration plugin)This document walks through both layers: the ordering work in the launchd port (Sections 4, 10, 11) and the three netconfigd paths that consume those primitives (Sections 5–7), with cross-references between them.
If you're confident on what Mach and DO are, skip ahead to section 3. The other terms — MIG, XPC, bootstrap, AF_UNIX framed messages — are easier to evaluate options against if they're not abstract.
Mach is the kernel's inter-process communication primitive on macOS (and on Hurd, NeXTSTEP, and a few research kernels). It's not a library; it's built into the kernel, like FreeBSD's pipes or sockets. Two programs talk by sending each other messages — typed, structured data — through Mach ports, which are kernel-managed mailboxes.
Concretely, a Mach message exchange looks like:
// Sender
mach_msg_header_t msg;
msg.msgh_remote_port = some_port; // who to send to
msg.msgh_local_port = reply_port; // where reply comes back
msg.msgh_id = 1234; // operation code
mach_msg(&msg, MACH_SEND_MSG, sizeof(msg), 0,
MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// Receiver, on the other end of some_port:
mach_msg(&msg, MACH_RCV_MSG, 0, sizeof(msg),
my_recv_port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
Important properties that distinguish Mach from "just sockets":
EVFILT_MACHPORT, etc.Key takeaway: Mach is a kernel feature. "Porting Mach" means either implementing it as a userspace library (slow, semantics partial) or putting it in the FreeBSD kernel (large, invasive).
Writing raw mach_msg calls is tedious and error-prone, so Apple ships a code generator: MIG. You write an interface definition (a .defs file) that looks like an RPC schema:
// SystemConfiguration's actual MIG interface (excerpt from configd):
subsystem config 20000;
routine configopen(
server : mach_port_t;
name : xmlData;
options: xmlData;
out session : mach_port_t;
out status : int);
routine configget(
server : mach_port_t;
key : xmlData;
out value : xmlDataOut;
out status : int);
MIG generates two C files: a client stub (configopen() as a function the caller can use) and a server dispatcher (a switch on msgh_id that calls into your handler). Configd's IPC contract is defined as MIG, and configd's source includes generated stubs for config.defs, shared_dns_info.defs, helper.defs, and several others.
Key takeaway: MIG is just a code generator on top of Mach. If you replace Mach, you replace MIG too — every config_user.c / configMIGServer.c stub becomes hand-written transport code.
XPC is Apple's modern higher-level IPC API, introduced in Mac OS X 10.7 (2011). It hides Mach behind a friendlier interface:
// XPC client (in configd's nwi/network_information.c):
xpc_connection_t conn = xpc_connection_create_mach_service(
"com.apple.SystemConfiguration.configd",
queue,
XPC_CONNECTION_MACH_SERVICE_PRIVILEGED);
xpc_object_t request = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_uint64(request, "cmd", NWI_GET_STATE);
xpc_connection_send_message_with_reply(conn, request, queue,
^(xpc_object_t reply) {
// process reply asynchronously
});
XPC adds:
Crucially, XPC still uses Mach as transport. xpc_connection_create_mach_service() literally looks up a Mach port via the bootstrap server. Without Mach, XPC has no wire.
Key takeaway: XPC and Mach are not separable in Apple's stack. Anything that uses XPC is using Mach underneath.
MachServicesHow does a client know which Mach port to send to? It asks the bootstrap server, a registry that maps human-readable names like com.apple.SystemConfiguration.configd to the kernel port object.
On Apple, the bootstrap server is part of launchd itself. When a daemon's plist contains:
<key>MachServices</key>
<dict>
<key>com.apple.SystemConfiguration.configd</key>
<true/>
</dict>
...launchd does three things:
com.apple.SystemConfiguration.configd in its bootstrap registry, attaching a Mach port.bootstrap_look_up() with that name, launchd hands them a send right to the port.This is the dependency-resolution mechanism we don't have. "Service A depends on service B" on Apple is expressed by service A calling bootstrap_look_up("B"), blocking until launchd has B running.
Key takeaway: MachServices in a plist is a request to launchd's bootstrap server. The freebsd-launchd port doesn't implement bootstrap, so the key is silently ignored.
Unix-domain sockets are FreeBSD's native IPC. They look and act like network sockets (socket(), bind(), connect(), read(), write()), except the address is a filesystem path (/var/run/foo.sock) and the kernel keeps the bytes in memory rather than going over the wire.
// Server
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = { .sun_family = AF_UNIX };
strcpy(addr.sun_path, "/var/run/configd.sock");
bind(s, (struct sockaddr*)&addr, sizeof(addr));
listen(s, 5);
int c = accept(s, NULL, NULL);
read(c, buf, sizeof(buf));
// Client
int c = socket(AF_UNIX, SOCK_STREAM, 0);
connect(c, (struct sockaddr*)&addr, sizeof(addr));
write(c, msg, len);
What you give up versus Mach:
SCM_RIGHTS is the closest analog and is more limited)What you get in exchange: it works on every Unix, has well-understood semantics, doesn't need any kernel changes, and the file system gives you a natural namespace.
"Framed messages", which appears in the freebsd-launchd ipc.c, just means you put a small header (length + opcode) on top of each message so the receiver knows where one ends and the next begins. The launchd port already does this:
// from freebsd-launchd src/src/ipc_proto.c (sketch)
struct msg_hdr {
uint32_t length;
uint32_t cmd;
} __packed;
// Client sends: write(s, &hdr, sizeof hdr); write(s, payload, hdr.length);
// Server reads: read(s, &hdr, sizeof hdr); read(s, payload, hdr.length);
That's the pattern Option 1 would extend to configd: replace MIG-generated mach_msg calls with framed AF_UNIX writes, replace XPC dictionary passing with serialized blobs over the same socket.
DO is a Foundation-level remote-object system. Your daemon vends an Objective-C object; another process gets a transparent proxy and calls methods on it as if it were local.
// Server (the daemon)
@interface NCNetworkStore : NSObject
- (NSString *)valueForKey:(NSString *)key;
- (void)setValue:(NSString *)v forKey:(NSString *)k;
@end
NCNetworkStore *store = [[NCNetworkStore alloc] init];
NSConnection *conn = [NSConnection serviceConnectionWithName:@"NCNetworkStore"
rootObject:store];
// Process now serves DO requests on a runloop.
// Client (some other process)
id<NCNetworkStoreProto> proxy =
(id)[NSConnection rootProxyForConnectionWithRegisteredName:@"NCNetworkStore"
host:nil];
NSString *v = [proxy valueForKey:@"State:/Network/Interface/wlan0/IPv4"];
Underneath, DO uses:
NSMessagePort = AF_UNIX local sockets, NSSocketPort = TCP)org.gnustep.gdomap.plist)What you get:
What you don't get:
mach_msg stub)Key takeaway: DO is the "Objective-C native" alternative. It pairs well with new code; pairing it with imported Apple C source is awkward.
Three pieces of imported Apple source matter here:
| What | Version | State |
|---|---|---|
src/ — launchd |
launchd-842.1.4 |
Daemon rewritten: Mach RPC server replaced with AF_UNIX framed messages (per ipc.c:11-13).
Recognizes 5 plist keys: Label, Program, ProgramArguments, RunAtLoad, KeepAlive.
No MachServices, no Sockets, no WatchPaths.
liblaunch/liblaunch.c (Apple client lib with Mach hooks) is preserved as source but not built or linked.
|
configd/src/ — configd |
configd-963.270.3 |
Imported as-is. Still depends on Mach + MIG + XPC throughout. Includes plugin tree (IPMonitor, IPConfiguration, KernelEventMonitor, etc.), SC framework headers, scutil, IPMonitorControl, nwi.
Build script make-configd.sh exists; daemon would be installed as netconfigd.
Not currently shipped on the live ISO.
|
| GNUstep base | upstream |
In the system domain at /System/Library/.
gdomap runs as org.gnustep.gdomap.plist on Gershwin (per gershwin-on-freebsd repo).
DO infrastructure is operational.
|
So: a launchd that doesn't speak Mach, a configd that only speaks Mach, and a parallel DO infrastructure that's already running for other reasons.
This section is the foundation for evaluating the three options realistically. The question "how does service A wait for service B?" is exactly the question Mach was designed to answer for Apple, and it's the question this launchd port currently handles only by luck.
The complete set, in alphabetical filename order (which is the order the launchd port scans them at boot):
| # | Plist label | What it does | Lifecycle |
|---|---|---|---|
| 1 | org.freebsd.cron |
Run scheduled tasks from /etc/crontab, /var/cron/tabs/. Self-daemonizes (parent forks + exits, child detaches). |
RunAtLoad, KeepAlive=false, cron -s |
| 2 | org.freebsd.dhcpcd |
DHCP v4 + v6, RA/SLAAC processing, RTM_NEWLINK hot-plug. Master mode covers all interfaces; recognizes bridge/lagg/vlan layering. Supervises wpa_supplicant for wifi interfaces via its hook. | RunAtLoad, KeepAlive=true, dhcpcd -B (foreground) |
| 3 | org.freebsd.dmesg.boot |
Save kernel ring buffer to /var/log/dmesg.boot. Self-mitigates by mkdir -p /var/log first. |
RunAtLoad once, exits |
| 4 | org.freebsd.getty.console |
Spawn getty on /dev/ttyu0 (serial console). |
RunAtLoad, KeepAlive=true (respawn after each session) |
| 5 | org.freebsd.getty.vty0 |
Spawn getty on /dev/ttyv0 (VGA console). Wrapper sleeps 24h if device is absent (serial-only hardware). |
RunAtLoad, KeepAlive=true |
| 6 | org.freebsd.kmodloader |
Run devmatch(8), kldload missing drivers for unattached devices in the live device tree. Plus PCI-vendor scan for the radeonkms-vs-amdgpu split. |
RunAtLoad once, exits |
| 7 | org.freebsd.sysctl |
Apply /etc/sysctl.conf. No-op on a default live ISO (file is comment-only). |
RunAtLoad once, exits |
| 8 | org.freebsd.syslogd |
Drain kernel /dev/klog + Unix socket /var/run/log to /var/log/messages. |
RunAtLoad, KeepAlive=true, syslogd -F -ss |
| 9 | org.freebsd.varrun |
Defensive one-shot: mkdir /var/run /var/log /var/spool /var/empty, sticky /tmp, touch the standard log files. Replaces conventional rc.d/var + rc.d/cleanvar. |
RunAtLoad once, exits |
Three mechanisms, in decreasing order of importance:
/System/Library/LaunchDaemons/ in readdir() order, which on UFS is filename-sorted. That's the only ordering primitive the daemon recognizes. org.freebsd.cron.plist < org.freebsd.dhcpcd.plist < ... < org.freebsd.varrun.plist in lexicographic order.
dmesg.boot's ProgramArguments includes /bin/mkdir -p /var/log exactly because varrun sorts after it. The plist comment notes this explicitly: "Self-sufficient is safer than relying on ordering."
/var/run, /var/log, /var/spool all exist in the unionfs lower because base.txz populated them. varrun creating them again is a no-op on the live ISO. On a system where /var were a fresh tmpfs, the ordering would matter and it would be wrong.
That is the entire ordering toolkit. MachServices, WatchPaths, KeepAlive's state-predicate subkeys (OtherJobEnabled, PathState, SuccessfulExit), LaunchEvents — none of those are parsed by the daemon. Plists may contain them; they're silently dropped (recall core.m:258-266 recognizes only six keys: Label, Program, ProgramArguments, Disabled, RunAtLoad, KeepAlive — all booleans or simple strings/arrays, no nested structure).
| Hypothetical "want" | What actually happens today |
|---|---|
cron wants /var/run/cron.pid; varrun should create /var/run first. |
cron sorts before varrun (c < v). On the live ISO this works only because /var/run already exists in base.txz. On a cleaner rootfs, cron's PID-file write would fail. |
syslogd wants to write /var/log/messages; varrun touches it. |
varrun runs LAST. syslogd starts first and uses an existing file (saved by base.txz pre-population). On a fresh-/var system, syslogd would get a write error. |
| dhcpcd should run after kmodloader so NIC drivers are loaded first. | dhcpcd sorts before kmodloader (d < k). dhcpcd survives because it's in master mode and watches PF_ROUTE for late-arriving interfaces. If it weren't, it would miss the NICs that kmodloader brings up moments later. |
| kmodloader's log lines should reach syslogd. | syslogd sorts after kmodloader (k < s). kmodloader's early stderr/syslog calls go to the kernel ring buffer or are dropped. They eventually appear in /var/log/messages because syslogd drains /dev/klog at startup, but they aren't real-time. |
| Any service wants config from a hypothetical netconfigd. | No mechanism. Phase 2's netconfigd would have to either be RunAtLoad and assume clients sort after it alphabetically, or implement its own readiness signaling (sentinel file? socket-connect retry loop?). This is exactly what Mach activation solves on Apple. |
The current state is "works because the test environment is forgiving." It is not a robust ordering model.
Apple's launchd has one core ordering mechanism — launch-on-demand activation via the bootstrap server — plus a few satellite mechanisms (Section 4.6) that handle cases Mach can't.
Here is the complete flow for "daemon B advertises a Mach service; daemon A consumes it":
<key>Label</key>
<string>com.apple.configd</string>
<key>ProgramArguments</key>
<array>
<string>/usr/libexec/configd</string>
</array>
<key>MachServices</key>
<dict>
<key>com.apple.SystemConfiguration.configd</key>
<true/>
</dict>
<!-- Note: NO RunAtLoad. Launch-on-demand is the default with MachServices. -->
Launchd reads B's plist, creates a Mach receive port, and stores the mapping "com.apple.SystemConfiguration.configd" → port in the bootstrap namespace. Daemon B itself is not running yet. Launchd holds the receive right; nobody is listening on it.
// In A's source:
mach_port_t configd_port;
kern_return_t kr = bootstrap_look_up(bootstrap_port,
"com.apple.SystemConfiguration.configd",
&configd_port);
if (kr != BOOTSTRAP_SUCCESS) { /* error */ }
// A now has a send right to B's port.
This single call has profound consequences:
ProgramArguments right now, then proceeds.configd_port queue in the kernel until B is ready. A doesn't have to know whether B is running yet.// In B's startup, after exec'ing /usr/libexec/configd:
mach_port_t my_recv_port;
bootstrap_check_in(bootstrap_port,
"com.apple.SystemConfiguration.configd",
&my_recv_port);
// B can now mach_msg(MACH_RCV) on my_recv_port and process A's messages.
The ordering between A and B is now implicit in the IPC contract. There is no "A starts before B" or "B starts before A" configuration anywhere. The actual rule is:
B is started exactly when somebody first wants to talk to it. Until then, B doesn't run. Once it's running, anyone who needs to talk to it can.
This is dependency injection at the OS level. Compared to alphabetical filename ordering: the current scheme requires every plist filename to rank correctly against every other, and breaks silently when a new service is added. Mach activation does away with the ordering question entirely — you don't order, you express what you want, and the bootstrap server resolves it.
You asked specifically: "How do we say varrun starts before cron with Mach? Do we modify cron?" The honest answer is: Mach activation doesn't actually solve this case directly. Walking through each plausible approach:
For Mach activation to apply, varrun would need to expose a Mach service. Cron would call bootstrap_look_up("com.gershwin.varrun"). Launchd would start varrun on demand.
This doesn't fit because varrun is a one-shot: it creates directories and exits. There is no daemon to register a Mach port, and no IPC for cron to do once it has a port. You'd have to redesign varrun as a long-running daemon that exposes some pointless query interface, just to give cron something to look up. Wrong tool.
Cron source modification: yes, you'd have to add the bootstrap_look_up call. But that's an ugly way to express "wait for the filesystem to be ready."
Apple's launchd has a KeepAlive dictionary with conditional subkeys. The relevant one here:
<!-- Cron's plist would add: -->
<key>KeepAlive</key>
<dict>
<key>OtherJobEnabled</key>
<dict>
<key>org.freebsd.varrun</key>
<true/>
</dict>
</dict>
This says: "only keep cron loaded while varrun's job has been enabled and successfully run." It's an explicit ordering at the launchd level. cron source is not modified.
Caveat: this key is not parsed by the freebsd-launchd port today. Adding support for it is a launchd port enhancement, separate from any Mach work.
<!-- Cron's plist would add: -->
<key>WatchPaths</key>
<array>
<string>/var/run</string>
</array>
Cron is launched when /var/run exists. cron source is not modified. Same caveat: not parsed by this launchd port.
Change cron's ProgramArguments to /bin/sh -c "/bin/mkdir -p /var/run; exec /usr/sbin/cron -s". No ordering needed; cron self-mitigates the way dmesg.boot already does (which has the comment: "Self-sufficient is safer than relying on ordering"). Ugly but works today.
| Scenario | Modify cron? | Why |
|---|---|---|
| "varrun before cron" — filesystem-state dependency. | No. | Right tools are launchd-side: KeepAlive { OtherJobEnabled }, WatchPaths. Mach is wrong tool for this case. Pragmatic workaround: self-mitigate in the plist (Approach D). |
| "cron consults a config service" — IPC dependency. | Yes. | Cron source calls bootstrap_look_up("com.gershwin.configservice"). The lookup blocks until the service is up. This is where Mach activation shines. |
The takeaway: Mach activation is the right answer for IPC-shaped dependencies between long-running daemons. It is the wrong answer for filesystem-state, exit-status, or boot-phase dependencies. Apple's launchd has separate keys for those, all currently absent in the freebsd-launchd port.
To complete the picture, the keys Apple's launchd recognizes for non-IPC dependencies. None of these is parsed by the freebsd-launchd port today.
| Key | What it does | Use case |
|---|---|---|
WatchPaths |
Start the daemon when any of these paths appear or change. | Wait for a directory to exist; pickup of dropped-in config files. |
QueueDirectories |
Like WatchPaths, but only fires when a directory becomes non-empty. | Spool processors, mail queues, log shippers. |
KeepAlive { OtherJobEnabled = { ... } } |
Run only while another job is loaded. | "cron only after varrun"; daemon dependency chains. |
KeepAlive { SuccessfulExit = false } |
Respawn unless the previous instance exited cleanly. | Crash-loop recovery without infinite respawn after intentional exit. |
KeepAlive { PathState = { ... } } |
Run only while a path exists / doesn't exist. | Conditional services tied to filesystem state. |
KeepAlive { NetworkState = ... } |
Run only when network is up. | Network-dependent services (less granular than IPC dep). |
LaunchEvents |
Activate on a notify(3) event. | Generic event-driven activation; configd publishes events for many things. |
Sockets |
Have launchd open the socket(s); fire daemon when a connection arrives. Daemon inherits the listening fd. | Socket-activation pattern; xinetd-style. syslogd's /var/run/log would be the natural example here. |
LimitLoadToSessionType |
Only load in matching sessions (Aqua, Background, LoginWindow). | Distinguishing GUI-session daemons from system daemons. |
Apple's launchd has a rich vocabulary for expressing dependencies. Mach activation is just one mechanism — the most powerful for IPC — and these others fill the gaps for non-IPC cases. Phase 2 of the launchd port should plan to grow at least WatchPaths, OtherJobEnabled, and Sockets support, alongside whatever IPC story is chosen.
MachServices in plists remains a no-op.bootstrap_look_up() instead call connect() on a known socket path (e.g. /var/run/configd.sock) with a retry loop. That retry loop is the AF_UNIX equivalent of waiting for the bootstrap port to be ready.WatchPaths / OtherJobEnabled / etc. in core.m — a separate launchd-port enhancement, not part of the configd work.MachServices with launch-on-demand activation.[NSConnection rootProxyForConnectionWithRegisteredName:host:] is the Foundation equivalent of bootstrap_look_up(). Calling it blocks until the name is registered in gdomap.[NSConnection serviceConnectionWithName:rootObject:]) is the equivalent of bootstrap_check_in().NCDaemon's clients), DO gives you the same model Mach gives Apple, just with NSPort instead of mach_port and gdomap instead of the bootstrap server.core.m beyond the current 6 (see section 11 for the full inventory). That's a separate work item from the netconfigd question — but it's adjacent, and Phase 2 should plan to tackle both. The varrun ↔ cron ordering is the canonical example of the non-Mach half of the problem.
Recommended Effort: weeks – months
Apply the same surgery to configd that was already applied to launchd. Specifically:
config_user.c, configMIGServer.c, shared_dns_infoUser.c, etc.) with hand-written client/server pairs over AF_UNIX. Same shape: a function call on the client becomes a framed write; the server dispatches by opcode.nwi/network_information_server.c, libSystemConfiguration/*) with a small wrapper library that imitates the XPC API surface but transports over the same AF_UNIX socket.bootstrap_look_up() with connect() to a well-known socket path (e.g. /var/run/configd.sock).vm_copy for large messages, send_once rights, port-right transfer. Most of configd doesn't use these — they're hot paths in IOKit and the BSD kernel layer, not the SC stack.Plugins/IPMonitor, Plugins/IPConfiguration, Plugins/KernelEventMonitor, etc. They're loaded into the daemon as bundles and talk to each other via the dynamic store, which is configd's own thing (not Mach-dependent).SCDynamicStore C client API. Code that uses SCDynamicStoreCreate(), SCDynamicStoreSetValue(), SCDynamicStoreCopyKeyList() keeps working — only the underlying transport changes.scutil as a command-line interface to the store.NetworkConfiguration.plist, etc.) — this is just XML.mach_msg(...) call site, every MIG stub, every xpc_* call.msgh_id, you write your own dispatcher reading framed messages from accept()'d sockets.SO_PEERCRED / getpeereid() on the Unix socket.SCM_RIGHTS or redesign.Hard to be precise without scoping each plugin individually, but the launchd port (a smaller project — one daemon, one IPC interface) was a multi-month effort. Configd has on the order of 8–12 MIG interfaces, plus XPC sites in the framework client library, plus the helper privilege-escalation bridge. Realistic estimate: 2–4 months of focused work to a working netconfigd + IPMonitor + IPConfiguration that can DHCP a wired interface. Wireless and IPv6 add more. Polish to match dhcpcd's current behavior is more on top.
Maximalist Effort: months – years
Make FreeBSD speak Mach. Three sub-paths exist, each with prior art:
mach_msg, ports, port rights, message queues entirely as a userspace library on top of (probably) AF_UNIX. The library would have to manage port-name allocation, port-right reference counting, message buffering — all the things the kernel does on Darwin. This is what Darling does (the macOS compatibility layer for Linux), via a kernel module that emulates Mach syscalls.
libports, libpager, etc.). In principle reusable. In practice, Hurd Mach has its own dialect and is not a drop-in for Apple's xnu Mach.
All three approaches assume you're willing to ship something that runs unmodified Apple source. That's the entire point of the option.
Userspace Mach with enough fidelity to run configd: 6–12 months minimum, with a person familiar with both Mach internals and FreeBSD kernel/userspace boundaries. Kernel-module path: 1–2 years, plus ongoing FreeBSD-version maintenance. Either way, configd doesn't ship for the duration of the Mach project.
Smallest scope Effort: weeks
Don't port configd. Treat the imported configd source as a reference, and write a fresh Objective-C daemon — call it NCDaemon — that fulfills the same role using GNUstep idioms.
Sketch of the architecture:
// NCDaemon.h
@protocol NCNetworkStoreProto
- (NSDictionary *)copyValueForKey:(NSString *)key;
- (void)setValue:(NSDictionary *)value forKey:(NSString *)key;
- (NSArray *)copyKeyList;
- (void)addObserver:(id<NCNetworkObserverProto>)observer
forKey:(NSString *)key;
@end
@interface NCNetworkStore : NSObject <NCNetworkStoreProto>
@end
// In NCDaemon's main:
NCNetworkStore *store = [[NCNetworkStore alloc] init];
NSConnection *conn = [NSConnection serviceConnectionWithName:@"NCNetworkStore"
rootObject:store];
[[NSRunLoop currentRunLoop] run];
// Plugin equivalents are NSBundle-loaded classes, e.g.:
// /System/Library/NCBundles/IPMonitor.bundle
// /System/Library/NCBundles/IPConfiguration.bundle
// Each registers with the NCNetworkStore on load and posts state changes.
Clients (including a thin SCDynamicStore-shaped C wrapper, if needed for Apple-source porting) call:
// Client side (in some other process)
id<NCNetworkStoreProto> store =
(id)[NSConnection rootProxyForConnectionWithRegisteredName:@"NCNetworkStore"
host:nil];
NSDictionary *ifaceState =
[store copyValueForKey:@"State:/Network/Interface/wlan0/IPv4"];
NCNetworkStore. Service ordering becomes "block on NSConnection rootProxyForConnectionWithRegisteredName: until gdomap publishes the name." That's the dependency-resolution primitive launchd is missing.Plugins/IPMonitor for what behavior to implement, but you write the implementation. Same for IPConfiguration (DHCP/RA/SLAAC), KernelEventMonitor (kernel notifications), etc.NCDaemon + DHCP plugin is on the order of weeks, not months.rootProxyForConnectionWithRegisteredName: blocks until the name is registered. That's exactly what was missing.SCDynamicStore's exact wire protocol or notification semantics), you'd be retrofitting.If DO is the IPC, then gdomap is the equivalent of Mach's bootstrap server. Both are name registries that have to be running before any IPC name can be looked up or registered. This creates a chicken-and-egg problem at boot: gdomap must run before any DO-using service.
Apple solved the structural twin of this problem by making the bootstrap server part of launchd PID 1. There is no "launchd starts before bootstrap" race because they are the same process. GNUstep separated them — gdomap is its own daemon — which means we have a real ordering question to answer.
Four approaches, organized by what they actually decide:
MachServices key behavior). Layers on top of either B or D. Doesn't compete with them.The strategic question for B vs D: Is launchd PID 1 on Gershwin structurally Apple's launchd (which IS the bootstrap server), or is it FreeBSD's PID 1 augmented with a GNUstep-shaped bootstrap dependency expressed as a child process?
Both work. They embody different commitments:
Neither is "correct" in the abstract — the choice depends on whether Gershwin commits to the Apple structural model at the PID 1 level or stays GNUstep-process-shaped.
The crude answer that works with the existing 6-key launchd parser. Rename gdomap's plist to sort before any DO-using service, or rename DO services to sort after gdomap. Currently org.gnustep.gdomap.plist sorts after org.freebsd.* (g > f), so all freebsd services run first. Two ways to flip the order:
com.gnustep.gdomap.plist (c < f), or aa.gnustep.gdomap.plist (a < everything). Breaks Apple's reverse-DNS convention for plist labels.org.gnustep.gdomap: e.g., put it under the org.gnustep. prefix as well, so alphabetical ordering puts gdomap first within its group. org.gnustep.NCDaemon.plist sorts after org.gnustep.gdomap.plist (d < N? no — uppercase 'N' is < lowercase 'g' in ASCII; need to be careful with case).Pros Zero code changes; works today.
Cons Brittle. Cross-vendor alphabetical dependencies fall over the moment a new service is added or labeled inconveniently. Acceptable as a Phase 2 stopgap, awkward as a long-term answer.
The smallest code change that makes the dependency robust. Treat gdomap the way Apple treats the bootstrap server — not as "just another plist," but as a launchd-internal dependency. launchd's main(), right after setting up its own state and before scanning /System/Library/LaunchDaemons/, spawns gdomap directly:
// In launchd's main(), src/src/launchd.c
int
main(int argc, char *argv[])
{
// ... existing init: signals, log, IPC socket, etc. ...
// Bootstrap dependency: gdomap must be running before any DO
// service can register its name or any client can resolve one.
// Apple integrates the bootstrap server directly into launchd
// PID 1. We approximate that by spawning gdomap as our first
// child before scanning any plist.
pid_t gdomap_pid;
char *gdomap_argv[] = { "/usr/bin/gdomap", "-f", NULL };
if (posix_spawn(&gdomap_pid, gdomap_argv[0], NULL, NULL,
gdomap_argv, environ) == 0) {
wait_for_gdomap_listening(); // brief connect-retry to its UDP port
}
// ... existing plist scan and event loop ...
}
The principle: gdomap is a launchd-internal dependency, not a regular service. Apple does the same thing structurally — the bootstrap server is part of launchd itself, so it's "started" by virtue of launchd starting. We emulate that without merging the code: launchd's first action after self-init is to ensure gdomap is up. org.gnustep.gdomap.plist is no longer needed in LaunchDaemons/; gdomap's lifecycle becomes launchd's responsibility.
Refinements:
Pros Small launchd change (~30–50 lines). No new plist keys to parse. No upstream gdomap changes. Solves the bootstrap race robustly. Feasible inside Phase 2 without delaying netconfigd work.
Cons Still doesn't give launch-on-demand for DO services — daemons that vend DO services have to RunAtLoad, or clients have to retry until the registration appears. For full Apple-style activation semantics you still need Approach C below. Also: gdomap's lifecycle becomes implicit/hardcoded rather than declared.
This is the natural Phase 2 stepping stone: ship Approach B early to make the bootstrap dependency robust, then plan toward Approach C as a follow-on launchd-port enhancement. Approach C is unblocked by B (or builds on top of it).
DOServices key (full launch-on-demand)Add a new plist key analogous to Apple's MachServices. The semantics:
<key>Label</key>
<string>org.gershwin.NCDaemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/libexec/NCDaemon</string>
</array>
<key>DOServices</key>
<array>
<string>NCNetworkStore</string>
</array>
<!-- No RunAtLoad: launch-on-demand, just like MachServices on Apple. -->
What launchd does with this:
DOServices entries are noted but the daemon isn't started.[NSConnection rootProxyForConnectionWithRegisteredName:@"NCNetworkStore" host:nil], the lookup hits gdomap. If gdomap doesn't have a registration for the name, it signals launchd (via a hook into gdomap's lookup-miss path, or via a launchd-side proxy that intercepts misses) and launchd execs the daemon. The daemon registers via standard [NSConnection serviceConnectionWithName:rootObject:] on startup; gdomap publishes the name; the client's lookup completes.This is the GNUstep-native equivalent of Mach launch-on-demand activation. The user-facing developer experience is identical to Apple's: declare the service in your plist, register the name in your daemon's main(), and the lookup-side magic is launchd's responsibility.
Pros Clean, matches Apple's idiom for the GNUstep stack, handles dependency resolution transparently for every future DO daemon. One-time engineering cost, ongoing zero per-daemon cost.
Cons Requires DOServices parsing in core.m (small) plus a hook into gdomap's lookup-miss path (gdomap upstream change, or a launchd-side wrapper). Needs design work and possibly upstream coordination with the GNUstep project.
The architecturally cleanest option, mirroring Apple's "launchd IS the bootstrap server" decision. Embed gdomap's name-registration and lookup logic as part of the launchd PID 1 process. There is no separate gdomap daemon; clients still talk gdomap protocol, but the registry lives in launchd's address space.
Pros
DOServices-style on-demand activation needs no IPC between gdomap and launchd; they're the same process. The lookup-miss hook from Option B becomes an in-process function call.Cons
The gdomap-before-X dependency is real but bounded. For Phase 2 you can ship Option A (filename ordering) immediately and plan toward Option B as a launchd port enhancement. Option C is the long-term ideal if Gershwin commits to GNUstep-native architecture across the board.
Importantly, this dependency exists for any DO-based system on Gershwin, not just netconfigd. The moment a Gershwin daemon adopts DO for IPC, every future DO client of that daemon inherits the same gdomap-must-be-up requirement. That's an argument for solving it well early (Option B or C) rather than re-litigating it per daemon.
An NCDaemon + a DHCP-equivalent plugin that handles a wired Ethernet end-to-end: 3–6 weeks. Adding a wireless plugin (wlan-clone creation + wpa_supplicant coordination + association events) on top: another 2–4 weeks. IPv6 RA/SLAAC: another 2–3 weeks. Polish to match dhcpcd's current behavior: 2–3 weeks. Total to feature-parity-with-current-ISO: ~3 months, with the upside that each piece ships incrementally.
Add for the gdomap bootstrap solution depending on choice:
| Dimension | Option 1: AF_UNIX rewrite | Option 2: Port Mach | Option 3: DO daemon |
|---|---|---|---|
| Effort | 2–4 months | 6–24 months | ~3 months |
| Reuses configd source | Yes, substantially | Yes, fully | No (reference only) |
| Apple source compat | Apple source with patches | Apple source unmodified | Reference only |
| SCDynamicStore C API | Yes, native | Yes, native | Wrapper layer (extra work) |
| Future Apple-source ports | Same patches, re-applied | Free | Per-component decision |
| Mach dependency | Removed | Solved | Not present |
| Plugin model | Apple's bundles, in C | Apple's bundles, in C | NSBundles, in Objective-C |
| Idiomatic to Gershwin | Mixed (C/Objc, Apple style) | Mixed (C/Objc, Apple style) | Yes (Foundation/GNUstep) |
| Service-ordering primitive | Build it (socket connect) | Free (bootstrap_look_up) | Free (DO name registration) |
| Maintenance burden | Per-version transport patches | Mach implementation, forever | Whole API surface, forever |
| Risk profile | Moderate — bounded scope | High — open-ended Mach edge cases | Low–moderate — familiar tools |
| Strategic message | "Apple's code, ported" | "FreeBSD speaks Mach" | "GNUstep-native config layer" |
The right choice depends on what Gershwin is trying to be. Three different strategic positions, three different right answers:
| Strategic goal | Right option | Why |
|---|---|---|
| "Gershwin runs Apple's frameworks, ported to FreeBSD where necessary" | Option 1 | Maximizes reuse of the configd source you already imported, follows the launchd-port precedent, ships in a knowable timeframe. |
| "Gershwin is FreeBSD with Apple's runtime stack on top" | Option 2 | Pays the Mach cost once. Every future Apple component runs unmodified. Multi-year project; needs explicit strategic commitment. |
| "Gershwin is a GNUstep-centric desktop on FreeBSD, drawing inspiration from macOS" | Option 3 | Use GNUstep idioms top to bottom. Apple source is a reference for behavior, not an asset to ship. Ships fastest, fits the existing GNUstep infrastructure. |
The clearest signal in the existing tree is that Option 1 has already been chosen for launchd. The launchd port took Apple's source, ripped out Mach, replaced it with AF_UNIX framed messages, and kept the rest. Choosing Option 1 for configd is consistent with that precedent.
The second-clearest signal is that configd source was imported deliberately — that's a non-trivial commitment of bytes and license-tracking. Option 3 makes that import nearly worthless.
The configd-IPC question (Option 1 / 2 / 3) is not the central Phase 2 work item. It looks central because the doc has been organized around it, but the actual bottleneck is a level deeper.
The central work item is growing launchd's plist-key vocabulary.
Until launchd can express dependencies declaratively — WatchPaths for filesystem state, Sockets for activation, OtherJobEnabled for job-state, KeepAlive subkeys for refined supervision — every cross-service ordering question gets answered with "alphabetical filename + lucky timing + self-defensive scripts." That's true for the configd choice today, and it is also true for varrun ↔ cron, syslogd-first-or-lose-logs, and dhcpcd-after-kmodloader. The same engineering work that unblocks netconfigd ALSO unblocks every other current and future ordering question. Doing it inside an option-specific path means redoing it for the next problem.
Add the plist keys that make dependency resolution declarative. Suggested priority order:
WatchPaths — kqueue EVFILT_VNODE on each path; fires the daemon when a watched path appears or changes.
Solves: varrun → cron (cron's plist watches /var/run), the broad class of "wait for a file to exist before running." Wide utility, low complexity. Probably the first key to land.
Sockets activation — launchd bind()s and listen()s the socket itself, holds the listen fd, exec's the daemon when a connection arrives, hands the listen fd to the daemon (Apple's pattern uses env var; systemd's $LISTEN_FDS protocol works the same way).
Solves: syslogd-before-X (clients connect to /var/run/log; launchd starts syslogd lazily, no log-loss window because writes block until syslogd inherits the fd). Also unblocks the netconfigd IPC story regardless of which option is chosen — socket activation gives the right ordering for any AF_UNIX-based service.
KeepAlive subkeys: OtherJobEnabled, SuccessfulExit, PathState, NetworkState.
Solves: explicit job-to-job and job-to-state dependencies. "Don't run cron unless varrun has loaded successfully" becomes a one-line plist key. Less work than they sound — they're predicates the launchd run loop already needs to evaluate per-job.
Sockets-style activation under a well-known path — already in scope from item 2. If GNUstep-native (Option 3), this is the DOServices key from section 7.5 plus the gdomap-bootstrap decision. The shape is downstream of 2b; the work is real either way.
LaunchEvents (notify(3) / devd-event activation) — lower priority but the right primitive for hardware-readiness ordering: dhcpcd's plist subscribes to a "NIC attached" event rather than racing kmodloader. Pairs well with kmodloader's planned Phase 2 hot-plug shape.
Effort: 4–8 weeks. Independent of every other Phase 2 decision. Pays for itself by fixing existing brittleness (varrun, syslogd, dhcpcd ordering) AND being the prerequisite for netconfigd.
This is also the "doesn't suck like systemd" work. The keys above are scoped, optional, and orthogonal — each one solves one ordering problem cleanly, none of them require launchd to ingest the responsibilities of other daemons. Process supervision + activation primitives + dependency predicates is the right job for an init system. DHCP, DNS, journaling, and everything else stays in their own daemons.
Once Phase 2a is in place, the Option 1 / 2 / 3 choice has a different feel: ordering is no longer the decisive concern, because launchd has the primitives. The choice becomes about strategic identity:
| Lean | Path | Why |
|---|---|---|
| Apple-faithful | Option 1 (AF_UNIX rewrite of configd) | Reuses imported source, follows the launchd-port precedent, ships in 2–4 months from the start of 2b. |
| GNUstep-native | Option 3 (NCDaemon in Foundation/DO) | Idiomatic GNUstep code, smaller scope (~3 months from start of 2b), configd source becomes reference. The gdomap-bootstrap question (B vs D in 7.5) becomes the next architectural choice. |
| Maximalist | Option 2 (port Mach to FreeBSD) | Justified only if Gershwin's broader plan has multiple Apple-source consumers needing Mach. 6–24 months. Strategic for Apple-source compounding; over-built if the only client is configd. |
By the time 2a ships, you'll have months of additional thinking about which 2b path fits the project's identity. The decision doesn't have to be made now. None of 2a's work is wasted in any of the three branches.
With 2a in place, the rest fall out as small daemons that fire on the right events:
LaunchEvents on devd / kqueue device events to run ifconfig wlanN create wlandev <dev> when raw 802.11 transports attach. No more "kmodloader → ??? → dhcpcd" gap.The previous version of this section recommended Option 1 first — that was solving the configd-specific problem and leaving the rest of the cross-service ordering brittleness untouched. That's backwards. The same engineering work in launchd that unblocks netconfigd ALSO unblocks varrun↔cron, syslogd-first, dhcpcd-after-kmodloader, and every future ordering question. Doing 2a first is the leverage point. Doing it inside an option-specific path means redoing it for the next problem.
The strategic lean (Apple-faithful vs GNUstep-native) is a real choice the project has to make, but it's a choice 2a doesn't force. That's the whole point: the launchd capability work is identity-neutral foundation, and the netconfigd path becomes a downstream decision once the foundation is solid.
Cross-referencing Apple's complete LAUNCH_JOBKEY_* namespace (preserved upstream in src/liblaunch/launch.h — ~95 keys) against what the freebsd-launchd port's core.m actually parses today.
| Key | What it does |
|---|---|
Label | Identifier; must match plist filename's reverse-DNS prefix. |
Program | Path to executable (absolute). |
ProgramArguments | argv array. |
Disabled | Boolean; if true, plist is parsed but the job isn't loaded. |
RunAtLoad | Boolean; start the job at launchd startup (alphabetical scan order). |
KeepAlive | Boolean only — the dictionary form (OtherJobEnabled, SuccessfulExit, etc.) is not parsed. |
Everything below is in the upstream namespace but ignored by the daemon. Grouped by tier of importance for a Gershwin / FreeBSD-init use case.
The set of keys that, once landed, make freebsd-launchd a credible "doesn't-suck-like-systemd" FreeBSD init. Most are small; together they fix every current ordering problem.
| Key | What it does | Why we need it | Effort |
|---|---|---|---|
EnvironmentVariables |
Set env vars for the spawned process. | Already flagged in org.freebsd.varrun.plist's comment: we use absolute paths because PATH isn't set. |
Trivial. Parse dict, build envp for posix_spawn. |
UserName / GroupName |
Drop privileges before exec. | Required for any non-root daemon. Currently every plist runs as root. | Small. getpwnam/getgrnam, setgid/setuid. |
InitGroups |
Boolean: call initgroups(3) before drop? |
Pairs with UserName for full group membership. | Trivial. |
WorkingDirectory |
chdir() before exec. |
Standard daemon need. | Trivial. |
RootDirectory |
chroot() before exec. |
Privilege isolation for sensitive daemons. | Trivial. |
Umask |
Set umask before exec. | File-creation security baseline. | Trivial. |
StandardInPath / StandardOutPath / StandardErrorPath |
Redirect stdio to files. | Per-daemon log routing without shell wrappers. | Small. |
WatchPaths |
kqueue EVFILT_VNODE on each path; fires the daemon when one changes. |
Fixes filesystem-state ordering (varrun→cron). Broad utility for "wait for X file." | Medium. dispatch_source per path; integrate with run loop. |
Sockets |
launchd opens the listen socket itself, exec's daemon on first connection, hands the listen fd to the daemon via env (Apple's $LAUNCH_DAEMON_SOCKET_NAME; systemd's $LISTEN_FDS is the same idea). |
Fixes syslogd-first / cold-start daemons. Foundation primitive for AF_UNIX-based service activation. Unblocks the netconfigd IPC path regardless of which option is chosen. | Medium-large. bind/listen logic, fd inheritance, multiple sockets per job, both AF_INET and AF_UNIX shapes. |
KeepAlive { OtherJobEnabled } |
Run only while another named job is loaded. | Explicit job-to-job dependencies (cron after varrun). | Small. Predicate evaluator addition. |
KeepAlive { SuccessfulExit } |
Respawn unless prior instance exited 0. | Crash-loop recovery without infinite respawn after intentional exit. | Small. |
KeepAlive { PathState } |
Run only while a path exists / doesn't exist. | Conditional services tied to filesystem state. | Small. |
KeepAlive { NetworkState } |
Run only when network is up. | Network-dependent services. Could supersede some of dhcpcd-after-X gymnastics if the launchd-side primitive exists. | Small-medium. Needs link-state monitor source. |
KeepAlive { Crashed } |
Respawn only on abnormal exit. | Symmetric of SuccessfulExit. |
Trivial after SuccessfulExit. |
KeepAlive { AfterInitialDemand } |
Don't respawn until the service has been demanded once. | Cold-start activation guard for socket-activated services. | Small. |
LaunchEvents |
Activate on a generic event (Apple uses notify(3); FreeBSD analog would be devd events or EVFILT_USER). |
devd-event-driven activation: wlan-clone fires on "iwlwifi attached"; hot-plug story. | Medium. Hook into devd or notify-equivalent. |
ExitTimeOut |
Seconds between SIGTERM and SIGKILL on shutdown. | Graceful shutdown for daemons that need to flush. | Trivial. |
ThrottleInterval |
Minimum seconds between successive launches. | Anti-respawn-loop protection. Apple's default is 10s. | Trivial. |
| Key | What it does | Use case |
|---|---|---|
StartInterval | Run periodically every N seconds. | Per-daemon "every 5 minutes" without invoking cron. |
StartCalendarInterval | Run at calendar times (Minute / Hour / Day / Month / Weekday). | Per-daemon cron-like scheduling without cron. Could eventually retire org.freebsd.cron for system-managed jobs. |
QueueDirectories | Like WatchPaths, but fires only when dir is non-empty. | Spool processors, mail queues, log shippers. |
StartOnMount | Run when any filesystem is newly mounted. | Mount-event handlers (auto-fsck, indexing, etc.). |
HardResourceLimits / SoftResourceLimits | setrlimit wrapping. Subkeys: CPU, Data, FileSize, NumberOfFiles, NumberOfProcesses, Stack, Core, ResidentSetSize, MemoryLock. | Per-daemon resource caps. |
Nice | setpriority(2) before exec. | Background daemons. |
inetdCompatibility | Run as inetd-style: exec per connection, stdio = socket. | Legacy compatibility; small services that don't want their own socket logic. |
LaunchOnlyOnce | Don't relaunch even if a KeepAlive predicate would fire. | First-boot-only services. |
MultipleInstances | Allow concurrent instances (one per Sockets connection). | Connection-per-instance daemons. |
AbandonProcessGroup | Don't track child process group. | Daemons that fork trees and don't want launchd reaping them. |
EnableGlobbing | Glob-expand argv before exec. | Rare; convenience for some plists. |
BeginTransactionAtShutdown | Mark daemon as in-transaction during shutdown. | Niche; relevant only if Apple's transaction tracking lands. |
These would only land if the corresponding subsystem comes with them. Listing for completeness so the reader knows they're in Apple's namespace and being deliberately not-implemented.
MachServices, MachServiceLookupPolicies, PerJobMachServices, MachExceptionHandler, HopefullyExitsFirst, HopefullyExitsLast, plus the MACH_* special-port keys (HostSpecialPort, TaskSpecialPort, kUNCServer, ExceptionServer, HideUntilCheckIn, ResetAtClose, DrainMessagesOnCrash, EnterKernelDebuggerOnClose, PingEventUpdates).XPCDomain, XPCDomainBootstrapper, ServiceIPC.SandboxProfile, SandboxFlags, SandboxNamed. FreeBSD's CAPSICUM is the analog primitive but a different model — sandbox keys would be replaced by a CAPSICUM-shaped key, not ported verbatim.LimitLoadToSessionType, JoinGUISession, SessionCreate. Only relevant if Gershwin grows session types analogous to Aqua/Background/LoginWindow.LowPriorityIO, LowPriorityBackgroundIO. Apple's IO priority API; FreeBSD has rtprio with a different shape.JetsamMemoryLimit, JetsamPriority, JetsamProperties, JetsamMemoryLimitBackground. Memory-pressure killer; FreeBSD has different OOM mechanisms.EnterKernelDebuggerBeforeKill, WaitForDebugger, DisableASLR, Debug. Useful but Apple-shaped.CFBundleIdentifier, BinaryOrderPreference, BonjourFDs, QuarantineData, SecuritySessionUUID, POSIXSpawnType, Policies, EnableTransactions, TransactionCount, EventMonitor, ShutdownMonitor.EmbeddedHomeScreen, EmbeddedMainThreadPriority, EmbeddedPrivilegeDispensation.LimitLoadToHardware, LimitLoadFromHardware, LimitLoadToHosts, LimitLoadFromHosts, Disabled_MachineType, Disabled_ModelName. Niche; could land if a specific use case appears.OnDemand (superseded by KeepAlive in 10.4), IgnoreProcessGroupAtShutdown.LastExitStatus, PID, TransactionCount, AuditSessionID. Surfaced by launchctl list / launchctl print but never appear in plists as user input.Within Phase 2a (Section 10's foundation), the prerequisite chain suggests this order:
EnvironmentVariables — already flagged as missing in shipping plists; small win first.UserName, GroupName, InitGroups, WorkingDirectory, RootDirectory, Umask, Standard*Path. None depend on each other; lands in one push.WatchPaths — first event-driven activation. Fixes varrun → cron and similar. Establishes the dispatch_source-per-job pattern for everything that follows.KeepAlive subkeys — predicate-based supervision. Builds on the existing boolean KeepAlive; reuses much of the existing keep-alive logic.Sockets activation — bigger lift but unblocks the syslogd-first story and is a prerequisite for the Apple-faithful netconfigd path. Touches IPC fundamentals.LaunchEvents — devd integration. Enables wlan-clone, hot-plug, and similar event-driven daemons. Pairs naturally with kmodloader's planned Phase 2 long-running shape.StartInterval / StartCalendarInterval are the next logical group (could even retire org.freebsd.cron for system-managed periodic jobs once landed), then resource limits, then the rest.ExitTimeOut / ThrottleInterval — supervision polish; small additions whenever convenient.By the end of Tier 1 the freebsd-launchd port has roughly Apple's launchd-842 minus Mach — combined with the AF_UNIX surgery already done on ipc.c, that's a credible foundation for any of Section 10's Phase 2b paths AND a coherent answer to "what does a non-systemd FreeBSD init look like."
libdispatch usage tangles with Mach internally? On Apple, dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, ...) is a common pattern for receiving Mach messages on a queue. Replacements exist (DISPATCH_SOURCE_TYPE_READ on a socket fd) but each call site needs translation.vm_copy / out-of-line memory transfer get used? Probably rarely — configd messages are small — but worth grepping.SO_PEERCRED on FreeBSD gives you PID/UID/GID. The signing identity is gone — if any configd code-path depends on it, you're redesigning, not transporting.configd/src/ becomes ambiguous baggage — not actively shipped, not deleted, license obligations still tracked. Either commit to it or remove it.This analysis was prepared 2026-05-10. Source citations: NOTICE (launchd-842.1.4, configd-963.270.3 imports); src/src/ipc.c:11-13 (Mach-rewrite comment); src/src/core.m:258-266 (recognized plist keys); src/Makefile:48-55 (built daemon sources, liblaunch.c excluded); src/liblaunch/liblaunch.c:1008-1037 (dead Mach client code preserved); configd/src/nwi/network_information_server.c (XPC server-side); configd/src/configd.tproj/com.apple.configd.plist (MachServices declaration).