Darwin-shaped power management, built from Apple's own code. Apple's pmconfigd (shipped as powerd, PowerManagement-1846.120.8.0.1) runs as a launchd MachService and plays the role xnu's in-kernel IOPMrootDomain plays toward applications — the kIOMessageCanSystemSleep → WillSleep → HasPoweredOn sequence with per-client acknowledgements — over a small in-kernel bridge that publishes an IOPMrootDomain node and IOPMPowerSource nodes into the IORegistry NextBSD already runs in the kernel. One live bug comes first: FreeBSD's acpi_ReqSleepState enters S3 at once when nothing holds /dev/devctl open — “If devd(8) is not running, immediately enter the sleep state” — and devd is gone, so today every sleep request suspends with no process told. Hybrid by design: userspace now, shaped so the root domain can move into the kernel later, which turns out to be cheaper than assumed now that hwregd has been retired and the registry is in-kernel. Companion to EPIC E17 — E16 was already taken by Bluetooth.
/dev/devctl reader, acpiconf -s 3, a sleep button or a lid close suspends immediately, and no Resume event is ever sent. K1 fixes the gate.IORegisterForSystemPower / IOAllowPowerChange go to pmconfigd, which already contains Apple's fan-out and acknowledgement machinery (PMConnection.m). Its kernel-facing code shrinks to one shim over /dev/iopmroot.devctl_notify calls in-kernel and republishes them as IORegistry properties and Mach interest messages. /dev/devctl stays inert, as nextbsd#225 wants. The multi-reader netlink route (nlsysevent) is module-only and so absent on NextBSD.dev.hwpstate_intel.N.epp by power source. FreeBSD's powerd is replaced, and runs from an interim LaunchDaemon until then — nothing launches it today.powermanagement.defs passes property lists out-of-line, and MIG out-of-line data is broken in this kernel (nextbsd#66). Fix it, or route those calls over XPC — M1.| Piece | State |
|---|---|
Apple PowerManagement (pmconfigd, pmset, caffeinate, pmtool) | absent in scope since the Apple userland plan v3, never vendored |
Apple reboot / shutdown / halt | absent FreeBSD's ship; the compat audit's “vendored but deferred on IOPMLib” is stale |
FreeBSD powerd | ships, never started no LaunchDaemon runs it, so laptop CPU frequency is unmanaged |
| ACPI event consumers | none devd removed; nothing opens /dev/devctl or /dev/acpi |
| In-kernel IORegistry and Mach notifications | landed /dev/ioregistry (K1), matching notifications (K2, nextbsd#41); hwregd retired (nextbsd#218) |
| notifyd, ASL, configd, launchd MachServices | landed pmconfigd's 161 notify_* sites work against the real notifyd |
The hardware-registry plan already sketched this work as phase K3 — “Port IOPMAssertion*, IOPSCopyPowerSourcesInfo… Requires a small powerd daemon backing the assertion broker and ACPI-backed power source enumeration. Marker: pmset -g prints sensible output.” This plan is that phase, grown to cover sleep and wake.
Three principles hold the design together:
/dev/iopmroot (D2). Most are pmconfigd-internal state in this model.kIOMessageCanSystemSleep 0x270, WillSleep 0x280, WillNotSleep 0x290, WillPowerOn 0x320, HasPoweredOn 0x300) are Apple's, so unmodified clients and a later kernel move both see the same shapes.| Step | What happens | Owner |
|---|---|---|
| 1 · Request | Software (pmset sleepnow → IOPMSleepSystem), idle (no assertion and the idle timer expired), lid close or power button (pmconfigd's policy — the kernel's own lid, button and sleep-button actions are set to NONE), or low battery | pmconfigd |
| 2 · Kernel handshake opens | IOPMROOT_SLEEP{reason} → acpi_ReqSleepState, which now sees a registered listener (K1) and arms the acknowledgement timeout (45 s) instead of sleeping at once. The bridge sends pmconfigd “sleep requested {serial}” | bridge |
| 3 · CanSystemSleep | Idle sleep only — xnu does not send it for demand sleep. Any app may veto | pmconfigd |
| 4 · WillSleep | Every IORegisterForSystemPower client must acknowledge; 30 s budget per client (kWillSleepMaxTimeReq). Tardy clients are logged and sleep proceeds | pmconfigd |
| 5 · Acknowledge | IOPMROOT_ACK{serial, 0} → acpi_AckSleepState → acpi_EnterSleepState → S3. A veto sends IOPMROOT_ACK{serial, EBUSY}, clearing the request, and apps get WillNotSleep | bridge |
| 6 · Wake | power_resume → the bridge sets Wake Reason / Wake Type / SleepWakeUUID and sends “did wake” → pmconfigd sends WillPowerOn then HasPoweredOn, and one capability DidChange (v1: every wake is a full wake) | both |
/dev/iopmroot, which deregisters the listener, so sleep falls back to today's behaviour rather than hanging.Every ACPI event already goes acpi_UserNotify() → devctl_notify("ACPI", subsystem, ...), with subsystems Lid, Button, ACAD, CMBAT, Thermal, Suspend and Resume. Three ways to get them to pmconfigd were considered:
| Route | Verdict |
|---|---|
Open /dev/devctl in pmconfigd, as devd did | no single reader, and it is what nextbsd#225 wants to retire |
Netlink nlsysevent multicast group ACPI — what FreeBSD 15's own powerd uses | not available netlink_sysevent.c is not in sys/conf/files; it exists only as sys/modules/nlsysevent, and powerd kldloads it. NextBSD ships no modules. It would also be a second event bus beside the Mach channel, and it does not fix the sleep gate |
An in-kernel consumer on the devctl_notify hook | chosen the hook exists; K3 makes it a list. The bridge turns events into IORegistry properties and Mach interest messages, IOKit-shaped, and /dev/devctl stays inert |
devctl notification at all; it only fires the acpi_sleep_event eventhandler. K2 adds the missing Button event, and records the wake-time power-button status before FreeBSD clears it, so Wake Reason can say “Power Button”.pmconfigd finds batteries by matching IOPMPowerSource and re-reads every property on any interest message. It drops any battery missing BatteryInstalled, CurrentCapacity or MaxCapacity. K7 publishes one node per acpi_cmbat unit, mapped from FreeBSD's acpi_bif/bix/bst structures, plus a single AC-only source on machines with no battery — QEMU and desktops. acpi_cmbat's notify is the push signal; there is no Apple Smart Battery Manager, so IOPSRequestBatteryUpdate becomes a no-op.
pmconfigd republishes to clients through notify state bits on com.apple.system.powersources.* and the io_ps_copy_powersources_info blob behind IOPSCopyPowerSourcesInfo. Gershwin, which cannot use Mach, reads the same state from SCDynamicStore keys (D6).
The assertion engine is PMAssertions.c (7,650 lines). caffeinate is its ideal first client: 297 lines, two IOPMLib calls. Two Apple dependencies have no NextBSD equivalent and need a stand-in (V1):
IOHIDSystem and IOHIDEventSystemClient. NextBSD has evdev (/dev/input/event*) and the display server's own idle notifier.IODisplayWrangler for display power. NextBSD has none, so pmconfigd owns the display idle timer and blanks through DPMS or DRM.Apple's reboot and shutdown call reboot3(), which lives in macOS libxpc and does not exist on NextBSD. Their !__APPLE__ branch signals PID 1 — SIGINT reboot, SIGUSR1 halt, SIGUSR2 poweroff, SIGWINCH power cycle — and launchd already honours exactly that protocol (nextbsd#398). IOPMLib is needed only for shutdown -s. The one snag is reserve_reboot(), which takes a reboot lock from kextd over com.apple.KernelExtensionServer, a service kextd does not currently register (S1).
On macOS, pmset schedule sleep, shutdown and restart only post a distributed notification for loginwindow to act on. NextBSD has neither that loginwindow behaviour nor distnoted, so pmconfigd performs them itself (S2). Scheduled wake needs an RTC alarm armed at sleep time, and FreeBSD has no rtcwake — that is a spike (S3).
FreeBSD's powerd samples kern.cp_times and steps dev.cpu.0.freq. On Intel HWP machines that is nearly a no-op: hwpstate_intel registers as an info-only cpufreq driver, and the hardware picks the frequency. The real control is dev.hwpstate_intel.N.epp, from 0 (most performant) to 100 (most efficient). pmconfigd sets it by power source (D7). AMD's hwpstate_amd has no EPP or CPPC in 15.1, only discrete frequency levels, so there it either lifts powerd's adaptive loop or leaves the choice to the kernel. Until pmconfigd owns this, an interim LaunchDaemon starts base powerd, because nothing does today.
Apple's powermanagement.defs (IOKitUser, subsystem 73000) carries serialized property lists as out-of-line data in several routines — creating assertions, setting their properties, and copying power sources. MIG out-of-line data is broken in this kernel: src/WLAN/wlan.defs says so and uses only fixed-size arguments, and nextbsd#66 (E1) is open.
The hybrid's second half is cheaper than first assumed. The original reason to keep the root domain in userspace was “no in-kernel IOKit”, and that no longer holds: the IORegistry is in the kernel (hwregd was retired, nextbsd#218), and kernel-originated Mach sends already work (iokit_notify.c). Moving the root domain in means growing iokit_pmroot.c with a per-client registry, the fan-out and a 30 s acknowledgement callout, plus the idle, clamshell and power-button policy — roughly 1,000–1,500 kernel lines.
Four v1 choices keep that swap cheap:
'IOIN', used by both the kernel and pmconfigdR1 is the spike that decides whether and when to make the move.
| Target | What works |
|---|---|
| amd64 ACPI laptops | S3 supported and stable in FreeBSD for years. No S4, no s2idle/S0ix — a “Modern Standby only” laptop cannot sleep. Resume quality depends on DRM, xhci, NVMe and Wi-Fi resume per-model |
| QEMU / UTM (x86_64) | The whole request → notify → acknowledge → S3 → resume handshake, the fixed power button (system_powerdown), and an AC-only source. Not testable: batteries, lid, thermal zones, HWP |
| Raspberry Pi 5 | No sleep at all: acpi_ReqSleepState is x86-only and FreeBSD's PSCI does off and reset, not suspend. Power sources are AC-only; thermal and throttling come from the firmware sysctls |
hw.acpi.lid_switch_state is left set, the kernel and pmconfigd both act on a lid close. E1 sets the kernel's actions to NONE once pmconfigd owns them, keeping the kernel's power-button poweroff as a fallback only when pmconfigd is not running.kextmanager_lock_reboot, or compile reserve_reboot() out of Apple's reboot and shutdown?EPIC E17 — Power management, label area:power (new, in four repos). Full bodies for every ticket are on the E17 ticket drafts page, generated from the same source as this table. All were filed on 2026-09-21 under epic nextbsd#490.
| # | Ticket | Repo | Depends on | Status | Issue |
|---|---|---|---|---|---|
| K1 | kernel: acpi_ReqSleepState waits for a registered sleep listener instead of a /dev/devctl reader; ack timeout becomes tunable | nextbsd-kernel | — | ready | nextbsd-kernel#238 |
| K2 | kernel: fixed-feature power button emits an ACPI Button event, and the wake PWRBTN status is recorded before it is cleared | nextbsd-kernel | — | ready | nextbsd-kernel#239 |
| K3 | kernel: devctl_notify hook becomes a list, so an in-kernel bridge can consume ACPI events while /dev/devctl stays unopened | nextbsd-kernel | — | ready | nextbsd-kernel#240 |
| K4 | kernel: iopmroot bridge driver -- IOPMrootDomain0 node and /dev/iopmroot user client for sleep request, acknowledgement and state | nextbsd-kernel | K1, K3, K5, K6 | ready | nextbsd-kernel#243 |
| K5 | kernel: IORegistry property-provider hook, so a driver can add keys to its own node | nextbsd-kernel | — | ready | nextbsd-kernel#241 |
| K6 | kernel: IORegistry interest notifications -- IOREG_EVENT_INTEREST and an 'IOIN' Mach message carrying messageType, messageArgument and serial | nextbsd-kernel | — | ready | nextbsd-kernel#242 |
| K7 | kernel: IOPMPowerSource nodes for ACPI batteries and AC, with Apple's power-source keys | nextbsd-kernel | K4, K5 | ready | nextbsd-kernel#244 |
| M1 | Decide: pmconfigd's out-of-line MIG routines -- fix OOL in mach.ko (nextbsd#66) or route them over XPC | nextbsd | — | needs answer | nextbsd#491 |
| D1 | pmconfigd: vendor PowerManagement-1846.120.8.0.1 and boot a minimal daemon as the com.apple.powerd MachService | nextbsd-userland | — | ready | nextbsd-userland#224 |
| D2 | pmconfigd: one kernel shim -- root-domain selectors become /dev/iopmroot ioctls, and 'IOIN' interest messages drive the daemon | nextbsd-userland | D1, K4, L1 | ready | nextbsd-userland#226 |
| D3 | pmconfigd: power sources from IOPMPowerSource nodes -- kIOPS dictionaries, notify state bits, and an AC-only source when there is no battery | nextbsd-userland | D1, K7, M1 | ready | nextbsd-userland#227 |
| D4 | pmconfigd: assertion engine and idle-sleep policy -- PreventUserIdleSystemSleep/DisplaySleep, PreventSystemSleep, sleep and displaysleep timers | nextbsd-userland | D1, D2, M1 | ready | nextbsd-userland#228 |
| D5 | pmconfigd: S3 sleep/wake orchestration -- CanSystemSleep, WillSleep, WillNotSleep, WillPowerOn, HasPoweredOn, with a 30 s acknowledgement budget | nextbsd-userland | D2, K1, K4 | ready | nextbsd-userland#229 |
| D6 | pmconfigd: publish power state for Gershwin in SCDynamicStore -- battery %, AC, lid, sleep pending | nextbsd-userland | D3 | ready | nextbsd-userland#230 |
| D7 | pmconfigd: CPU power policy -- set Intel HWP EPP by AC/battery profile; replaces FreeBSD powerd, with an interim LaunchDaemon until then | nextbsd-userland | D3 | ready | nextbsd-userland#231 |
| L1 | libIOKit: IOServiceAddInterestNotification over 'IOIN', and IORegistryEntrySetCFProperty on the root domain | nextbsd-userland | K6 | ready | nextbsd-userland#225 |
| L2 | libIOKit: IOPowerSources (ps.subproj) -- IOPSCopyPowerSourcesInfo and friends, plus <IOKit/ps/*.h> | nextbsd-userland | D3, M1 | ready | nextbsd-userland#232 |
| L3 | libIOKit: IOPMAssertion API and the IOPMLib / IOPMLibPrivate headers | nextbsd-userland | D4, M1 | ready | nextbsd-userland#233 |
| L4 | libIOKit: IORegisterForSystemPower, IOAllowPowerChange, IOCancelPowerChange and IOPMSleepSystem re-aimed at pmconfigd | nextbsd-userland | D5, L1 | ready | nextbsd-userland#234 |
| C1 | pmset: port pmset.m -- -g, -g batt, -g assertions, -g log, sleepnow, displaysleep/sleep settings, schedule | nextbsd-userland | D1, D3, L2, L3, L4 | ready | nextbsd-userland#235 |
| C2 | caffeinate: port caffeinate.c | nextbsd-userland | L3 | ready | nextbsd-userland#236 |
| S1 | reboot/shutdown/halt: vendor Apple's system_cmds versions over the BSD PID-1 signal protocol launchd already honours | nextbsd-userland | — | ready | nextbsd-userland#237 |
| S2 | pmconfigd: pmset schedule sleep/shutdown/restart performed by pmconfigd itself; wake/poweron waits on S3's RTC alarm | nextbsd-userland | D1, S1 | ready | nextbsd-userland#238 |
| S3 | Spike: an RTC wake alarm for pmset schedule wake -- FreeBSD has no rtcwake | nextbsd-kernel | K4 | spike | nextbsd-kernel#245 |
| V1 | Display sleep and HID idle time without IODisplayWrangler -- pmconfigd owns display idle | nextbsd-userland | D4 | ready | nextbsd-userland#239 |
| T1 | CI: power boot markers -- PM-BOOT-OK, PM-PS-OK, PM-ASSERT-OK, and PM-SLEEP-OK through QEMU S3 | nextbsd | D1, D5, K2 | ready | nextbsd#492 |
| E1 | Packaging: power LaunchDaemons, build blocks, man pages, compat strip entries, and ACPI sysctl defaults | nextbsd-userland | D1, S1, D7 | ready | nextbsd-userland#240 |
| R1 | Spike: an in-kernel IOPMrootDomain -- move the sleep/wake fan-out, acknowledgements and policy into iopmroot | nextbsd-kernel | D5 | spike | nextbsd-kernel#246 |
Order: K1, K2, K3, K5, K6 and D1 start in parallel; M1 is decided early. K4 follows K1/K3/K5/K6, then K7. D2 needs K4 and L1; D3 needs K7; D4 then D5; L2/L3 follow D3/D4, L4 follows D5; C2 as soon as L3 exists, C1 last. S1 is independent; S2 after S1; S3 and R1 are spikes. T1 and E1 track the rest.
Linked, not sub-issues: nextbsd#225, nextbsd#66, nextbsd#115, nextbsd#395, nextbsd#179, nextbsd#182, nextbsd#390, nextbsd#428.
FreeBSD from freebsd/freebsd-src releng/15.1, the NextBSD kernel base. Apple from apple-oss-distributions: PowerManagement-1846.120.8.0.1, IOKitUser-100231.120.3, system_cmds-1042.120.1, and xnu main.
Verified directly while writing this: the sleep gate in acpi_ReqSleepState (“If devd(8) is not running, immediately enter the sleep state”, if (!devctl_process_running())); netlink_sysevent.c absent from sys/conf/files while sys/modules/nlsysevent/Makefile exists, and powerd.c kldloading it; hwregd retired (nextbsd#218, closed) and libIOKit reading /dev/ioregistry; E16 taken by nextbsd#473; nextbsd#66 open.
From the three research reports, not re-verified line by line: file and line references into pmconfigd, IOKitUser, system_cmds, xnu and the FreeBSD ACPI and cpufreq sources; line counts; message values; the selector table. The reports were read from cloned tags and cite paths throughout; treat exact line numbers as approximate.
Unverified: per-model S3 behaviour on current laptops; QEMU S3 defaults under UTM; Raspberry Pi 5 power-button and cpufreq attachment; whether NextBSD's libdispatch carries the private dispatch_mach_* API; whether launchd supports BOOTSTRAP_PRIVILEGED_SERVER lookups.
iokit/Kernel/IOPMrootDomain.cpp)