diff --git a/docs/TESTING.md b/docs/TESTING.md
index c35d997..8733394 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -64,6 +64,59 @@ Expected: `warm_empty_allocations=0 single_packet_allocations=2`, correct creden
The same source passed AddressSanitizer/UndefinedBehaviorSanitizer and leak detection. Allocation counts depend on this fixture/compiler/library; preserve the no-wasted-slot property rather than interpreting them as game performance. See [setup](SETUP.md#updating-an-existing-runtime) for server build/install/activation.
+## System sleep/wake notifications
+
+The power monitor uses `IORegisterForSystemPower`, an `IOPMrootDomain` user
+client and standard IOKit interest messages. `iokitd` listens to logind's
+`PrepareForSleep` signals on the host system bus. A delay inhibitor stays open
+until every notified application acknowledges sleep or the deadline expires.
+Wake notifications arrive in `SystemWillPowerOn`, `SystemHasPoweredOn` order.
+Registration also works without logind, with no host events in that case.
+
+Build the runtime and standalone probes:
+
+```sh
+cmake --build build/darling-src/build --target darlingserver iokitd --parallel 3
+python3 tools/build-riot-power-notify-check.py
+cc -Wall -Wextra -Werror -pthread \
+ -I build/darling-src/src/external/iokitd/src \
+ tools/riot-logind-sleep-check.c \
+ build/darling-src/src/external/iokitd/src/LogindSleep.c \
+ $(pkg-config --cflags --libs libsystemd) -o build/riot-logind-sleep-check
+build/riot-logind-sleep-check
+```
+
+The host check uses a private D-Bus daemon and fake logind. It covers absent
+services, delay-lock ownership, acknowledgement deadlines, stale
+acknowledgements, repeated sleep/wake, a logind restart, a full system-bus
+restart and a sleep signal during startup timeout discovery. Expected:
+`LOGIND_SLEEP PASS checks=57 failures=0`. It also runs with
+`-fsanitize=address,undefined`.
+
+`IOServiceOpen` sends an empty out-of-line descriptor. Older Darling servers
+reject that descriptor with `0x400` before the request reaches `iokitd`.
+Install the rebuilt server with Darling stopped, using the server installer in
+[setup](SETUP.md#updating-an-existing-runtime), before running:
+
+```sh
+python3 tools/riot-power-integration-check.py
+```
+
+The integration check creates its own prefix, copies the built `iokitd` into
+it and connects it exclusively to fake logind. It never suspends the host,
+starts Riot or uses the normal Darling prefix. Expected: `POWER_INTEGRATION
+PASS`, covering a full notification queue, two clients, dead destinations,
+failed-registration cleanup, the acknowledgement timeout, deregistration during
+sleep and sleep with no clients. Fixture logs stay under
+`build/power-integration-*`; Darling's overlay work directory may require root
+to remove.
+
+The integration check passed against the installed rebuilt server without an
+interposer. Its bootstrap run can also acquire an inhibitor, so readiness
+requires a new lock from the persistent launcher plus confirmation that its
+shell has started. Lock numbering is relative to that launcher, rather than
+assuming that the first recorded lock belongs to the test clients.
+
## Host URL hand-off regression
LaunchServices' D-Bus hand-off code (`LSHostOpenBus.c`) builds natively against the host's libsystemd. The check starts a private `dbus-daemon` with no service directories and a fake portal/systemd service, so nothing real is activated and no browser opens:
diff --git a/patches/IOKitUser.patch b/patches/IOKitUser.patch
index 0210a03..a5920f0 100644
--- a/patches/IOKitUser.patch
+++ b/patches/IOKitUser.patch
@@ -7,10 +7,10 @@ index 81ed8f7..3120abb 100644
Boolean cfReusedPort = false;
+#ifdef DARLING
-+ /* Fail soft instead of crashing: without iokitd some registry/power
-+ * lookups never produce a port, and callers (e.g. Chromium's power
-+ * monitor) pass the NULL through unchecked. CFRunLoopAddSource
-+ * already ignores NULL sources, so returning NULL here is safe. */
++ /* A notification port can still be absent when allocation fails or
++ * the IOKit service is unavailable. Keep the accessor tolerant of
++ * that failure; CFRunLoopAddSource already ignores NULL sources.
++ * Normal power registration is implemented by iokitd. */
+ if (!notify)
+ return NULL;
+#endif
diff --git a/patches/darlingserver.patch b/patches/darlingserver.patch
index 2c660fe..93505c0 100644
--- a/patches/darlingserver.patch
+++ b/patches/darlingserver.patch
@@ -1031,7 +1031,7 @@ index bbaab01..4f01234 100644
};
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
-index 7c2559b..712ff8f 100644
+index 7c2559b..202d5dc 100644
--- a/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
+++ b/duct-tape/xnu/osfmk/ipc/ipc_kmsg.c
@@ -77,6 +77,7 @@
@@ -1053,6 +1053,17 @@ index 7c2559b..712ff8f 100644
mr = MACH_RCV_INVALID_DATA;
size = 0;
} else {
+@@ -3819,7 +3824,9 @@ ipc_kmsg_copyin_body(
+ goto clean_message;
+ }
+
+- if ((size >= MSG_OOL_SIZE_SMALL) &&
++ /* Darling has a zero small-copy threshold. Empty OOL regions
++ * require no allocation and must not trip the overflow check. */
++ if (size != 0 && (size >= MSG_OOL_SIZE_SMALL) &&
+ (daddr->out_of_line.copy == MACH_MSG_PHYSICAL_COPY) &&
+ !(daddr->out_of_line.deallocate)) {
+ /*
diff --git a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c b/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
index 98ad438..221a1be 100644
--- a/duct-tape/xnu/osfmk/ipc/ipc_mqueue.c
diff --git a/patches/iokitd.patch b/patches/iokitd.patch
index e12b70b..1df128c 100644
--- a/patches/iokitd.patch
+++ b/patches/iokitd.patch
@@ -1,14 +1,26 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
-index 84161fd..1eafdc0 100644
+index 84161fd..3f7fd2b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
-@@ -36,7 +36,9 @@ set(iokitd_sources
+@@ -17,6 +17,8 @@ include_directories(
+ ${X11_Xrandr_INCLUDE_PATH}
+ )
+ include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR})
++# IOPMrootDomain.mm loads the host's libsystemd through the ELF loader.
++include_directories(${CMAKE_SOURCE_DIR}/src/startup/mldr/elfcalls)
+
+ mig(iokitmig.defs)
+ mig(powermanagement.defs)
+@@ -36,7 +38,12 @@ set(iokitd_sources
src/IODisplayConnect.mm
src/IODisplayConnectX11.mm
src/PowerAssertions.mm
+ src/PowerSources.mm
src/PowerAssertionsX11.mm
+ src/IOPlatformExpertDevice.mm
++ src/IOPMrootDomain.mm
++ src/IOInterestNotification.mm
++ src/LogindSleep.c
src/IOSurfaceRoot.mm
src/IOSurface.mm
src/IOUserClient.mm
@@ -407,23 +419,34 @@ index aaed046..7a190c3 100644
+ }
+}
diff --git a/src/main.mm b/src/main.mm
-index 37ce9b8..d29f01d 100644
+index 37ce9b8..e23aea0 100644
--- a/src/main.mm
+++ b/src/main.mm
-@@ -32,6 +32,7 @@
+@@ -32,6 +32,8 @@
#include "IODisplayConnectX11.h"
#include "PowerAssertions.h"
#include "IOSurfaceRoot.h"
+#include "IOPlatformExpertDevice.h"
++#include "IOPMrootDomain.h"
extern "C" {
#include "iokitmigServer.h"
-@@ -146,5 +147,6 @@ static void discoverAllDevices()
+@@ -127,6 +129,8 @@ int main(int argc, const char** argv)
+ dispatch_resume(deathSource);
+ dispatch_resume(pwrMgmtPortSource);
+
++ IOPMrootDomain::instance()->startHostSleepMonitor();
++
+ os_log(OS_LOG_DEFAULT, "iokitd up and running.");
+
+ dispatch_main();
+@@ -146,5 +150,7 @@ static void discoverAllDevices()
// Trick to make sure the root object gets instantiated first and gets the lowest ID
IORegistryEntry::root();
IODisplayConnectX11::discoverDevices(registry);
+ IOPlatformExpertDevice::registerSelf(registry);
IOSurfaceRoot::registerSelf(registry);
++ IOPMrootDomain::registerSelf(registry);
}
diff --git a/src/stubs-powermanagement.c b/src/stubs-powermanagement.c
index a568ecc..929746c 100644
@@ -453,10 +476,91 @@ index a568ecc..929746c 100644
/* Routine io_ps_copy_chargelog */
diff --git a/src/stubs.c b/src/stubs.c
-index 91d11a1..a19d0d6 100644
+index 91d11a1..37551e1 100644
--- a/src/stubs.c
+++ b/src/stubs.c
-@@ -642,18 +642,6 @@ kern_return_t is_io_server_version
+@@ -45,15 +45,6 @@ kern_return_t is_io_registry_entry_get_property_bytes
+ return KERN_NOT_SUPPORTED;
+ }
+
+-kern_return_t is_io_service_close
+-(
+- mach_port_t connection
+-)
+-{
+- STUB();
+- return KERN_NOT_SUPPORTED;
+-}
+-
+ kern_return_t is_io_connect_get_service
+ (
+ mach_port_t connection,
+@@ -325,20 +316,6 @@ kern_return_t is_io_service_add_notification
+ return KERN_NOT_SUPPORTED;
+ }
+
+-kern_return_t is_io_service_add_interest_notification
+-(
+- mach_port_t service,
+- io_name_t type_of_interest,
+- mach_port_t wake_port,
+- io_async_ref_t reference,
+- mach_msg_type_number_t referenceCnt,
+- mach_port_t *notification
+-)
+-{
+- STUB();
+- return KERN_NOT_SUPPORTED;
+-}
+-
+ kern_return_t is_io_service_acknowledge_notification
+ (
+ mach_port_t service,
+@@ -439,22 +416,6 @@ kern_return_t is_io_object_get_bundle_identifier
+ return KERN_NOT_SUPPORTED;
+ }
+
+-kern_return_t is_io_service_open_extended
+-(
+- mach_port_t service,
+- task_t owningTask,
+- uint32_t connect_type,
+- NDR_record_t ndr,
+- io_buf_ptr_t properties,
+- mach_msg_type_number_t propertiesCnt,
+- kern_return_t *result,
+- mach_port_t *connection
+-)
+-{
+- STUB();
+- return KERN_NOT_SUPPORTED;
+-}
+-
+ kern_return_t is_io_connect_map_memory_into_task
+ (
+ mach_port_t connection,
+@@ -533,20 +494,6 @@ kern_return_t is_io_service_add_notification_64
+ return KERN_NOT_SUPPORTED;
+ }
+
+-kern_return_t is_io_service_add_interest_notification_64
+-(
+- mach_port_t service,
+- io_name_t type_of_interest,
+- mach_port_t wake_port,
+- io_async_ref64_t reference,
+- mach_msg_type_number_t referenceCnt,
+- mach_port_t *notification
+-)
+-{
+- STUB();
+- return KERN_NOT_SUPPORTED;
+-}
+-
+ kern_return_t is_io_service_add_notification_ool_64
+ (
+ mach_port_t master_port,
+@@ -642,18 +589,6 @@ kern_return_t is_io_server_version
}
@@ -475,7 +579,7 @@ index 91d11a1..a19d0d6 100644
kern_return_t is_io_service_match_property_table_bin
(
mach_port_t service,
-@@ -682,22 +670,6 @@ kern_return_t is_io_service_add_notification_bin
+@@ -682,22 +617,6 @@ kern_return_t is_io_service_add_notification_bin
return KERN_NOT_SUPPORTED;
}
@@ -498,7 +602,7 @@ index 91d11a1..a19d0d6 100644
kern_return_t is_io_registry_entry_from_path_ool
(
mach_port_t master_port,
-@@ -736,31 +708,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name
+@@ -736,31 +655,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name
return KERN_NOT_SUPPORTED;
}
@@ -615,3 +719,1657 @@ new file mode 100644
+ targetServiceRegistry->registerService(device);
+ device->registerInPlane(kIOServicePlane, "Platform", IORegistryEntry::root());
+}
+diff --git a/src/IOInterestNotification.h b/src/IOInterestNotification.h
+new file mode 100644
+index 0000000..62be712
+--- /dev/null
++++ b/src/IOInterestNotification.h
+@@ -0,0 +1,66 @@
++/*
++ This file is part of Darling.
++
++ Copyright (C) 2026 Darling developers
++
++ Darling is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ Darling is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with Darling. If not, see .
++*/
++
++#ifndef IOKITD_IOINTERESTNOTIFICATION_H
++#define IOKITD_IOINTERESTNOTIFICATION_H
++#include "IOObject.h"
++#include
++#include
++#include
++
++class IOService;
++
++// The object returned by IOServiceAddInterestNotification() (the kernel's
++// IOServiceMessageUserNotification). It lives until the client releases it,
++// and sends service messages to the client's notification port in the same
++// layout as the kernel, which IODispatchCalloutFromMessage() decodes.
++class IOInterestNotification : public IOObject
++{
++public:
++ // Takes over the send right to wakePort. reference holds referenceSize bytes
++ // of the client's async reference (io_async_ref64_t or io_async_ref_t).
++ IOInterestNotification(IOService* service, const char* interestType, mach_port_t wakePort,
++ const void* reference, size_t referenceSize, bool client64);
++ ~IOInterestNotification();
++
++ const char* className() const override;
++
++ const std::string& interestType() const { return m_interestType; }
++ IOService* service() const { return m_service; }
++
++ // Queues messageType without blocking; retries in order when the port is full.
++ kern_return_t deliver(uint32_t messageType, uint64_t argument);
++private:
++ kern_return_t sendMessage(uint32_t messageType, uint64_t argument);
++ kern_return_t flushMessages();
++ void scheduleRetry();
++ bool wakePortIsAlive() const;
++ struct Message { uint32_t type; uint64_t argument; };
++ std::deque m_messages;
++ bool m_retryScheduled = false;
++ bool m_failed = false;
++ IOService* m_service;
++ std::string m_interestType;
++ mach_port_t m_wakePort;
++ OSAsyncReference64 m_reference;
++ size_t m_referenceSize;
++ bool m_client64;
++};
++
++#endif
+diff --git a/src/IOInterestNotification.mm b/src/IOInterestNotification.mm
+new file mode 100644
+index 0000000..1aa41a9
+--- /dev/null
++++ b/src/IOInterestNotification.mm
+@@ -0,0 +1,286 @@
++/*
++ This file is part of Darling.
++
++ Copyright (C) 2026 Darling developers
++
++ Darling is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ Darling is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with Darling. If not, see .
++*/
++
++#include "IOInterestNotification.h"
++#include "IOService.h"
++#include "iokitd.h"
++#include
++#include
++#include
++#include
++#include
++#include
++
++extern "C" {
++#include "iokitmigServer.h"
++}
++
++// Mirrors xnu's IOServiceMessageUserNotification::PingMsg: the provider is
++// passed as a port descriptor, followed by the notification header, the
++// client's async reference and the message content.
++struct InterestMessageHead
++{
++ mach_msg_header_t header;
++ mach_msg_body_t body;
++ mach_msg_port_descriptor_t provider;
++ mach_msg_size_t size;
++ natural_t type;
++};
++
++// The kernel's kIOUserNotifyMaxMessageSize, reported in the header's size field.
++static const mach_msg_size_t kMaxMessageArgumentSize = 64;
++
++// IODispatchCalloutFromMessage() finds the notification header right after the descriptors.
++static_assert(offsetof(InterestMessageHead, size) == sizeof(mach_msg_header_t) + sizeof(mach_msg_body_t)
++ + sizeof(mach_msg_port_descriptor_t), "notification header must follow the port descriptor");
++static_assert(offsetof(OSNotificationHeader64, reference) == 2 * sizeof(natural_t), "unexpected header layout");
++static_assert(sizeof(IOServiceInterestContent64) == sizeof(natural_t) + sizeof(io_user_reference_t),
++ "unexpected interest content layout");
++
++IOInterestNotification::IOInterestNotification(IOService* service, const char* interestType,
++ mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64)
++: m_service(service), m_interestType(interestType), m_wakePort(wakePort),
++ m_referenceSize(std::min(referenceSize, sizeof(m_reference))), m_client64(client64)
++{
++ std::memset(m_reference, 0, sizeof(m_reference));
++ std::memcpy(m_reference, reference, m_referenceSize);
++ m_service->addInterest(this);
++}
++
++IOInterestNotification::~IOInterestNotification()
++{
++ m_service->removeInterest(this);
++ if (MACH_PORT_VALID(m_wakePort))
++ mach_port_deallocate(mach_task_self(), m_wakePort);
++}
++
++const char* IOInterestNotification::className() const
++{
++ return "IOServiceMessageUserNotification";
++}
++
++kern_return_t IOInterestNotification::sendMessage(uint32_t messageType, uint64_t argument)
++{
++ // Header, reference (at most OSAsyncReference64) and IOServiceInterestContent64
++ alignas(8) uint8_t buffer[sizeof(InterestMessageHead) + sizeof(OSAsyncReference64)
++ + sizeof(IOServiceInterestContent64)];
++ std::memset(buffer, 0, sizeof(buffer));
++
++ InterestMessageHead* head = reinterpret_cast(buffer);
++ head->header.msgh_bits = MACH_MSGH_BITS_COMPLEX
++ | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND);
++ head->header.msgh_remote_port = m_wakePort;
++ // Received as msgh_remote_port: the client checks that it still holds this notification.
++ head->header.msgh_local_port = port();
++ head->header.msgh_id = kOSNotificationMessageID;
++
++ head->body.msgh_descriptor_count = 1;
++ head->provider.name = m_service->port();
++ head->provider.disposition = MACH_MSG_TYPE_MAKE_SEND;
++ head->provider.type = MACH_MSG_PORT_DESCRIPTOR;
++
++ // A pointer-sized argument: 8 bytes for 64-bit clients, 4 otherwise.
++ // The low bits of the type would carry (size & 3), which is 0 for both.
++ size_t argumentSize = m_client64 ? sizeof(io_user_reference_t) : sizeof(uint32_t);
++ head->size = kMaxMessageArgumentSize + sizeof(IOServiceInterestContent64);
++ head->type = kIOServiceMessageNotificationType;
++
++ uint8_t* p = buffer + sizeof(InterestMessageHead);
++ std::memcpy(p, m_reference, m_referenceSize);
++ p += m_referenceSize;
++
++ natural_t type = messageType;
++ std::memcpy(p, &type, sizeof(type));
++ p += sizeof(type);
++ if (m_client64)
++ std::memcpy(p, &argument, sizeof(argument));
++ else
++ {
++ uint32_t argument32 = (uint32_t) argument;
++ std::memcpy(p, &argument32, sizeof(argument32));
++ }
++ p += argumentSize;
++
++ head->header.msgh_size = (mach_msg_size_t) (p - buffer);
++
++ // Never block iokitd on a client that does not drain its port.
++ kern_return_t kr = mach_msg(&head->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, head->header.msgh_size,
++ 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
++
++ if (kr != KERN_SUCCESS)
++ {
++ // A pseudo-receive on failed delivery returns newly copied rights.
++ // mach_msg_destroy handles the remote right/descriptors, but not local.
++ mach_msg_type_name_t local = MACH_MSGH_BITS_LOCAL(head->header.msgh_bits);
++ if (local == MACH_MSG_TYPE_MOVE_SEND || local == MACH_MSG_TYPE_MOVE_SEND_ONCE)
++ mach_port_deallocate(mach_task_self(), head->header.msgh_local_port);
++ mach_msg_destroy(&head->header);
++ }
++ return kr;
++}
++
++bool IOInterestNotification::wakePortIsAlive() const
++{
++ mach_port_type_t type = 0;
++ return mach_port_type(mach_task_self(), m_wakePort, &type) == KERN_SUCCESS
++ && (type & MACH_PORT_TYPE_SEND);
++}
++
++void IOInterestNotification::scheduleRetry()
++{
++ if (m_retryScheduled)
++ return;
++ m_retryScheduled = true;
++ mach_port_t notificationPort = port();
++ // Do not retain a send right while waiting: deregistration must still
++ // destroy this object. Look it up again on the same serial queue.
++ IOInterestNotification* expected = this;
++ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
++ auto current = dynamic_cast(IOObject::lookup(notificationPort));
++ if (current != expected)
++ return;
++ current->m_retryScheduled = false;
++ current->flushMessages();
++ });
++}
++
++kern_return_t IOInterestNotification::flushMessages()
++{
++ while (!m_messages.empty())
++ {
++ kern_return_t kr = MACH_SEND_INVALID_DEST;
++ if (wakePortIsAlive())
++ {
++ const auto& message = m_messages.front();
++ kr = sendMessage(message.type, message.argument);
++ // Mach can report success if the destination died during the send.
++ if (!wakePortIsAlive())
++ kr = MACH_SEND_INVALID_DEST;
++ }
++ if (kr == MACH_SEND_TIMED_OUT || kr == MACH_SEND_INTERRUPTED || kr == MACH_SEND_NO_BUFFER)
++ {
++ scheduleRetry();
++ return KERN_SUCCESS; // accepted; preserve message order until the port drains
++ }
++ if (kr != KERN_SUCCESS)
++ {
++ m_failed = true;
++ m_messages.clear();
++ os_log_error(OS_LOG_DEFAULT, "iokitd: notification destination unavailable: 0x%x", kr);
++ mach_port_t notificationPort = port();
++ IOInterestNotification* expected = this;
++ // Defer removal until tellApps has finished building its pending set.
++ dispatch_async(dispatch_get_main_queue(), ^{
++ auto current = dynamic_cast(IOObject::lookup(notificationPort));
++ if (current == expected)
++ current->m_service->removeInterest(current);
++ });
++ return kr;
++ }
++ m_messages.pop_front();
++ }
++ return KERN_SUCCESS;
++}
++
++kern_return_t IOInterestNotification::deliver(uint32_t messageType, uint64_t argument)
++{
++ if (m_failed)
++ return MACH_SEND_INVALID_DEST;
++ m_messages.push_back({messageType, argument});
++ return flushMessages();
++}
++
++static kern_return_t addInterestNotification(mach_port_t service, const char* interestType,
++ mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64,
++ mach_port_t* notification)
++{
++ *notification = MACH_PORT_NULL;
++
++ // On failure dispatch_mig_server destroys the request, including wakePort.
++ // Only a successful registration takes ownership of that send right.
++ IOService* s = dynamic_cast(IOObject::lookup(service));
++ if (!s)
++ return kIOReturnBadArgument;
++ if (!MACH_PORT_VALID(wakePort))
++ return kIOReturnBadArgument;
++ if (!s->acceptsInterest(interestType))
++ return kIOReturnUnsupported;
++
++ IOInterestNotification* n;
++ try
++ {
++ n = new IOInterestNotification(s, interestType, wakePort, reference, referenceSize, client64);
++ }
++ catch (const std::exception& e)
++ {
++ os_log_error(OS_LOG_DEFAULT, "iokitd: cannot create interest notification: %s", e.what());
++ return kIOReturnNoResources;
++ }
++
++ *notification = n->port();
++ // The reply carries a copy of the send right; drop ours afterwards.
++ n->releaseLater();
++ return kIOReturnSuccess;
++}
++
++kern_return_t is_io_service_add_interest_notification_64
++(
++ mach_port_t service,
++ io_name_t type_of_interest,
++ mach_port_t wake_port,
++ io_async_ref64_t reference,
++ mach_msg_type_number_t referenceCnt,
++ mach_port_t *notification
++)
++{
++ if (referenceCnt > kOSAsyncRef64Count)
++ {
++ *notification = MACH_PORT_NULL;
++ return kIOReturnBadArgument;
++ }
++
++ OSAsyncReference64 zreference = {};
++ std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0]));
++
++ return addInterestNotification(service, type_of_interest, wake_port, zreference,
++ sizeof(zreference), true, notification);
++}
++
++kern_return_t is_io_service_add_interest_notification
++(
++ mach_port_t service,
++ io_name_t type_of_interest,
++ mach_port_t wake_port,
++ io_async_ref_t reference,
++ mach_msg_type_number_t referenceCnt,
++ mach_port_t *notification
++)
++{
++ if (referenceCnt > kOSAsyncRefCount)
++ {
++ *notification = MACH_PORT_NULL;
++ return kIOReturnBadArgument;
++ }
++
++ io_async_ref_t zreference = {};
++ std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0]));
++
++ return addInterestNotification(service, type_of_interest, wake_port, zreference,
++ sizeof(zreference), false, notification);
++}
+diff --git a/src/IOObject.cpp b/src/IOObject.cpp
+index 1a40da6..232f150 100644
+--- a/src/IOObject.cpp
++++ b/src/IOObject.cpp
+@@ -50,12 +50,17 @@ IOObject::IOObject()
+ throw std::runtime_error("Failed to create dispatch source");
+
+ dispatch_source_set_event_handler(m_dispatchSource, ^{
+- dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize, iokit_server);
++ dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize,
++ [](mach_msg_header_t* request, mach_msg_header_t* reply) -> boolean_t {
++ if (request->msgh_id == MACH_NOTIFY_NO_SENDERS)
++ return IOObject::deathNotify(request, reply);
++ return iokit_server(request, reply);
++ });
+ });
+ dispatch_resume(m_dispatchSource);
+
+ mach_port_t oldTargetOfNotification = MACH_PORT_NULL;
+- kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, g_deathPort, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification);
++ kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, m_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification);
+ if (kr != KERN_SUCCESS)
+ throw std::runtime_error("Failed to setup MACH_NOTIFY_NO_SENDERS");
+
+@@ -64,12 +69,15 @@ IOObject::IOObject()
+
+ IOObject::~IOObject()
+ {
+- if (m_dispatchSource)
+- dispatch_release(m_dispatchSource);
+-
+ m_objects.erase(m_port);
+-
+- mach_port_deallocate(mach_task_self(), m_port);
++ // A receive source must be cancelled before its receive right is destroyed.
++ // The cancellation handler captures the port value, never the dying object.
++ mach_port_t port = m_port;
++ dispatch_source_set_cancel_handler(m_dispatchSource, ^{
++ mach_port_destroy(mach_task_self(), port);
++ });
++ dispatch_source_cancel(m_dispatchSource);
++ dispatch_release(m_dispatchSource);
+ }
+
+ void IOObject::retain()
+@@ -113,12 +121,30 @@ boolean_t IOObject::deathNotify(mach_msg_header_t *request, mach_msg_header_t *r
+
+ if (Request->not_header.msgh_id == MACH_NOTIFY_NO_SENDERS)
+ {
+- IOObject* object = lookup(request->msgh_remote_port);
+-
+- if (object != nullptr)
++ // No-senders messages carry no object name in their body. They are
++ // delivered to the object's own receive right, identified by local_port.
++ mach_port_t port = request->msgh_local_port;
++ dispatch_async(dispatch_get_main_queue(), ^{
++ IOObject* object = lookup(port);
++ if (!object)
++ return;
++ mach_port_status_t status;
++ mach_msg_type_number_t count = MACH_PORT_RECEIVE_STATUS_COUNT;
++ if (mach_port_get_attributes(mach_task_self(), port, MACH_PORT_RECEIVE_STATUS,
++ (mach_port_info_t) &status, &count) != KERN_SUCCESS)
++ return;
++ if (status.mps_srights)
++ {
++ // A new send right was made after this notification was queued.
++ mach_port_t old = MACH_PORT_NULL;
++ mach_port_request_notification(mach_task_self(), port, MACH_NOTIFY_NO_SENDERS,
++ status.mps_mscount, port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &old);
++ if (MACH_PORT_VALID(old))
++ mach_port_deallocate(mach_task_self(), old);
++ return;
++ }
+ delete object;
+- else
+- os_log_error(OS_LOG_DEFAULT, "Received MACH_NOTIFY_NO_SENDERS for port %d, which matches no objects", request->msgh_remote_port);
++ });
+
+ Reply->Head.msgh_bits = 0;
+ Reply->Head.msgh_remote_port = MACH_PORT_NULL;
+diff --git a/src/IOPMrootDomain.h b/src/IOPMrootDomain.h
+new file mode 100644
+index 0000000..9f9fc95
+--- /dev/null
++++ b/src/IOPMrootDomain.h
+@@ -0,0 +1,89 @@
++/*
++ This file is part of Darling.
++
++ Copyright (C) 2026 Darling developers
++
++ Darling is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ Darling is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with Darling. If not, see .
++*/
++
++#ifndef IOKITD_IOPMROOTDOMAIN_H
++#define IOKITD_IOPMROOTDOMAIN_H
++
++#include "IOUserClient.h"
++#include "ServiceRegistry.h"
++#include