NextBSD power management — pmconfigd as the root domain, over an in-kernel ACPI bridge Research plan · new sub-project

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

TL;DR

1. Where NextBSD is

PieceState
Apple PowerManagement (pmconfigd, pmset, caffeinate, pmtool)absent in scope since the Apple userland plan v3, never vendored
Apple reboot / shutdown / haltabsent FreeBSD's ship; the compat audit's “vendored but deferred on IOPMLib” is stale
FreeBSD powerdships, never started no LaunchDaemon runs it, so laptop CPU frequency is unmanaged
ACPI event consumersnone devd removed; nothing opens /dev/devctl or /dev/acpi
In-kernel IORegistry and Mach notificationslanded /dev/ioregistry (K1), matching notifications (K2, nextbsd#41); hwregd retired (nextbsd#218)
notifyd, ASL, configd, launchd MachServiceslanded 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.

2. The design — pmconfigd over a kernel bridge

Power management architecture Applications and the pmset and caffeinate tools call libIOKit, which talks to pmconfigd over MIG and XPC. Gershwin, which cannot use Mach, reads state from SCDynamicStore and notify. pmconfigd plays IOPMrootDomain toward apps. Below the user-kernel boundary, pmconfigd issues ioctls on /dev/iopmroot to the iopmroot bridge, and the bridge sends it Mach interest messages. The bridge publishes IOPMrootDomain and IOPMPowerSource nodes and sits on FreeBSD's ACPI sleep path and event hook. pmconfigd sets CPU energy preference through sysctl. Applications IORegisterForSystemPower · IOPMAssertion* pmset · caffeinate C1 · C2 Gershwin no Mach — reads keys and the CLI (D6) libIOKit — IOPMLib · IOPowerSources · interest notifications L1–L4 · power calls re-aimed at pmconfigd, not IOServiceOpen pmconfigd — com.apple.powerd MachService, playing IOPMrootDomain toward apps assertions · idle and display sleep · power sources · sleep/wake fan-out with 30 s acknowledgements · CPU energy policy D1–D7 · MIG com.apple.PowerManagement.control · XPC com.apple.iokit.powerdxpc publishes to notifyd and configd (SCDynamicStore) MIG · XPC SCDynamicStore · notify user kernel /dev/iopmroot: SLEEP · ACK · SETPROP 'IOIN' Mach interest messages sysctl iopmroot bridge — IOPMrootDomain0 · IOPMPowerSource0..N in /dev/ioregistry · property provider (K5) · interest messages (K6) K4 · K7 · kern.sleeptime / kern.waketime FreeBSD ACPI acpi_ReqSleepState / AckSleepState — waits for the bridge (K1) lid · button (K2) · AC · battery · thermal → devctl_notify hook list (K3) /dev/devctl stays unopened (nextbsd#225) cpufreq / HWP dev.hwpstate_intel.N.epp dev.cpu.N.freq (AMD) set by pmconfigd (D7)

Three principles hold the design together:

3. Sleep and wake, step by step

StepWhat happensOwner
1 · RequestSoftware (pmset sleepnowIOPMSleepSystem), 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 batterypmconfigd
2 · Kernel handshake opensIOPMROOT_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 · CanSystemSleepIdle sleep only — xnu does not send it for demand sleep. Any app may vetopmconfigd
4 · WillSleepEvery IORegisterForSystemPower client must acknowledge; 30 s budget per client (kWillSleepMaxTimeReq). Tardy clients are logged and sleep proceedspmconfigd
5 · AcknowledgeIOPMROOT_ACK{serial, 0}acpi_AckSleepStateacpi_EnterSleepState → S3. A veto sends IOPMROOT_ACK{serial, EBUSY}, clearing the request, and apps get WillNotSleepbridge
6 · Wakepower_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
The timeout order matters. The kernel's force timeout must exceed pmconfigd's 30 s app budget, or the kernel sleeps while apps are still being asked. FreeBSD's default is 10 s; K1 makes it a tunable defaulting to 45 s. A crashed pmconfigd closes /dev/iopmroot, which deregisters the listener, so sleep falls back to today's behaviour rather than hanging.

4. ACPI events — through the kernel, not a daemon

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:

RouteVerdict
Open /dev/devctl in pmconfigd, as devd didno single reader, and it is what nextbsd#225 wants to retire
Netlink nlsysevent multicast group ACPI — what FreeBSD 15's own powerd usesnot 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 hookchosen 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
A gap in FreeBSD's own event path. The fixed-feature power button — the one QEMU and many laptops use — emits no 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”.

5. Power sources

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

6. Assertions, idle and display sleep

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

7. reboot, shutdown, halt and scheduled events

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

8. CPU policy — a knob, not a loop

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.

9. The MIG decision

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.

Recommendation: fix nextbsd#66. Apple's MIG interface then works unchanged, and every other Darwin daemon that uses out-of-line data benefits. Routing those calls over XPC would work — Apple's clients already use XPC for some power operations — but it patches every affected routine and diverges from Apple's code. This is ticket M1, a decision rather than work.

10. The kernel-later path

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:

R1 is the spike that decides whether and when to make the move.

11. Hardware reality

TargetWhat works
amd64 ACPI laptopsS3 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 5No 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

12. Risks

R1 — resume breaks on real hardware. S3 is only as good as each driver's resume path. Test the known-good ThinkPad class first, and treat DRM resume as the likeliest failure.
R2 — pmconfigd is large and Apple-shaped. About 36k lines, with Objective-C, private libdispatch, entitlement and ASL dependencies. The v1 subset is much smaller, but the porting shims (D1) are real work.
R3 — the kernel sleeps while apps are still being asked. Guarded by K1's 45 s default sitting above the 30 s app budget.
R4 — the MIG decision stalls the epic. Several tickets (D3, D4, L2, L3, C1) wait on M1. Decide it early.
R5 — lid policy splits between the kernel and pmconfigd. If 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.

13. Open questions

Q1. M1 — fix MIG out-of-line data (nextbsd#66), or route over XPC? Recommended: fix #66.
Q2. kextd's reboot lock — serve a trivial kextmanager_lock_reboot, or compile reserve_reboot() out of Apple's reboot and shutdown?
Q3. Should applications also hear about shutdown? On macOS apps are shut down by launchd and loginwindow, not told by the root domain; pmconfigd could hook launchd's shutdown path, but Apple does not.
Q4. Which laptops define the acceptance set for S3 — and does the project's T420 count as the reference machine?
Q5. The RTC wake alarm (S3): which FreeBSD mechanism, and on which hardware?

14. E17 ticket map

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.

#TicketRepoDepends onStatusIssue
K1kernel: acpi_ReqSleepState waits for a registered sleep listener instead of a /dev/devctl reader; ack timeout becomes tunablenextbsd-kernelreadynextbsd-kernel#238
K2kernel: fixed-feature power button emits an ACPI Button event, and the wake PWRBTN status is recorded before it is clearednextbsd-kernelreadynextbsd-kernel#239
K3kernel: devctl_notify hook becomes a list, so an in-kernel bridge can consume ACPI events while /dev/devctl stays unopenednextbsd-kernelreadynextbsd-kernel#240
K4kernel: iopmroot bridge driver -- IOPMrootDomain0 node and /dev/iopmroot user client for sleep request, acknowledgement and statenextbsd-kernelK1, K3, K5, K6readynextbsd-kernel#243
K5kernel: IORegistry property-provider hook, so a driver can add keys to its own nodenextbsd-kernelreadynextbsd-kernel#241
K6kernel: IORegistry interest notifications -- IOREG_EVENT_INTEREST and an 'IOIN' Mach message carrying messageType, messageArgument and serialnextbsd-kernelreadynextbsd-kernel#242
K7kernel: IOPMPowerSource nodes for ACPI batteries and AC, with Apple's power-source keysnextbsd-kernelK4, K5readynextbsd-kernel#244
M1Decide: pmconfigd's out-of-line MIG routines -- fix OOL in mach.ko (nextbsd#66) or route them over XPCnextbsdneeds answernextbsd#491
D1pmconfigd: vendor PowerManagement-1846.120.8.0.1 and boot a minimal daemon as the com.apple.powerd MachServicenextbsd-userlandreadynextbsd-userland#224
D2pmconfigd: one kernel shim -- root-domain selectors become /dev/iopmroot ioctls, and 'IOIN' interest messages drive the daemonnextbsd-userlandD1, K4, L1readynextbsd-userland#226
D3pmconfigd: power sources from IOPMPowerSource nodes -- kIOPS dictionaries, notify state bits, and an AC-only source when there is no batterynextbsd-userlandD1, K7, M1readynextbsd-userland#227
D4pmconfigd: assertion engine and idle-sleep policy -- PreventUserIdleSystemSleep/DisplaySleep, PreventSystemSleep, sleep and displaysleep timersnextbsd-userlandD1, D2, M1readynextbsd-userland#228
D5pmconfigd: S3 sleep/wake orchestration -- CanSystemSleep, WillSleep, WillNotSleep, WillPowerOn, HasPoweredOn, with a 30 s acknowledgement budgetnextbsd-userlandD2, K1, K4readynextbsd-userland#229
D6pmconfigd: publish power state for Gershwin in SCDynamicStore -- battery %, AC, lid, sleep pendingnextbsd-userlandD3readynextbsd-userland#230
D7pmconfigd: CPU power policy -- set Intel HWP EPP by AC/battery profile; replaces FreeBSD powerd, with an interim LaunchDaemon until thennextbsd-userlandD3readynextbsd-userland#231
L1libIOKit: IOServiceAddInterestNotification over 'IOIN', and IORegistryEntrySetCFProperty on the root domainnextbsd-userlandK6readynextbsd-userland#225
L2libIOKit: IOPowerSources (ps.subproj) -- IOPSCopyPowerSourcesInfo and friends, plus <IOKit/ps/*.h>nextbsd-userlandD3, M1readynextbsd-userland#232
L3libIOKit: IOPMAssertion API and the IOPMLib / IOPMLibPrivate headersnextbsd-userlandD4, M1readynextbsd-userland#233
L4libIOKit: IORegisterForSystemPower, IOAllowPowerChange, IOCancelPowerChange and IOPMSleepSystem re-aimed at pmconfigdnextbsd-userlandD5, L1readynextbsd-userland#234
C1pmset: port pmset.m -- -g, -g batt, -g assertions, -g log, sleepnow, displaysleep/sleep settings, schedulenextbsd-userlandD1, D3, L2, L3, L4readynextbsd-userland#235
C2caffeinate: port caffeinate.cnextbsd-userlandL3readynextbsd-userland#236
S1reboot/shutdown/halt: vendor Apple's system_cmds versions over the BSD PID-1 signal protocol launchd already honoursnextbsd-userlandreadynextbsd-userland#237
S2pmconfigd: pmset schedule sleep/shutdown/restart performed by pmconfigd itself; wake/poweron waits on S3's RTC alarmnextbsd-userlandD1, S1readynextbsd-userland#238
S3Spike: an RTC wake alarm for pmset schedule wake -- FreeBSD has no rtcwakenextbsd-kernelK4spikenextbsd-kernel#245
V1Display sleep and HID idle time without IODisplayWrangler -- pmconfigd owns display idlenextbsd-userlandD4readynextbsd-userland#239
T1CI: power boot markers -- PM-BOOT-OK, PM-PS-OK, PM-ASSERT-OK, and PM-SLEEP-OK through QEMU S3nextbsdD1, D5, K2readynextbsd#492
E1Packaging: power LaunchDaemons, build blocks, man pages, compat strip entries, and ACPI sysctl defaultsnextbsd-userlandD1, S1, D7readynextbsd-userland#240
R1Spike: an in-kernel IOPMrootDomain -- move the sleep/wake fan-out, acknowledgements and policy into iopmrootnextbsd-kernelD5spikenextbsd-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.

15. References and confidence

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.