Files
darling-lol/patches/iokitd.patch
T

3607 lines
114 KiB
Diff

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 84161fd..5b23944 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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,13 @@ 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/HostPowerInhibit.c
src/IOSurfaceRoot.mm
src/IOSurface.mm
src/IOUserClient.mm
diff --git a/src/IORegistryEntry.mm b/src/IORegistryEntry.mm
index 50c10fe..7026e1c 100644
--- a/src/IORegistryEntry.mm
+++ b/src/IORegistryEntry.mm
@@ -1,11 +1,15 @@
#include "IORegistryEntry.h"
#include <stack>
#include <IOKit/IOCFSerialize.h>
+#include <CoreFoundation/CFNumber.h>
#include <cstring>
#include <vector>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOReturn.h>
#include <sstream>
+#include <limits>
+#import <Foundation/NSData.h>
+#import <Foundation/NSNumber.h>
#include "IOIterator.h"
extern "C" {
#include "iokitmigServer.h"
@@ -29,6 +33,12 @@ IORegistryEntry::~IORegistryEntry()
{
std::unique_lock l(m_entriesByIdMutex);
m_entriesById.erase(m_id);
+ // Registry requests and object teardown run on iokitd's main dispatch queue.
+ // Detach both directions so a later traversal cannot follow a deleted entry.
+ for (auto& plane : m_parents)
+ for (auto* parent : plane.second) parent->m_children[plane.first].erase(this);
+ for (auto& plane : m_children)
+ for (auto* child : plane.second) child->m_parents[plane.first].erase(this);
}
IORegistryEntry* IORegistryEntry::findById(uint64_t id)
@@ -79,29 +89,42 @@ std::set<IORegistryEntry*> IORegistryEntry::getChildren(const char* plane)
void IORegistryEntry::registerInPlane(const char* plane, const char* pathName, IORegistryEntry* parent)
{
+ if (!plane || !*plane || !pathName || !*pathName || !parent || parent == this) return;
+ std::set<IORegistryEntry*> visited;
+ std::vector<IORegistryEntry*> pending { parent };
+ while (!pending.empty()) {
+ auto* current = pending.back(); pending.pop_back();
+ if (current == this) return; // refuse cycles, including indirect ones
+ if (!visited.insert(current).second) continue;
+ for (auto* ancestor : current->getParents(plane)) pending.push_back(ancestor);
+ }
m_name[plane] = pathName;
+ if (parent == root()) parent->m_name.emplace(plane, "Root");
auto it = m_parents.find(plane);
if (it == m_parents.end())
- it = m_parents.insert(std::make_pair(std::string_view(plane), std::set<IORegistryEntry*>())).first;
+ it = m_parents.insert(std::make_pair(std::string(plane), std::set<IORegistryEntry*>())).first;
it->second.insert(parent);
it = parent->m_children.find(plane);
if (it == parent->m_children.end())
- it = parent->m_children.insert(std::make_pair(std::string_view(plane), std::set<IORegistryEntry*>())).first;
+ it = parent->m_children.insert(std::make_pair(std::string(plane), std::set<IORegistryEntry*>())).first;
it->second.insert(this);
}
std::string IORegistryEntry::getPath(const char* plane)
{
+ if (!inPlane(plane)) return "?unsup_plane?";
std::stack<std::string> components;
+ std::set<IORegistryEntry*> visited;
IORegistryEntry* cur = this;
while (true)
{
+ if (!visited.insert(cur).second) return "?unsup_plane?";
std::set<IORegistryEntry*> parents = cur->getParents(plane);
if (parents.empty())
{
@@ -113,18 +136,22 @@ std::string IORegistryEntry::getPath(const char* plane)
components.push(cur->getName(plane));
- IORegistryEntry* parent = *parents.begin();
+ IORegistryEntry* parent = nullptr;
+ for (auto* candidate : parents) if (candidate->inPlane(plane)) { parent = candidate; break; }
+ if (!parent) return "?unsup_plane?";
cur = parent;
}
std::string path = plane;
path += ":";
+ if (components.empty()) path += "/";
while (!components.empty())
{
path += "/";
path += components.top();
components.pop();
+ if (path.size() > 65535) return "?unsup_plane?";
}
return path;
@@ -166,11 +193,15 @@ kern_return_t is_io_registry_entry_get_path
io_string_t path
)
{
+ if (!path) return kIOReturnBadArgument;
IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
if (!e)
return kIOReturnBadArgument;
+ if (!plane || !memchr(plane, 0, sizeof(io_name_t)) || !e->inPlane(plane)) return kIOReturnBadArgument;
std::string mypath = e->getPath(plane);
+ if (mypath == "?unsup_plane?") return kIOReturnBadArgument;
+ if (mypath.size() >= sizeof(io_string_t)) { path[0] = 0; return kIOReturnNoSpace; }
strlcpy(path, mypath.c_str(), sizeof(io_string_t));
return kIOReturnSuccess;
@@ -386,55 +417,59 @@ kern_return_t is_io_registry_entry_get_location_in_plane
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_registry_entry_from_path
-(
- mach_port_t master_port,
- io_string_t path,
- mach_port_t *registry_entry
-)
+bool IORegistryEntry::inPlane(const char* plane) const
{
- *registry_entry = 0;
-
- std::vector<std::string> components;
- std::istringstream iss(path);
-
- for (std::string token; std::getline(iss, token, '/'); )
- {
- if (!token.empty())
- components.push_back(std::move(token));
+ if (!plane || !*plane || m_name.find(plane) == m_name.end()) return false;
+ std::set<const IORegistryEntry*> visited;
+ std::vector<const IORegistryEntry*> pending { this };
+ while (!pending.empty()) {
+ auto* current = pending.back(); pending.pop_back();
+ if (current == root()) return true;
+ if (!visited.insert(current).second) continue;
+ auto parents = current->m_parents.find(plane);
+ if (parents != current->m_parents.end())
+ for (auto* parent : parents->second) pending.push_back(parent);
}
-
- if (components.empty())
- return kIOReturnBadArgument;
-
- std::string& planeName = components[0];
- if (planeName[planeName.length()-1] != ':')
- return kIOReturnBadArgument;
- else
- planeName.resize(planeName.length()-1); // remove the final colon
-
- IORegistryEntry* current = IORegistryEntry::root();
- for (size_t i = 1; i < components.size(); i++)
- {
- auto children = current->getChildren(planeName.c_str());
- IORegistryEntry* next = nullptr;
+ return false;
+}
- for (IORegistryEntry* e : children)
- {
- if (strcasecmp(e->getName(planeName.c_str()).c_str(), components[i].c_str()) == 0)
- {
- next = e;
- break;
- }
+IORegistryEntry* IORegistryEntry::fromPath(std::string_view path)
+{
+ if (path.empty() || path.size() > 65535 || path.find('\0') != std::string_view::npos) return nullptr;
+ auto colon = path.find(':');
+ if (colon == 0 || colon == std::string_view::npos || colon >= sizeof(io_name_t)) return nullptr;
+ std::string plane(path.substr(0, colon));
+ if (plane.find('/') != std::string::npos) return nullptr;
+ auto* current = root();
+ if (!current->inPlane(plane.c_str())) return nullptr;
+ path.remove_prefix(colon + 1);
+ if (!path.empty() && path.front() != '/') return nullptr;
+ while (!path.empty()) {
+ path.remove_prefix(1);
+ auto slash = path.find('/');
+ auto name = path.substr(0, slash);
+ if (!name.empty()) {
+ if (name == "." || name == "..") return nullptr;
+ IORegistryEntry* next = nullptr;
+ for (auto* child : current->getChildren(plane.c_str()))
+ if (child->getName(plane.c_str()) == name) { next = child; break; }
+ if (!next) return nullptr;
+ current = next;
}
-
- if (!next)
- return kIOReturnNoDevice;
- current = next;
+ if (slash == std::string_view::npos) break;
+ path.remove_prefix(slash);
}
+ return current; // borrowed until this main-queue request completes
+}
- *registry_entry = current->port();
-
+kern_return_t is_io_registry_entry_from_path(mach_port_t master_port, io_string_t path, mach_port_t* registry_entry)
+{
+ if (!registry_entry) return kIOReturnBadArgument;
+ *registry_entry = MACH_PORT_NULL;
+ if (!path || !memchr(path, 0, sizeof(io_string_t))) return kIOReturnBadArgument;
+ auto* entry = IORegistryEntry::fromPath(path);
+ if (!entry) return kIOReturnNoDevice;
+ *registry_entry = entry->port();
return kIOReturnSuccess;
}
@@ -513,3 +548,189 @@ kern_return_t is_io_registry_entry_get_property_bin
return kr;
}
+
+kern_return_t is_io_registry_entry_get_property_bin_buf
+(
+ mach_port_t registry_entry,
+ io_name_t plane,
+ io_name_t property_name,
+ uint32_t options,
+ mach_vm_address_t buf,
+ mach_vm_size_t *bufsize,
+ io_buf_ptr_t *properties,
+ mach_msg_type_number_t *propertiesCnt
+)
+{
+ IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!e)
+ return kIOReturnBadArgument;
+
+ *propertiesCnt = 0;
+ *properties = nullptr;
+
+ NSString* propName = [NSString stringWithUTF8String: property_name];
+ id value = nil;
+
+ if ((options & kIORegistryIterateRecursively) && plane[0])
+ value = [findKey(e, plane, propName) objectForKey: propName];
+ else
+ value = [e->getProperties() objectForKey: propName];
+
+ if (!value)
+ return kIOReturnNotFound;
+
+ // Unlike io_registry_entry_get_property_bin, the reply carries the value itself.
+ // The caller's buffer lives in another task, so always answer out-of-line, which the
+ // protocol allows when the result does not go into 'buf'.
+ CFDataRef data = IOCFSerialize((CFTypeRef) value, kIOCFSerializeToBinary);
+ if (!data)
+ return kIOReturnNoMemory;
+
+ *propertiesCnt = CFDataGetLength(data);
+ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true);
+
+ if (kr == kIOReturnSuccess)
+ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt);
+ else
+ *propertiesCnt = 0;
+
+ CFRelease(data);
+ return kr;
+}
+
+kern_return_t is_io_registry_entry_get_properties_bin_buf
+(
+ mach_port_t registry_entry,
+ mach_vm_address_t buf,
+ mach_vm_size_t *bufsize,
+ io_buf_ptr_t *properties,
+ mach_msg_type_number_t *propertiesCnt
+)
+{
+ IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!e)
+ return kIOReturnBadArgument;
+
+ *propertiesCnt = 0;
+ *properties = nullptr;
+
+ // The caller's buffer lives in another task, so always answer out-of-line, which the
+ // protocol allows when the result does not go into 'buf'.
+ CFDataRef data = IOCFSerialize((CFTypeRef) e->getProperties(), kIOCFSerializeToBinary);
+ if (!data)
+ return kIOReturnNoMemory;
+
+ *propertiesCnt = CFDataGetLength(data);
+ kern_return_t kr = vm_allocate(mach_task_self(), (vm_address_t*) properties, *propertiesCnt, true);
+
+ if (kr == kIOReturnSuccess)
+ memcpy(*properties, CFDataGetBytePtr(data), *propertiesCnt);
+ else
+ *propertiesCnt = 0;
+
+ CFRelease(data);
+ return kr;
+}
+
+// OOL input ownership transfers to this server; release it on every exit path.
+kern_return_t is_io_registry_entry_from_path_ool(mach_port_t master_port, io_string_inband_t path,
+ io_buf_ptr_t path_ool, mach_msg_type_number_t path_oolCnt, kern_return_t* result, mach_port_t* registry_entry)
+{
+ struct Input {
+ io_buf_ptr_t bytes; mach_msg_type_number_t size;
+ ~Input() { if (bytes && size) vm_deallocate(mach_task_self(), (vm_address_t)bytes, size); }
+ } input {path_ool, path_oolCnt};
+ if (!result || !registry_entry) return kIOReturnBadArgument;
+ *registry_entry = MACH_PORT_NULL;
+ *result = kIOReturnBadArgument;
+ std::string_view fullPath;
+ if (path_oolCnt || path_ool) {
+ if (!path_ool || !path_oolCnt || path_oolCnt > 65536 || !path || path[0] ||
+ strnlen(path_ool, path_oolCnt) != path_oolCnt - 1) return kIOReturnSuccess;
+ fullPath = std::string_view(path_ool, path_oolCnt - 1);
+ } else {
+ if (!path || !memchr(path, 0, sizeof(io_string_inband_t))) return kIOReturnSuccess;
+ fullPath = path;
+ }
+ auto* entry = IORegistryEntry::fromPath(fullPath);
+ if (entry) { *registry_entry = entry->port(); *result = kIOReturnSuccess; }
+ else *result = kIOReturnNoDevice;
+ return kIOReturnSuccess;
+}
+
+kern_return_t is_io_registry_entry_get_path_ool(mach_port_t registry_entry, io_name_t plane,
+ io_string_inband_t path, io_buf_ptr_t* path_ool, mach_msg_type_number_t* path_oolCnt)
+{
+ if (!path || !path_ool || !path_oolCnt) return kIOReturnBadArgument;
+ path[0] = 0; *path_ool = nullptr; *path_oolCnt = 0;
+ auto* entry = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !plane || !memchr(plane, 0, sizeof(io_name_t)) || !entry->inPlane(plane)) return kIOReturnBadArgument;
+ auto value = entry->getPath(plane);
+ if (value == "?unsup_plane?") return kIOReturnBadArgument;
+ if (value.size() < sizeof(io_string_inband_t)) {
+ strlcpy(path, value.c_str(), sizeof(io_string_inband_t));
+ } else {
+ // MIG uses physicalcopy with deallocate=false. The main queue completes
+ // each reply before its next request, so one bounded reusable buffer
+ // keeps the sender's bytes alive without leaking one VM allocation/call.
+ static std::string reply;
+ reply.assign(value);
+ *path_ool = const_cast<char*>(reply.data());
+ *path_oolCnt = reply.size(); // IOKit creates a CFString from bytes, excluding NUL
+ }
+ return kIOReturnSuccess;
+}
+
+kern_return_t is_io_registry_entry_in_plane(mach_port_t registry_entry, io_name_t plane, boolean_t* inPlane)
+{
+ if (!inPlane) return kIOReturnBadArgument;
+ *inPlane = FALSE;
+ auto* entry = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !plane || !memchr(plane, 0, sizeof(io_name_t))) return kIOReturnBadArgument;
+ *inPlane = entry->inPlane(plane);
+ return kIOReturnSuccess;
+}
+
+kern_return_t is_io_registry_entry_get_property_bytes(mach_port_t registry_entry, io_name_t property_name,
+ io_struct_inband_t data, mach_msg_type_number_t* dataCnt)
+{
+ if (!dataCnt) return kIOReturnBadArgument;
+ size_t capacity = std::min<size_t>(*dataCnt, sizeof(io_struct_inband_t));
+ *dataCnt = 0;
+ auto* entry = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !property_name || !memchr(property_name, 0, sizeof(io_name_t))) return kIOReturnBadArgument;
+ NSString* key = [NSString stringWithUTF8String:property_name];
+ if (!key) return kIOReturnBadArgument;
+ id value = [entry->getProperties() objectForKey:key];
+ if (!value) return kIOReturnNoResources;
+ const void* bytes = nullptr;
+ size_t length = 0;
+ uint64_t number = 0;
+ if ([value isKindOfClass:[NSData class]]) {
+ bytes = [value bytes]; length = [value length];
+ } else if ([value isKindOfClass:[NSString class]]) {
+ bytes = [value UTF8String];
+ if (!bytes) return kIOReturnBadArgument;
+ length = strlen((const char*)bytes) + 1;
+ } else if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((CFTypeRef)value) == CFBooleanGetTypeID()) {
+ bytes = [value boolValue] ? "Yes" : "No";
+ length = [value boolValue] ? sizeof("Yes") : sizeof("No");
+ } else if ([value isKindOfClass:[NSNumber class]]) {
+ const char* type = [value objCType];
+ switch (type ? type[0] : 0) {
+ case 'c': case 'C': case 'B': length = 1; break;
+ case 's': case 'S': length = 2; break;
+ case 'i': case 'I': length = 4; break;
+ case 'l': case 'L': length = sizeof(long); break;
+ case 'q': case 'Q': length = 8; break;
+ default: return kIOReturnBadArgument;
+ }
+ number = [value unsignedLongLongValue]; bytes = &number;
+ } else return kIOReturnBadArgument;
+ if (length > std::numeric_limits<mach_msg_type_number_t>::max()) return kIOReturnNoSpace;
+ if (length > capacity) { *dataCnt = capacity; return kIOReturnIPCError; }
+ *dataCnt = length;
+ if (length && !data) { *dataCnt = 0; return kIOReturnBadArgument; }
+ if (length) memcpy(data, bytes, length);
+ return kIOReturnSuccess;
+}
diff --git a/src/IOSurfaceRoot.mm b/src/IOSurfaceRoot.mm
index d7e6fab..d860582 100644
--- a/src/IOSurfaceRoot.mm
+++ b/src/IOSurfaceRoot.mm
@@ -21,13 +21,17 @@
#include "iokitd.h"
#include "IOSurface.h"
#include "IODisplayConnectX11.h"
+#include <EGL/eglext.h>
#include <IOKit/IOCFUnserialize.h>
#include <stdexcept>
#include <iostream>
IOSurfaceRoot::IOSurfaceRoot()
{
- m_display = eglGetDisplay(IODisplayConnectX11::getDisplay());
+ // The native display is an Xlib Display. eglGetDisplay() would let Mesa
+ // guess the platform from EGL_PLATFORM, which Wayland sessions often set
+ // to "wayland"; Mesa then treats the Display as a wl_display and crashes.
+ m_display = eglGetPlatformDisplay(EGL_PLATFORM_X11_KHR, IODisplayConnectX11::getDisplay(), NULL);
if (m_display == EGL_NO_DISPLAY)
return;
diff --git a/src/PowerSources.mm b/src/PowerSources.mm
new file mode 100644
index 0000000..70ca9bd
--- /dev/null
+++ b/src/PowerSources.mm
@@ -0,0 +1,139 @@
+extern "C" {
+#include "powermanagementServer.h"
+}
+#import <Foundation/NSArray.h>
+#import <Foundation/NSDictionary.h>
+#import <Foundation/NSNumber.h>
+#import <Foundation/NSString.h>
+#import <Foundation/NSData.h>
+#import <Foundation/NSPropertyList.h>
+#include <fstream>
+#include <string>
+#include <cstring>
+#include <dirent.h>
+#include <mach/mach.h>
+#include <vector>
+
+// The guest has no /sys; the host's is exposed under the system root
+static const char* const kPowerSupplyDir = "/Volumes/SystemRoot/sys/class/power_supply";
+
+static bool readSysfs(const std::string& dir, const char* name, std::string& value)
+{
+ std::ifstream file(dir + "/" + name);
+ if (!file || !std::getline(file, value))
+ return false;
+ return true;
+}
+
+static bool readSysfsLong(const std::string& dir, const char* name, long long& value)
+{
+ std::string text;
+ if (!readSysfs(dir, name, text))
+ return false;
+ char* end = nullptr;
+ value = strtoll(text.c_str(), &end, 10);
+ return end != text.c_str();
+}
+
+static std::vector<std::string> powerSupplies(const char* wantedType)
+{
+ std::vector<std::string> result;
+ DIR* d = opendir(kPowerSupplyDir);
+ if (!d)
+ return result;
+
+ while (struct dirent* e = readdir(d))
+ {
+ if (e->d_name[0] == '.')
+ continue;
+ std::string dir = std::string(kPowerSupplyDir) + "/" + e->d_name;
+ std::string type;
+ if (readSysfs(dir, "type", type) && type == wantedType)
+ result.push_back(dir);
+ }
+ closedir(d);
+ return result;
+}
+
+static NSArray* copyInternalBatteries()
+{
+ bool onAC = false;
+ for (const std::string& ac : powerSupplies("Mains"))
+ {
+ long long online;
+ if (readSysfsLong(ac, "online", online) && online)
+ onAC = true;
+ }
+
+ NSMutableArray* sources = [NSMutableArray array];
+ for (const std::string& dir : powerSupplies("Battery"))
+ {
+ long long present = 1, capacity, energyNow, energyFull, powerNow, voltage;
+ std::string status;
+ readSysfsLong(dir, "present", present);
+ if (!present || !readSysfs(dir, "status", status) || !readSysfsLong(dir, "capacity", capacity))
+ continue;
+
+ NSMutableDictionary* ps = [NSMutableDictionary dictionary];
+ ps[@"Name"] = [NSString stringWithFormat: @"InternalBattery-%lu", (unsigned long) [sources count]];
+ ps[@"Type"] = @"InternalBattery";
+ ps[@"Transport Type"] = @"Internal";
+ ps[@"Power Source ID"] = @((int) [sources count]);
+ ps[@"Is Present"] = @YES;
+ ps[@"Current Capacity"] = @((int) capacity);
+ ps[@"Max Capacity"] = @100;
+ ps[@"Power Source State"] = onAC ? @"AC Power" : @"Battery Power";
+ ps[@"Is Charging"] = @(status == "Charging");
+ ps[@"Is Charged"] = @(status == "Full");
+
+ if (readSysfsLong(dir, "voltage_now", voltage))
+ ps[@"Voltage"] = @((int) (voltage / 1000)); // uV -> mV
+
+ // Remaining time in minutes from the energy and power draw reported by the host; -1 when unknown
+ long long minutes = -1;
+ if (readSysfsLong(dir, "power_now", powerNow) && powerNow > 0)
+ {
+ if (status == "Discharging" && readSysfsLong(dir, "energy_now", energyNow))
+ minutes = energyNow * 60 / powerNow;
+ else if (status == "Charging" && readSysfsLong(dir, "energy_now", energyNow) && readSysfsLong(dir, "energy_full", energyFull))
+ minutes = (energyFull - energyNow) * 60 / powerNow;
+ }
+ if (status == "Charging")
+ ps[@"Time to Full Charge"] = @((int) minutes);
+ else
+ ps[@"Time to Empty"] = @((int) minutes);
+
+ [sources addObject: ps];
+ }
+ return [sources copy];
+}
+
+kern_return_t _io_ps_copy_powersources_info
+(
+ mach_port_t server,
+ audit_token_t token,
+ int pstype,
+ vm_offset_t *powersources,
+ mach_msg_type_number_t *powersourcesCnt,
+ int *return_code
+)
+{
+ *powersources = 0;
+ *powersourcesCnt = 0;
+ *return_code = 0;
+
+ // The host only exposes internal batteries; there is no UPS data to report
+ NSArray* sources = copyInternalBatteries();
+ NSData* data = [NSPropertyListSerialization dataWithPropertyList: sources
+ format: NSPropertyListBinaryFormat_v1_0 options: 0 error: nil];
+ if (!data)
+ return KERN_FAILURE;
+
+ kern_return_t kr = vm_allocate(mach_task_self(), powersources, [data length], VM_FLAGS_ANYWHERE);
+ if (kr != KERN_SUCCESS)
+ return kr;
+
+ memcpy((void*) *powersources, [data bytes], [data length]);
+ *powersourcesCnt = [data length];
+ return KERN_SUCCESS;
+}
diff --git a/src/ServiceRegistry.h b/src/ServiceRegistry.h
index 34e74ad..0d0740b 100644
--- a/src/ServiceRegistry.h
+++ b/src/ServiceRegistry.h
@@ -32,6 +32,7 @@ public:
static ServiceRegistry* instance();
void registerService(IOService* service);
IOIterator* iteratorForMatchingServices(NSDictionary* criteria) const;
+ IOObject* firstMatchingService(NSDictionary* criteria) const;
private:
std::list<IOService*> m_registeredServices;
};
diff --git a/src/ServiceRegistry.mm b/src/ServiceRegistry.mm
index aaed046..7a190c3 100644
--- a/src/ServiceRegistry.mm
+++ b/src/ServiceRegistry.mm
@@ -24,6 +24,9 @@
#include <IOKit/IOReturn.h>
#include <os/log.h>
#include <stdexcept>
+#include <string>
+#include <cstring>
+#include <vector>
#include "IOIterator.h"
extern "C" {
@@ -49,6 +52,16 @@ IOIterator* ServiceRegistry::iteratorForMatchingServices(NSDictionary* criteria)
return new IOIterator(matching);
}
+IOObject* ServiceRegistry::firstMatchingService(NSDictionary* criteria) const
+{
+ for (IOService* service : m_registeredServices)
+ {
+ if (service->matches(criteria))
+ return service;
+ }
+ return nullptr;
+}
+
void ServiceRegistry::registerService(IOService* service)
{
m_registeredServices.push_back(service);
@@ -118,3 +131,91 @@ kern_return_t is_io_service_get_matching_services
// Old unsupported API
return kIOReturnUnsupported;
}
+
+static NSDictionary* unserializeMatching(const char* caller, io_struct_inband_t matching, mach_msg_type_number_t matchingCnt)
+{
+ CFStringRef errorString = nullptr;
+ CFTypeRef criteria = IOCFUnserializeBinary(matching, matchingCnt, nullptr, 0, &errorString);
+
+ if (!criteria)
+ throwCFStringException(CFSTR("cannot parse 'matching': %@"), errorString);
+
+ if (CFGetTypeID(criteria) != CFDictionaryGetTypeID())
+ {
+ CFRelease(criteria);
+ throw std::runtime_error(std::string(caller) + "(): dictionary expected");
+ }
+
+ return (NSDictionary*) criteria;
+}
+
+kern_return_t is_io_service_get_matching_service_bin
+(
+ mach_port_t master_port,
+ io_struct_inband_t matching,
+ mach_msg_type_number_t matchingCnt,
+ mach_port_t *service
+)
+{
+ *service = MACH_PORT_NULL;
+
+ try
+ {
+ NSDictionary* criteria = unserializeMatching("io_service_get_matching_service_bin", matching, matchingCnt);
+ IOObject* match = ServiceRegistry::instance()->firstMatchingService(criteria);
+ CFRelease(criteria);
+
+ if (!match)
+ return kIOReturnNotFound;
+
+ *service = match->port();
+ return kIOReturnSuccess;
+ }
+ catch (const std::exception& e)
+ {
+ os_log_error(OS_LOG_DEFAULT, "is_io_service_get_matching_service_bin: %s", e.what());
+ return kIOReturnBadArgument;
+ }
+}
+
+kern_return_t is_io_service_add_notification_bin_64
+(
+ mach_port_t master_port,
+ io_name_t notification_type,
+ io_struct_inband_t matching,
+ mach_msg_type_number_t matchingCnt,
+ mach_port_t wake_port,
+ io_async_ref64_t reference,
+ mach_msg_type_number_t referenceCnt,
+ mach_port_t *notification
+)
+{
+ *notification = MACH_PORT_NULL;
+
+ try
+ {
+ NSDictionary* criteria = unserializeMatching("io_service_add_notification_bin_64", matching, matchingCnt);
+ IOIterator* iterator;
+
+ // The registry is fixed once iokitd has started, so no service is ever published or
+ // terminated later. The notification object is the iterator of the services that match
+ // now (as in IOKit for the publish/matched types); termination types start out empty.
+ if (!strcmp(notification_type, "IOServicePublish") ||
+ !strcmp(notification_type, "IOServiceFirstMatch") ||
+ !strcmp(notification_type, "IOServiceMatched"))
+ iterator = ServiceRegistry::instance()->iteratorForMatchingServices(criteria);
+ else
+ iterator = new IOIterator(std::vector<IOObject*>());
+ CFRelease(criteria);
+
+ *notification = iterator->port();
+ iterator->releaseLater();
+
+ return kIOReturnSuccess;
+ }
+ catch (const std::exception& e)
+ {
+ os_log_error(OS_LOG_DEFAULT, "is_io_service_add_notification_bin_64: %s", e.what());
+ return kIOReturnBadArgument;
+ }
+}
diff --git a/src/main.mm b/src/main.mm
index 37ce9b8..e23aea0 100644
--- a/src/main.mm
+++ b/src/main.mm
@@ -32,6 +32,8 @@
#include "IODisplayConnectX11.h"
#include "PowerAssertions.h"
#include "IOSurfaceRoot.h"
+#include "IOPlatformExpertDevice.h"
+#include "IOPMrootDomain.h"
extern "C" {
#include "iokitmigServer.h"
@@ -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..c2d4063 100644
--- a/src/stubs-powermanagement.c
+++ b/src/stubs-powermanagement.c
@@ -131,25 +131,6 @@ kern_return_t _io_pm_last_wake_time
}
-/* Routine io_pm_assertion_set_properties */
-
-kern_return_t _io_pm_assertion_set_properties
-(
- mach_port_t server,
- audit_token_t token,
- int assertion_id,
- vm_offset_t props,
- mach_msg_type_number_t propsCnt,
- int *disableAppSleep,
- int *enableAppSleep,
- int *enTrIntensity,
- int *return_code
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
-
/* Routine io_pm_assertion_copy_details */
kern_return_t _io_pm_assertion_copy_details
@@ -368,22 +349,6 @@ kern_return_t _io_ps_release_pspowersource
}
-/* Routine io_ps_copy_powersources_info */
-
-kern_return_t _io_ps_copy_powersources_info
-(
- mach_port_t server,
- audit_token_t token,
- int pstype,
- vm_offset_t *powersources,
- mach_msg_type_number_t *powersourcesCnt,
- int *return_code
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
-
/* Routine io_ps_copy_chargelog */
diff --git a/src/stubs.c b/src/stubs.c
index 91d11a1..5a00091 100644
--- a/src/stubs.c
+++ b/src/stubs.c
@@ -33,26 +33,7 @@ kern_return_t is_io_registry_iterator_exit_entry
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_registry_entry_get_property_bytes
-(
- mach_port_t registry_entry,
- io_name_t property_name,
- io_struct_inband_t data,
- mach_msg_type_number_t *dataCnt
-)
-{
- STUB();
- 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
(
@@ -124,16 +105,7 @@ kern_return_t is_io_registry_entry_set_properties
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_registry_entry_in_plane
-(
- mach_port_t registry_entry,
- io_name_t plane,
- boolean_t *inPlane
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
+
kern_return_t is_io_service_get_busy_state
(
@@ -325,20 +297,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 +397,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 +475,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 +570,6 @@ kern_return_t is_io_server_version
}
-kern_return_t is_io_service_get_matching_service_bin
-(
- mach_port_t master_port,
- io_struct_inband_t matching,
- mach_msg_type_number_t matchingCnt,
- mach_port_t *service
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
-
kern_return_t is_io_service_match_property_table_bin
(
mach_port_t service,
@@ -682,48 +598,9 @@ kern_return_t is_io_service_add_notification_bin
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_service_add_notification_bin_64
-(
- mach_port_t master_port,
- io_name_t notification_type,
- io_struct_inband_t matching,
- mach_msg_type_number_t matchingCnt,
- 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_registry_entry_from_path_ool
-(
- mach_port_t master_port,
- io_string_inband_t path,
- io_buf_ptr_t path_ool,
- mach_msg_type_number_t path_oolCnt,
- kern_return_t *result,
- mach_port_t *registry_entry
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
-kern_return_t is_io_registry_entry_get_path_ool
- (
- mach_port_t registry_entry,
- io_name_t plane,
- io_string_inband_t path,
- io_buf_ptr_t *path_ool,
- mach_msg_type_number_t *path_oolCnt
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
+
kern_return_t is_io_device_tree_entry_exists_with_name
(
@@ -736,31 +613,3 @@ kern_return_t is_io_device_tree_entry_exists_with_name
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_registry_entry_get_properties_bin_buf
-(
- mach_port_t registry_entry,
- mach_vm_address_t buf,
- mach_vm_size_t *bufsize,
- io_buf_ptr_t *properties,
- mach_msg_type_number_t *propertiesCnt
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
-
-kern_return_t is_io_registry_entry_get_property_bin_buf
-(
- mach_port_t registry_entry,
- io_name_t plane,
- io_name_t property_name,
- uint32_t options,
- mach_vm_address_t buf,
- mach_vm_size_t *bufsize,
- io_buf_ptr_t *properties,
- mach_msg_type_number_t *propertiesCnt
-)
-{
- STUB();
- return KERN_NOT_SUPPORTED;
-}
diff --git a/src/IOPlatformExpertDevice.h b/src/IOPlatformExpertDevice.h
new file mode 100644
index 0000000..38ad635
--- /dev/null
+++ b/src/IOPlatformExpertDevice.h
@@ -0,0 +1,21 @@
+#ifndef IOKITD_IOPLATFORMEXPERTDEVICE_H
+#define IOKITD_IOPLATFORMEXPERTDEVICE_H
+
+#include "IOService.h"
+#include "ServiceRegistry.h"
+
+// The root platform device (what IOServiceMatching("IOPlatformExpertDevice") finds on macOS).
+// Its model strings name a fixed MacBookPro16,1. IOPlatformUUID and IOPlatformSerialNumber are
+// derived from the host machine-id, so they differ per host and stay stable across reinstalls.
+class IOPlatformExpertDevice : public IOService
+{
+public:
+ const char* className() const override;
+ NSDictionary* matchingDictionary() override;
+ NSDictionary* getProperties() override;
+ bool conformsTo(const char* className) override;
+
+ static void registerSelf(ServiceRegistry* targetServiceRegistry);
+};
+
+#endif
diff --git a/src/IOPlatformExpertDevice.mm b/src/IOPlatformExpertDevice.mm
new file mode 100644
index 0000000..71b9f4d
--- /dev/null
+++ b/src/IOPlatformExpertDevice.mm
@@ -0,0 +1,98 @@
+#include "IOPlatformExpertDevice.h"
+#include "PlatformIdentity.h"
+#import <Foundation/NSString.h>
+#import <Foundation/NSData.h>
+#import <Foundation/NSArray.h>
+#import <Foundation/NSCharacterSet.h>
+#import <Foundation/NSDictionary.h>
+#include <CommonCrypto/CommonDigest.h>
+#include <uuid/uuid.h>
+#include <unistd.h>
+#include <cstring>
+
+// Platform expert strings are NUL-terminated data
+static NSData* dataForString(NSString* string)
+{
+ const char* utf8 = [string UTF8String];
+ return [NSData dataWithBytes: utf8 length: strlen(utf8) + 1];
+}
+
+const char* IOPlatformExpertDevice::className() const
+{
+ return "IOPlatformExpertDevice";
+}
+
+NSDictionary* IOPlatformExpertDevice::matchingDictionary()
+{
+ return @{
+ @"IOProviderClass": @"IOPlatformExpertDevice",
+ @"IOClass": @"IOPlatformExpertDevice",
+ };
+}
+
+// Per-host identifiers, stable like a real Mac's: a SHA-256 of the host machine-id and a fixed
+// string, so the raw machine-id (which machine-id(5) says not to expose) never reaches apps.
+// The serial's "DARLING-" form is longer than any Apple serial and cannot match a real Mac.
+static bool hostIdentifiers(NSString** uuid, NSString** serial)
+{
+ static const char app[] = "darling-lol IOPlatformExpertDevice";
+ uuid_t machineID;
+ struct timespec wait = { 0, 0 };
+ if (gethostuuid(machineID, &wait) != 0)
+ return false;
+
+ unsigned char input[sizeof(machineID) + sizeof(app)];
+ std::memcpy(input, machineID, sizeof(machineID));
+ std::memcpy(input + sizeof(machineID), app, sizeof(app));
+ unsigned char hash[CC_SHA256_DIGEST_LENGTH];
+ CC_SHA256(input, sizeof(input), hash);
+
+ hash[6] = (hash[6] & 0x0f) | 0x40; // version 4
+ hash[8] = (hash[8] & 0x3f) | 0x80; // RFC 4122 variant
+ uuid_string_t text;
+ uuid_unparse_upper(hash, text);
+ // Owned (not autoreleased): the caller keeps them in statics for the daemon's lifetime
+ *uuid = [[NSString alloc] initWithUTF8String: text];
+ *serial = [[NSString alloc] initWithFormat: @"DARLING-%02X%02X%02X%02X", hash[16], hash[17], hash[18], hash[19]];
+ return true;
+}
+
+NSDictionary* IOPlatformExpertDevice::getProperties()
+{
+ // MacBookPro16,1 model strings; the Linux DMI values describe the host hardware, not the Mac we emulate
+ NSMutableDictionary* properties = [NSMutableDictionary dictionaryWithDictionary: @{
+ @"manufacturer": dataForString(@"Apple Inc."),
+ @"model": dataForString(@"MacBookPro16,1"),
+ @"product-name": dataForString(@"MacBookPro16,1"),
+ @"board-id": dataForString(@"Mac-E1008331FDC96864"),
+ }];
+
+ static NSString* uuid;
+ static NSString* serial;
+ static bool known = hostIdentifiers(&uuid, &serial);
+ if (known)
+ {
+ properties[@"IOPlatformUUID"] = uuid;
+ properties[@"IOPlatformSerialNumber"] = serial;
+ }
+ // Opt-in, immutable startup snapshot. No configuration preserves the existing
+ // per-host identifiers; daemon restart is the explicit activation boundary.
+ static NSDictionary* overrides = [DarlingPlatformIdentity::load("/private/etc/darling-platform.plist") copy];
+ if (overrides) [properties addEntriesFromDictionary:overrides];
+ return properties;
+}
+
+bool IOPlatformExpertDevice::conformsTo(const char* className)
+{
+ if (std::strcmp(className, "IOPlatformDevice") == 0)
+ return true;
+ return IOService::conformsTo(className);
+}
+
+void IOPlatformExpertDevice::registerSelf(ServiceRegistry* targetServiceRegistry)
+{
+ static IOPlatformExpertDevice* device = new IOPlatformExpertDevice;
+
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef IOKITD_IOINTERESTNOTIFICATION_H
+#define IOKITD_IOINTERESTNOTIFICATION_H
+#include "IOObject.h"
+#include <IOKit/OSMessageNotification.h>
+#include <string>
+#include <deque>
+
+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<Message> 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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "IOInterestNotification.h"
+#include "IOService.h"
+#include "iokitd.h"
+#include <IOKit/IOReturn.h>
+#include <os/log.h>
+#include <algorithm>
+#include <cstddef>
+#include <cstring>
+#include <new>
+
+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<InterestMessageHead*>(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<IOInterestNotification*>(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<IOInterestNotification*>(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<IOService*>(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 <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef IOKITD_IOPMROOTDOMAIN_H
+#define IOKITD_IOPMROOTDOMAIN_H
+
+#include "IOUserClient.h"
+#include "ServiceRegistry.h"
+#include <map>
+
+struct iokitd_logind;
+
+// The power management root (IOPower:/IOPowerConnection/IOPMrootDomain).
+// IORegisterForSystemPower() opens it and registers for
+// kIOAppPowerStateInterest; host suspend and resume, as announced by
+// systemd-logind, are passed on as kIOMessageSystemWillSleep (which clients
+// acknowledge with IOAllowPowerChange()), kIOMessageSystemWillPowerOn and
+// kIOMessageSystemHasPoweredOn.
+class IOPMrootDomain : public IOService
+{
+public:
+ static IOPMrootDomain* instance();
+ static void registerSelf(ServiceRegistry* targetServiceRegistry);
+
+ const char* className() const override;
+ NSDictionary* matchingDictionary() override;
+ NSDictionary* getProperties() override;
+ bool conformsTo(const char* className) override;
+ IOReturn newUserClient(uint32_t type, IOUserClient** client) override;
+ bool acceptsInterest(const char* type) override;
+
+ // IOAllowPowerChange() / IOCancelPowerChange()
+ IOReturn allowPowerChange(uint64_t notificationID);
+ IOReturn cancelPowerChange(uint64_t notificationID);
+
+ // Follows host sleep through logind, without blocking the caller.
+ void startHostSleepMonitor();
+
+ // Main queue
+ void systemWillSleep(uint64_t cycle);
+ void systemDidWake(uint64_t cycle);
+protected:
+ void interestRemoved(IOInterestNotification* notification) override;
+private:
+ IOPMrootDomain() = default;
+ uint32_t nextNotificationID(uint16_t index);
+ void tellApps(uint32_t messageType);
+ void checkSleepReady();
+
+ struct iokitd_logind* m_logind = nullptr;
+ bool m_sleeping = false;
+ bool m_sleepReadySent = false;
+ uint64_t m_cycle = 0;
+ uint16_t m_serial = 0;
+ // Outstanding kIOMessageSystemWillSleep acknowledgements by notification ID
+ std::map<uint32_t, IOInterestNotification*> m_pendingAcks;
+};
+
+// The connection returned by IOServiceOpen() on the root domain
+class RootDomainUserClient : public IOUserClient
+{
+public:
+ RootDomainUserClient(IOPMrootDomain* owner);
+
+ const char* className() const override;
+ NSDictionary* matchingDictionary() override;
+ IOExternalMethod* getExternalMethodForIndex(UInt32 index) override;
+ IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments* arguments) override;
+private:
+ IOPMrootDomain* m_owner;
+};
+
+#endif
diff --git a/src/IOPMrootDomain.mm b/src/IOPMrootDomain.mm
new file mode 100644
index 0000000..9b6d30b
--- /dev/null
+++ b/src/IOPMrootDomain.mm
@@ -0,0 +1,342 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "IOPMrootDomain.h"
+#include "IOInterestNotification.h"
+#include "LogindSleep.h"
+#include <IOKit/IOKitKeys.h>
+#include <IOKit/IOMessage.h>
+#include <IOKit/pwr_mgt/IOPMLibDefs.h>
+#include <dispatch/dispatch.h>
+#include <elfcalls.h>
+#include <os/log.h>
+#include <cstdlib>
+#include <cstring>
+#import <Foundation/NSString.h>
+
+extern "C" struct elf_calls* _elfcalls;
+
+// The IOPower plane parent of the root domain, as on macOS
+class IOPowerConnection : public IORegistryEntry
+{
+public:
+ const char* className() const override { return "IOPowerConnection"; }
+};
+
+IOPMrootDomain* IOPMrootDomain::instance()
+{
+ static IOPMrootDomain* domain = new IOPMrootDomain;
+ return domain;
+}
+
+void IOPMrootDomain::registerSelf(ServiceRegistry* targetServiceRegistry)
+{
+ IOPMrootDomain* domain = instance();
+ static IOPowerConnection* connection = new IOPowerConnection;
+
+ targetServiceRegistry->registerService(domain);
+ domain->registerInPlane(kIOServicePlane, "IOPMrootDomain", IORegistryEntry::root());
+ connection->registerInPlane(kIOPowerPlane, "IOPowerConnection", IORegistryEntry::root());
+ domain->registerInPlane(kIOPowerPlane, "IOPMrootDomain", connection);
+}
+
+const char* IOPMrootDomain::className() const
+{
+ return "IOPMrootDomain";
+}
+
+NSDictionary* IOPMrootDomain::matchingDictionary()
+{
+ return @{
+ @"IOProviderClass": @"IOPMrootDomain",
+ @"IOClass": @"IOPMrootDomain",
+ };
+}
+
+NSDictionary* IOPMrootDomain::getProperties()
+{
+ return @{
+ @"IOClass": @"IOPMrootDomain",
+ @"IOUserClientClass": @"RootDomainUserClient",
+ };
+}
+
+bool IOPMrootDomain::conformsTo(const char* className)
+{
+ if (std::strcmp(className, "IOPMrootDomain") == 0)
+ return true;
+ return IOService::conformsTo(className);
+}
+
+IOReturn IOPMrootDomain::newUserClient(uint32_t type, IOUserClient** client)
+{
+ try
+ {
+ *client = new RootDomainUserClient(this);
+ return kIOReturnSuccess;
+ }
+ catch (const std::exception& e)
+ {
+ os_log_error(OS_LOG_DEFAULT, "iokitd: cannot open IOPMrootDomain: %s", e.what());
+ *client = nullptr;
+ return kIOReturnNoResources;
+ }
+}
+
+bool IOPMrootDomain::acceptsInterest(const char* type)
+{
+ // General interest messages (clamshell, thermal...) are never sent here,
+ // but registering for them is valid.
+ return std::strcmp(type, kIOAppPowerStateInterest) == 0
+ || std::strcmp(type, kIOGeneralInterest) == 0;
+}
+
+// Like xnu: the change serial number in the upper half, the client in the lower one.
+uint32_t IOPMrootDomain::nextNotificationID(uint16_t index)
+{
+ return (uint32_t(m_serial) << 16) | index;
+}
+
+void IOPMrootDomain::tellApps(uint32_t messageType)
+{
+ uint16_t index = 0;
+
+ m_serial++;
+ for (IOInterestNotification* n : interests(kIOAppPowerStateInterest))
+ {
+ uint32_t id = messageType == kIOMessageSystemWillSleep ? nextNotificationID(++index) : 0;
+
+ if (n->deliver(messageType, id) == KERN_SUCCESS && messageType == kIOMessageSystemWillSleep)
+ m_pendingAcks[id] = n;
+ }
+}
+
+void IOPMrootDomain::systemWillSleep(uint64_t cycle)
+{
+ os_log(OS_LOG_DEFAULT, "iokitd: host is going to sleep, notifying applications");
+
+ m_cycle = cycle;
+ m_sleeping = true;
+ m_sleepReadySent = false;
+ m_pendingAcks.clear();
+
+ // kIOMessageCanSystemSleep is not sent: logind only announces a suspend
+ // that has already been decided, so there is nothing applications could veto.
+ tellApps(kIOMessageSystemWillSleep);
+ checkSleepReady();
+}
+
+void IOPMrootDomain::systemDidWake(uint64_t cycle)
+{
+ os_log(OS_LOG_DEFAULT, "iokitd: host has woken up, notifying applications");
+
+ m_sleeping = false;
+ m_pendingAcks.clear();
+
+ tellApps(kIOMessageSystemWillPowerOn);
+ tellApps(kIOMessageSystemHasPoweredOn);
+}
+
+void IOPMrootDomain::checkSleepReady()
+{
+ if (!m_sleeping || m_sleepReadySent || !m_pendingAcks.empty() || !m_logind)
+ return;
+
+ m_sleepReadySent = true;
+ iokitd_logind_sleep_ready(m_logind, m_cycle);
+}
+
+IOReturn IOPMrootDomain::allowPowerChange(uint64_t notificationID)
+{
+ // Unknown or late IDs are ignored, as on macOS.
+ m_pendingAcks.erase(uint32_t(notificationID));
+ checkSleepReady();
+ return kIOReturnSuccess;
+}
+
+IOReturn IOPMrootDomain::cancelPowerChange(uint64_t notificationID)
+{
+ // Only an idle sleep query (kIOMessageCanSystemSleep) can be cancelled,
+ // and those are never sent. Treat it as an acknowledgement, like macOS
+ // does for kIOMessageSystemWillSleep.
+ return allowPowerChange(notificationID);
+}
+
+void IOPMrootDomain::interestRemoved(IOInterestNotification* notification)
+{
+ for (auto it = m_pendingAcks.begin(); it != m_pendingAcks.end(); )
+ {
+ if (it->second == notification)
+ it = m_pendingAcks.erase(it);
+ else
+ ++it;
+ }
+ checkSleepReady();
+}
+
+// Host libsystemd, loaded through the ELF loader. iokitd shares the host's
+// user and network, so the system bus socket is reachable directly.
+
+static void logindLog(const char* message)
+{
+ os_log(OS_LOG_DEFAULT, "iokitd: %{public}s", message);
+}
+
+static bool loadLogindApi(struct iokitd_logind_api* api)
+{
+ void *systemd, *libc;
+
+ if (_elfcalls == nullptr)
+ return false;
+ systemd = _elfcalls->dlopen("libsystemd.so.0");
+ if (systemd == nullptr)
+ return false;
+ libc = _elfcalls->dlopen("libc.so.6");
+ if (libc == nullptr)
+ {
+ _elfcalls->dlclose(systemd);
+ return false;
+ }
+
+ std::memset(api, 0, sizeof(*api));
+#define BIND(lib, field) \
+ if ((*(void**) &api->field = _elfcalls->dlsym(lib, #field)) == nullptr) \
+ goto fail;
+ BIND(systemd, sd_bus_new);
+ BIND(systemd, sd_bus_set_address);
+ BIND(systemd, sd_bus_set_bus_client);
+ BIND(systemd, sd_bus_start);
+ BIND(systemd, sd_bus_set_method_call_timeout);
+ BIND(systemd, sd_bus_flush_close_unref);
+ BIND(systemd, sd_bus_match_signal);
+ BIND(systemd, sd_bus_call_method);
+ BIND(systemd, sd_bus_get_property_trivial);
+ BIND(systemd, sd_bus_message_read);
+ BIND(systemd, sd_bus_message_unref);
+ BIND(systemd, sd_bus_slot_unref);
+ BIND(systemd, sd_bus_error_free);
+ BIND(systemd, sd_bus_process);
+ BIND(systemd, sd_bus_wait);
+ // The inhibitor is a host descriptor; use the host's calls on it.
+ BIND(libc, fcntl);
+ BIND(libc, close);
+#undef BIND
+ api->getenv = getenv;
+ api->log = logindLog;
+ // The monitor and both libraries live until process exit, which closes
+ // the host bus and inhibitor descriptors. stop() is used by native tests.
+ return true;
+
+fail:
+ _elfcalls->dlclose(libc);
+ _elfcalls->dlclose(systemd);
+ return false;
+}
+
+const struct iokitd_logind_api* iokitd_host_power_api(void)
+{
+ static struct iokitd_logind_api api;
+ static bool available;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{ available = loadLogindApi(&api); });
+ return available ? &api : nullptr;
+}
+
+static void logindWillSleep(void* context, uint64_t cycle)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ IOPMrootDomain::instance()->systemWillSleep(cycle);
+ });
+}
+
+static void logindDidWake(void* context, uint64_t cycle)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ IOPMrootDomain::instance()->systemDidWake(cycle);
+ });
+}
+
+void IOPMrootDomain::startHostSleepMonitor()
+{
+ // Connecting and taking the inhibitor are blocking D-Bus calls.
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ const struct iokitd_logind_api* api = iokitd_host_power_api();
+ static const struct iokitd_logind_callbacks callbacks = { logindWillSleep, logindDidWake };
+ struct iokitd_logind* logind = nullptr;
+
+ if (!api)
+ {
+ os_log(OS_LOG_DEFAULT, "iokitd: host libsystemd not available, no sleep/wake notifications");
+ return;
+ }
+
+ int r = iokitd_logind_start(api, &callbacks, nullptr, &logind);
+ if (r < 0)
+ {
+ os_log(OS_LOG_DEFAULT, "iokitd: logind sleep monitor not started (%d), no sleep/wake notifications", r);
+ return;
+ }
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ IOPMrootDomain* domain = IOPMrootDomain::instance();
+ domain->m_logind = logind;
+ // A suspend may have been announced before we got here.
+ domain->checkSleepReady();
+ });
+ });
+}
+
+RootDomainUserClient::RootDomainUserClient(IOPMrootDomain* owner)
+: m_owner(owner)
+{
+}
+
+const char* RootDomainUserClient::className() const
+{
+ return "RootDomainUserClient";
+}
+
+NSDictionary* RootDomainUserClient::matchingDictionary()
+{
+ return @{};
+}
+
+IOExternalMethod* RootDomainUserClient::getExternalMethodForIndex(UInt32 index)
+{
+ return nullptr;
+}
+
+IOReturn RootDomainUserClient::externalMethod(uint32_t selector, IOExternalMethodArguments* arguments)
+{
+ arguments->scalarOutputCount = 0;
+ arguments->structureOutputSize = 0;
+
+ switch (selector)
+ {
+ case kPMAllowPowerChange:
+ case kPMCancelPowerChange:
+ if (arguments->scalarInputCount != 1)
+ return kIOReturnBadArgument;
+ if (selector == kPMAllowPowerChange)
+ return m_owner->allowPowerChange(arguments->scalarInput[0]);
+ return m_owner->cancelPowerChange(arguments->scalarInput[0]);
+ default:
+ return kIOReturnUnsupported;
+ }
+}
diff --git a/src/IOService.h b/src/IOService.h
index ab8f5c3..08d34e7 100644
--- a/src/IOService.h
+++ b/src/IOService.h
@@ -23,6 +23,11 @@
#include "IORegistryEntry.h"
#include <IOKit/IOKitLib.h>
#include <IOKit/IOKitLibPrivate.h>
+#include <list>
+#include <vector>
+
+class IOInterestNotification;
+class IOUserClient;
class IOService : public IORegistryEntry
{
@@ -32,10 +37,23 @@ public:
bool matches(NSDictionary* dict);
bool conformsTo(const char* className) override;
+ // IOServiceOpen(). On success *client holds the reference handed to the caller.
+ virtual IOReturn newUserClient(uint32_t type, IOUserClient** client);
+
+ // IOServiceAddInterestNotification(). Services opt in per interest type.
+ virtual bool acceptsInterest(const char* type);
+ void addInterest(IOInterestNotification* notification);
+ void removeInterest(IOInterestNotification* notification);
+ std::vector<IOInterestNotification*> interests(const char* type) const;
+
uint32_t busyState() const { return m_busyState; }
uint64_t accumulatedBusyTime() const { return m_accumulatedBusyTime; }
uint64_t state() const { return m_state; }
protected:
+ // Called after a client released its interest notification
+ virtual void interestRemoved(IOInterestNotification* notification) {}
+
+ std::list<IOInterestNotification*> m_interests;
uint32_t m_busyState = 0;
uint64_t m_accumulatedBusyTime = 0;
uint64_t m_state = kIOServiceRegisteredState | kIOServiceMatchedState;
diff --git a/src/IOService.mm b/src/IOService.mm
index 0316b30..25904fa 100644
--- a/src/IOService.mm
+++ b/src/IOService.mm
@@ -18,6 +18,11 @@
*/
#include "IOService.h"
+#include "IOInterestNotification.h"
+#include "IOUserClient.h"
+#include <algorithm>
+#include <cstring>
+#include <os/log.h>
#import <Foundation/NSArray.h>
extern "C" {
@@ -71,6 +76,92 @@ bool IOService::conformsTo(const char* className)
return IOObject::conformsTo(className);
}
+IOReturn IOService::newUserClient(uint32_t type, IOUserClient** client)
+{
+ *client = nullptr;
+ return kIOReturnUnsupported;
+}
+
+bool IOService::acceptsInterest(const char* type)
+{
+ return false;
+}
+
+void IOService::addInterest(IOInterestNotification* notification)
+{
+ m_interests.push_back(notification);
+}
+
+void IOService::removeInterest(IOInterestNotification* notification)
+{
+ m_interests.remove(notification);
+ interestRemoved(notification);
+}
+
+std::vector<IOInterestNotification*> IOService::interests(const char* type) const
+{
+ std::vector<IOInterestNotification*> result;
+ for (IOInterestNotification* notification : m_interests)
+ {
+ if (notification->interestType() == type)
+ result.push_back(notification);
+ }
+ return result;
+}
+
+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
+)
+{
+ *connection = MACH_PORT_NULL;
+
+ // The rights and memory in the request are ours once we return success.
+ if (properties)
+ vm_deallocate(mach_task_self(), (vm_address_t) properties, propertiesCnt);
+ if (MACH_PORT_VALID(owningTask))
+ mach_port_deallocate(mach_task_self(), owningTask);
+
+ IOService* s = dynamic_cast<IOService*>(IOObject::lookup(service));
+ if (!s)
+ {
+ *result = kIOReturnBadArgument;
+ return KERN_SUCCESS;
+ }
+
+ IOUserClient* client = nullptr;
+ *result = s->newUserClient(connect_type, &client);
+
+ if (*result == kIOReturnSuccess && client)
+ {
+ *connection = client->port();
+ // The reply carries a copy of the send right; drop ours afterwards.
+ client->releaseLater();
+ }
+ else if (*result == kIOReturnSuccess)
+ *result = kIOReturnUnsupported;
+
+ return KERN_SUCCESS;
+}
+
+kern_return_t is_io_service_close
+(
+ mach_port_t connection
+)
+{
+ // The connection goes away once the caller releases its last reference.
+ if (!dynamic_cast<IOUserClient*>(IOObject::lookup(connection)))
+ return kIOReturnBadArgument;
+ return kIOReturnSuccess;
+}
+
kern_return_t is_io_service_get_state
(
mach_port_t service,
diff --git a/src/IOUserClient.h b/src/IOUserClient.h
index 82c10db..adeb966 100644
--- a/src/IOUserClient.h
+++ b/src/IOUserClient.h
@@ -28,7 +28,8 @@
class IOUserClient : public IOService
{
public:
- IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments *arguments);
+ // The default dispatches through getExternalMethodForIndex()
+ virtual IOReturn externalMethod(uint32_t selector, IOExternalMethodArguments *arguments);
IOExternalMethod *getTargetAndMethodForIndex(IOService **targetP, UInt32 index);
virtual IOExternalMethod *getExternalMethodForIndex(UInt32 index) = 0;
};
diff --git a/src/LogindSleep.c b/src/LogindSleep.c
new file mode 100644
index 0000000..1d6aa79
--- /dev/null
+++ b/src/LogindSleep.c
@@ -0,0 +1,455 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "LogindSleep.h"
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define LOGIND_NAME "org.freedesktop.login1"
+#define LOGIND_PATH "/org/freedesktop/login1"
+#define LOGIND_MANAGER "org.freedesktop.login1.Manager"
+
+#define USEC_PER_SEC 1000000ULL
+/* logind's default InhibitDelayMaxSec */
+#define DEFAULT_LOGIND_DELAY_USEC (5 * USEC_PER_SEC)
+/* What macOS gives applications to answer kIOMessageSystemWillSleep */
+#define MAX_ACK_TIMEOUT_USEC (30 * USEC_PER_SEC)
+#define MIN_ACK_TIMEOUT_USEC (USEC_PER_SEC / 10)
+/* Upper bound on how long the bus thread sleeps before checking deadlines and stop */
+#define POLL_INTERVAL_USEC (200 * 1000ULL)
+#define CALL_TIMEOUT_USEC (5 * USEC_PER_SEC)
+
+struct iokitd_logind
+{
+ struct iokitd_logind_api api;
+ struct iokitd_logind_callbacks callbacks;
+ void* context;
+
+ void* bus;
+ void* slot;
+ void* owner_slot;
+ char* address;
+ pthread_t thread;
+ int thread_started;
+ uint64_t ack_timeout_usec;
+
+ /* Shared with iokitd_logind_sleep_ready() */
+ pthread_mutex_t lock;
+ int inhibitor_fd;
+ int sleeping;
+ uint64_t cycle;
+ uint64_t deadline_usec;
+ int stop;
+
+ /* Bus thread only */
+ int want_inhibitor;
+};
+
+static uint64_t now_usec(void)
+{
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (uint64_t) ts.tv_sec * USEC_PER_SEC + (uint64_t) ts.tv_nsec / 1000;
+}
+
+static void log_message(struct iokitd_logind* l, const char* format, ...)
+ __attribute__((format(printf, 2, 3)));
+
+static void log_message(struct iokitd_logind* l, const char* format, ...)
+{
+ char buffer[256];
+ va_list ap;
+
+ if (l->api.log == NULL)
+ return;
+ va_start(ap, format);
+ vsnprintf(buffer, sizeof(buffer), format, ap);
+ va_end(ap);
+ l->api.log(buffer);
+}
+
+static int api_complete(const struct iokitd_logind_api* api)
+{
+ return api != NULL && api->sd_bus_new && api->sd_bus_set_address && api->sd_bus_set_bus_client
+ && api->sd_bus_start && api->sd_bus_set_method_call_timeout && api->sd_bus_flush_close_unref && api->sd_bus_match_signal
+ && api->sd_bus_call_method && api->sd_bus_get_property_trivial && api->sd_bus_message_read
+ && api->sd_bus_message_unref && api->sd_bus_slot_unref && api->sd_bus_error_free
+ && api->sd_bus_process && api->sd_bus_wait && api->fcntl && api->close && api->getenv;
+}
+
+/* Must hold l->lock. */
+static void close_inhibitor_locked(struct iokitd_logind* l)
+{
+ if (l->inhibitor_fd >= 0)
+ {
+ l->api.close(l->inhibitor_fd);
+ l->inhibitor_fd = -1;
+ }
+}
+
+/* Bus thread (or start, before the thread exists). */
+static void take_inhibitor(struct iokitd_logind* l)
+{
+ struct iokitd_bus_error error = { NULL, NULL, 0 };
+ void* reply = NULL;
+ int fd = -1, own = -1, r;
+
+ r = l->api.sd_bus_call_method(l->bus, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER, "Inhibit",
+ &error, &reply, "ssss", "sleep", "Darling",
+ "Notify macOS applications before the system sleeps", "delay");
+ if (r >= 0)
+ r = l->api.sd_bus_message_read(reply, "h", &fd);
+ /* The descriptor belongs to the reply message; keep our own copy. */
+ if (r >= 0)
+ {
+ own = l->api.fcntl(fd, IOKITD_LINUX_F_DUPFD_CLOEXEC, 3);
+ if (own < 0)
+ r = -IOKITD_LINUX_ENOMEM;
+ }
+ if (r < 0)
+ log_message(l, "logind: cannot take a sleep delay lock: %s (%d)",
+ error.message ? error.message : "no reply", r);
+
+ if (reply != NULL)
+ l->api.sd_bus_message_unref(reply);
+ l->api.sd_bus_error_free(&error);
+
+ if (own >= 0)
+ {
+ pthread_mutex_lock(&l->lock);
+ close_inhibitor_locked(l);
+ l->inhibitor_fd = own;
+ pthread_mutex_unlock(&l->lock);
+ }
+}
+
+static int on_prepare_for_sleep(void* m, void* userdata, struct iokitd_bus_error* ret_error)
+{
+ struct iokitd_logind* l = userdata;
+ int start = 0;
+ uint64_t cycle;
+
+ (void) ret_error;
+ if (l->api.sd_bus_message_read(m, "b", &start) < 0)
+ return 0;
+
+ pthread_mutex_lock(&l->lock);
+ if (start)
+ {
+ l->cycle++;
+ l->sleeping = 1;
+ l->deadline_usec = now_usec() + l->ack_timeout_usec;
+ }
+ else
+ {
+ l->sleeping = 0;
+ /* If logind suspended without waiting for us, the old lock is still valid. */
+ l->want_inhibitor = l->inhibitor_fd < 0;
+ }
+ cycle = l->cycle;
+ pthread_mutex_unlock(&l->lock);
+
+ if (start)
+ {
+ if (l->callbacks.will_sleep)
+ l->callbacks.will_sleep(l->context, cycle);
+ }
+ else if (l->callbacks.did_wake)
+ l->callbacks.did_wake(l->context, cycle);
+ return 0;
+}
+
+/* logind may appear after iokitd starts, or restart while we are awake. */
+static int on_logind_owner_changed(void* m, void* userdata, struct iokitd_bus_error* error)
+{
+ struct iokitd_logind* l = userdata;
+ const char *name, *old_owner, *new_owner;
+ (void) error;
+ if (l->api.sd_bus_message_read(m, "sss", &name, &old_owner, &new_owner) < 0
+ || strcmp(name, LOGIND_NAME) != 0)
+ return 0;
+ pthread_mutex_lock(&l->lock);
+ close_inhibitor_locked(l);
+ l->sleeping = 0;
+ l->want_inhibitor = new_owner[0] != '\0';
+ pthread_mutex_unlock(&l->lock);
+ return 0;
+}
+
+static void close_bus(struct iokitd_logind* l)
+{
+ if (l->slot) l->api.sd_bus_slot_unref(l->slot);
+ if (l->owner_slot) l->api.sd_bus_slot_unref(l->owner_slot);
+ if (l->bus) l->api.sd_bus_flush_close_unref(l->bus);
+ l->slot = l->owner_slot = l->bus = NULL;
+}
+
+static int connect_bus(struct iokitd_logind* l)
+{
+ int r;
+ r = l->api.sd_bus_new(&l->bus);
+ if (r >= 0)
+ r = l->api.sd_bus_set_address(l->bus, l->address);
+ if (r >= 0)
+ r = l->api.sd_bus_set_bus_client(l->bus, 1);
+ if (r >= 0)
+ r = l->api.sd_bus_set_method_call_timeout(l->bus, CALL_TIMEOUT_USEC);
+ if (r >= 0)
+ r = l->api.sd_bus_start(l->bus);
+ /* Subscribe before taking the lock so no announcement is missed. */
+ if (r >= 0)
+ r = l->api.sd_bus_match_signal(l->bus, &l->slot, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER,
+ "PrepareForSleep", on_prepare_for_sleep, l);
+ if (r >= 0)
+ r = l->api.sd_bus_match_signal(l->bus, &l->owner_slot, "org.freedesktop.DBus",
+ "/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged",
+ on_logind_owner_changed, l);
+ if (r < 0)
+ close_bus(l);
+ return r;
+}
+
+static void connection_lost(struct iokitd_logind* l)
+{
+ pthread_mutex_lock(&l->lock);
+ close_inhibitor_locked(l);
+ l->sleeping = 0;
+ l->want_inhibitor = 0;
+ pthread_mutex_unlock(&l->lock);
+ close_bus(l);
+}
+
+static void* bus_thread(void* arg)
+{
+ struct iokitd_logind* l = arg;
+ uint64_t next_reconnect = 0, backoff = USEC_PER_SEC;
+
+ for (;;)
+ {
+ uint64_t wait = POLL_INTERVAL_USEC, now;
+ int r;
+
+ pthread_mutex_lock(&l->lock);
+ if (l->stop)
+ {
+ pthread_mutex_unlock(&l->lock);
+ break;
+ }
+ pthread_mutex_unlock(&l->lock);
+
+ if (!l->bus)
+ {
+ now = now_usec();
+ if (now < next_reconnect)
+ {
+ struct timespec pause = {0, POLL_INTERVAL_USEC * 1000};
+ nanosleep(&pause, NULL);
+ continue;
+ }
+ r = connect_bus(l);
+ if (r < 0)
+ {
+ log_message(l, "logind: reconnect failed (%d), retrying", r);
+ next_reconnect = now_usec() + backoff;
+ backoff = backoff < 15 * USEC_PER_SEC ? backoff * 2 : 30 * USEC_PER_SEC;
+ continue;
+ }
+ backoff = USEC_PER_SEC;
+ take_inhibitor(l);
+ log_message(l, "logind: system bus reconnected");
+ }
+
+ r = l->api.sd_bus_process(l->bus, NULL);
+ if (r < 0)
+ {
+ log_message(l, "logind: system bus connection lost (%d), reconnecting", r);
+ connection_lost(l);
+ next_reconnect = 0;
+ continue;
+ }
+ if (r > 0)
+ continue;
+
+ if (l->want_inhibitor)
+ {
+ l->want_inhibitor = 0;
+ take_inhibitor(l);
+ }
+
+ now = now_usec();
+ pthread_mutex_lock(&l->lock);
+ if (l->sleeping && l->inhibitor_fd >= 0)
+ {
+ if (now >= l->deadline_usec)
+ {
+ log_message(l, "logind: applications did not acknowledge sleep in time");
+ close_inhibitor_locked(l);
+ }
+ else if (l->deadline_usec - now < wait)
+ wait = l->deadline_usec - now;
+ }
+ pthread_mutex_unlock(&l->lock);
+
+ r = l->api.sd_bus_wait(l->bus, wait);
+ if (r < 0 && r != -IOKITD_LINUX_EINTR)
+ {
+ log_message(l, "logind: waiting on the system bus failed (%d), reconnecting", r);
+ connection_lost(l);
+ next_reconnect = 0;
+ }
+ }
+
+ /* Never hold up a suspend once nobody is listening. */
+ pthread_mutex_lock(&l->lock);
+ close_inhibitor_locked(l);
+ pthread_mutex_unlock(&l->lock);
+ return NULL;
+}
+
+static uint64_t derive_ack_timeout(struct iokitd_logind* l)
+{
+ struct iokitd_bus_error error = { NULL, NULL, 0 };
+ uint64_t max_delay = 0, timeout;
+ int r;
+
+ if (l->api.ack_timeout_usec)
+ return l->api.ack_timeout_usec;
+
+ r = l->api.sd_bus_get_property_trivial(l->bus, LOGIND_NAME, LOGIND_PATH, LOGIND_MANAGER,
+ "InhibitDelayMaxUSec", &error, 't', &max_delay);
+ l->api.sd_bus_error_free(&error);
+ if (r < 0 || max_delay == 0)
+ max_delay = DEFAULT_LOGIND_DELAY_USEC;
+
+ timeout = max_delay > 2 * USEC_PER_SEC ? max_delay - USEC_PER_SEC : max_delay / 2;
+ if (timeout > MAX_ACK_TIMEOUT_USEC)
+ timeout = MAX_ACK_TIMEOUT_USEC;
+ if (timeout < MIN_ACK_TIMEOUT_USEC)
+ timeout = MIN_ACK_TIMEOUT_USEC;
+ return timeout;
+}
+
+static void destroy(struct iokitd_logind* l)
+{
+ close_bus(l);
+ free(l->address);
+ // No bus thread exists here (startup failure or stop has joined it).
+ close_inhibitor_locked(l);
+ pthread_mutex_destroy(&l->lock);
+ free(l);
+}
+
+int iokitd_logind_start(const struct iokitd_logind_api* api,
+ const struct iokitd_logind_callbacks* callbacks, void* context,
+ struct iokitd_logind** ret)
+{
+ struct iokitd_logind* l;
+ const char* address;
+ int r;
+
+ *ret = NULL;
+ if (!api_complete(api) || callbacks == NULL)
+ return -IOKITD_LINUX_ENOSYS;
+
+ l = calloc(1, sizeof(*l));
+ if (l == NULL)
+ return -IOKITD_LINUX_ENOMEM;
+ l->api = *api;
+ l->callbacks = *callbacks;
+ l->context = context;
+ l->inhibitor_fd = -1;
+ // Signal callbacks can run while synchronous startup calls pump the bus.
+ l->ack_timeout_usec = api->ack_timeout_usec ? api->ack_timeout_usec
+ : DEFAULT_LOGIND_DELAY_USEC - USEC_PER_SEC;
+ pthread_mutex_init(&l->lock, NULL);
+
+ address = api->getenv("DBUS_SYSTEM_BUS_ADDRESS");
+ if (address == NULL || address[0] == '\0')
+ address = IOKITD_LOGIND_DEFAULT_SYSTEM_BUS;
+
+ l->address = strdup(address);
+ if (!l->address)
+ {
+ destroy(l);
+ return -IOKITD_LINUX_ENOMEM;
+ }
+ r = connect_bus(l);
+ if (r < 0)
+ {
+ log_message(l, "logind: cannot use the system bus at %s (%d)", address, r);
+ destroy(l);
+ return r;
+ }
+
+ l->ack_timeout_usec = derive_ack_timeout(l);
+ take_inhibitor(l);
+
+ r = pthread_create(&l->thread, NULL, bus_thread, l);
+ if (r != 0)
+ {
+ destroy(l);
+ return -IOKITD_LINUX_ENOMEM;
+ }
+ l->thread_started = 1;
+ *ret = l;
+ return 0;
+}
+
+void iokitd_logind_sleep_ready(struct iokitd_logind* l, uint64_t cycle)
+{
+ if (l == NULL)
+ return;
+ pthread_mutex_lock(&l->lock);
+ if (l->sleeping && l->cycle == cycle)
+ close_inhibitor_locked(l);
+ pthread_mutex_unlock(&l->lock);
+}
+
+uint64_t iokitd_logind_ack_timeout_usec(const struct iokitd_logind* l)
+{
+ return l ? l->ack_timeout_usec : 0;
+}
+
+int iokitd_logind_holds_inhibitor(struct iokitd_logind* l)
+{
+ int held;
+
+ if (l == NULL)
+ return 0;
+ pthread_mutex_lock(&l->lock);
+ held = l->inhibitor_fd >= 0;
+ pthread_mutex_unlock(&l->lock);
+ return held;
+}
+
+void iokitd_logind_stop(struct iokitd_logind* l)
+{
+ if (l == NULL)
+ return;
+ pthread_mutex_lock(&l->lock);
+ l->stop = 1;
+ pthread_mutex_unlock(&l->lock);
+ if (l->thread_started)
+ pthread_join(l->thread, NULL);
+ destroy(l);
+}
diff --git a/src/LogindSleep.h b/src/LogindSleep.h
new file mode 100644
index 0000000..a161a68
--- /dev/null
+++ b/src/LogindSleep.h
@@ -0,0 +1,148 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ * Follows host suspend/resume through systemd-logind on the system bus.
+ *
+ * At start a "delay" sleep inhibitor is taken. When logind announces a
+ * suspend (PrepareForSleep(true)), will_sleep() is called and the inhibitor
+ * is kept until iokitd_logind_sleep_ready() is called for that cycle or the
+ * acknowledgement timeout expires, whichever comes first; then it is closed
+ * so the suspend can proceed. After resume (PrepareForSleep(false)),
+ * did_wake() is called and a new inhibitor is taken.
+ *
+ * The bus is owned by one background thread; the callbacks run on it.
+ * iokitd_logind_sleep_ready() may be called from any thread.
+ *
+ * This part has no Darling dependencies: every host function comes from the
+ * table below, so it can also be built natively on Linux against libsystemd
+ * for testing. Errors are negative Linux errno values as returned by sd-bus.
+ */
+
+#ifndef IOKITD_LOGIND_SLEEP_H_
+#define IOKITD_LOGIND_SLEEP_H_
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Linux values; the guest's numbering differs. */
+#define IOKITD_LINUX_EINVAL 22
+#define IOKITD_LINUX_ENOMEM 12
+#define IOKITD_LINUX_ENOSYS 38
+#define IOKITD_LINUX_EINTR 4
+#define IOKITD_LINUX_F_DUPFD_CLOEXEC 1030
+
+#define IOKITD_LOGIND_DEFAULT_SYSTEM_BUS "unix:path=/run/dbus/system_bus_socket"
+
+/* Layout of sd_bus_error. */
+struct iokitd_bus_error
+{
+ const char* name;
+ const char* message;
+ int _need_free;
+};
+
+typedef int (*iokitd_bus_handler)(void* m, void* userdata, struct iokitd_bus_error* ret_error);
+
+struct iokitd_logind_api
+{
+ /* libsystemd (sd-bus); bus, message and slot objects are opaque */
+ int (*sd_bus_new)(void** ret);
+ int (*sd_bus_set_address)(void* bus, const char* address);
+ int (*sd_bus_set_bus_client)(void* bus, int b);
+ int (*sd_bus_start)(void* bus);
+ int (*sd_bus_set_method_call_timeout)(void* bus, uint64_t timeout);
+ void* (*sd_bus_flush_close_unref)(void* bus);
+ int (*sd_bus_match_signal)(void* bus, void** ret_slot, const char* sender, const char* path,
+ const char* interface, const char* member, iokitd_bus_handler callback, void* userdata);
+ int (*sd_bus_call_method)(void* bus, const char* destination, const char* path,
+ const char* interface, const char* member, struct iokitd_bus_error* ret_error,
+ void** reply, const char* types, ...);
+ int (*sd_bus_get_property_trivial)(void* bus, const char* destination, const char* path,
+ const char* interface, const char* member, struct iokitd_bus_error* ret_error,
+ char type, void* ret_ptr);
+ int (*sd_bus_message_read)(void* m, const char* types, ...);
+ void* (*sd_bus_message_unref)(void* m);
+ void* (*sd_bus_slot_unref)(void* slot);
+ void (*sd_bus_error_free)(struct iokitd_bus_error* e);
+ int (*sd_bus_process)(void* bus, void** ret);
+ int (*sd_bus_wait)(void* bus, uint64_t timeout_usec);
+
+ /* Host file descriptors (the inhibitor lock) */
+ int (*fcntl)(int fd, int cmd, ...);
+ int (*close)(int fd);
+
+ /* Process environment (DBUS_SYSTEM_BUS_ADDRESS) */
+ char* (*getenv)(const char* name);
+
+ /* Optional diagnostics */
+ void (*log)(const char* message);
+
+ /*
+ * How long to wait for applications to acknowledge a suspend before
+ * releasing the inhibitor anyway; 0 derives it from logind's
+ * InhibitDelayMaxUSec (one second less, so the release always comes
+ * before logind gives up on us).
+ */
+ uint64_t ack_timeout_usec;
+};
+
+/* Shared host symbol table. Only implemented in the guest iokitd bridge. */
+const struct iokitd_logind_api* iokitd_host_power_api(void);
+
+struct iokitd_logind_callbacks
+{
+ /* Suspend is imminent. Call iokitd_logind_sleep_ready(cycle) once clients agreed. */
+ void (*will_sleep)(void* context, uint64_t cycle);
+ /* The system has resumed (or the suspend failed). */
+ void (*did_wake)(void* context, uint64_t cycle);
+};
+
+struct iokitd_logind;
+
+/*
+ * Connects to the system bus (DBUS_SYSTEM_BUS_ADDRESS or the standard
+ * socket), subscribes to PrepareForSleep, takes the delay inhibitor and
+ * starts the bus thread. A missing logind is not an error: no events arrive
+ * then. Returns 0 or a negative errno (no bus, incomplete api).
+ */
+int iokitd_logind_start(const struct iokitd_logind_api* api,
+ const struct iokitd_logind_callbacks* callbacks, void* context,
+ struct iokitd_logind** ret);
+
+/* Releases the inhibitor if it is still held for this suspend cycle. */
+void iokitd_logind_sleep_ready(struct iokitd_logind* logind, uint64_t cycle);
+
+/* Acknowledgement timeout in use, in microseconds. */
+uint64_t iokitd_logind_ack_timeout_usec(const struct iokitd_logind* logind);
+
+/* 1 if a delay inhibitor is currently held. */
+int iokitd_logind_holds_inhibitor(struct iokitd_logind* logind);
+
+/* Stops the bus thread, closes the inhibitor and the bus. */
+void iokitd_logind_stop(struct iokitd_logind* logind);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/HostPowerInhibit.c b/src/HostPowerInhibit.c
new file mode 100644
index 0000000..cdcb933
--- /dev/null
+++ b/src/HostPowerInhibit.c
@@ -0,0 +1,118 @@
+/* Host power-save inhibitors. GPL-3.0-or-later, like iokitd. */
+#include "HostPowerInhibit.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+struct iokitd_power_inhibit {
+ struct iokitd_logind_api api;
+ int idle_fd;
+ void* screen_bus;
+ uint32_t screen_cookie;
+ int screen_held;
+};
+
+static int connect_bus(const struct iokitd_logind_api* api, const char* address, void** bus)
+{
+ int r = api->sd_bus_new(bus);
+ if (r >= 0) r = api->sd_bus_set_address(*bus, address);
+ if (r >= 0) r = api->sd_bus_set_bus_client(*bus, 1);
+ /* Assertions are synchronous MIG calls; do not stall the IOKit queue for
+ * sd-bus's default 25-second service timeout. */
+ if (r >= 0) r = api->sd_bus_set_method_call_timeout(*bus, 500000);
+ if (r >= 0) r = api->sd_bus_start(*bus);
+ return r;
+}
+
+static int take_idle_lock(struct iokitd_power_inhibit* inhibitor)
+{
+ const struct iokitd_logind_api* api = &inhibitor->api;
+ const char* address = api->getenv("DBUS_SYSTEM_BUS_ADDRESS");
+ struct iokitd_bus_error error = {0};
+ void *bus = NULL, *reply = NULL;
+ int fd = -1;
+ if (!address || !address[0]) address = IOKITD_LOGIND_DEFAULT_SYSTEM_BUS;
+ int r = connect_bus(api, address, &bus);
+ if (r >= 0)
+ r = api->sd_bus_call_method(bus, "org.freedesktop.login1", "/org/freedesktop/login1",
+ "org.freedesktop.login1.Manager", "Inhibit", &error, &reply, "ssss",
+ "idle", "Darling", "macOS application prevents automatic idle sleep", "block");
+ if (r >= 0) r = api->sd_bus_message_read(reply, "h", &fd);
+ if (r >= 0) {
+ inhibitor->idle_fd = api->fcntl(fd, IOKITD_LINUX_F_DUPFD_CLOEXEC, 3);
+ if (inhibitor->idle_fd < 0) r = -IOKITD_LINUX_ENOMEM;
+ }
+ if (reply) api->sd_bus_message_unref(reply);
+ api->sd_bus_error_free(&error);
+ if (bus) api->sd_bus_flush_close_unref(bus);
+ return r;
+}
+
+static int take_screen_lock(struct iokitd_power_inhibit* inhibitor)
+{
+ const struct iokitd_logind_api* api = &inhibitor->api;
+ const char* address = api->getenv("DBUS_SESSION_BUS_ADDRESS");
+ char fallback[512];
+ if (!address || !address[0]) {
+ const char* runtime = api->getenv("XDG_RUNTIME_DIR");
+ int length = runtime && runtime[0]
+ ? snprintf(fallback, sizeof(fallback), "unix:path=%s/bus", runtime)
+ : snprintf(fallback, sizeof(fallback), "unix:path=/run/user/%u/bus", (unsigned)getuid());
+ if (length < 0 || (size_t)length >= sizeof(fallback)) return -IOKITD_LINUX_EINVAL;
+ address = fallback;
+ }
+ struct iokitd_bus_error error = {0};
+ void* reply = NULL;
+ int r = connect_bus(api, address, &inhibitor->screen_bus);
+ if (r >= 0)
+ r = api->sd_bus_call_method(inhibitor->screen_bus, "org.freedesktop.ScreenSaver",
+ "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", "Inhibit",
+ &error, &reply, "ss", "Darling", "macOS application prevents display sleep");
+ if (r >= 0) r = api->sd_bus_message_read(reply, "u", &inhibitor->screen_cookie);
+ if (r >= 0) inhibitor->screen_held = 1;
+ if (reply) api->sd_bus_message_unref(reply);
+ api->sd_bus_error_free(&error);
+ return r;
+}
+
+int iokitd_power_inhibit_create(const struct iokitd_logind_api* api, int display,
+ struct iokitd_power_inhibit** result)
+{
+ *result = NULL;
+ if (!api || !api->sd_bus_new || !api->sd_bus_set_address || !api->sd_bus_set_bus_client ||
+ !api->sd_bus_set_method_call_timeout || !api->sd_bus_start || !api->sd_bus_call_method ||
+ !api->sd_bus_message_read || !api->sd_bus_message_unref || !api->sd_bus_error_free ||
+ !api->sd_bus_flush_close_unref || !api->fcntl || !api->close || !api->getenv)
+ return -IOKITD_LINUX_ENOSYS;
+ struct iokitd_power_inhibit* inhibitor = calloc(1, sizeof(*inhibitor));
+ if (!inhibitor) return -IOKITD_LINUX_ENOMEM;
+ inhibitor->api = *api;
+ inhibitor->idle_fd = -1;
+ int r = take_idle_lock(inhibitor);
+ if (r >= 0 && display) r = take_screen_lock(inhibitor);
+ if (r < 0) {
+ iokitd_power_inhibit_destroy(inhibitor);
+ return r;
+ }
+ *result = inhibitor;
+ return 0;
+}
+
+void iokitd_power_inhibit_destroy(struct iokitd_power_inhibit* inhibitor)
+{
+ if (!inhibitor) return;
+ const struct iokitd_logind_api* api = &inhibitor->api;
+ if (inhibitor->screen_held) {
+ struct iokitd_bus_error error = {0};
+ void* reply = NULL;
+ api->sd_bus_call_method(inhibitor->screen_bus, "org.freedesktop.ScreenSaver",
+ "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", "UnInhibit",
+ &error, &reply, "u", inhibitor->screen_cookie);
+ if (reply) api->sd_bus_message_unref(reply);
+ api->sd_bus_error_free(&error);
+ }
+ if (inhibitor->screen_bus) api->sd_bus_flush_close_unref(inhibitor->screen_bus);
+ if (inhibitor->idle_fd >= 0) api->close(inhibitor->idle_fd);
+ free(inhibitor);
+}
diff --git a/src/HostPowerInhibit.h b/src/HostPowerInhibit.h
new file mode 100644
index 0000000..fed7fbe
--- /dev/null
+++ b/src/HostPowerInhibit.h
@@ -0,0 +1,18 @@
+/* Host power-save inhibitors. GPL-3.0-or-later, like iokitd. */
+#ifndef IOKITD_HOST_POWER_INHIBIT_H
+#define IOKITD_HOST_POWER_INHIBIT_H
+#include "LogindSleep.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+struct iokitd_power_inhibit;
+/* display=0 prevents automatic system idle sleep through logind. display=1
+ * additionally inhibits the desktop ScreenSaver; it fails if that service is
+ * unavailable, allowing the caller to try a real X11 display backend. */
+int iokitd_power_inhibit_create(const struct iokitd_logind_api* api, int display,
+ struct iokitd_power_inhibit** result);
+void iokitd_power_inhibit_destroy(struct iokitd_power_inhibit* inhibitor);
+#ifdef __cplusplus
+}
+#endif
+#endif
diff --git a/src/PowerAssertions.h b/src/PowerAssertions.h
index 1e4a975..3b0d416 100644
--- a/src/PowerAssertions.h
+++ b/src/PowerAssertions.h
@@ -28,11 +28,11 @@ void initPM();
class PowerAssertion
{
public:
- // How many AppAssertions instances reference this assertion (*not* a sum of references within all these AppAssertions instances)
- void addRef();
+ // One backend hold per enabled assertion; retaining an ID does not add holds.
+ IOReturn addRef();
void delRef();
protected:
- virtual void activate() = 0;
+ virtual IOReturn activate() = 0;
virtual void deactivate() = 0;
private:
int m_numHoldingApps = 0;
@@ -44,15 +44,17 @@ private:
AppAssertions(int pid);
~AppAssertions();
public:
- static AppAssertions* get(int pid);
+ static AppAssertions* get(int pid, bool create = true);
struct HeldAssertion
{
int refcount;
PowerAssertion* assertion;
+ bool enabled;
};
HeldAssertion* getAssertion(int assertionId);
- int createAssertion(PowerAssertion* which);
+ IOReturn createAssertion(PowerAssertion* which, bool enabled, int* assertionId);
+ IOReturn setEnabled(HeldAssertion* assertion, bool enabled);
void killAssertion(int assertionId);
private:
std::unordered_map<int, HeldAssertion> m_heldAssertions;
diff --git a/src/PowerAssertions.mm b/src/PowerAssertions.mm
index fa55d2f..810fe04 100644
--- a/src/PowerAssertions.mm
+++ b/src/PowerAssertions.mm
@@ -19,9 +19,11 @@
#include "PowerAssertions.h"
#include "PowerAssertionsX11.h"
+#include "HostPowerInhibit.h"
#include <stdexcept>
#include <iostream>
#include <string>
+#include <climits>
#include <CoreFoundation/CoreFoundation.h>
#include <bsm/libbsm.h>
#import <Foundation/NSString.h>
@@ -30,34 +32,52 @@ extern "C" {
#include "powermanagementServer.h"
}
+class PowerAssertionPreventIdleSleep : public PowerAssertion
+{
+ struct iokitd_power_inhibit* m_host = nullptr;
+ IOReturn activate() override
+ {
+ const auto* api = iokitd_host_power_api();
+ return api && iokitd_power_inhibit_create(api, 0, &m_host) == 0
+ ? kIOReturnSuccess : kIOReturnNotReady;
+ }
+ void deactivate() override
+ {
+ iokitd_power_inhibit_destroy(m_host);
+ m_host = nullptr;
+ }
+};
+
std::unordered_map<int, AppAssertions*> AppAssertions::m_instances;
static std::unordered_map<std::string, PowerAssertion*> g_assertionKinds;
void initPM()
{
- NSString* str = (NSString*) kIOPMAssertPreventUserIdleDisplaySleep;
- g_assertionKinds.insert(std::make_pair([str UTF8String], new PowerAssertionPreventDisplaySleep));
+ // Legacy and modern spellings share one backend and one hold count.
+ static PowerAssertionPreventIdleSleep idle;
+ static PowerAssertionPreventDisplaySleep display;
+ g_assertionKinds = {
+ {[(NSString*)kIOPMAssertionTypeNoIdleSleep UTF8String], &idle},
+ {[(NSString*)kIOPMAssertPreventUserIdleSystemSleep UTF8String], &idle},
+ {[(NSString*)kIOPMAssertionTypeNoDisplaySleep UTF8String], &display},
+ {[(NSString*)kIOPMAssertPreventUserIdleDisplaySleep UTF8String], &display},
+ };
}
-static PowerAssertion* getByType(NSString* type)
-{
- auto it = g_assertionKinds.find([type UTF8String]);
- if (it != g_assertionKinds.end())
- return it->second;
- return nullptr;
-}
-
-void PowerAssertion::addRef()
+IOReturn PowerAssertion::addRef()
{
if (m_numHoldingApps == 0)
- activate();
+ {
+ IOReturn result = activate();
+ if (result != kIOReturnSuccess) return result;
+ }
m_numHoldingApps++;
+ return kIOReturnSuccess;
}
void PowerAssertion::delRef()
{
- m_numHoldingApps--;
- if (m_numHoldingApps == 0)
+ if (m_numHoldingApps > 0 && --m_numHoldingApps == 0)
deactivate();
}
@@ -66,37 +86,31 @@ AppAssertions::AppAssertions(int pid)
m_processSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
if (!m_processSource)
throw std::runtime_error("dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC) failed");
-
- if (m_processSource)
- {
- dispatch_source_set_event_handler(m_processSource, ^{
- m_instances.erase(pid);
- delete this;
- });
- dispatch_resume(m_processSource);
- }
+ dispatch_source_set_event_handler(m_processSource, ^{
+ m_instances.erase(pid);
+ delete this;
+ });
+ dispatch_resume(m_processSource);
}
AppAssertions::~AppAssertions()
{
- for (auto& [assertionId, ha] : m_heldAssertions)
- ha.assertion->delRef();
-
- if (m_processSource != nullptr)
- dispatch_release(m_processSource);
+ for (auto& item : m_heldAssertions)
+ if (item.second.enabled) item.second.assertion->delRef();
+ dispatch_source_cancel(m_processSource);
+ dispatch_release(m_processSource);
}
-AppAssertions* AppAssertions::get(int pid)
+AppAssertions* AppAssertions::get(int pid, bool create)
{
auto it = m_instances.find(pid);
- if (it != m_instances.end())
- return it->second;
-
+ if (it != m_instances.end()) return it->second;
+ if (!create) return nullptr;
try
{
- AppAssertions* aa = new AppAssertions(pid);
- m_instances.insert(std::make_pair(pid, aa));
- return aa;
+ AppAssertions* app = new AppAssertions(pid);
+ m_instances.emplace(pid, app);
+ return app;
}
catch (const std::exception& e)
{
@@ -108,126 +122,168 @@ AppAssertions* AppAssertions::get(int pid)
AppAssertions::HeldAssertion* AppAssertions::getAssertion(int assertionId)
{
auto it = m_heldAssertions.find(assertionId);
- if (it != m_heldAssertions.end())
- return &it->second;
- return nullptr;
+ return it == m_heldAssertions.end() ? nullptr : &it->second;
}
-int AppAssertions::createAssertion(PowerAssertion* which)
+IOReturn AppAssertions::createAssertion(PowerAssertion* which, bool enabled, int* assertionId)
{
+ if (m_nextAssertionNumber == INT_MAX) return kIOReturnNoResources;
+ if (enabled)
+ {
+ IOReturn result = which->addRef();
+ if (result != kIOReturnSuccess) return result;
+ }
int number = m_nextAssertionNumber++;
- HeldAssertion held = { 1, which };
- m_heldAssertions.insert(std::make_pair(number, held));
- which->addRef();
- return number;
+ try { m_heldAssertions.emplace(number, HeldAssertion{1, which, enabled}); }
+ catch (...)
+ {
+ if (enabled) which->delRef();
+ return kIOReturnNoMemory;
+ }
+ *assertionId = number;
+ return kIOReturnSuccess;
+}
+
+IOReturn AppAssertions::setEnabled(HeldAssertion* held, bool enabled)
+{
+ if (held->enabled == enabled) return kIOReturnSuccess;
+ if (enabled)
+ {
+ IOReturn result = held->assertion->addRef();
+ if (result != kIOReturnSuccess) return result;
+ }
+ else held->assertion->delRef();
+ held->enabled = enabled;
+ return kIOReturnSuccess;
}
void AppAssertions::killAssertion(int assertionId)
{
auto it = m_heldAssertions.find(assertionId);
- if (it == m_heldAssertions.end())
- return;
-
- it->second.assertion->delRef();
+ if (it == m_heldAssertions.end()) return;
+ if (it->second.enabled) it->second.assertion->delRef();
m_heldAssertions.erase(it);
}
-kern_return_t _io_pm_assertion_create
-(
- mach_port_t server,
- audit_token_t token,
- vm_offset_t props,
- mach_msg_type_number_t propsCnt,
- int *assertion_id,
- int *disableAppSleep,
- int *enTrIntensity,
- int *return_code
-)
+// These MIG routines return their IOReturn in return_code and KERN_SUCCESS for
+// transport. Consequently they own (and consume) the request's OOL data even
+// when validation fails; dispatch_mig_server must not destroy it a second time.
+static CFDictionaryRef copyProperties(vm_offset_t props, mach_msg_type_number_t count)
{
- if (!props)
- return KERN_INVALID_ARGUMENT;
-
- CFDataRef data = CFDataCreateWithBytesNoCopy(nullptr, (const UInt8*) props, propsCnt, kCFAllocatorNull);
- CFDictionaryRef properties = (CFDictionaryRef) CFPropertyListCreateWithData(nullptr, data, kCFPropertyListImmutable, nullptr, nullptr);
-
- CFRelease(data);
-
- vm_deallocate(mach_task_self(), props, propsCnt);
-
- if (!properties)
- return KERN_INVALID_ARGUMENT;
-
- if (CFGetTypeID(properties) != CFDictionaryGetTypeID())
+ CFDictionaryRef result = nullptr;
+ if (props && count)
{
- std::cerr << "Unexpected properties data type\n";
- CFRelease(properties);
- return KERN_INVALID_ARGUMENT;
+ CFDataRef data = CFDataCreateWithBytesNoCopy(nullptr, (const UInt8*)props, count, kCFAllocatorNull);
+ if (data)
+ {
+ result = (CFDictionaryRef)CFPropertyListCreateWithData(nullptr, data, kCFPropertyListImmutable, nullptr, nullptr);
+ CFRelease(data);
+ }
}
-
- CFStringRef cftype = (CFStringRef) CFDictionaryGetValue(properties, kIOPMAssertionTypeKey);
-
- if (!cftype || CFStringGetTypeID() != CFGetTypeID(cftype))
+ if (props) vm_deallocate(mach_task_self(), props, count);
+ if (result && CFGetTypeID(result) != CFDictionaryGetTypeID())
{
- std::cerr << "kIOPMAssertionTypeKey is not a string\n";
- CFRelease(properties);
- return KERN_INVALID_ARGUMENT;
+ CFRelease(result);
+ result = nullptr;
}
+ return result;
+}
+
+static bool assertionLevel(CFDictionaryRef properties, bool* enabled)
+{
+ CFTypeRef number = CFDictionaryGetValue(properties, kIOPMAssertionLevelKey);
+ if (!number) return true;
+ int level;
+ if (CFGetTypeID(number) != CFNumberGetTypeID() ||
+ !CFNumberGetValue((CFNumberRef)number, kCFNumberIntType, &level) ||
+ (level != kIOPMAssertionLevelOff && level != kIOPMAssertionLevelOn)) return false;
+ *enabled = level == kIOPMAssertionLevelOn;
+ return true;
+}
- PowerAssertion* pa = getByType((NSString*) cftype);
- if (pa == nullptr)
+kern_return_t _io_pm_assertion_create(mach_port_t server, audit_token_t token,
+ vm_offset_t props, mach_msg_type_number_t propsCnt, int* assertion_id,
+ int* disableAppSleep, int* enTrIntensity, int* return_code)
+{
+ *assertion_id = kIOPMNullAssertionID;
+ *disableAppSleep = 0;
+ *enTrIntensity = -1;
+ *return_code = kIOReturnBadArgument;
+ CFDictionaryRef properties = copyProperties(props, propsCnt);
+ if (!properties) return KERN_SUCCESS;
+ CFTypeRef type = CFDictionaryGetValue(properties, kIOPMAssertionTypeKey);
+ bool enabled = true;
+ CFTypeRef timeout = CFDictionaryGetValue(properties, kIOPMAssertionTimeoutKey);
+ double seconds = 0;
+ if (timeout && (CFGetTypeID(timeout) != CFNumberGetTypeID() ||
+ !CFNumberGetValue((CFNumberRef)timeout, kCFNumberDoubleType, &seconds)))
+ *return_code = kIOReturnBadArgument;
+ else if (seconds != 0)
+ // Timed assertions need expiration scheduling. Do not silently turn
+ // a timed request into an indefinite host inhibition.
+ *return_code = kIOReturnUnsupported;
+ else if (type && CFGetTypeID(type) == CFStringGetTypeID() && assertionLevel(properties, &enabled))
{
- std::cerr << "Unsupported assertion type\n";
- CFRelease(properties);
- return KERN_INVALID_ARGUMENT;
+ auto it = g_assertionKinds.find([(NSString*)type UTF8String]);
+ if (it == g_assertionKinds.end()) *return_code = kIOReturnUnsupported;
+ else
+ {
+ pid_t callerPid;
+ audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr);
+ AppAssertions* app = AppAssertions::get(callerPid);
+ *return_code = app ? app->createAssertion(it->second, enabled, assertion_id) : kIOReturnNoResources;
+ }
}
+ CFRelease(properties);
+ return KERN_SUCCESS;
+}
- pid_t callerPid;
- audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr);
-
- // std::cout << "Creating assertion for PID " << callerPid << std::endl;
- *assertion_id = AppAssertions::get(callerPid)->createAssertion(pa);
- *return_code = 0;
-
- return KERN_SUCCESS;
-}
-
-kern_return_t _io_pm_assertion_retain_release
-(
- mach_port_t server,
- audit_token_t token,
- int assertion_id,
- int action,
- int *retainCnt,
- int *disableAppSleep,
- int *enableAppSleep,
- int *return_code
-)
+kern_return_t _io_pm_assertion_retain_release(mach_port_t server, audit_token_t token,
+ int assertion_id, int action, int* retainCnt, int* disableAppSleep,
+ int* enableAppSleep, int* return_code)
{
+ *retainCnt = *disableAppSleep = *enableAppSleep = 0;
+ *return_code = kIOReturnNotFound;
pid_t callerPid;
audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr);
-
- AppAssertions* appAssertions = AppAssertions::get(callerPid);
- AppAssertions::HeldAssertion* ha = appAssertions->getAssertion(assertion_id);
-
- if (!ha)
- {
- std::cerr << "_io_pm_assertion_retain_release() cannot find assertion " << assertion_id << std::endl;
- return KERN_INVALID_ARGUMENT;
- }
-
- *return_code = 0;
+ AppAssertions* app = AppAssertions::get(callerPid, false);
+ auto* held = app ? app->getAssertion(assertion_id) : nullptr;
+ if (!held) return KERN_SUCCESS;
if (action == kIOPMAssertionMIGDoRetain)
{
- *retainCnt = ++ha->refcount;
+ if (held->refcount == INT_MAX) { *return_code = kIOReturnNoResources; return KERN_SUCCESS; }
+ *retainCnt = ++held->refcount;
}
else if (action == kIOPMAssertionMIGDoRelease)
{
- *retainCnt = --ha->refcount;
- if (ha->refcount == 0)
- appAssertions->killAssertion(assertion_id);
+ *retainCnt = --held->refcount;
+ if (!held->refcount) app->killAssertion(assertion_id);
}
- else
- return KERN_INVALID_ARGUMENT;
+ else { *return_code = kIOReturnBadArgument; return KERN_SUCCESS; }
+ *return_code = kIOReturnSuccess;
+ return KERN_SUCCESS;
+}
- return KERN_NOT_SUPPORTED;
+kern_return_t _io_pm_assertion_set_properties(mach_port_t server, audit_token_t token,
+ int assertion_id, vm_offset_t props, mach_msg_type_number_t propsCnt,
+ int* disableAppSleep, int* enableAppSleep, int* enTrIntensity, int* return_code)
+{
+ *disableAppSleep = *enableAppSleep = 0;
+ *enTrIntensity = -1;
+ *return_code = kIOReturnBadArgument;
+ CFDictionaryRef properties = copyProperties(props, propsCnt);
+ if (!properties) return KERN_SUCCESS;
+ bool enabled = true;
+ if (CFDictionaryGetCount(properties) != 1 || !CFDictionaryContainsKey(properties, kIOPMAssertionLevelKey))
+ *return_code = kIOReturnUnsupported;
+ else if (assertionLevel(properties, &enabled))
+ {
+ pid_t callerPid;
+ audit_token_to_au32(token, nullptr, nullptr, nullptr, nullptr, nullptr, &callerPid, nullptr, nullptr);
+ AppAssertions* app = AppAssertions::get(callerPid, false);
+ auto* held = app ? app->getAssertion(assertion_id) : nullptr;
+ *return_code = held ? app->setEnabled(held, enabled) : kIOReturnNotFound;
+ }
+ CFRelease(properties);
+ return KERN_SUCCESS;
}
diff --git a/src/PowerAssertionsX11.h b/src/PowerAssertionsX11.h
index 2905edd..3b4ea10 100644
--- a/src/PowerAssertionsX11.h
+++ b/src/PowerAssertionsX11.h
@@ -20,12 +20,16 @@
#ifndef POWERASSERTIONSX11_H
#define POWERASSERTIONSX11_H
#include "PowerAssertions.h"
+#include "HostPowerInhibit.h"
class PowerAssertionPreventDisplaySleep : public PowerAssertion
{
protected:
- void activate() override;
+ IOReturn activate() override;
void deactivate() override;
+private:
+ struct iokitd_power_inhibit* m_host = nullptr;
+ bool m_changedDPMS = false;
};
#endif
diff --git a/src/PowerAssertionsX11.mm b/src/PowerAssertionsX11.mm
index 7a6fcfa..26a7d68 100644
--- a/src/PowerAssertionsX11.mm
+++ b/src/PowerAssertionsX11.mm
@@ -26,28 +26,37 @@
#include <X11/extensions/dpms.h>
#undef BOOL
-void PowerAssertionPreventDisplaySleep::activate()
+IOReturn PowerAssertionPreventDisplaySleep::activate()
{
- Display* display = IODisplayConnectX11::getDisplay();
- if (!display)
- return;
+ const auto* api = iokitd_host_power_api();
+ if (api && iokitd_power_inhibit_create(api, 1, &m_host) == 0)
+ return kIOReturnSuccess;
+ // Desktop inhibition is preferred on Wayland; retain the real X11
+ // fallback when no ScreenSaver service is available.
+ Display* display = IODisplayConnectX11::getDisplay();
int eventBase, errorBase;
- if (DPMSQueryExtension(display, &eventBase, &errorBase))
- {
- DPMSDisable(display);
- }
+ CARD16 power;
+ unsigned char enabled;
+ if (!display || !DPMSQueryExtension(display, &eventBase, &errorBase) ||
+ !DPMSCapable(display) || !DPMSInfo(display, &power, &enabled))
+ return kIOReturnNotReady;
+ if (api)
+ iokitd_power_inhibit_create(api, 0, &m_host);
+ m_changedDPMS = enabled;
+ if (m_changedDPMS) DPMSDisable(display);
+ XFlush(display);
+ return kIOReturnSuccess;
}
void PowerAssertionPreventDisplaySleep::deactivate()
{
- Display* display = IODisplayConnectX11::getDisplay();
- if (!display)
- return;
-
- int eventBase, errorBase;
- if (DPMSQueryExtension(display, &eventBase, &errorBase))
+ iokitd_power_inhibit_destroy(m_host);
+ m_host = nullptr;
+ if (m_changedDPMS)
{
- DPMSEnable(display);
+ Display* display = IODisplayConnectX11::getDisplay();
+ if (display) { DPMSEnable(display); XFlush(display); }
+ m_changedDPMS = false;
}
}
diff --git a/src/IORegistryEntry.h b/src/IORegistryEntry.h
index 90155ed..e955bd4 100644
--- a/src/IORegistryEntry.h
+++ b/src/IORegistryEntry.h
@@ -19,6 +19,8 @@ public:
std::string_view getPathName(const char* plane);
std::string getPath(const char* plane);
+ bool inPlane(const char* plane) const;
+ static IORegistryEntry* fromPath(std::string_view path);
std::set<IORegistryEntry*> getParents(const char* plane);
std::set<IORegistryEntry*> getChildren(const char* plane);
std::string getName(const char* plane = nullptr);
@@ -32,8 +34,8 @@ public:
void registerInPlane(const char* plane, const char* pathName, IORegistryEntry* parent);
protected:
// The key is the plane name, e.g. kIOServicePlane (as declared in IOKit/IOKitKeys.h)
- std::unordered_map<std::string_view, std::string> m_name;
- std::unordered_map<std::string_view, std::set<IORegistryEntry*>> m_parents, m_children;
+ std::unordered_map<std::string, std::string> m_name;
+ std::unordered_map<std::string, std::set<IORegistryEntry*>> m_parents, m_children;
uint64_t m_id;
static std::unordered_map<uint64_t, IORegistryEntry*> m_entriesById;
diff --git a/src/PlatformIdentity.h b/src/PlatformIdentity.h
new file mode 100644
index 0000000..1f83b65
--- /dev/null
+++ b/src/PlatformIdentity.h
@@ -0,0 +1,78 @@
+#ifndef DARLING_PLATFORM_IDENTITY_H
+#define DARLING_PLATFORM_IDENTITY_H
+#import <Foundation/Foundation.h>
+#include <uuid/uuid.h>
+#include <cstdio>
+#include <cstring>
+#include <array>
+
+namespace DarlingPlatformIdentity {
+inline bool printable(id value, size_t maximum) {
+ if (![value isKindOfClass:[NSString class]]) return false;
+ const char* text = [value UTF8String];
+ if (!text) return false;
+ size_t length = strlen(text);
+ if (!length || length > maximum || length != [value length]) return false;
+ for (size_t i = 0; i < length; ++i) if ((unsigned char)text[i] < 32 || (unsigned char)text[i] > 126) return false;
+ return true;
+}
+
+// Validate the entire schema before exposing any overrides. Strings on device
+// tree properties are NUL-terminated NSData; serial/UUID retain NSString types.
+inline NSDictionary* properties(id configuration) {
+ if (![configuration isKindOfClass:[NSDictionary class]] || [configuration count] != 2) return nil;
+ id schema = [configuration objectForKey:@"schema"];
+ if (![schema isKindOfClass:[NSNumber class]] || CFGetTypeID((CFTypeRef)schema) == CFBooleanGetTypeID() ||
+ strchr("fd", [schema objCType][0]) || [schema longLongValue] != 1) return nil;
+ id source = [configuration objectForKey:@"properties"];
+ if (![source isKindOfClass:[NSDictionary class]] || [source count] > 9) return nil;
+ NSMutableDictionary* result = [NSMutableDictionary dictionary];
+ for (id key in source) {
+ if (![key isKindOfClass:[NSString class]]) return nil;
+ id value = [source objectForKey:key];
+ if ([key isEqual:@"IOPlatformUUID"]) {
+ if (!printable(value, 36) || [value length] != 36) return nil;
+ uuid_t uuid;
+ if (uuid_parse([value UTF8String], uuid) || uuid_is_null(uuid)) return nil;
+ uuid_string_t text; uuid_unparse_upper(uuid, text);
+ result[key] = [NSString stringWithUTF8String:text];
+ unsigned char bytes[16]; memcpy(bytes, uuid, sizeof(bytes));
+ for (int i = 0; i < 4; ++i) bytes[i] = uuid[3-i];
+ bytes[4] = uuid[5]; bytes[5] = uuid[4]; bytes[6] = uuid[7]; bytes[7] = uuid[6];
+ NSData* systemID = [NSData dataWithBytes:bytes length:16];
+ if (source[@"system-id"] && ![source[@"system-id"] isEqual:systemID]) return nil;
+ result[@"system-id"] = systemID;
+ } else if ([key isEqual:@"ROM"] || [key isEqual:@"system-id"]) {
+ if (![value isKindOfClass:[NSData class]] || [value length] != ([key isEqual:@"ROM"] ? 6 : 16)) return nil;
+ result[key] = value;
+ } else if ([key isEqual:@"IOPlatformSerialNumber"]) {
+ if (!printable(value, 64)) return nil;
+ result[key] = value;
+ } else if ([key isEqual:@"manufacturer"] || [key isEqual:@"model"] || [key isEqual:@"product-name"] ||
+ [key isEqual:@"board-id"] || [key isEqual:@"mlb-serial-number"]) {
+ if (!printable(value, [key isEqual:@"mlb-serial-number"] ? 64 : 128)) return nil;
+ const char* text = [value UTF8String];
+ result[key] = [NSData dataWithBytes:text length:strlen(text)+1];
+ } else return nil;
+ }
+ if (result[@"model"] && !result[@"product-name"]) result[@"product-name"] = result[@"model"];
+ return result;
+}
+
+inline NSDictionary* load(const char* path) {
+ FILE* file = fopen(path, "rb");
+ if (!file) return nil; // no overrides is the normal, unchanged default
+ std::array<unsigned char, 65537> buffer;
+ size_t size = fread(buffer.data(), 1, buffer.size(), file);
+ bool failed = ferror(file); fclose(file);
+ if (!failed && size <= 65536) {
+ NSData* data = [NSData dataWithBytes:buffer.data() length:size];
+ id config = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL];
+ NSDictionary* overrides = properties(config);
+ if (overrides) return overrides;
+ }
+ fprintf(stderr, "iokitd: invalid darling-platform.plist; retaining default platform properties\n");
+ return nil;
+}
+}
+#endif