Replace devd's text-parsing autoload + monolithic event fanout with a structured, queryable, Mach-RPC-driven hardware registry (hwregd) inspired by macOS IORegistry — and port Apple's IOKit userland framework as a thin facade on top. Status: draft; synthesized from five parallel research agents on 2026-05-16.
hwregd, a native FreeBSD hardware-registry daemon with an IOKit-shaped Mach-RPC API. Inputs: libdevinfo (newbus tree), /dev/devctl (kernel event stream — same source devd reads), /boot/kernel/linker.hints (PCI/USB→module mapping), /dev/pci + PCIOCGETCONF (enrichment). Output: structured registry tree + Mach watch notifications + automatic kldload. ~9 weeks v1.hwregd, NOT a literal port of the Mach RPC device.defs surface. The CF-flavored API (IOServiceMatching, IORegistryEntryCreateCFProperty, IOServiceAddMatchingNotification) is the cheap half; the kernel mock would be the expensive half. ~6-8 weeks for the "minimum useful" subset (ioreg, system_profiler work).HardwareMatch plist key so device arrival can trigger jobs — replaces devd's text-rule actions with the native launchd dispatch model. ~1.5 weeks.88694f0 so this is a current gap, not a future improvement.HardwareMatch (~1.5 wks) — device-arrival job dispatch via the new Mach-RPC criteria-watch API.ioreg -l works; Apple binaries that query hardware just work.ioreg-shape introspection working before configd means the whole hwregd+IOKit track ships as one cohesive block.hwregd) designHardwareMatch keyFreeBSD ships two device-management components:
devd(8) — text-rule daemon that reads /dev/devctl, parses line-format events (+em0 at ..., ?, !system=ACPI subsystem=...), and matches them against regex rules in /etc/devd/*.conf to fire shell actions.devmatch(8) — structured matcher that walks libdevinfo + parses /boot/kernel/linker.hints's MDT_PNP_INFO records to determine which .ko to kldload for a given device.devmatch is fine — it's already structured-data-driven, MIT-class licensed, and easy to lift. devd is the weak link:
zfs.conf rules (PR 183335).hw.bus.devctl_queue fills before devd connects./var/run/devd.pipe's line format and inherit every drift bug.Apple's IOKit/IORegistry solves these by exposing a structured in-memory tree of device objects, each with a typed property bag, with a Mach-RPC API for query (IOServiceGetMatchingServices) and watch (IOServiceAddMatchingNotification). Multiple clients subscribe independently. Predicates are dictionary-shaped, not regex-shaped. Events deliver as Mach messages with payload, not as text lines to be re-parsed.
Crucially, none of this requires the IOKit kernel. The structure can be synthesized in userland from FreeBSD's existing kernel facilities (libdevinfo for tree, /dev/devctl for events, /dev/pci for enrichment, linker.hints for module matching). What's missing is the userland daemon that maintains the structure and serves the queries — that's hwregd.
A new FreeBSD daemon. IOKit-shaped Mach-RPC API, but the implementation talks to libdevinfo / /dev/devctl / linker.hints / kldload(2) directly. No Apple-source code in the implementation.
Effort: ~9 weeks v1.
Port Apple's IOKitUser (~32k LoC, already at /Users/jmaloney/Documents/launchd/IOKitUser/) as a thin CF wrapper over hwregd's Mach-RPC. IOServiceMatching calls translate to hwreg_lookup. The 27 io_* MIG ops re-bind to our daemon, not to a kernel-resident IOService graph.
Effort: 6-8 weeks for minimum-useful subset.
Add a HardwareMatch plist key. launchd subscribes to hwregd; on device arrival matching the criteria, fires the job. Replaces devd's text-rule actions for the cases that matter (dhclient on link-up, etc.).
Effort: ~1.5 weeks.
Track 1 is the load-bearing piece. Track 2 makes it Apple-API-compatible. Track 3 gives it teeth via launchd. Together they replace devd's role in our stack with a queryable structured registry.
hwregd)| Source | Purpose | FreeBSD mechanism |
|---|---|---|
libdevinfo | Initial newbus snapshot + per-device pnpinfo, driver name, location, flags | devinfo_init(), devinfo_foreach_device_child(); backed by sysctl hw.bus.info + hw.bus.devices.<idx> |
/dev/devctl | Hot-plug events (+attach, -detach, ?nomatch, !notify) | Single-reader cdev exposed by sys/kern/kern_devctl.c; line-format !system=X subsystem=Y type=Z key=value\n |
/boot/kernel/linker.hints | PCI/USB/ACPI ID → module mapping for auto-load | Binary file written by kldxref(8); record types MDT_MODULE, MDT_VERSION, MDT_PNP_INFO per sys/sys/module.h |
/dev/pci + PCIOCGETCONF | PCI enrichment (BARs, class code, subvendor) | struct pci_conf per sys/sys/pciio.h; same path pciconf -lv uses |
| USB ioctls | USB port/speed enrichment | USB_DEVICEINFO, libusb20 |
sysctl hw.acpi.thermal.*, dev.cpu.N.*, hw.acpi.battery.* | Power/thermal property enrichment | Poll @ 5s, push CHANGED events to watchers |
Tree of HWNode records with parent/child edges. Each node carries a typed property bag implemented as xpc_object_t (we already ship libxpc; this keeps the daemon CF-free — an IOKitUser facade adds the CF flavor at the API boundary).
struct hw_node {
uint64_t id; /* registry-unique, monotonic */
uint64_t parent_id; /* 0 = root */
char path[128]; /* e.g. "/pci0/pci0:0:25:0/em0" */
char class_name[64]; /* "PCIDevice", "USBDevice", "CPU", "Disk" */
char bsd_name[32]; /* "em0", "da0", "ugen0.3" or "" */
xpc_object_t props; /* typed property bag */
uint32_t state; /* PROBED|ATTACHED|DETACHED|FAILED */
uint32_t refcnt; /* for stale-handle detection */
};
Example records (props shown as JSON for brevity):
// Intel Gigabit NIC
{ "class":"PCIDevice", "path":"/pci0/pci0:0:25:0/em0",
"vendor_id":0x8086, "device_id":0x153a, "subsystem":"0x20708086",
"class_code":0x020000, "driver":"em", "bsd_name":"em0",
"irq":20, "bars":[{"type":"mem","base":0xf7c00000,"size":0x20000}],
"link_state":"up", "mac":"a4:5e:60:..." }
// USB mass-storage stick
{ "class":"USBDevice", "path":"/pci0/.../uhub0/umass0",
"vendor_id":0x0951, "product_id":0x1666, "speed":"high",
"vendor_str":"Kingston", "product_str":"DataTraveler",
"driver":"umass", "bsd_name":"da1", "serial":"..." }
// CPU
{ "class":"CPU", "path":"/cpus/cpu0",
"vendor":"GenuineIntel", "model":"Core i7-8650U", "core_id":0,
"freq_mhz":1900, "thermal_c":48.5, "p_state":"P0" }
MIG-generated. Subset mirrors IOKitLib's surface so the Track 2 facade is mechanical:
kern_return_t hwreg_get_root(mach_port_t srv, uint64_t *root_id);
kern_return_t hwreg_lookup(mach_port_t srv,
xpc_object_t criteria, /* {"vendor_id":0x8086,...} or {"class":"USBDevice"} */
uint64_t **ids, mach_msg_type_number_t *cnt);
kern_return_t hwreg_get_properties(mach_port_t srv,
uint64_t id, xpc_object_t *props_out);
kern_return_t hwreg_get_children(mach_port_t srv,
uint64_t id, uint64_t **ids, mach_msg_type_number_t *cnt);
kern_return_t hwreg_watch(mach_port_t srv,
xpc_object_t criteria,
uint32_t event_mask, /* ARRIVED|DEPARTED|CHANGED */
mach_port_t notify_port, /* client-supplied send right */
uint64_t *watcher_id);
kern_return_t hwreg_unwatch(mach_port_t srv, uint64_t watcher_id);
kern_return_t hwreg_load_driver(mach_port_t srv,
uint64_t id,
char loaded_module[64],
int32_t *error_code);
kern_return_t hwreg_retain(mach_port_t srv, uint64_t id);
kern_return_t hwreg_release(mach_port_t srv, uint64_t id);
Watch notifications arrive as Mach messages of:
struct hwreg_event {
mach_msg_header_t header;
uint64_t watcher_id;
uint64_t node_id;
uint32_t event; /* ARRIVED|DEPARTED|CHANGED */
};
Clients run a libdispatch dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, ...) on the notify port — our Phase E libdispatch Mach RECV backend already supports this.
| IOKitLib call | hwregd equivalent |
|---|---|
IOServiceMatching("IOPCIDevice") | xpc_dict {"class":"PCIDevice"} |
IOServiceGetMatchingServices() | hwreg_lookup() |
IOServiceAddMatchingNotification() | hwreg_watch(criteria, ARRIVED, port) |
IORegistryEntryCreateCFProperties() | hwreg_get_properties() |
IOObjectRetain/Release() | hwreg_retain/release() |
IOIteratorNext() | Iterate returned id array client-side |
IORegistryGetRootEntry() | hwreg_get_root() |
IORegistryEntryGetChildIterator() | hwreg_get_children() |
+-----------------------------+
boot / hot-plug | hwregd |
---------------- | |
1. /dev/devctl | |
arrival event --> | 2. parse event, build node |
| state=PROBED |
| |
| 3. props.driver == ""? |
| yes -> lookup |
| (vendor:device) in |
| linker.hints cache |
| |
| 4. kldload(<module>) ----+--> kernel
| |
| 5. kernel attaches driver, |
ATTACH event <------ | emits ATTACH on devctl |
| |
| 6. enrich node from |
| libdevinfo + sysctls; |
| state=ATTACHED |
| |
| 7. fire matching watchers |
| (incl. launchd's |
| HardwareMatch jobs) |
+-----------------------------+
The matching logic in step 3 is lifted from sbin/devmatch/devmatch.c's search_hints() (~200 LoC), with the printf("kldload %s\n", mod) shellout replaced by a direct kldload(2) syscall. Dependency closure happens kernel-side at linker_load_dependencies() (sys/kern/kern_linker.c:2316-2389), so the daemon emits one syscall per device, not N.
.ko / dep mismatch: kldload returns -1, node moves to state=FAILED, props.load_error="ENOENT"; CHANGED event fires so a UI can surface it. Retried on next kldxref change (we watch the file via kqueue EVFILT_VNODE)./etc/hwregd/priority.conf first, then newbus probe score, then alphabetical.state=DETACHED but stays in the tree until refcnt==0. RPCs against detached nodes return KERN_INVALID_OBJECT (mirrors IOKit's kIOReturnNoDevice). One final DEPARTED event fires for watchers./dev/devctl: set hw.bus.devctl_queue=4096 at daemon startup (kernel default is 1000); buffered events drain on connect. Comment in devaddq(): "We do send data, even when we have no listeners, because we wish to avoid races relating to startup and restart of listening applications."HardwareMatch plist keyNew plist keys for jobs that should fire on hardware events:
<key>HardwareMatch</key>
<array>
<dict>
<key>class</key><string>NetworkInterface</string>
<key>media_type</key><string>ethernet</string>
</dict>
</array>
<key>HardwareMatchAction</key>
<string>StartOnArrival</string> <!-- or KeepAlive, OneShot -->
At boot, launchd opens a Mach connection to hwregd's bootstrap-registered port (com.apple.hwregd), registers hwreg_watch for the union of all HardwareMatch criteria across its plist set. On ARRIVED, launchd treats the event like an OtherJobActive keepalive trigger and spawns the job, exposing HWREG_NODE_ID, HWREG_BSD_NAME, HWREG_DRIVER in the job's environment. On DEPARTED with HardwareMatchAction=KeepAlive, launchd SIGTERMs the job.
Example replacement plist for the devd dhclient.conf rule (notify 0 on IFNET LINK_UP):
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key><string>org.freebsd.dhclient.ethernet</string>
<key>ProgramArguments</key>
<array>
<string>/sbin/dhclient</string>
<string>$HWREG_BSD_NAME</string>
</array>
<key>HardwareMatch</key>
<array>
<dict>
<key>class</key><string>NetworkInterface</string>
<key>media_type</key><string>ethernet</string>
</dict>
</array>
<key>HardwareMatchAction</key><string>StartOnArrival</string>
</dict>
</plist>
(In practice we'd use the configd/IPConfiguration stack rather than shelling out to FreeBSD's dhclient — this is the illustrative shape.)
Apple's IOKitUser repo (apple-oss-distributions/IOKitUser, already vendored at /Users/jmaloney/Documents/launchd/IOKitUser/) is ~32k LoC of CF-flavored facade over Mach RPC routines defined in osfmk/device/device.defs. Every IOKitLib.h call decomposes to 1-2 MIG round-trips to a kernel-resident IOService graph.
Direct port is infeasible because the kernel side doesn't exist for us — we'd have to mock the entire MIG surface anyway. The leverage is instead to re-aim IOKitUser's MIG client at hwregd:
hwregd binary + IOKit-shape Mach API. IORegistryEntry* walk, IOServiceMatching (class match), IOServiceGetMatchingService(s), IORegistryEntryCreateCFProperty/ies. Marker: ioreg -l works against libdevinfo-backed tree. Smoke test asserts ioreg -p IOService -k IOClass returns sensible output.
IOServiceAddMatchingNotification, IOServiceAddInterestNotification, IONotificationPortCreate, IODispatchCalloutFromMessage. Backing: hwreg_watch() + Mach message delivery via libdispatch MACH_RECV source. Marker: ioreg -w 0 -f follow-mode works, prints arrivals as devices appear.
Port IOPMAssertion*, IOPSCopyPowerSourcesInfo/List. Requires a small powerd daemon backing the assertion broker (idle-sleep prevention) and ACPI-backed power source enumeration. Marker: pmset -g prints sensible output.
IOHIDManagerCreate, IOUSBHostInterfaceCreate, audio HAL plumbing. Port only when a consumer (GNUstep app, CoreAudio shim) actually needs it. Each facade gets its own backing FreeBSD subsystem (evdev, libusb, sndio/cuse).
IOServiceOpen / IOConnectCallMethodThe kernel-driver user-client RPC. No clean mapping to FreeBSD without per-driver bridge code. Consumers (GPU drivers, CoreAudio HALs, Bluetooth controllers) would each need a custom per-driver ioctl bridge. Out of scope.
Code-shape example for K1's matching path:
// On the IOKitUser facade side:
io_iterator_t it;
CFDictionaryRef m = IOServiceMatching("IONetworkInterface");
IOServiceGetMatchingServices(kIOMainPortDefault, m, &it);
// Translates to Mach RPC:
// hwreg_lookup(srv,
// xpc_dict_create_with_kv("class", "NetworkInterface"),
// &ids, &cnt);
// hwregd walks libdevinfo for nodes whose dd_drivername matches
// the ifnet driver list (em, igb, re, virtio_net, ...) and returns
// the synthesized node ids.
Loading unmodified Apple kexts on a FreeBSD kernel would require three pillars, each large:
linker_class is pluggable (sys/kern/kern_linker.c) so a link_macho_class can be added without kernel patches in principle — but it must port kxld (Apple's Mach-O kernel linker, ~30k LoC in xnu/libkern/kxld/) and the split-segment relocation model.IOService, IORegistryEntry, IOPCIDevice, IOWorkLoop, IOMemoryDescriptor, plus the full OSMetaClass/OSObject RTTI runtime that drives Apple's OSDynamicCast and IOService matching. Several hundred classes. Even a minimal IOPCIFamily + IONetworkingFamily subset is ~50k LoC of C++ that must compile against FreeBSD's C kernel.IOMalloc/IOFree, IOLockAlloc, IOSleep/IODelay, current_task(), thread_call, Mach VM (vm_map_t is named the same on both kernels but the struct is incompatible). Plus in-kernel Block.h closures — FreeBSD base ships Block.h userland but no in-kernel BlocksRuntime.Effort: 3-5 engineer-years for a credible subset. Reference point: FreeBSD's linuxkpi (sys/compat/linuxkpi/common/) is 30,660 LoC across 47 .c files + 407 headers and has been actively developed for ~10 years to cover a comparable foreign in-kernel ABI; IOKit is arguably wider (C++ RTTI + Mach VM model on top of everything Linux had).
Plus Apple's kernel ABI is unstable across point releases — kexts compiled for 10.15 break on 11.0; we'd have to pin a Darwin version. License: APSL 2.0 (usable, viral within the loader module).
A DEXT is a launchd-managed process that registers as IOUserService, communicates with a kernel-side IOUserServer shim via Mach over an IOUserClient port, and exposes IOServiceDispatchSource/IODispatchQueue on top of libdispatch.
The bottom half is already there: Mach IPC (mach.ko), libxpc, libdispatch's MACH_RECV backend. The top half:
IOUserServer shim — small mach.ko module proxying between an in-kernel newbus device_t and a userspace Mach port. ~3-5k LoC.IOKitUser + a DriverKit.framework-shape API (IOService, IOUserClient, OSAction, IODispatchQueue) on top of libCoreFoundation. ~20-40k LoC, much in IOKitUser already.io_* op table — kernel side needs to implement the 27 MIG ops IOKitLib.c calls. Reconstructible from headers; not vendored locally.Effort: 8-18 months for a working subset that runs a vendor-supplied USB-class DEXT.
Risks: DEXT entitlement/sandbox model intricate (we'd no-op all entitlements); DriverKit framework headers public but no open-source implementation exists (reverse-engineer from headers + libIOKit traffic).
Defer until configd + hwregd + IOKitUser facade are green. At that point, the marginal cost of adding DriverKit is much lower because the substrate exists.
See section 7.
Most of the substrate already exists:
cuse(3) (sys/fs/cuse/cuse.c, 2036 LoC; lib/libcuse/cuse_lib.c, 794 LoC) — userland char devices, today.netmap — userspace networking.The plan: wrap these in IOKit-shape classes. An IOService/IOUserClient userland API that internally calls cuse_alloc_dev/cuse_wait_and_process instead of IOConnectCallMethod. Use the same IOKitPersonalities Info.plist format and IOServiceMatching predicate engine (we already have OSKext.c's plist parser in IOKitUser).
Effort: 4-8 months. Most work is the matching/personality engine + the userland class hierarchy; the kernel substrate exists.
Coexistence with newbus: native — cuse is already a first-class newbus citizen. No conflict possible.
Trade-off: we get the DriverKit API shape but not binary compatibility with Apple's DEXTs. For our user goal ("FreeBSD modernization first, macOS compat second"), this is the right trade-off.
Walkthrough — Realtek USB Ethernet "looks like a DEXT":
usbus0 enumerates → hwregd sees ARRIVAL event.vendor=0x0bda,product=0x8153 against linker.hints → no kernel driver, but matches an IOKitPersonalities entry in com.realtek.AppleRTL8153.driver/Contents/Info.plist.HardwareMatch event; launchd matches the personality and spawns the driver process under a sandboxed UID.cuse_alloc_unit_number() + cuse_dev_create() to expose /dev/cuse0, runs a userland USB transfer loop via libusb, wraps a userland tun(4)/tap(4) for the IP side.This section originally described a two-phase devd→hwregd transition. As of commit 88694f0 (2026-05-16), FreeBSD-devd + FreeBSD-devmatch are not in the runtime rootfs at all. hwregd becomes the sole /dev/devctl reader from day one — no coexistence required, no /var/run/devd.pipe intermediary, no rule-duplication risk. The retained coexistence concern is third-party FreeBSD ports (libudev-devd, libinput, Xorg) that historically consumed /var/run/devd.pipe; hwregd can optionally reproduce that pipe format for back-compat or those consumers can be pushed upstream toward an hwregd backend.
/dev/devctl directlySingle-reader cdev, no contention since devd is absent. Same line-format the kernel has always emitted (+attach, -detach, ?nomatch, !notify system=X subsystem=Y type=Z key=value). Parse byte-by-byte, dispatch to the matching/event-fanout pipeline.
Risk avoided: the original "dhclient started twice" risk doesn't exist because there's no devd dhclient.conf rule firing in parallel.
/var/run/devd.pipe consumerslibudev-devd (used by libinput, used by Xorg) historically reads /var/run/devd.pipe. We don't have those packages installed in the current rootfs, so this is a deferred concern. Two options for when it becomes relevant:
/var/run/devd.pipe's exact line format as a compat output. Cheap, ~50 LoC.Defer until a real consumer (likely Xorg or Wayland-on-FreeBSD when we add graphics) needs it.
| Capability | FreeBSD provider | Status in plan |
|---|---|---|
| Auto-load kld for unmatched buses | devmatch's matching algorithm | Lift verbatim into hwregd |
| newbus probe/attach | Kernel device_attach() in subr_bus.c | Unchanged — hwregd consumes events, doesn't probe |
| Module dep resolution | linker_load_dependencies() in kern_linker.c | Unchanged — single kldload pulls deps |
| ZFS event logging | devd's zfs.conf regex rules | Replace with zfsd / native ZFS event hook |
| Xorg input hot-plug | libudev-devd reading /var/run/devd.pipe | Phase 2: hwregd reproduces pipe format, or push libudev-devd upstream |
| ACPI lid / power events | devd's acpiconf.conf notify rules | HardwareMatch plists for {class:"ACPIEvent"} |
The original draft of this plan assumed devd remained in place during the transition. As of commit 88694f0 (2026-05-16), FreeBSD-devd and FreeBSD-devmatch were removed from pkglist-base.txt alongside FreeBSD-rc, FreeBSD-syslogd, etc. The system currently boots to login because the kernel's own newbus probe attaches drivers for hardware present at boot (em/virtio drivers in BOOT_MODULES), but several capabilities are now gone:
kldload — a PCI/USB device that needs a not-yet-loaded driver silently fails to attach. devmatch's role.This makes hwregd a current gap, not a future improvement. The sequencing below has been revised: hwregd Phase 0 (minimum viable, ~3-4 weeks) comes first, before configd + IPConfiguration, because IPConfiguration needs the interface-arrival and link-state events that hwregd provides.
Registry-first recommended order — fully replace devd+devmatch and stand up the IOKit-shaped query/watch API before configd:
sbin/devmatch/devmatch.c's search_hints() parser into a daemon. Open /dev/devctl directly. kldload(2) on every ?nomatch event. Parse +attach/-detach/!notify system=IFNET events; expose them via a simple Mach-RPC publish/subscribe (no full IORegistry yet — just an event bus). Closes the devd-removed gap.hwreg_lookup, hwreg_get_properties, hwreg_watch with criteria dicts). Phase 0's simple event bus becomes a special case of Phase 1's hwreg_watch. Phase 0 clients keep working unchanged.HardwareMatch plist key — ~1.5 weeks. Builds on hwregd Phase 1's watch criteria. Services declare hardware dependencies in their plists instead of needing custom event-bus client code.ioreg works, IOServiceAddMatchingNotification works. Unlocks Apple-source userland that queries hardware (system_profiler equivalents). This is the milestone where devd+devmatch are fully replaced and the IOKit query API is live.After step 4 we hit a decision point: pick whichever of the following has the most actual demand at that time. None of them block each other — they're independent tracks downstream of the hwregd/IOKit foundation.
HardwareMatch for link/arrival events.pmset, IOPMAssertion, IOPSCopyPowerSourcesInfo. Depends only on IOKitUser facade.Why registry-first instead of configd-first: hwregd Phase 1 + HardwareMatch + IOKitUser facade are independent of configd, and we have no current network-management consumer that requires DHCP working in the VM. Replacing devd+devmatch and standing up ioreg-shape introspection first means the entire hwregd+IOKit track ships as one cohesive block, after which the configd/powerd/DriverKit ordering becomes a pure demand-driven call.
| Milestone | Single-engineer effort | Critical dependency |
|---|---|---|
| hwregd Phase 0 (kldload-on-nomatch + interface/link Mach events) | ~3-4 weeks | — |
| hwregd Phase 1 (full IORegistry, IOKit-shape Mach API) | ~5-6 weeks | Phase 0 |
launchd HardwareMatch key | ~1.5 weeks | Phase 1 |
| IOKitUser K1 (registry + matching) | ~4 weeks | Phase 1 |
| IOKitUser K2 (notifications) | ~2 weeks | K1 |
| ↓ decision point — pick by demand; downstream items are independent ↓ | ||
| configd v1 (SCDynamicStore + SCPreferences) | ~6 weeks | — |
| IPConfiguration port | ~1 week | configd |
| powerd + IOKitUser K3 (IOPMAssertion, IOPSCopyPowerSourcesInfo) | ~4 weeks | IOKitUser K1+K2 |
| Native DriverKit-shape framework | ~4-8 months | hwregd Phase 1 + IOKitUser K1+K2 |
Cumulative critical path through the foundation block (registry + IOKit facade, devd/devmatch fully replaced, ioreg works): ~3.5 + 5.5 + 1.5 + 4 + 2 ≈ ~4 months.
After the foundation block lands the four downstream options are independent. With one engineer they're picked one at a time by demand; with two engineers the network-mgmt track (configd + IPConfiguration, ~7 weeks) and the Apple-services track (powerd or DriverKit-shape) can run in parallel.
get_properties, or only via timer + push CHANGED events? Trade-off: Mach round-trip count vs. staleness window.HardwareMatch to target "this specific disk" via hashed location + serial?IOServiceOpen/IOConnectCallMethod exposes per-driver IPC channels. Do we want hwregd to broker these (proxying to per-driver Mach ports)? Or do FreeBSD drivers stay pure cdevs and hwregd is read-only metadata?kldload is all-or-nothing. Pre-probe with kldxref -d to detect dep mismatch before attempting load and present a clean error?hwreg_load_driver? Root only? Capsicum-style entitlements via XPC peer credentials?/var/run/devd.pipe's line format from hwregd, or push libudev-devd upstream toward a structured hwregd backend? Affects ports tree compat surface.| Path | Verdict | Effort | When |
|---|---|---|---|
| hwregd Phase 0 (kldload-on-nomatch + interface events) | PURSUE FIRST | ~3-4 wks | Now — closes the devd-removed gap |
| hwregd Phase 1 (full IORegistry + IOKit-shape API) | PURSUE | ~5-6 wks | Immediately after Phase 0 |
launchd HardwareMatch plist key | PURSUE | ~1.5 wks | After hwregd Phase 1 |
| IOKitUser facade on hwregd (K1+K2) | PURSUE | ~6 wks | After hwregd Phase 1 — closes the foundation block; devd+devmatch fully replaced, ioreg live |
| ↓ decision point — pick by demand ↓ | |||
| configd + IPConfiguration (network-management track) | PURSUE | ~7 wks | When DHCP / DNS / SCPreferences become a real consumer need |
| powerd + IOKitUser K3 (IOPMAssertion, IOPSCopyPowerSourcesInfo) | PURSUE | ~4 wks | When power-mgmt becomes a real consumer need (laptop / suspend-resume / battery) |
| FreeBSD-native DriverKit-shape framework (cuse + IOKit personalities) | PURSUE | ~4-8 mo | When a real userspace-driver consumer surfaces |
| Apple DriverKit / DEXT binary compat | DEFER | ~8-18 mo | Research now, build later if real demand surfaces |
| Kernel-side kext support (Mach-O kexts on FreeBSD kernel) | SKIP | ~3-5 yrs | Apple's kernel ABI is unstable across point releases anyway |
| Extend configd to do hardware | SKIP | — | Mixes concerns; macOS keeps them separate too. configd does network/system policy; hardware discovery deserves its own daemon |
| Just keep devd as-is | SKIP | 0 | Forfeits the structured-data win; doesn't unblock IOKitUser-shaped consumers |
cgit.freebsd.org/src/tree/)sys/kern/kern_devctl.c — devctl_notify(), devaddq(), dev_cdevsw, devsoftc.devq, sysctls hw.bus.devctl_queue + hw.bus.devctl_nomatch_enabledsys/kern/subr_bus.c — newbus core, device_handle_nomatch (eventhandler), kernel power-event devctl_notify callersys/kern/kern_linker.c — linker_hints_lookup(), kern_kldload(), linker_load_dependencies() (auto-dep at line ~2316-2389)sys/sys/module.h — MDT_DEPEND/MODULE/VERSION/PNP_INFO constantssys/sys/linker.h — LINKER_HINTS_VERSIONsys/sys/pciio.h — struct pci_conf, struct pci_conf_io, PCIOCGETCONFlib/libdevinfo/devinfo.h + lib/libdevinfo/devinfo.c — newbus tree API, hw.bus.* sysctlsusr.sbin/kldxref/kldxref.c — record writer, PNP DSL parsersbin/devmatch/devmatch.c — reference structured matcher (lift search_hints())sbin/devmatch/devmatch.8 — match algorithm man pageusr.sbin/pciconf/pciconf.c — PCIOCGETCONF flowsbin/devd/devd.cc — /dev/devctl reader pattern, /var/run/devd.pipe broadcastersbin/devd/devd.conf + sbin/devd/*.conf — default rule set (devmatch, dhclient, snd, bluetooth, autofs, hyperv, zfs, moused, power_profile, nvmf, syscons, apple, asus, uath)sbin/devd/devd.conf.5 — rule grammar (attach, detach, nomatch, notify)libexec/rc/rc.d/devd, libexec/rc/rc.d/devmatch — rc.d boot orderingsys/fs/cuse/cuse.c, lib/libcuse/cuse_lib.c — userland char-device substrate for native DriverKit-shape frameworksys/compat/linuxkpi/common/ — 30,660 LoC precedent for foreign in-kernel ABI emulation effort estimate/Users/jmaloney/Documents/launchd/IOKitUser/IOKitLib.h — ~90 user-facing IOKit functions/Users/jmaloney/Documents/launchd/IOKitUser/IOKitLib.c — 27 distinct io_* MIG ops we'd re-aim at hwregd/Users/jmaloney/Documents/launchd/IOKitUser/kext.subproj/OSKext.c — kext_request MIG, 20,988 LoC; IOKitPersonalities plist parser we'd lift for the native DriverKit framework/Users/jmaloney/Documents/launchd/IOKitUser/kext.subproj/MachOFileAbstraction.hpp + AdjustForNewSegmentLocation.cpp — kxld glue (skip path, retained for reference)/Users/jmaloney/Documents/launchd/IOKitUser/pwr_mgt.subproj/IOPMLib.h — IOPMAssertion API surface for K3/Users/jmaloney/Documents/launchd/IOKitUser/ps.subproj/IOPowerSources.h — IOPS API surface for K3/Users/jmaloney/Documents/launchd/configd/ — reference for why hardware discovery should be a separate daemon/Users/jmaloney/Documents/launchd/bootp/IPConfiguration.bproj/ — IPConfiguration daemon for the network-bringup integration story/Users/jmaloney/Documents/launchd/ravynos/sbin/devd/devd.cc — devd.pipe wire format reference for Phase 1 coexistencephase_e_libdispatch_mach_recv — Mach RECV libdispatch source backend; hwregd watch notifications dispatch through thisphase_g2_complete — bootstrap server cross-process discovery; hwregd registers as com.apple.hwregd via thisphase_i3_pid1_launchd_boot — PID-1 launchd working; HardwareMatch integration extends thisinstall_layout_policies — /usr/lib/system/ install path; daemons install at /usr/sbin/hwregd per Apple-Unix conventionDraft synthesized from five parallel research-agent reports on 2026-05-16: (1) IOKit userland port scope, (2) linker.hints + newbus + PCI exploitation, (3) devmatch/devd functional audit, (4) hardware-registry daemon design, (5) kext + DriverKit feasibility. Please flag anything that should change before this becomes a committed plan.